mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
Compare commits
23
Commits
64a0ea397c
...
811f32760e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
811f32760e | ||
|
|
01381a0404 | ||
|
|
0f02b0fdb1 | ||
|
|
3be02abf47 | ||
|
|
f91d6b676c | ||
|
|
1ebf191f94 | ||
|
|
ccad803023 | ||
|
|
21159caf31 | ||
|
|
ea4819a21d | ||
|
|
6b882b3ccf | ||
|
|
96bd36c50b | ||
|
|
46fbd837b3 | ||
|
|
796a57a115 | ||
|
|
62a2dae5ba | ||
|
|
532836c058 | ||
|
|
2fced2241b | ||
|
|
21b5fc2d92 | ||
|
|
1f753ab5fa | ||
|
|
3ed9501be5 | ||
|
|
7311251f30 | ||
|
|
7625cf450d | ||
|
|
450eb209b6 | ||
|
|
8c5c39b3c3 |
@@ -344,6 +344,7 @@ jobs:
|
||||
- build-linux
|
||||
- test
|
||||
- benchmark
|
||||
- integration
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
@@ -488,6 +489,7 @@ jobs:
|
||||
needs:
|
||||
- test
|
||||
- benchmark
|
||||
- integration
|
||||
outputs:
|
||||
names: ${{ steps.trace-cases.outputs.names }}
|
||||
steps:
|
||||
|
||||
@@ -276,9 +276,13 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
|
||||
@@ -82,6 +82,14 @@ namespace MobileGL::MG_Config {
|
||||
#endif
|
||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
||||
Bool DisableSubgroup = false;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
// promises 64-bit precision, and that is the one thing the narrowing cannot deliver.
|
||||
// Off by default so an application that checks the string before using doubles keeps
|
||||
// its float path; on for measuring what the conformance suite makes of the demoted
|
||||
// precision. See the DemoteFloat64Pass header and the "fp64" POST row.
|
||||
Bool AdvertiseFp64 = false;
|
||||
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
|
||||
@@ -167,6 +167,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
features.AvoidSamplerMipmapMinFilter =
|
||||
|
||||
@@ -960,6 +960,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||
}
|
||||
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
|
||||
// shader compiles and runs already - it is narrowed to 32 bits before the module
|
||||
// reaches this backend - so an application that simply uses doubles needs nothing
|
||||
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
|
||||
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
|
||||
// make an application that checks the string take a path MobileGL cannot honour.
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
||||
}
|
||||
// Only advertised when the device driver actually has usable timer queries
|
||||
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||
@@ -1002,6 +1011,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
||||
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
|
||||
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
|
||||
funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
|
||||
funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
|
||||
funcsTable.GL.DrawRangeElements = DrawRangeElements;
|
||||
funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
|
||||
|
||||
@@ -2905,11 +2905,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
void SetCurrentBaseVertex(Int32 baseVertex) {
|
||||
if (const auto program = GetCurrentBackendProgram()) {
|
||||
program->SetBaseVertex(baseVertex);
|
||||
}
|
||||
}
|
||||
|
||||
Bool CurrentProgramReadsDrawID() {
|
||||
const auto program = GetCurrentBackendProgram();
|
||||
return program != nullptr && program->ReadsDrawID();
|
||||
}
|
||||
|
||||
Bool CurrentProgramReadsBaseVertex() {
|
||||
const auto program = GetCurrentBackendProgram();
|
||||
return program != nullptr && program->ReadsBaseVertex();
|
||||
}
|
||||
|
||||
// The two questions above, asked from BEFORE PrepareForDraw - where neither can be
|
||||
// answered honestly. GetCurrentBackendProgram only sees a twin that a previous draw
|
||||
// already synced, and a twin from before a relink still carries the previous link's
|
||||
// uniform locations, so "no" there means "not known yet" at least as often as it
|
||||
// means no. The multi-draw compute tier has to decide whether to flatten a batch
|
||||
// before PrepareForDraw runs (its dispatch cannot come after the draw state), and
|
||||
// flattening a batch that turns out to need per-sub-draw values is unrecoverable -
|
||||
// so an unanswerable program counts as needing them.
|
||||
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices) {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
const auto program = GetCurrentBackendProgram();
|
||||
if (!currentProgram || program == nullptr ||
|
||||
program->GetSyncedLinkVersion() != currentProgram->GetLinkVersion()) {
|
||||
return true;
|
||||
}
|
||||
return program->ReadsDrawID() || (batchCarriesBaseVertices && program->ReadsBaseVertex());
|
||||
}
|
||||
|
||||
static Bool SupportsNativeIndirectDraws() {
|
||||
const auto& version = g_GLESCapabilities.GLESVersion;
|
||||
const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1);
|
||||
@@ -2947,16 +2976,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource->id);
|
||||
}
|
||||
}
|
||||
// gl_BaseVertex has no SSBO view of its own: the command's baseVertex word is read
|
||||
// from the CPU shadow, so a command whose baseVertex a compute shader wrote this
|
||||
// frame is not observable here (baseInstance is, through the view above). Feeding
|
||||
// the stale-but-usually-correct shadow beats leaving the uniform at the previous
|
||||
// draw's value, which is what a program reading gl_BaseVertex saw before.
|
||||
const Bool feedBaseVertex = CurrentProgramReadsBaseVertex();
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
const SizeT cmdByteOffset = commandOffset + static_cast<SizeT>(i) * stride;
|
||||
SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
if (paramsBinding >= 0 && backendProgram) {
|
||||
// baseInstance is the 5th word of DrawElementsIndirectCommand.
|
||||
backendProgram->SetBaseInstanceWordIndex(static_cast<Int32>((cmdByteOffset + 16) / 4));
|
||||
if (feedBaseVertex) {
|
||||
DrawElementsIndirectCommand cmd{};
|
||||
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
||||
SetCurrentBaseVertex(cmd.baseVertex);
|
||||
}
|
||||
} else {
|
||||
DrawElementsIndirectCommand cmd{};
|
||||
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
||||
SetCurrentBaseInstance(cmd.baseInstance);
|
||||
SetCurrentBaseVertex(cmd.baseVertex);
|
||||
}
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(cmdByteOffset));
|
||||
}
|
||||
@@ -2969,6 +3010,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
SetCurrentBaseInstance(cmd.baseInstance);
|
||||
SetCurrentBaseVertex(cmd.baseVertex);
|
||||
const auto indexByteOffset = static_cast<SizeT>(cmd.firstIndex) * indexSize;
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
|
||||
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
|
||||
@@ -2977,12 +3019,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
SetCurrentDrawID(0);
|
||||
SetCurrentBaseInstance(0);
|
||||
SetCurrentBaseVertex(0);
|
||||
}
|
||||
|
||||
static void ExecuteArraysIndirectCommands(GLenum mode, const Uint8* commandBytes, SizeT commandOffset,
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
|
||||
GLsizei drawcount, GLsizei stride, const char* label) {
|
||||
(void)label;
|
||||
// DrawArraysIndirectCommand has no baseVertex word, so gl_BaseVertex is zero for every
|
||||
// command here. Written BEFORE the draws, not merely restored after them: the previous
|
||||
// draw is what leaves a stale value, and restoring afterwards would only protect the
|
||||
// NEXT draw while these commands ran with the stale one.
|
||||
SetCurrentBaseVertex(0);
|
||||
const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
|
||||
if (useNative) {
|
||||
const auto backendProgram = GetCurrentBackendProgram();
|
||||
@@ -3269,7 +3317,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||
SetCurrentBaseVertex(0);
|
||||
}
|
||||
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||
@@ -3279,6 +3329,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::None;
|
||||
PrepareForDraw(syncBit);
|
||||
|
||||
// This loop IS the emulation - there is no batched tier for the non-indexed form -
|
||||
// so each sub-draw has to be given its own gl_DrawID here, exactly as the indexed
|
||||
// ladder and the indirect executors do. Without it every sub-draw of a
|
||||
// glMultiDrawArrays read draw index 0.
|
||||
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
// Client-side arrays are uploaded per sub-draw range, like the single DrawArrays path.
|
||||
@@ -3288,8 +3343,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
(*backendVAOSlot)->SyncClientSideAttributesForDrawArrays(currentVAO, first[i], count[i]);
|
||||
}
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
g_GLESFuncs.glDrawArrays(mode, first[i], count[i]);
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
}
|
||||
|
||||
// Both glMultiDrawElements entry points are emulated - ES has neither in core - by the
|
||||
@@ -3403,6 +3460,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
// Both counts are read from the CPU shadow, which a buffer with no shadow does not
|
||||
// have - MappedData() is null there and the reads below would be a null dereference,
|
||||
// not a wrong picture. The DirectVulkan twin declines the same way.
|
||||
if (parameterBuffer->MappedData() == nullptr || drawBuffer->MappedData() == nullptr) {
|
||||
MGLOG_E("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or "
|
||||
"draw-indirect buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
Uint32 actualDrawCount = 0;
|
||||
std::memcpy(&actualDrawCount, parameterBuffer->MappedData() + drawcount, sizeof(actualDrawCount));
|
||||
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
||||
@@ -3444,11 +3510,79 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
drawcount, stride, "MultiDrawArraysIndirect");
|
||||
}
|
||||
|
||||
// The non-indexed twin of MultiDrawElementsIndirectCount, and structurally identical to it:
|
||||
// ES has no GL_PARAMETER_BUFFER at all, so the draw count is read from the CPU shadow of the
|
||||
// bound one and the batch degenerates into an ordinary indirect multi-draw of that many
|
||||
// commands. Missing from the backend table until now, which made every
|
||||
// glMultiDrawArraysIndirectCount an INVALID_OPERATION ("backend does not support
|
||||
// indirect-parameter array draws") on DirectGLES while the extension was advertised.
|
||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount,
|
||||
GLsizei stride) {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||
#endif
|
||||
if (maxdrawcount <= 0) {
|
||||
return;
|
||||
}
|
||||
if (stride == 0) {
|
||||
stride = sizeof(DrawArraysIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawArraysIndirectCommand));
|
||||
return;
|
||||
}
|
||||
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!drawBuffer) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
|
||||
return;
|
||||
}
|
||||
if (!parameterBuffer) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
|
||||
return;
|
||||
}
|
||||
|
||||
drawBuffer->SyncPersistentMappedRange();
|
||||
parameterBuffer->SyncPersistentMappedRange();
|
||||
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
|
||||
sizeof(DrawArraysIndirectCommand);
|
||||
if (commandBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
return;
|
||||
}
|
||||
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
return;
|
||||
}
|
||||
|
||||
// See the indexed twin: no CPU shadow means no count to read, not a wrong one.
|
||||
if (parameterBuffer->MappedData() == nullptr || drawBuffer->MappedData() == nullptr) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or "
|
||||
"draw-indirect buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
Uint32 actualDrawCount = 0;
|
||||
std::memcpy(&actualDrawCount, parameterBuffer->MappedData() + drawcount, sizeof(actualDrawCount));
|
||||
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
||||
ExecuteArraysIndirectCommands(mode, drawBuffer->MappedData() + commandOffset, commandOffset, drawBuffer,
|
||||
static_cast<GLsizei>(actualDrawCount), stride, "MultiDrawArraysIndirectCount");
|
||||
}
|
||||
|
||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void* indices, GLint basevertex) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex);
|
||||
SetCurrentBaseVertex(0);
|
||||
}
|
||||
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
||||
@@ -3457,12 +3591,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices);
|
||||
}
|
||||
|
||||
// True when the driver will apply baseInstance to the vertex fetch itself, in which case the
|
||||
// attribute-offset emulation must stay out of the way. SetCurrentBaseInstance is orthogonal
|
||||
// and runs either way - it feeds the shader's gl_BaseInstance, not the fetch.
|
||||
inline Bool UseNativeBaseInstance() {
|
||||
return g_GLESCapabilities.SupportsBaseInstance;
|
||||
}
|
||||
|
||||
// The emulated shift has to be in place before PrepareForDraw, because that is what syncs the
|
||||
// VAO; a zero here is what un-shifts the arrays for the next ordinary draw.
|
||||
inline Uint32 EmulatedFetchBaseInstance(GLuint baseinstance) {
|
||||
return UseNativeBaseInstance() ? 0u : static_cast<Uint32>(baseinstance);
|
||||
}
|
||||
|
||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, type, indices, instancecount,
|
||||
basevertex, baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
}
|
||||
SetCurrentBaseVertex(0);
|
||||
SetCurrentBaseInstance(0);
|
||||
}
|
||||
|
||||
@@ -3470,15 +3625,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLsizei instancecount, GLint basevertex) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
SetCurrentBaseVertex(0);
|
||||
}
|
||||
|
||||
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLuint baseinstance) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, type, indices, instancecount,
|
||||
baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
}
|
||||
SetCurrentBaseInstance(0);
|
||||
}
|
||||
|
||||
@@ -3514,9 +3677,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
GLuint baseinstance) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
|
||||
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
|
||||
PrepareForDraw(syncBit);
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawArraysInstancedBaseInstanceEXT(mode, first, count, instancecount, baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
||||
}
|
||||
SetCurrentBaseInstance(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride);
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount,
|
||||
GLsizei stride);
|
||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void* indices, GLint basevertex);
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
|
||||
|
||||
@@ -155,6 +155,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// (possibly GPU-written) indirect command buffer, so its declaration expands into a
|
||||
// std430 SSBO view of that buffer indexed by a CPU-computed word index, with the plain
|
||||
// mg_BaseInstance uniform as the fallback for non-indirect draws.
|
||||
//
|
||||
// The word index is stored ONE-BASED, so that zero - the value every GLSL uniform starts
|
||||
// at - is the "not an indirect draw" sentinel. Nothing seeds this uniform before a
|
||||
// program's first draw, and the non-indirect draw entry points never write it at all, so a
|
||||
// zero-based index with a negative sentinel would leave every such draw reading
|
||||
// mg_indirectWords[0] out of a storage buffer no one bound. That is not a silent zero on a
|
||||
// real driver: it returned garbage on Adreno, and a garbage gl_BaseInstance pushed the CTS
|
||||
// shader_draw_parameters geometry clean off screen.
|
||||
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType) {
|
||||
if (shaderType != GL_VERTEX_SHADER) {
|
||||
return source;
|
||||
@@ -208,12 +216,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
" { highp uint mg_indirectWords[]; };\n";
|
||||
if (rebaseInstanceId) {
|
||||
machinery += String("#define ") + ZERO_BASED_INSTANCE_ID_NAME + " (gl_InstanceID - ((" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " >= 0) ? int(mg_indirectWords[uint(" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ")]) : 0))\n";
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " > 0) ? int(mg_indirectWords[uint(" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " - 1)]) : 0))\n";
|
||||
}
|
||||
machinery += String("#define ") + BASE_INSTANCE_LOWERED_NAME + " ((" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " >= 0) ? int(mg_indirectWords[uint(" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ")]) : " + BASE_INSTANCE_UNIFORM_NAME + ")";
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " > 0) ? int(mg_indirectWords[uint(" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " - 1)]) : " + BASE_INSTANCE_UNIFORM_NAME + ")";
|
||||
source.replace(pos, declaration.size(), machinery);
|
||||
break;
|
||||
}
|
||||
@@ -1473,6 +1481,78 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ES 3.1 core. Queried through the loader rather than the version, because the whole
|
||||
// point of using it is to express something the pointer API cannot, and falling back
|
||||
// silently on a driver that lacks it is better than crashing on a null entry point.
|
||||
inline Bool HasVertexBindingApi() {
|
||||
return g_GLESFuncs.glBindVertexBuffer != nullptr && g_GLESFuncs.glVertexAttribFormat != nullptr &&
|
||||
g_GLESFuncs.glVertexAttribIFormat != nullptr && g_GLESFuncs.glVertexAttribBinding != nullptr &&
|
||||
g_GLESFuncs.glVertexBindingDivisor != nullptr;
|
||||
}
|
||||
|
||||
// Draw state, not VAO state: set by the baseInstance draw entry points around
|
||||
// PrepareForDraw and back to zero as soon as the draw is issued.
|
||||
Uint32 g_pendingFetchBaseInstance = 0;
|
||||
|
||||
void SetPendingFetchBaseInstance(Uint32 baseInstance) {
|
||||
g_pendingFetchBaseInstance = baseInstance;
|
||||
}
|
||||
|
||||
Uint32 GetPendingFetchBaseInstance() {
|
||||
return g_pendingFetchBaseInstance;
|
||||
}
|
||||
|
||||
// The "+ baseInstance" of GL's instanced-array element index, expressed as a byte shift
|
||||
// of the array's own offset. Only divisor'd arrays step per instance, so only they move.
|
||||
//
|
||||
// baseInstance is added to the ELEMENT index, not to instance/divisor - the divisor
|
||||
// therefore does not appear here, and the shift is a whole number of strides.
|
||||
//
|
||||
// A resolved stride of zero is the binding model's "never advance" (see
|
||||
// VertexAttribute::Stride), so such an array reads the same element for every instance
|
||||
// and a baseInstance cannot move it. The arithmetic already yields zero for that case.
|
||||
inline SizeT BaseInstanceByteShift(const MG_State::GLState::VertexAttribute& attrib, Uint32 baseInstance) {
|
||||
if (baseInstance == 0 || attrib.Divisor == 0) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<SizeT>(baseInstance) * static_cast<SizeT>(attrib.Stride);
|
||||
}
|
||||
|
||||
// Declares one attribute through the ES binding-point API, the only spelling that can
|
||||
// carry a stride of zero. Returns false when the attribute has no usable buffer, in
|
||||
// which case nothing was emitted.
|
||||
inline Bool SyncZeroStrideAttribute(Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib) {
|
||||
const auto& bufferObject = attrib.Buffer;
|
||||
if (!bufferObject) {
|
||||
MGLOG_W("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
|
||||
return false;
|
||||
}
|
||||
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject);
|
||||
if (!backendResource || backendResource->id == 0) {
|
||||
MGLOG_E("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!attrib.IsInteger) {
|
||||
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : attrib.Size;
|
||||
g_GLESFuncs.glVertexAttribFormat(attribIndex, glSize,
|
||||
MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
|
||||
attrib.Normalized ? GL_TRUE : GL_FALSE, 0);
|
||||
} else {
|
||||
g_GLESFuncs.glVertexAttribIFormat(attribIndex, attrib.Size,
|
||||
MG_Util::ConvertDataTypeToGLEnum(attrib.Type), 0);
|
||||
}
|
||||
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
|
||||
// The resolved offset goes on the binding point, not into a relative offset: the
|
||||
// relative offset is capped by GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET (2047 at
|
||||
// minimum) while a buffer offset is not, so anything else would break on a large
|
||||
// one. BindBufferId is bypassed deliberately - glBindVertexBuffer binds into the
|
||||
// VAO's binding point, not the GL_ARRAY_BUFFER target that cache tracks.
|
||||
g_GLESFuncs.glBindVertexBuffer(attribIndex, backendResource->id,
|
||||
static_cast<GLintptr>(attrib.Offset), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendVertexArrayObject::SyncToBackend(
|
||||
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -1495,7 +1575,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Uint16 currentIndexBufferVersion = stateVAOObject->GetIndexBufferBindingSlot().GetVersion();
|
||||
const Bool attributesDirty = !m_hasSyncedConfigVersion || m_syncedConfigVersion != currentConfigVersion;
|
||||
const Bool indexBufferDirty = currentIndexBufferVersion != m_syncedIndexBufferVersion;
|
||||
if (!attributesDirty && !indexBufferDirty) {
|
||||
|
||||
// The baseInstance shift lives in the attribute offsets the driver already holds, so
|
||||
// a change of baseInstance has to re-emit the divisor'd arrays even when the frontend
|
||||
// config version says nothing moved - and equally has to un-shift them for the next
|
||||
// draw that carries no baseInstance. Resting state is 0 on both sides, so a program
|
||||
// that never calls a *BaseInstance entry point never pays for this compare.
|
||||
const Uint32 fetchBaseInstance = g_pendingFetchBaseInstance;
|
||||
const Bool baseInstanceDirty = m_syncedFetchBaseInstance != fetchBaseInstance;
|
||||
const Bool emitAttributes = attributesDirty || baseInstanceDirty;
|
||||
if (!emitAttributes && !indexBufferDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1503,8 +1592,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const auto& allAttributeVersions = stateVAOObject->GetAllAttributeVersions();
|
||||
const auto& allAttributes = stateVAOObject->GetAllAttributes();
|
||||
for (Uint attribIndex = 0; attribIndex < allAttributes.size() && attributesDirty; ++attribIndex) {
|
||||
for (Uint attribIndex = 0; attribIndex < allAttributes.size() && emitAttributes; ++attribIndex) {
|
||||
const auto& attrib = allAttributes[attribIndex];
|
||||
// Only the divisor'd arrays carry the shift, and only an enabled one is worth
|
||||
// re-emitting - a disabled array has no pointer the draw could fetch through,
|
||||
// and may well have no buffer to bind either.
|
||||
const Bool needsSyncBaseInstance = baseInstanceDirty && attrib.Enabled && attrib.Divisor != 0;
|
||||
Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion !=
|
||||
m_syncedAttributeVersions[attribIndex].SwitchVersion;
|
||||
if (needsSyncSwitch) {
|
||||
@@ -1519,7 +1612,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_syncedAttributeVersions[attribIndex].FormatVersion;
|
||||
Bool needsSyncBuffer = allAttributeVersions[attribIndex].BufferVersion !=
|
||||
m_syncedAttributeVersions[attribIndex].BufferVersion;
|
||||
if (!needsSyncFormat && !needsSyncBuffer) continue;
|
||||
if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue;
|
||||
|
||||
// Defence in depth. The frontend already declines glVertexAttribLFormat on this
|
||||
// backend (SupportsFloat64VertexAttributes is false - ES has no GL_DOUBLE vertex
|
||||
@@ -1529,28 +1622,90 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// FormatVersion, not SwitchVersion, so the 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.
|
||||
if (attrib.IsLong) {
|
||||
MGLOG_E("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array, which this "
|
||||
//
|
||||
// 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
|
||||
// long, is not declined by the frontend, and still has no ES vertex format.
|
||||
// Leaving that one enabled did not merely raise INVALID_ENUM - the Adreno
|
||||
// driver dereferenced null inside the next draw and took the process with it
|
||||
// (SIGSEGV in libGLESv2_adreno, KHR-GL43.vertex_attrib_binding.basic-input-case4),
|
||||
// because the array stayed enabled with no pointer the failed call could set.
|
||||
// The type test therefore covers the storage, not the spelling.
|
||||
if (attrib.IsLong || attrib.Type == DataType::Float64) {
|
||||
MGLOG_I("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array, which this "
|
||||
"backend cannot feed - disabling the array",
|
||||
attribIndex);
|
||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A resolved stride of zero is the binding model's "never advance" (see
|
||||
// VertexAttribute::Stride) and glVertexAttribPointer cannot say it - a zero
|
||||
// stride argument there means "tightly packed" instead, i.e. exactly the
|
||||
// opposite. ES 3.1's binding-point API can, so a zero-stride attribute takes
|
||||
// that spelling: its own binding point (index == attribute index, the default
|
||||
// mapping) carrying the buffer, the whole resolved offset and stride 0, with
|
||||
// the format at relative offset 0. Everything the pointer call would have set
|
||||
// for this attribute is set here too, so the two spellings stay interchangeable
|
||||
// from one sync to the next.
|
||||
if (attrib.Stride == 0 && HasVertexBindingApi()) {
|
||||
if (!SyncZeroStrideAttribute(attribIndex, attrib)) {
|
||||
continue;
|
||||
}
|
||||
// No BaseInstanceByteShift here on purpose: a zero stride never advances, so
|
||||
// the shift is zero by construction and adding it would only obscure that.
|
||||
if (needsSyncFormat) {
|
||||
g_GLESFuncs.glVertexBindingDivisor(attribIndex, attrib.Divisor);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!BindAttributeBuffer(attrib)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// GL_BGRA as a vertex SIZE is desktop-only; ES has no equivalent and rejects
|
||||
// it. That rejection is not benign: it leaves the array ENABLED with no
|
||||
// pointer, and the Adreno driver then dereferences null inside the next draw
|
||||
// and kills the process rather than reporting an error (SIGSEGV in
|
||||
// libGLESv2_adreno, KHR-GL43.vertex_attrib_binding.basic-input-case5). So the
|
||||
// refusal has to be observed and the array disabled.
|
||||
//
|
||||
// Deliberately ONLY this format. Everything else MobileGL can reach here is ES
|
||||
// core - the packed 2_10_10_10 pair included, whose size the frontend has
|
||||
// already pinned to the 4 that ES requires - so nothing else can be refused,
|
||||
// and the per-draw sync must not grow a glGetError round trip (a driver
|
||||
// pipeline stall) for the formats real applications actually use. BGRA is also
|
||||
// still ATTEMPTED rather than refused up front: some ES drivers do accept it,
|
||||
// and the ones that do should keep working.
|
||||
const Bool formatMayBeRefused = attrib.IsBgra;
|
||||
if (formatMayBeRefused) {
|
||||
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
} // start from a clean slate so the check below is about THIS call
|
||||
}
|
||||
|
||||
const SizeT fetchOffset = attrib.Offset + BaseInstanceByteShift(attrib, fetchBaseInstance);
|
||||
|
||||
if (!attrib.IsInteger) {
|
||||
// GL_BGRA is passed to the driver as the size argument (the driver reorders BGRA).
|
||||
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : attrib.Size;
|
||||
g_GLESFuncs.glVertexAttribPointer(
|
||||
attribIndex, glSize, MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
|
||||
attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride, (const void*)attrib.Offset);
|
||||
attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride, (const void*)fetchOffset);
|
||||
} else {
|
||||
g_GLESFuncs.glVertexAttribIPointer(attribIndex, attrib.Size,
|
||||
MG_Util::ConvertDataTypeToGLEnum(attrib.Type), attrib.Stride,
|
||||
(const void*)attrib.Offset);
|
||||
(const void*)fetchOffset);
|
||||
}
|
||||
|
||||
if (formatMayBeRefused && g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
MGLOG_I("DirectGLES: the driver refused the vertex format of attribute %u "
|
||||
"(size=%d bgra=%d type=%s) - disabling the array so the draw cannot "
|
||||
"fetch through a pointer the driver never accepted",
|
||||
attribIndex, attrib.Size, attrib.IsBgra ? 1 : 0,
|
||||
MG_Util::ConvertGLEnumToString(MG_Util::ConvertDataTypeToGLEnum(attrib.Type)).c_str());
|
||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (needsSyncFormat) {
|
||||
@@ -1584,6 +1739,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_syncedConfigVersion = currentConfigVersion;
|
||||
m_hasSyncedConfigVersion = true;
|
||||
}
|
||||
if (emitAttributes) {
|
||||
m_syncedFetchBaseInstance = fetchBaseInstance;
|
||||
}
|
||||
}
|
||||
|
||||
void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays(
|
||||
@@ -1601,9 +1759,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Same reason as SyncToBackend: there is no ES vertex format for a 64-bit array, and
|
||||
// this path only ever reaches glVertexAttribPointer/IPointer.
|
||||
if (attrib.IsLong) {
|
||||
// Same reason as SyncToBackend, including why the test is on the storage rather
|
||||
// than on IsLong: there is no ES vertex format for a 64-bit array, and this path
|
||||
// only ever reaches glVertexAttribPointer/IPointer.
|
||||
if (attrib.IsLong || attrib.Type == DataType::Float64) {
|
||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||
continue;
|
||||
}
|
||||
@@ -4316,6 +4475,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
|
||||
|
||||
// Blocks a transform-feedback capture request names a member of ("StageData" of
|
||||
// "StageData.attrib[0]"). The Adreno ES driver accepts such a request, links, and
|
||||
// then captures nothing at all for it, so those blocks - and ONLY those - get
|
||||
// flattened into per-member variables below, in EVERY stage, so a producer and its
|
||||
// consumer keep matching. gl_PerVertex members ("gl_Position") carry no block
|
||||
// prefix and so never enter this set.
|
||||
std::set<String> xfbCaptureBlockNames;
|
||||
for (const auto& xfbVarying : stateProgramObject->GetTransformFeedbackVaryings()) {
|
||||
const SizeT dot = xfbVarying.name.find('.');
|
||||
if (dot != String::npos && dot > 0) {
|
||||
xfbCaptureBlockNames.insert(xfbVarying.name.substr(0, dot));
|
||||
}
|
||||
}
|
||||
std::set<String> flattenedXfbBlockNames;
|
||||
|
||||
for (int index = 0; index < attachedShaders.size(); ++index) {
|
||||
auto& shader = attachedShaders[index];
|
||||
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
|
||||
@@ -4358,6 +4532,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &loweredSpirv;
|
||||
}
|
||||
|
||||
// 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) &&
|
||||
!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) &&
|
||||
!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
|
||||
@@ -4439,11 +4647,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
spvcSession.Compile(&result);
|
||||
|
||||
if (!result) {
|
||||
MG_Util::ShaderTranspiler::ResultInfo r;
|
||||
r.log += "Failed to compile the shader to GLSL: \n";
|
||||
r.log += spvcSession.GetLastErrorString();
|
||||
r.errc = -5;
|
||||
MGLOG_E("%s", r.log.c_str());
|
||||
// MGLOG_I, for the same reason as the compile- and link-failure diagnostics
|
||||
// below: every CI, retrace and release build compiles at
|
||||
// MOBILEGL_LOG_LEVEL_INFO, where MGLOG_E expands to nothing. 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_I("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;
|
||||
}
|
||||
@@ -4559,8 +4774,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings();
|
||||
Vector<const GLchar*> xfbNames;
|
||||
xfbNames.reserve(xfbVaryings.size());
|
||||
for (const auto& xfbVarying : xfbVaryings) {
|
||||
xfbNames.push_back(xfbVarying.name.c_str());
|
||||
// A block this build flattened no longer HAS the member the application asked
|
||||
// for; it has the variable that replaced it. Everything else - including a
|
||||
// member of a block that was left alone - keeps the application's spelling.
|
||||
// Storage first, pointers after: xfbNames holds pointers into these strings.
|
||||
Vector<String> rewrittenXfbNames(xfbVaryings.size());
|
||||
for (SizeT nameIndex = 0; nameIndex < xfbVaryings.size(); ++nameIndex) {
|
||||
String flatName;
|
||||
if (!flattenedXfbBlockNames.empty() &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(
|
||||
xfbVaryings[nameIndex].name, flattenedXfbBlockNames, flatName)) {
|
||||
rewrittenXfbNames[nameIndex] = std::move(flatName);
|
||||
} else {
|
||||
rewrittenXfbNames[nameIndex] = xfbVaryings[nameIndex].name;
|
||||
}
|
||||
}
|
||||
for (const auto& xfbName : rewrittenXfbNames) {
|
||||
xfbNames.push_back(xfbName.c_str());
|
||||
}
|
||||
MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(),
|
||||
m_backendProgramId);
|
||||
@@ -4594,6 +4824,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_baseInstanceUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId,
|
||||
BASE_INSTANCE_UNIFORM_NAME);
|
||||
m_drawIdUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, DRAW_ID_UNIFORM_NAME);
|
||||
m_baseVertexUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId,
|
||||
BASE_VERTEX_UNIFORM_NAME);
|
||||
m_baseInstanceWordIndexUniformLocation =
|
||||
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME);
|
||||
// The mg_IndirectParams block binding is baked into the ESSL (ES cannot rebind
|
||||
@@ -4772,14 +5004,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast<GLint>(baseInstance));
|
||||
}
|
||||
// A direct value disables the indirect-command-buffer read.
|
||||
SetBaseInstanceWordIndex(-1);
|
||||
}
|
||||
|
||||
// The uniform is written one-based so that its GLSL initial value, zero, already reads
|
||||
// as "no indirect command" - see PromoteDrawParameterGlobalsToUniforms.
|
||||
void BackendProgramObjectImpl::SetBaseInstanceWordIndex(Int32 wordIndex) const {
|
||||
if (m_baseInstanceWordIndexUniformLocation >= 0) {
|
||||
g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, -1);
|
||||
g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation,
|
||||
wordIndex < 0 ? 0 : wordIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SetBaseInstanceWordIndex(Int32 wordIndex) const {
|
||||
if (m_baseInstanceWordIndexUniformLocation >= 0) {
|
||||
g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, wordIndex);
|
||||
void BackendProgramObjectImpl::SetBaseVertex(Int32 baseVertex) const {
|
||||
if (m_baseVertexUniformLocation >= 0) {
|
||||
g_GLESFuncs.glUniform1i(m_baseVertexUniformLocation, baseVertex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,14 +82,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
|
||||
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
|
||||
void CheckPrimitiveRestartSupported(GLenum indexType);
|
||||
// Feed the current program's gl_BaseInstance / gl_DrawID emulation uniforms. Both are
|
||||
// no-ops when the program does not read the corresponding builtin.
|
||||
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
|
||||
// uniforms. All are no-ops when the program does not read the corresponding builtin.
|
||||
void SetCurrentBaseInstance(Uint32 baseInstance);
|
||||
void SetCurrentDrawID(Uint32 drawId);
|
||||
// GL's gl_BaseVertex is the base-vertex parameter of an indexed draw and zero for every
|
||||
// command that has none - including all the DrawArrays forms - so every draw path that
|
||||
// does not carry one must leave this at zero rather than inherit the last draw's value.
|
||||
void SetCurrentBaseVertex(Int32 baseVertex);
|
||||
// True when the current program actually reads gl_DrawID, i.e. when a batched
|
||||
// (single driver call) multi-draw tier would have to feed it one value for the whole
|
||||
// batch and would therefore be wrong.
|
||||
Bool CurrentProgramReadsDrawID();
|
||||
// Same question for gl_BaseVertex: a batched multi-draw tier cannot give each sub-draw
|
||||
// its own base vertex through a uniform either.
|
||||
Bool CurrentProgramReadsBaseVertex();
|
||||
// Both of the above, conservatively, for a caller that must decide BEFORE PrepareForDraw
|
||||
// has synced the program - where "does not read it" is indistinguishable from "cannot be
|
||||
// asked yet". Answers true whenever the backend twin is missing or predates the current
|
||||
// link.
|
||||
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
|
||||
|
||||
template <typename StateObject, typename BackendObject>
|
||||
class StateBackendObjectRegistry {
|
||||
@@ -454,6 +466,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint32 m_syncedConfigVersion = 0;
|
||||
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
||||
m_syncedAttributeVersions;
|
||||
// Byte shift currently baked into the instanced arrays' offsets by the baseInstance
|
||||
// emulation (see SetPendingFetchBaseInstance). It is draw state, not VAO state, so it
|
||||
// is deliberately NOT covered by the config version: the frontend never bumps for it.
|
||||
// Kept here because it describes what was last EMITTED, which is what the next sync
|
||||
// has to correct.
|
||||
Uint32 m_syncedFetchBaseInstance = 0;
|
||||
};
|
||||
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
||||
@@ -467,6 +485,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void InvalidateVAOBindingCache();
|
||||
// ES resets the binding to 0 when the currently bound VAO is deleted.
|
||||
void NoteVAOIdDeleted(Uint id);
|
||||
|
||||
// baseInstance emulation for drivers without GL_EXT_base_instance. GL fetches an
|
||||
// instanced array at element "floor(instance / divisor) + baseInstance", and ES has no
|
||||
// way to say the "+ baseInstance" part - so it is folded into the attribute's own byte
|
||||
// offset (baseInstance * stride) for every divisor'd array, which is exactly equivalent.
|
||||
// Must be set BEFORE PrepareForDraw so the VAO sync sees it, and cleared after the draw
|
||||
// so the next one refetches from element 0; ScopedFetchBaseInstance does both.
|
||||
void SetPendingFetchBaseInstance(Uint32 baseInstance);
|
||||
Uint32 GetPendingFetchBaseInstance();
|
||||
|
||||
class ScopedFetchBaseInstance {
|
||||
public:
|
||||
explicit ScopedFetchBaseInstance(Uint32 baseInstance) { SetPendingFetchBaseInstance(baseInstance); }
|
||||
~ScopedFetchBaseInstance() { SetPendingFetchBaseInstance(0); }
|
||||
ScopedFetchBaseInstance(const ScopedFetchBaseInstance&) = delete;
|
||||
ScopedFetchBaseInstance& operator=(const ScopedFetchBaseInstance&) = delete;
|
||||
};
|
||||
} // namespace VertexArrayImpl
|
||||
|
||||
namespace TextureImpl {
|
||||
@@ -1025,9 +1060,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SetBaseInstance(Uint32 baseInstance) const;
|
||||
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
||||
void SetDrawID(Uint32 drawId) const;
|
||||
void SetBaseVertex(Int32 baseVertex) const;
|
||||
// True when the transpiled program kept a gl_DrawID uniform, i.e. SetDrawID
|
||||
// actually reaches a shader read rather than being discarded.
|
||||
Bool ReadsDrawID() const { return m_drawIdUniformLocation >= 0; }
|
||||
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
||||
// uniform write, and only such a program needs the reset after one.
|
||||
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||
// False when the last SyncToBackend could not produce a usable program (a
|
||||
@@ -1077,6 +1116,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint m_backendGlobalUBOId = 0;
|
||||
Int m_baseInstanceUniformLocation = -1;
|
||||
Int m_drawIdUniformLocation = -1;
|
||||
Int m_baseVertexUniformLocation = -1;
|
||||
Int m_baseInstanceWordIndexUniformLocation = -1;
|
||||
Int m_indirectParamsBinding = -1;
|
||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||
|
||||
@@ -274,17 +274,22 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// the batch's own shape - not the driver - rules it out; the compute tier keeps
|
||||
// its remaining feasibility checks inside its implementation, where the data it
|
||||
// has to walk is already in hand.
|
||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool hasIndexBuffer) {
|
||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
|
||||
Bool hasIndexBuffer) {
|
||||
ResolveTierOnce();
|
||||
GLESMultiDrawMode tier = g_resolvedTier;
|
||||
|
||||
// Batched tiers issue one driver entry for the whole batch, so the emulated
|
||||
// gl_DrawID uniform can only hold one value across every sub-draw. A program
|
||||
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
|
||||
// own index (the spec's value); nothing else observes the difference.
|
||||
// own index (the spec's value); nothing else observes the difference. The
|
||||
// emulated gl_BaseVertex is one uniform for the same reason, so a batch whose
|
||||
// sub-draws carry their own base vertices unrolls too - even the Ext tier,
|
||||
// which hands the driver the whole basevertex array, can only leave ONE value
|
||||
// in the uniform the shader reads.
|
||||
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
|
||||
tier == GLESMultiDrawMode::Compute;
|
||||
if (batched && programReadsDrawID) {
|
||||
if (batched && (programReadsDrawID || perSubDrawBaseVertex)) {
|
||||
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
||||
: GLESMultiDrawMode::DrawElements;
|
||||
}
|
||||
@@ -371,7 +376,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID) {
|
||||
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID,
|
||||
Bool feedBaseVertex) {
|
||||
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
|
||||
const SizeT indexSize = IndexTypeSize(type);
|
||||
if (indexSize == 0) return false;
|
||||
@@ -413,10 +419,12 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
} else {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
}
|
||||
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
|
||||
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
|
||||
@@ -428,15 +436,17 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID, Bool feedBaseVertex) {
|
||||
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||
basevertex ? basevertex[i] : 0);
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
|
||||
return true;
|
||||
}
|
||||
@@ -446,7 +456,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID,
|
||||
Bool feedBaseVertex) {
|
||||
const SizeT indexSize = IndexTypeSize(type);
|
||||
if (indexSize == 0) return false;
|
||||
|
||||
@@ -500,11 +511,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
// The base vertex is folded into the rewritten index stream here, so the
|
||||
// driver sees none - but gl_BaseVertex still has to report the value the
|
||||
// application passed for this sub-draw.
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||
cursor += static_cast<SizeT>(count[i]);
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
|
||||
return true;
|
||||
@@ -837,8 +853,15 @@ void main() {
|
||||
// afterwards would mean unpicking the program, SSBO and index bindings
|
||||
// PrepareForDraw just made, and a dispatch inside an open transform feedback
|
||||
// span is not legal at all. On success it hands back a flattened index stream.
|
||||
// A batch whose sub-draws carry their own base vertices cannot be flattened either
|
||||
// when the program reads gl_BaseVertex: one draw call leaves one uniform value.
|
||||
// Asked conservatively because this decision precedes PrepareForDraw - see
|
||||
// CurrentProgramMayNeedPerSubDrawBuiltins. Flattening is the irreversible half:
|
||||
// once the batch is one draw the values are gone, whereas declining to flatten only
|
||||
// costs the unrolled tier.
|
||||
FlattenedStream flattened;
|
||||
if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) {
|
||||
if (ResolvedTier() == GLESMultiDrawMode::Compute &&
|
||||
!CurrentProgramMayNeedPerSubDrawBuiltins(basevertex != nullptr)) {
|
||||
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
|
||||
}
|
||||
|
||||
@@ -852,8 +875,11 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Now that PrepareForDraw has synced the program, both questions have real answers;
|
||||
// the tier choice and the per-sub-draw feeds use those, not the guess above.
|
||||
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
||||
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, hasIndexBuffer);
|
||||
const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
|
||||
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer);
|
||||
|
||||
Bool drawn = false;
|
||||
switch (tier) {
|
||||
@@ -861,16 +887,19 @@ void main() {
|
||||
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::MultiIndirect:
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID);
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID,
|
||||
feedBaseVertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::Indirect:
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID);
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID,
|
||||
feedBaseVertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::BaseVertex:
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::DrawElements:
|
||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
|
||||
feedBaseVertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::Compute:
|
||||
// Its pre-pass ran above; reaching here means it declined this batch's shape.
|
||||
@@ -883,8 +912,13 @@ void main() {
|
||||
// below are the floor: a base-vertex replay where the driver has one, and the
|
||||
// rewritten index stream where it does not. Both are safe for any batch these
|
||||
// entry points can receive.
|
||||
if (!drawn) drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
if (!drawn) drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
if (!drawn) {
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||
}
|
||||
if (!drawn) {
|
||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
|
||||
feedBaseVertex);
|
||||
}
|
||||
if (!drawn) {
|
||||
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||
"the batch was dropped",
|
||||
|
||||
@@ -539,6 +539,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||
}
|
||||
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
|
||||
// shader compiles and runs already - it is narrowed to 32 bits before the module
|
||||
// reaches this backend - so an application that simply uses doubles needs nothing
|
||||
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
|
||||
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
|
||||
// make an application that checks the string take a path MobileGL cannot honour.
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
||||
}
|
||||
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
|
||||
// only advertised when the device actually supports timestamp queries and the
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||
@@ -877,7 +886,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
|
||||
// Never, on any device, and no longer for the reason it used to be. It used to track
|
||||
// shaderFloat64 because a `dvec3` input needed the Float64 capability to exist in the
|
||||
// module at all; a 64-bit vertex FETCH was already impossible (VK_FORMAT_R64*_SFLOAT is
|
||||
// optional and lavapipe reports zero bufferFeatures for all four), so the attribute
|
||||
// arrived as its 32-bit word pair and PackDoubleVertexInputsPass bitcast it back.
|
||||
//
|
||||
// The shader half of that is gone: every 64-bit float is narrowed before any module
|
||||
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input
|
||||
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float`
|
||||
// input would be silent garbage. Reconstructing the value would mean decoding the
|
||||
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the
|
||||
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to
|
||||
// fetch 2N uint components where the application declared N doubles, which a dvec3 or
|
||||
// dvec4 cannot even express within one attribute location.
|
||||
//
|
||||
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they
|
||||
// already were on Espryt and on every real mobile device (Adreno and Mali both report
|
||||
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still
|
||||
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds
|
||||
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
||||
|
||||
@@ -1979,13 +1979,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// cannot be corrected and instanced draws with a non-zero baseInstance misrender; this
|
||||
// detects the case so the user gets one warning instead of silent corruption.
|
||||
Bool ProgramFactory::ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInInstanceIndex);
|
||||
}
|
||||
|
||||
// GL's gl_BaseVertex and Vulkan's BaseVertex agree for indexed draws and disagree for every
|
||||
// other command, so a program declaring the builtin needs the ZeroBaseVertex variant when a
|
||||
// non-indexed draw uses it (see CompileOptionBit::ZeroBaseVertex). "Declares" rather than
|
||||
// "reads" is the honest word and the useful one: the zeroing pass keeps the variable, so
|
||||
// both variants of a program answer this question identically.
|
||||
Bool ProgramFactory::ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
|
||||
}
|
||||
|
||||
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||
SpvBuiltIn builtin) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||
for (Uint32 variableIndex = 0; variableIndex < entryPoint.input_variable_count; ++variableIndex) {
|
||||
const SpvReflectInterfaceVariable* variable = entryPoint.input_variables[variableIndex];
|
||||
if (variable != nullptr &&
|
||||
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
||||
variable->built_in == SpvBuiltInInstanceIndex) {
|
||||
variable->built_in == builtin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2244,6 +2258,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkProgramObject& entry) const {
|
||||
entry.activeVertexInputLocationMask = 0;
|
||||
entry.vertexInputTypes.fill(0);
|
||||
entry.readsBaseVertexBuiltin = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Vertex) {
|
||||
@@ -2265,6 +2280,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.readsBaseVertexBuiltin = ReflectedReadsBaseVertexBuiltin(reflectModule);
|
||||
|
||||
if (!m_shaderDrawParametersEnabled && ReflectedReadsInstanceIndexBuiltin(reflectModule)) {
|
||||
static Bool s_warnedInstanceIndexUnsupported = false;
|
||||
if (!s_warnedInstanceIndexUnsupported) {
|
||||
@@ -2909,6 +2926,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a FragCoordYFlip variant also depends on the baked default-framebuffer height, so
|
||||
// that height rides in the free high half of the key. Flags occupy the low bits, and a
|
||||
// height cannot exceed the 16 bits a swapchain extent fits in.
|
||||
//
|
||||
// "The low bits" is load-bearing and was until now only a comment: a flag that reached
|
||||
// bit 16 would alias the height and two different variants would share one memo slot.
|
||||
static_assert(static_cast<Uint>(CompileOptionBit::ZeroBaseVertex) < (1u << 16),
|
||||
"CompileOptionBit values must stay below bit 16: GetOrCreateProgram packs the "
|
||||
"default-framebuffer height into the high half of the same memo key");
|
||||
const Uint memoKey = (flags & CompileOptionBit::FragCoordYFlip)
|
||||
? (flags.GetRaw() | (m_defaultFramebufferHeight << 16))
|
||||
: flags.GetRaw();
|
||||
@@ -3027,6 +3050,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// The non-indexed variant of a vertex stage that reads gl_BaseVertex: GL wants zero
|
||||
// there, Vulkan's builtin would hand it the draw's firstVertex. Requested per draw
|
||||
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
|
||||
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex
|
||||
// and for the baseVertex word of an indexed indirect command.
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
|
||||
(flags & CompileOptionBit::ZeroBaseVertex)) {
|
||||
Vector<Uint> zeroedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
|
||||
zeroedSpirv)) {
|
||||
moduleSpirvs[i] = std::move(zeroedSpirv);
|
||||
} else {
|
||||
// Failing open keeps the native builtin, which is the pre-fix behavior:
|
||||
// gl_BaseVertex reads firstVertex on a DrawArrays instead of zero.
|
||||
MGLOG_E("ProgramFactory: failed to zero gl_BaseVertex for program %u; non-indexed "
|
||||
"draws will read the draw's first vertex from it instead of zero",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
// A 64-bit vertex input has to arrive as its 32-bit word pair: VK_FORMAT_R64*_SFLOAT is
|
||||
// optional and lavapipe advertises none of them at all. The pass is unconditional so it
|
||||
// always agrees with the Float64 case in VertexInputStateFactory::ToVkVertexFormat, and
|
||||
|
||||
@@ -60,6 +60,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// PositionYFlip (the two are the same fact about the same draws) except under a
|
||||
// quarter turn, which this renderer does not convert rectangles for either.
|
||||
FragCoordYFlip = 1 << 7,
|
||||
// Replaces the vertex stage's gl_BaseVertex reads with zero. GL defines the builtin
|
||||
// as zero for every drawing command that has no baseVertex parameter - all the
|
||||
// DrawArrays forms - while Vulkan's BaseVertex reports firstVertex there. Set only
|
||||
// for a non-indexed draw whose program actually reads the builtin, so nothing else
|
||||
// acquires a second program/pipeline variant. See ZeroBaseVertexPass.
|
||||
ZeroBaseVertex = 1 << 8,
|
||||
};
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
@@ -129,6 +135,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||
Bool fragmentReplacesDepth = false;
|
||||
// The vertex module declares the BaseVertex builtin. Selects the ZeroBaseVertex
|
||||
// program variant for non-indexed draws, and is deliberately a property of the
|
||||
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
||||
// declared, so both variants answer the same and the draw path can ask either.
|
||||
Bool readsBaseVertexBuiltin = false;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
@@ -179,6 +190,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -192,6 +204,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
@@ -231,6 +244,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -244,6 +258,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
@@ -341,6 +356,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
|
||||
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
|
||||
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
// True when an entry point declares the BaseVertex builtin, i.e. when a non-indexed
|
||||
// draw with this program has to take the ZeroBaseVertex variant.
|
||||
static Bool ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
// Shared by the two above: does any entry point list an input variable decorated with
|
||||
// this builtin?
|
||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
|
||||
@@ -153,8 +153,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint32 sourceStride =
|
||||
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
|
||||
// Verbatim, zero included. The frontend already resolved a pointer call's
|
||||
// "tightly packed" stride 0 into the element size (see VertexAttribute::Stride),
|
||||
// so a zero here is the binding model's stride 0 - every vertex reads the same
|
||||
// element - which is exactly what a zero VkVertexInputBindingDescription::stride
|
||||
// means. Substituting the element size fetched a fresh element per vertex and ran
|
||||
// off the end of the buffer (KHR-GL43.vertex_attrib_binding.basic-input-case7/8).
|
||||
// Client-memory arrays cannot reach zero: they only exist on the pointer path.
|
||||
const Uint32 sourceStride = static_cast<Uint32>(attr.Stride);
|
||||
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
||||
attr.Type == DataType::Uint2101010Rev;
|
||||
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
|
||||
@@ -175,10 +181,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
Uint32 stride = sourceStride;
|
||||
if (conversion == VertexStreamConversion::Repack) {
|
||||
stride = static_cast<Uint32>(attribByteSize);
|
||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||
// A converted stream is tightly packed, so its stride is the converted element
|
||||
// size - unless the source stride is zero, which does not describe a packing at
|
||||
// all but "never advance". That survives the conversion unchanged: the draw path
|
||||
// converts exactly one element and every vertex reads it.
|
||||
if (sourceStride != 0) {
|
||||
if (conversion == VertexStreamConversion::Repack) {
|
||||
stride = static_cast<Uint32>(attribByteSize);
|
||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||
}
|
||||
}
|
||||
const VkVertexInputRate inputRate =
|
||||
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
|
||||
|
||||
@@ -3546,9 +3546,11 @@ void main() {
|
||||
const auto& attr = vao.GetAttribute(bindingLocation);
|
||||
const SizeT elementSize =
|
||||
VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||
const SizeT sourceStride =
|
||||
attr.Stride > 0 ? static_cast<SizeT>(attr.Stride) : elementSize;
|
||||
if (sourceBufferShared->MappedData() == nullptr || elementSize == 0 || sourceStride == 0 ||
|
||||
// Zero is a legal binding stride and means "never advance" (see
|
||||
// VertexAttribute::Stride), so it is NOT folded into the element size here -
|
||||
// it selects the single-element conversion below instead.
|
||||
const SizeT sourceStride = static_cast<SizeT>(attr.Stride);
|
||||
if (sourceBufferShared->MappedData() == nullptr || elementSize == 0 ||
|
||||
baseOffset > sourceSize || elementSize > sourceSize - baseOffset) {
|
||||
MGLOG_E("UploadAndBindVertexStreams skipped: invalid converted source binding=%zu "
|
||||
"location=%u base=%zu size=%zu element=%zu stride=%zu",
|
||||
@@ -3557,7 +3559,8 @@ void main() {
|
||||
}
|
||||
|
||||
sourceBufferShared->SyncPersistentMappedRange();
|
||||
const SizeT availableElementCount = 1 + (sourceSize - baseOffset - elementSize) / sourceStride;
|
||||
const SizeT availableElementCount =
|
||||
sourceStride == 0 ? 1 : 1 + (sourceSize - baseOffset - elementSize) / sourceStride;
|
||||
const Bool cacheable = !sourceBufferShared->IsBackendPersistentMapped();
|
||||
// Convert only what this draw can fetch instead of the whole buffer tail.
|
||||
// Instance-rate bindings index by instance, not the vertex range, so they
|
||||
@@ -4737,6 +4740,7 @@ void main() {
|
||||
hasPatchedVertexAttributes = true;
|
||||
}
|
||||
VertexInputStateBuilder syntheticVertexInputBuilder;
|
||||
VkPipelineVertexInputStateCreateInfo syntheticVertexInputState{};
|
||||
const VkPipelineVertexInputStateCreateInfo* pipelineVertexInputState = &vis.state;
|
||||
if (missingAttribMask != 0 || hasPatchedVertexAttributes) {
|
||||
for (const auto& binding : vis.bindings) {
|
||||
@@ -4764,7 +4768,16 @@ void main() {
|
||||
syntheticVertexInputBuilder.AddAttribute(location, syntheticBinding, format, 0);
|
||||
++syntheticBinding;
|
||||
}
|
||||
pipelineVertexInputState = &syntheticVertexInputBuilder.Build();
|
||||
syntheticVertexInputState = syntheticVertexInputBuilder.Build();
|
||||
// Carry the divisor chain over. The synthetic rebuild copies bindings and
|
||||
// attributes only, and it keeps every real binding's INDEX, so the divisor
|
||||
// descriptions built for them stay valid - but dropping the pNext silently
|
||||
// demoted every instanced binding to divisor 1. This path runs whenever the
|
||||
// program declares an input the VAO does not feed (which is most capture
|
||||
// shaders: KHR-GL43.vertex_attrib_binding declares 16 inputs and enables three),
|
||||
// so the loss was near-total rather than a corner case.
|
||||
syntheticVertexInputState.pNext = vis.state.pNext;
|
||||
pipelineVertexInputState = &syntheticVertexInputState;
|
||||
}
|
||||
auto cullFaceEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace);
|
||||
auto depthTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest);
|
||||
@@ -5821,7 +5834,40 @@ void main() {
|
||||
m_lastLodParamsSum = 0; // filled below once the sampled set is known
|
||||
}
|
||||
}
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||
// GL's gl_BaseVertex is zero for every command without a baseVertex parameter, while
|
||||
// Vulkan's builtin reports the draw's firstVertex; a non-indexed draw therefore takes
|
||||
// the zeroed program variant. The question is about the program's SPIR-V, not about
|
||||
// this draw, so it is memoized on (program lifetime, backend-state version): only the
|
||||
// very first draw of a program pays the extra lookup, and a program used exclusively
|
||||
// with non-indexed draws never resolves - never compiles, never re-stamps - the
|
||||
// variant no draw of it would use.
|
||||
const Bool nonIndexedDraw = !(aspects & DrawSetupAspect::IndexBuffer);
|
||||
const Uint64 baseVertexProgramLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 baseVertexProgramVersion = program.GetBackendStateVersion();
|
||||
const Bool baseVertexQueryKnown = m_lastBaseVertexQueryValid &&
|
||||
m_lastBaseVertexProgramLifetimeId == baseVertexProgramLifetimeId &&
|
||||
m_lastBaseVertexProgramVersion == baseVertexProgramVersion;
|
||||
if (baseVertexQueryKnown && nonIndexedDraw && m_lastBaseVertexReads) {
|
||||
transformFlags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
|
||||
}
|
||||
const ProgramFactory::VkProgramObject* resolvedProgramObj =
|
||||
&m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||
if (!baseVertexQueryKnown) {
|
||||
// Read the answer out of the entry BEFORE any second lookup: that lookup may
|
||||
// insert and move every entry of the open-addressing cache, dangling the
|
||||
// reference. The zeroing pass leaves the variable declared, so the variant just
|
||||
// resolved answers the same as the base one either way.
|
||||
const Bool readsBaseVertex = resolvedProgramObj->readsBaseVertexBuiltin;
|
||||
m_lastBaseVertexQueryValid = true;
|
||||
m_lastBaseVertexProgramLifetimeId = baseVertexProgramLifetimeId;
|
||||
m_lastBaseVertexProgramVersion = baseVertexProgramVersion;
|
||||
m_lastBaseVertexReads = readsBaseVertex;
|
||||
if (nonIndexedDraw && readsBaseVertex) {
|
||||
transformFlags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
|
||||
resolvedProgramObj = &m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||
}
|
||||
}
|
||||
const auto& programObj = *resolvedProgramObj;
|
||||
// For the snapshot's memoised entry pointer: if anything below inserts into the
|
||||
// program cache (blit/aux program compiles), the epoch moves and the snapshot
|
||||
// stores no pointer for this draw - the fast path then re-looks-up once.
|
||||
|
||||
@@ -773,6 +773,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||
|
||||
// Does the current program's vertex stage declare the BaseVertex builtin? A property
|
||||
// of the program's SPIR-V, so (lifetime id, backend-state version) is the whole key.
|
||||
//
|
||||
// Memoized rather than re-asked because asking means resolving the UN-zeroed program
|
||||
// variant, and a program that only ever draws non-indexed would then compile a variant
|
||||
// no draw uses AND re-stamp its use every draw, so the idle sweep could never retire
|
||||
// it. With the memo the answer is known before the first lookup and only the variant
|
||||
// the draw actually needs is resolved.
|
||||
Bool m_lastBaseVertexQueryValid = false;
|
||||
Uint64 m_lastBaseVertexProgramLifetimeId = 0;
|
||||
Uint32 m_lastBaseVertexProgramVersion = 0;
|
||||
Bool m_lastBaseVertexReads = false;
|
||||
|
||||
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
|
||||
// render-pass caches are open-addressing maps whose entries move on
|
||||
// insert, so no pointers into them are cached; the pipeline handle is
|
||||
|
||||
@@ -1003,9 +1003,9 @@ DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenu
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindImageTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
|
||||
|
||||
@@ -620,6 +620,34 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1);
|
||||
}
|
||||
|
||||
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own, lower
|
||||
// one (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
|
||||
// The multisample TEXTURE path already resolves the limit per format
|
||||
// (GL_Texture.cpp, GetMaxTextureSamplesForFormat); renderbuffers only ever compared
|
||||
// against GL_MAX_SAMPLES, so on a driver where the two differ - Adreno reports
|
||||
// GL_MAX_SAMPLES 4 and GL_MAX_INTEGER_SAMPLES 1 - an integer renderbuffer accepted a
|
||||
// sample count the format cannot deliver, and said GL_NO_ERROR about it.
|
||||
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return std::numeric_limits<Int>::max();
|
||||
}
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
|
||||
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
|
||||
GLenum normalizedFormat = GL_RGBA;
|
||||
GLenum normalizedType = GL_UNSIGNED_BYTE;
|
||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(normalizedInternalFormat,
|
||||
PixelFormatNormalizeOptionBit::None,
|
||||
&normalizedInternalFormat, &normalizedFormat,
|
||||
&normalizedType);
|
||||
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
|
||||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
|
||||
if (!isIntegerFormat) {
|
||||
return GetMaxRenderbufferSamples_State();
|
||||
}
|
||||
return std::max(dynamicParameters.MaxIntegerSamples, 1);
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -641,7 +669,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferStorageSamples_State(GLsizei samples, const char* caller) {
|
||||
Bool ValidateRenderbufferStorageSamples_State(GLsizei samples, TextureInternalFormat format, const char* caller) {
|
||||
if (samples < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -649,9 +677,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Int maxSamples = GetMaxRenderbufferSamples_State();
|
||||
// TODO: Resolve the remaining per-internalformat renderbuffer sample limits once
|
||||
// glGetInternalformativ is backed; integer formats are handled below.
|
||||
const Int maxSamples = GetMaxRenderbufferSamplesForFormat_State(format);
|
||||
if (samples > maxSamples) {
|
||||
// TODO: Use per-internalformat renderbuffer sample limits once glGetInternalformativ is backed.
|
||||
// GL 4.6 core 9.2.4 makes asking for more samples than the format supports
|
||||
// INVALID_OPERATION, not INVALID_VALUE - the count is well formed, this format just
|
||||
// cannot deliver it. Only a negative count is INVALID_VALUE.
|
||||
@@ -659,7 +688,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
std::format("Sample count {} exceeds GL_MAX_SAMPLES ({}).", samples, maxSamples)));
|
||||
std::format("Sample count {} exceeds this format's sample limit ({}).", samples, maxSamples)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -684,7 +713,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
if (!TextureImpl::ValidateTextureInternalFormat(format)) return;
|
||||
|
||||
if (!ValidateRenderbufferStorageSamples_State(samples, kCaller)) return;
|
||||
if (!ValidateRenderbufferStorageSamples_State(samples, format, kCaller)) return;
|
||||
if (!ValidateRenderbufferStorageSize_State(width, height, kCaller)) return;
|
||||
|
||||
renderbufferObject->AllocateStorage({width, height});
|
||||
@@ -931,7 +960,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
if (!TextureImpl::ValidateTextureInternalFormat(format)) return;
|
||||
if (!ValidateRenderbufferStorageSamples_State(samples, "NamedRenderbufferStorageMultisample_State")) return;
|
||||
if (!ValidateRenderbufferStorageSamples_State(samples, format, "NamedRenderbufferStorageMultisample_State"))
|
||||
return;
|
||||
if (!ValidateRenderbufferStorageSize_State(width, height, "NamedRenderbufferStorageMultisample_State")) return;
|
||||
|
||||
renderbufferObject->AllocateStorage({width, height});
|
||||
|
||||
@@ -51,6 +51,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
||||
constexpr GLint kFrontendMaxVertexAtomicCounters = 0;
|
||||
// Zero counters means zero buffers to hold them. These have to be ANSWERED rather than
|
||||
// left to the default INVALID_ENUM: a well-behaved application queries the limit exactly
|
||||
// to find out that the stage cannot do this, and an error instead both leaves its output
|
||||
// untouched (so it reads uninitialised memory and may conclude the opposite) and leaves a
|
||||
// GL error pending that surfaces at whatever unrelated call checks next.
|
||||
constexpr GLint kFrontendMaxGeometryAtomicCounterBuffers = 0;
|
||||
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
|
||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
|
||||
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
|
||||
// One atomic counter is a uint, and a buffer never has to hold more counters than the
|
||||
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
|
||||
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
||||
@@ -174,6 +183,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return frontendCount;
|
||||
}
|
||||
|
||||
// A per-stage or combined BLOCK count is an amount of indexed binding points an
|
||||
// application will occupy, and GL 4.6 table 23.64 orders the two accordingly:
|
||||
// MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= every per-stage count,
|
||||
// and the same for the shader-storage family. The two families are answered from
|
||||
// unrelated places here - frontend constants, backend dynamic parameters, and a few
|
||||
// hard-coded TODOs - so nothing kept them ordered, and a backend that reports Vulkan
|
||||
// descriptor-indexing counts advertised 256 compute uniform blocks over 36 binding
|
||||
// points. KHR-GL44.multi_bind.dispatch_bind_buffers_base reads the block count and binds
|
||||
// that many buffers in ONE glBindBuffersBase, which is then INVALID_OPERATION before it
|
||||
// binds anything. Clamping is the only direction available: the binding count is the
|
||||
// capacity of the state layer's indexed-binding array, not a number we may inflate.
|
||||
GLint ClampBlockCountToBindingPoints(GLint blockCount, BufferTarget bufferTarget) {
|
||||
const GLint bindingPoints = static_cast<GLint>(GetIndexedBufferQueryPointCount(bufferTarget));
|
||||
return std::min(std::max(blockCount, 0), bindingPoints);
|
||||
}
|
||||
|
||||
GLint ClampUniformBlockCount(GLint blockCount) {
|
||||
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::Uniform);
|
||||
}
|
||||
|
||||
GLint ClampStorageBlockCount(GLint blockCount) {
|
||||
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
|
||||
}
|
||||
|
||||
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
|
||||
if (pname == GL_DRAW_BUFFER) {
|
||||
drawBufferIndex = 0;
|
||||
@@ -1397,7 +1430,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxCombinedAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
||||
*params = kFrontendMaxCombinedUniformBlocks;
|
||||
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_DUAL_SOURCE_DRAW_BUFFERS:
|
||||
*params = 1; // TODO
|
||||
@@ -1412,7 +1445,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxFragmentAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
|
||||
*params = kFrontendMaxFragmentInputComponents;
|
||||
@@ -1429,13 +1462,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxFragmentUniformVectors;
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_UNIFORM_BLOCKS:
|
||||
*params = kFrontendMaxFragmentUniformBlocks;
|
||||
*params = ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxGeometryAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxGeometryAtomicCounterBuffers;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
|
||||
*params = kFrontendMaxGeometryInputComponents;
|
||||
@@ -1458,7 +1494,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxGeometryTotalOutputComponents;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_UNIFORM_BLOCKS:
|
||||
*params = kFrontendMaxGeometryUniformBlocks;
|
||||
*params = ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
|
||||
*params = kFrontendMaxGeometryUniformComponents;
|
||||
@@ -1490,9 +1526,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxTessControlAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxTessControlAtomicCounterBuffers;
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxTessEvaluationAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxTessEvaluationAtomicCounterBuffers;
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS:
|
||||
*params = 0;
|
||||
return;
|
||||
@@ -1500,10 +1542,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = 0;
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
return;
|
||||
case GL_MAX_TEXTURE_LOD_BIAS:
|
||||
*params = 15; // TODO
|
||||
@@ -1520,13 +1562,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_VERTEX_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxVertexAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxVertexAtomicCounterBuffers;
|
||||
return;
|
||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||
*params = MG_Backend::pActiveBackendObject
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
||||
return;
|
||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
return;
|
||||
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
|
||||
*params = kFrontendMaxVertexUniformComponents;
|
||||
@@ -1538,7 +1583,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxVertexOutputComponents;
|
||||
return;
|
||||
case GL_MAX_VERTEX_UNIFORM_BLOCKS:
|
||||
*params = kFrontendMaxVertexUniformBlocks;
|
||||
*params = ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks);
|
||||
return;
|
||||
case GL_NUM_COMPRESSED_TEXTURE_FORMATS:
|
||||
*params = 0; // compressed texture upload entrypoints are still unimplemented
|
||||
@@ -1938,13 +1983,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.SubgroupQuadOperationsInAllStages ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
case GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS:
|
||||
*params = dynamicParameters.MaxComputeShaderStorageBlocks;
|
||||
*params = ClampStorageBlockCount(dynamicParameters.MaxComputeShaderStorageBlocks);
|
||||
break;
|
||||
case GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS:
|
||||
*params = dynamicParameters.MaxCombinedShaderStorageBlocks;
|
||||
*params = ClampStorageBlockCount(dynamicParameters.MaxCombinedShaderStorageBlocks);
|
||||
break;
|
||||
case GL_MAX_COMPUTE_UNIFORM_BLOCKS:
|
||||
*params = dynamicParameters.MaxComputeUniformBlocks;
|
||||
*params = ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks);
|
||||
break;
|
||||
case GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS:
|
||||
*params = dynamicParameters.MaxComputeTextureImageUnits;
|
||||
@@ -2074,7 +2119,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
||||
break;
|
||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
||||
*params = kFrontendMaxAtomicCounterBufferSize;
|
||||
// The conformance suite splits this evenly across every advertised binding point and
|
||||
// binds all of them in one glBindBuffersRange
|
||||
// (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
|
||||
// rejects with INVALID_VALUE before it binds anything. Floor the advertised size at
|
||||
// one counter per binding point.
|
||||
*params = std::max<GLint>(
|
||||
kFrontendMaxAtomicCounterBufferSize,
|
||||
static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint)));
|
||||
break;
|
||||
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
||||
*params = dynamicParameters.MaxTextureBufferSize;
|
||||
|
||||
@@ -850,7 +850,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// vector per column - while the value glGetUniform* must return is tightly packed
|
||||
// columns * rows floats. Only mat4 is the same either way; every other shape needs the
|
||||
// padding undone, and the readback has to undo exactly what UniformMatrixfv_Object put
|
||||
// there. Returns false when `ttype` is not a float matrix (nothing to unpack).
|
||||
// there. Returns false when there is nothing here to unpack.
|
||||
//
|
||||
// A DOUBLE matrix is declined not because it is laid out differently - it is not, the
|
||||
// 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
|
||||
// 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) {
|
||||
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
|
||||
const Int columns = ttype->getMatrixCols();
|
||||
@@ -909,7 +914,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
||||
Memcpy(params, pUBO + offset, size);
|
||||
// Never more than the uniform actually occupies. `size` is the GL type size,
|
||||
// which for a `double` uniform is twice its storage - every 64-bit float is
|
||||
// narrowed before the module reaches a backend, so the slot holds floats. The
|
||||
// typed entry points (glGetUniformdv and friends) go through
|
||||
// GetUniformScalar_State, which converts component by component; this raw
|
||||
// copy has no type to convert with, so it is bounded rather than converted.
|
||||
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
|
||||
}
|
||||
}
|
||||
// TODO: handle 1i variant as texture unit
|
||||
@@ -960,22 +971,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
||||
}
|
||||
|
||||
// A double-precision uniform is the one case where the stored component type can
|
||||
// differ from the queried one for a non-opaque uniform, and the difference is not
|
||||
// just a reinterpretation: it is twice as wide, so a raw copy would overrun the
|
||||
// caller's buffer as well as return nonsense. Read component by component and let
|
||||
// GL's conversion rules (7.6: round to nearest for the integer queries) apply.
|
||||
// A double-precision uniform is the one case where the stored component type differs
|
||||
// from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are
|
||||
// narrowed to 32 bits before the module reaches a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
|
||||
// component, laid out exactly like the float-typed twin of this uniform - std140
|
||||
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return
|
||||
// two components reinterpreted as one. Read component by component and let GL's
|
||||
// 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
|
||||
// stored it and not here.
|
||||
if (ttype->getBasicType() == glslang::EbtDouble) {
|
||||
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
|
||||
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
|
||||
: (ttype->isVector() ? ttype->getVectorSize() : 1);
|
||||
// The slot the linker handed out is exactly `columns` columns wide, so it also
|
||||
// states the column stride - which for a double matrix is not a float's 16 bytes.
|
||||
const SizeT columnStride = columns > 0 ? size / static_cast<SizeT>(columns) : size;
|
||||
// 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.
|
||||
const SizeT columnStride = 4 * sizeof(GLfloat);
|
||||
for (Int column = 0; column < columns; ++column) {
|
||||
for (Int row = 0; row < rows; ++row) {
|
||||
GLdouble component = 0.0;
|
||||
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLdouble),
|
||||
GLfloat component = 0.0f;
|
||||
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat),
|
||||
sizeof(component));
|
||||
if constexpr (std::is_integral_v<T>) {
|
||||
// Rounded to the nearest integer and clamped into the queried type's
|
||||
@@ -1248,36 +1264,39 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// glUniform*d / glUniformMatrix*dv. The vector forms need nothing beyond the shared
|
||||
// upload template - it is already typed on the component - but a matrix does: the
|
||||
// column stride the linker used for a double matrix is not the 16 bytes a float one
|
||||
// gets. It is not guessed here; the slot the uniform was given is exactly `columns`
|
||||
// columns wide, so dividing states the stride the rest of the pipeline agreed on.
|
||||
template <typename Program>
|
||||
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value, Int columns, Int rows) {
|
||||
const SizeT slotSize = programObject.GetUniformSizesInBytes(location);
|
||||
const SizeT columnStride = columns > 0 ? slotSize / static_cast<SizeT>(columns) : slotSize;
|
||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||
Vector<GLdouble> column(static_cast<SizeT>(rows));
|
||||
for (GLint matrix = 0; matrix < count; ++matrix) {
|
||||
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
|
||||
if (!programObject.IsValidUniformLocation(location + matrix)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + matrix, "the current program object");
|
||||
return;
|
||||
}
|
||||
const GLdouble* source = value + matrix * componentCount;
|
||||
for (Int c = 0; c < columns; ++c) {
|
||||
for (Int r = 0; r < rows; ++r) {
|
||||
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
|
||||
}
|
||||
Uniform_State<1>(programObject, location + matrix, column.data(), c * columnStride);
|
||||
for (Int r = 1; r < rows; ++r) {
|
||||
Uniform_State<1>(programObject, location + matrix, column.data() + r,
|
||||
c * columnStride + r * sizeof(GLdouble));
|
||||
}
|
||||
}
|
||||
// glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the
|
||||
// transpile chain narrows every 64-bit float in the shader to 32 bits
|
||||
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
|
||||
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
|
||||
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
|
||||
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
|
||||
// path is what keeps the two in step; a separate double-shaped layout here would write
|
||||
// 8-byte components into 4-byte slots and silently address the wrong ones.
|
||||
//
|
||||
// The narrowing is the same static_cast the shader's own arithmetic now performs, so the
|
||||
// value the shader reads is the value glUniform*d was given, at float precision.
|
||||
template <GLsizei ItemCount>
|
||||
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
|
||||
if (value == nullptr || count <= 0) {
|
||||
// Same shape as the float entry points: the location validation still runs, and a
|
||||
// null pointer is left to fault exactly where glUniform*fv would.
|
||||
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
|
||||
return;
|
||||
}
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
Uniformv_State<ItemCount>(location, count, narrowed.data());
|
||||
}
|
||||
|
||||
template <GLsizei ItemCount>
|
||||
void ProgramUniformvNarrowed_State(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
if (value == nullptr || count <= 0) {
|
||||
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
|
||||
return;
|
||||
}
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
|
||||
}
|
||||
|
||||
// glUniformMatrix*fv / glProgramUniformMatrix*fv, every shape (square and non-square).
|
||||
@@ -1326,6 +1345,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// glUniformMatrix*dv / glProgramUniformMatrix*dv. Narrowed to the float form and handed
|
||||
// straight to it: after DemoteFloat64Pass a `dmat4` uniform is a `mat4` in the shader and a
|
||||
// mat4-shaped slot in the global UBO, columns padded to a vec4 and all. Everything else
|
||||
// about the call - transpose handling, the array-element walk, the opaque-uniform refusal -
|
||||
// is then the one implementation both spellings share.
|
||||
template <typename Program>
|
||||
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value, Int columns, Int rows) {
|
||||
if (value == nullptr || count <= 0) return;
|
||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
UniformMatrixfv_Object(programObject, "glUniformMatrixdv", location, count, transpose, narrowed.data(),
|
||||
columns, rows, "the current program object");
|
||||
}
|
||||
|
||||
// Helper function to transpose a 2x2 matrix
|
||||
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
|
||||
// Input matrix is in column-major order (OpenGL default)
|
||||
@@ -2089,71 +2124,71 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void Uniform1d(GLint location, GLdouble v0) {
|
||||
const GLdouble v[] = {v0};
|
||||
Uniformv_State<1>(location, 1, v);
|
||||
UniformvNarrowed_State<1>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<1>(location, count, value);
|
||||
UniformvNarrowed_State<1>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0) {
|
||||
const GLdouble v[] = {v0};
|
||||
ProgramUniformv_State<1>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<1>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<1>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<1>(program, location, count, value);
|
||||
}
|
||||
void Uniform2d(GLint location, GLdouble v0, GLdouble v1) {
|
||||
const GLdouble v[] = {v0, v1};
|
||||
Uniformv_State<2>(location, 1, v);
|
||||
UniformvNarrowed_State<2>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform2dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<2>(location, count, value);
|
||||
UniformvNarrowed_State<2>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1) {
|
||||
const GLdouble v[] = {v0, v1};
|
||||
ProgramUniformv_State<2>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<2>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<2>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<2>(program, location, count, value);
|
||||
}
|
||||
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
|
||||
const GLdouble v[] = {v0, v1, v2};
|
||||
Uniformv_State<3>(location, 1, v);
|
||||
UniformvNarrowed_State<3>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform3dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<3>(location, count, value);
|
||||
UniformvNarrowed_State<3>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
|
||||
const GLdouble v[] = {v0, v1, v2};
|
||||
ProgramUniformv_State<3>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<3>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<3>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<3>(program, location, count, value);
|
||||
}
|
||||
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
|
||||
const GLdouble v[] = {v0, v1, v2, v3};
|
||||
Uniformv_State<4>(location, 1, v);
|
||||
UniformvNarrowed_State<4>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform4dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<4>(location, count, value);
|
||||
UniformvNarrowed_State<4>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
|
||||
const GLdouble v[] = {v0, v1, v2, v3};
|
||||
ProgramUniformv_State<4>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<4>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<4>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<4>(program, location, count, value);
|
||||
}
|
||||
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
@@ -210,11 +210,66 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
|
||||
// ---- model construction --------------------------------------------------------
|
||||
|
||||
// GL_REFERENCED_BY_*_SHADER for an ARRAYED block instance, refined per element.
|
||||
//
|
||||
// glslang records a block reference by walking up to the base symbol and calling
|
||||
// addBlockName with the whole ARRAY type, which ORs the referencing stage into every
|
||||
// element at once - it has not resolved the subscript yet at that point. So reading
|
||||
// "e[0].b" marks both TrickyBlock[0] and TrickyBlock[1] as referenced by the fragment
|
||||
// stage (KHR-GL43.program_interface_query.uniform-block-types).
|
||||
//
|
||||
// The MEMBER masks are exact: EShReflectionAllBlockVariables enumerates every member of
|
||||
// every element with the stage mask suppressed, and only the dereference chain actually
|
||||
// walked turns a bit on - and that chain carries the subscript. So the union of a block
|
||||
// instance's members is the reference set of that instance.
|
||||
//
|
||||
// Applied ONLY to arrayed instances, because for a scalar block glslang is already exact.
|
||||
// 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
|
||||
// back to the block's own mask there would restore the over-approximation.
|
||||
Vector<Uint32> BuildBlockStagesFromMembers(const glslang::TProgram& reflection, Int blockCount) {
|
||||
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
|
||||
const Int uniformCount = mutableReflection.getNumUniformVariables();
|
||||
for (Int index = 0; index < uniformCount; ++index) {
|
||||
const auto& uniform = mutableReflection.getUniform(index);
|
||||
const Int owner = uniform.index;
|
||||
if (owner < 0 || owner >= blockCount) continue;
|
||||
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
|
||||
}
|
||||
return stagesByBlock;
|
||||
}
|
||||
|
||||
// UNIFORM blocks only, and that scope is load-bearing rather than cautious. The member
|
||||
// names glslang produces for a uniform block array carry the subscript
|
||||
// ("TrickyBlock[0].b", via EShReflectionStrictArraySuffix), so each element's members are
|
||||
// distinct entries and the bits land on the right one. A SHADER STORAGE block array does
|
||||
// NOT get that treatment - its buffer variables reflect under one subscript-free spelling
|
||||
// shared by every element - so a union over them credits element 0 and starves the rest.
|
||||
// KHR-GL43.program_interface_query.ssb-types is the case that says so: it reads ss[0] and
|
||||
// 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
|
||||
// blocks therefore keep that mask untouched.
|
||||
Uint32 UniformBlockStages(const glslang::TObjectReflection& block, const Vector<Uint32>& stagesFromMembers,
|
||||
Int tIndex) {
|
||||
String arrayBase;
|
||||
Uint element = 0;
|
||||
Bool malformed = false;
|
||||
if (!SplitTrailingSubscript(block.name, arrayBase, element, malformed) || malformed) {
|
||||
return static_cast<Uint32>(block.stages);
|
||||
}
|
||||
if (tIndex < 0 || tIndex >= static_cast<Int>(stagesFromMembers.size())) {
|
||||
return static_cast<Uint32>(block.stages);
|
||||
}
|
||||
return stagesFromMembers[static_cast<SizeT>(tIndex)];
|
||||
}
|
||||
|
||||
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
||||
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
|
||||
blockKind.assign(blockCount, BlockKind::Uniform);
|
||||
blockInterfaceIndex.assign(blockCount, -1);
|
||||
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
|
||||
|
||||
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
||||
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex);
|
||||
@@ -260,8 +315,8 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
||||
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
||||
if (tIndex >= 0 && tIndex < blockCount) {
|
||||
resource.stages =
|
||||
static_cast<Uint32>(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex).stages);
|
||||
resource.stages = UniformBlockStages(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex),
|
||||
stagesFromMembers, tIndex);
|
||||
}
|
||||
model.uniformBlocks.push_back(Move(resource));
|
||||
}
|
||||
@@ -352,6 +407,17 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
}
|
||||
}
|
||||
|
||||
// A built-in interface block that a shader redeclares with fewer members keeps the
|
||||
// omitted ones in its type when the redeclaration is ANONYMOUS - glslang hides them
|
||||
// (basic type void) instead of erasing them, because the original shared declaration
|
||||
// has to stay usable. Only the instance-named form erases. So a separable vertex
|
||||
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
|
||||
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
|
||||
// are not part of its output interface.
|
||||
Bool IsHiddenBlockMember(const glslang::TType* type) {
|
||||
return type != nullptr && type->getBasicType() == glslang::EbtVoid;
|
||||
}
|
||||
|
||||
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
|
||||
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||
|
||||
@@ -359,6 +425,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
for (Int index = 0; index < inputCount; ++index) {
|
||||
const auto& refl = mutableReflection.getPipeInput(index);
|
||||
const glslang::TType* type = refl.getType();
|
||||
if (IsHiddenBlockMember(type)) continue;
|
||||
Resource resource;
|
||||
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
||||
// names; GL enumerates the GL spellings.
|
||||
@@ -373,18 +440,28 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
model.programInputs.push_back(Move(resource));
|
||||
}
|
||||
|
||||
// A color number, and therefore a color INDEX, exists only for a fragment stage's
|
||||
// outputs. The output interface belongs to the program's last stage, so for a
|
||||
// separable tessellation/geometry/vertex program these are varyings: asking the
|
||||
// frag-data maps about them can still answer a location (a tess-control output
|
||||
// carries its own layout(location=N)), and a location then manufactures a color
|
||||
// index of 0 where GL requires -1
|
||||
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
|
||||
const Bool lastStageIsFragment = mutableReflection.getIntermediate(EShLangFragment) != nullptr;
|
||||
const Int outputCount = mutableReflection.getNumPipeOutputs();
|
||||
for (Int index = 0; index < outputCount; ++index) {
|
||||
const auto& refl = mutableReflection.getPipeOutput(index);
|
||||
const glslang::TType* type = refl.getType();
|
||||
if (IsHiddenBlockMember(type)) continue;
|
||||
Resource resource;
|
||||
resource.name = WithArraySuffix(refl.name, type);
|
||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
||||
if (resource.location < 0) {
|
||||
// A built-in output (gl_FragDepth, gl_SampleMask) and a non-fragment stage
|
||||
// output both have no location, and therefore no color index either.
|
||||
if (resource.location < 0 || !lastStageIsFragment) {
|
||||
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
|
||||
// non-fragment stage's outputs have no color number at all - either way there
|
||||
// is no color index.
|
||||
resource.locationIndex = -1;
|
||||
} else {
|
||||
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "GL_Sampler.h"
|
||||
#include "Validators.h"
|
||||
#include "../Getter/GL_Getter.h"
|
||||
#include "../Texture/GL_Texture.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -269,15 +270,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The number of texture units a sampler may be bound to. GL 3.3 core 3.8.2 names
|
||||
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, which is what the backend advertises; the frontend's
|
||||
// MAX_TEXTURE_IMAGE_UNITS is only the capacity of the unit array, so it is a clamp on the
|
||||
// answer and never the answer itself - gating on it alone accepts every unit up to 192 no
|
||||
// matter what the driver reports.
|
||||
// The number of texture units a sampler may be bound to is the same count a TEXTURE may be
|
||||
// bound to - GL 3.3 core 3.8.2 names GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS for both - so it is
|
||||
// computed once, in GetCombinedTextureImageUnitCount, and named here for the sampler-side
|
||||
// readers below. Two copies of that arithmetic is how glBindSamplers and glBindTextures would
|
||||
// come to disagree about which units exist.
|
||||
static GLint GetSamplerBindableTextureUnitCount() {
|
||||
GLint maxTextureUnits = 0;
|
||||
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
|
||||
return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
return GetCombinedTextureImageUnitCount();
|
||||
}
|
||||
|
||||
void BindSampler_State(GLuint unit, GLuint sampler) {
|
||||
|
||||
@@ -4549,6 +4549,132 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit), changed);
|
||||
}
|
||||
|
||||
GLint GetCombinedTextureImageUnitCount() {
|
||||
GLint maxTextureUnits = 0;
|
||||
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
|
||||
return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// ARB_multi_bind checks the whole [first, first + count) range before binding anything and
|
||||
// reports an overrun as INVALID_OPERATION - not the INVALID_VALUE the single-bind entry
|
||||
// points report for an out-of-range unit, and not after binding the in-range prefix.
|
||||
Bool ValidateMultiBindUnitRange(GLuint first, GLsizei count, GLint unitCount, const char* funcName) {
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName, "count must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(unitCount)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
std::format("first + count ({} + {}) exceeds the {} available units.",
|
||||
first, count, unitCount)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ARB_multi_bind states the equivalence to a loop of single binds "except that <textures>
|
||||
// will not be created if they do not exist": glBindTexture instantiates a name GenTextures
|
||||
// merely reserved, the multi-bind entry points must refuse it. The error class is
|
||||
// INVALID_OPERATION for both of them, where the scalar glBindImageTexture reports
|
||||
// INVALID_VALUE - hence the check here rather than inside BindImageTexture.
|
||||
//
|
||||
// Deliberately PER ELEMENT: the extension defines these calls as a loop, so a bad entry
|
||||
// costs its own unit and leaves the rest of the range bound.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> ResolveMultiBindTexture(GLuint texture, GLsizei index,
|
||||
const char* funcName) {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> textureObject =
|
||||
MG_State::pGLContext->GetTextureObject(texture);
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", funcName,
|
||||
std::format("textures[{}] ({}) is not the name of an existing texture object.", index,
|
||||
texture)));
|
||||
}
|
||||
return textureObject;
|
||||
}
|
||||
|
||||
// ARB_multi_bind: an element naming texture zero unbinds EVERY target of its unit, i.e.
|
||||
// rebinds each target's default texture object - the unit's initial state. Same rule
|
||||
// glBindTextureUnit(unit, 0) follows.
|
||||
void UnbindAllTargetsOnUnit(Int unit) {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
Bool changed = false;
|
||||
for (auto& slot : textureUnit.GetAllBindingSlots()) {
|
||||
if (slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()))) changed = true;
|
||||
}
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(unit, changed);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ARB_multi_bind: glBindTextures binds each texture to ITS OWN target on unit <first> + i, so
|
||||
// there is no target parameter and no way to express it through glBindTexture - the per-unit,
|
||||
// by-object form glBindTextureUnit uses is the one that matches. A NULL <textures> unbinds the
|
||||
// whole range.
|
||||
void BindTextures(GLuint first, GLsizei count, const GLuint* textures) {
|
||||
if (!ValidateMultiBindUnitRange(first, count, GetCombinedTextureImageUnitCount(), __func__)) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
const GLuint texture = textures ? textures[i] : 0;
|
||||
const Int unit = static_cast<Int>(first) + i;
|
||||
if (texture == 0) {
|
||||
UnbindAllTargetsOnUnit(unit);
|
||||
continue;
|
||||
}
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> textureObject =
|
||||
ResolveMultiBindTexture(texture, i, __func__);
|
||||
if (!textureObject) continue;
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const Bool changed = textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject);
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(unit, changed);
|
||||
}
|
||||
}
|
||||
|
||||
// ARB_multi_bind: glBindImageTextures is a loop of glBindImageTexture with every parameter but
|
||||
// the unit and the texture fixed by the spec - level 0, layered, layer 0, READ_WRITE, and the
|
||||
// texture's own internal format. An element that names texture zero resets the unit.
|
||||
void BindImageTextures(GLuint first, GLsizei count, const GLuint* textures) {
|
||||
if (!ValidateMultiBindUnitRange(first, count, static_cast<GLint>(GetAdvertisedImageUnitCount()), __func__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
const GLuint texture = textures ? textures[i] : 0;
|
||||
const GLuint unit = first + static_cast<GLuint>(i);
|
||||
if (texture == 0) {
|
||||
BindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8);
|
||||
continue;
|
||||
}
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> textureObject =
|
||||
ResolveMultiBindTexture(texture, i, __func__);
|
||||
if (!textureObject) continue;
|
||||
|
||||
// "An INVALID_OPERATION error is generated if the internal format of any texture is not
|
||||
// supported for image textures" - a texture that has never been given storage has no
|
||||
// format at all and lands here too, rather than being reported as a bad enum by the
|
||||
// scalar path.
|
||||
const GLenum format = MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
|
||||
if (!IsValidImageTextureFormat(format)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("textures[{}] ({}) has an internal format that is not supported for image "
|
||||
"textures.",
|
||||
i, texture)));
|
||||
continue;
|
||||
}
|
||||
BindImageTexture(unit, texture, 0, GL_TRUE, 0, GL_READ_WRITE, format);
|
||||
}
|
||||
}
|
||||
|
||||
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!textureObject) return;
|
||||
|
||||
@@ -132,5 +132,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CompressedTexImage1D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
|
||||
GLsizei imageSize, const void* data);
|
||||
void BindTexture(GLenum target, GLuint texture);
|
||||
void BindTextures(GLuint first, GLsizei count, const GLuint* textures);
|
||||
void BindImageTextures(GLuint first, GLsizei count, const GLuint* textures);
|
||||
void ActiveTexture(GLenum texture);
|
||||
// The number of texture image units a texture or a sampler may be bound to: what the backend
|
||||
// advertises as GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, clamped by the frontend's fixed unit-array
|
||||
// capacity. Shared so the texture and sampler multi-bind range checks cannot drift apart.
|
||||
GLint GetCombinedTextureImageUnitCount();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -315,9 +315,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
auto offset = reinterpret_cast<SizeT>(pointer);
|
||||
|
||||
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false);
|
||||
const int effectiveStride = EffectiveVertexStride(stride, size, type);
|
||||
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false, effectiveStride);
|
||||
vao->BindAttributeBuffer(index, vbo);
|
||||
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, size, type));
|
||||
vao->MirrorPointerIntoBinding(index, vbo, offset, effectiveStride);
|
||||
}
|
||||
|
||||
void VertexAttribPointer_State(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,
|
||||
@@ -345,9 +346,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// backend can pick the reversed VkFormat / pass GL_BGRA through to a GLES driver.
|
||||
const bool isBgra = (size == static_cast<GLint>(GL_BGRA));
|
||||
const int effectiveSize = isBgra ? 4 : size;
|
||||
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra);
|
||||
const int effectiveStride = EffectiveVertexStride(stride, effectiveSize, type);
|
||||
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra,
|
||||
effectiveStride);
|
||||
vao->BindAttributeBuffer(index, vbo);
|
||||
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, effectiveSize, type));
|
||||
vao->MirrorPointerIntoBinding(index, vbo, offset, effectiveStride);
|
||||
}
|
||||
|
||||
void BindVertexArray_State(GLuint array) {
|
||||
|
||||
@@ -50,6 +50,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/CrossFrameBufferScenario.cpp
|
||||
Scenarios/ResidentIndexScenario.cpp
|
||||
Scenarios/MultiDrawScenario.cpp
|
||||
Scenarios/DrawParametersScenario.cpp
|
||||
Scenarios/AsyncCompileScenario.cpp
|
||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||
@@ -60,6 +61,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/SsboArrayLengthScenario.cpp
|
||||
Scenarios/DoublePrecisionScenario.cpp
|
||||
Scenarios/UniformInitializerScenario.cpp
|
||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||
Scenarios/ProgramPipelineScenario.cpp
|
||||
@@ -68,6 +70,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/Glsl420DeclarationScenario.cpp
|
||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||
Scenarios/BufferTextureScenario.cpp
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -94,6 +94,89 @@ namespace MGITest {
|
||||
}
|
||||
}
|
||||
|
||||
// A per-stage block count is an amount of BINDING POINTS an application will use, so it
|
||||
// can never exceed the number of binding points that exist. GL 4.6 Table 23.64 states the
|
||||
// relation the other way round (MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS
|
||||
// >= every per-stage count), and DirectVulkan broke it by clamping the two families
|
||||
// independently: a device reporting 256 compute uniform blocks and 84 uniform binding
|
||||
// points passes both ceilings and still cannot serve
|
||||
// KHR-GL44.multi_bind.dispatch_bind_buffers_base, which reads the block count and binds
|
||||
// that many buffers in one glBindBuffersBase - INVALID_OPERATION before a single bind.
|
||||
TEST_F(AdvertisedLimitsScenario, PerStageBlockCountsFitInTheirBindingPoints) {
|
||||
struct Relation {
|
||||
GLenum blocks;
|
||||
const char* blocksName;
|
||||
GLenum bindings;
|
||||
const char* bindingsName;
|
||||
};
|
||||
const Relation relations[] = {
|
||||
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
|
||||
{GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
|
||||
{GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
|
||||
{GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
|
||||
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS",
|
||||
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS"},
|
||||
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS",
|
||||
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS"},
|
||||
};
|
||||
for (const Relation& relation : relations) {
|
||||
GLint blocks = -1;
|
||||
GLint bindings = -1;
|
||||
glGetIntegerv(relation.blocks, &blocks);
|
||||
glGetIntegerv(relation.bindings, &bindings);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.blocksName;
|
||||
EXPECT_LE(blocks, bindings)
|
||||
<< relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = "
|
||||
<< bindings << "; a shader may declare more blocks than there are binding points to bind them to";
|
||||
}
|
||||
}
|
||||
|
||||
// KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's
|
||||
// binding points at MAX_<target>_SIZE / MAX_<target>_BINDINGS and binds all of them in
|
||||
// one glBindBuffersRange. That quotient has to be a legal BindBufferRange size, which
|
||||
// makes the two limits of every indexed family a PAIR: advertise a size that does not
|
||||
// survive division by the binding count and the call fails with INVALID_VALUE before any
|
||||
// of it binds.
|
||||
TEST_F(AdvertisedLimitsScenario, IndexedTargetSizeSurvivesDivisionByItsBindingCount) {
|
||||
struct IndexedFamily {
|
||||
GLenum maxSize;
|
||||
const char* maxSizeName;
|
||||
GLenum maxBindings;
|
||||
const char* maxBindingsName;
|
||||
GLint sizeGranularity; // BindBufferRange's size rule for the target
|
||||
};
|
||||
const IndexedFamily families[] = {
|
||||
{GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, "GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE",
|
||||
GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, "GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS", 1},
|
||||
{GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS, "GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS",
|
||||
GL_MAX_TRANSFORM_FEEDBACK_BUFFERS, "GL_MAX_TRANSFORM_FEEDBACK_BUFFERS", 4},
|
||||
{GL_MAX_UNIFORM_BLOCK_SIZE, "GL_MAX_UNIFORM_BLOCK_SIZE", GL_MAX_UNIFORM_BUFFER_BINDINGS,
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS", 1},
|
||||
{GL_MAX_SHADER_STORAGE_BLOCK_SIZE, "GL_MAX_SHADER_STORAGE_BLOCK_SIZE",
|
||||
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", 1},
|
||||
};
|
||||
for (const IndexedFamily& family : families) {
|
||||
GLint maxSize = -1;
|
||||
GLint maxBindings = -1;
|
||||
glGetIntegerv(family.maxSize, &maxSize);
|
||||
glGetIntegerv(family.maxBindings, &maxBindings);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << family.maxSizeName;
|
||||
ASSERT_GT(maxBindings, 0) << family.maxBindingsName;
|
||||
const GLint perBinding = maxSize / maxBindings;
|
||||
EXPECT_GT(perBinding, 0)
|
||||
<< family.maxSizeName << " (" << maxSize << ") / " << family.maxBindingsName << " ("
|
||||
<< maxBindings << ") is zero, and BindBufferRange rejects a zero size";
|
||||
EXPECT_EQ(perBinding % family.sizeGranularity, 0)
|
||||
<< family.maxSizeName << " (" << maxSize << ") / " << family.maxBindingsName << " ("
|
||||
<< maxBindings << ") = " << perBinding << " is not a multiple of the "
|
||||
<< family.sizeGranularity << "-byte size granularity BindBufferRange requires for it";
|
||||
}
|
||||
}
|
||||
|
||||
// The OOM case in isolation, because it is the one with a known CTS victim and the one a
|
||||
// future refactor is most likely to reintroduce by copying the Vulkan limit back.
|
||||
TEST_F(AdvertisedLimitsScenario, ComputeUniformBlocksIsAnAmountAnApplicationCouldActuallyAllocate) {
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.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 - GLSL DOUBLES, RUN AT SINGLE PRECISION.
|
||||
//
|
||||
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
|
||||
// Magma cannot build a module that declares the Float64 capability, and ESSL has no fp64 type
|
||||
// at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
|
||||
// profile") and the program never reaches the driver. MobileGL therefore narrows every 64-bit
|
||||
// float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than declining the
|
||||
// shader: `double` compiles and runs everywhere, at float precision.
|
||||
//
|
||||
// The narrowing is only half a contract. The other half is the API side: the global UBO is
|
||||
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the
|
||||
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now
|
||||
// std140-padded like any other matrix's. Every one of those is a byte offset that fails
|
||||
// silently - the uniform simply reads as something else - so the cases below set values
|
||||
// through the API and have the SHADER report what it saw.
|
||||
//
|
||||
// What is deliberately NOT asserted: that the values are exact to double precision. They are
|
||||
// not, and cannot be. Every expectation here is the float value of the double that was set,
|
||||
// which is the whole point.
|
||||
|
||||
#include <cmath>
|
||||
#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 {
|
||||
|
||||
// Doubles in every shape the demotion has to handle - a scalar, a vector, a matrix
|
||||
// whose column stride changes, an array whose element stride changes - all reported
|
||||
// through one float SSBO so a single readback says which one moved.
|
||||
constexpr const char* kComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform double uScalar;
|
||||
uniform dvec3 uVector;
|
||||
uniform dmat4 uMatrix;
|
||||
uniform double uArray[3];
|
||||
layout(std430, binding = 0) buffer Output {
|
||||
float g_out[];
|
||||
};
|
||||
void main() {
|
||||
g_out[0] = float(uScalar);
|
||||
g_out[1] = float(uVector.x);
|
||||
g_out[2] = float(uVector.y);
|
||||
g_out[3] = float(uVector.z);
|
||||
// Column-major [column][row]. Off-diagonal entries catch a column-stride mistake that a
|
||||
// diagonal-only check reads straight past.
|
||||
g_out[4] = float(uMatrix[0][0]);
|
||||
g_out[5] = float(uMatrix[0][3]);
|
||||
g_out[6] = float(uMatrix[3][0]);
|
||||
g_out[7] = float(uMatrix[3][3]);
|
||||
g_out[8] = float(uArray[0]);
|
||||
g_out[9] = float(uArray[1]);
|
||||
g_out[10] = float(uArray[2]);
|
||||
// Arithmetic on doubles, including an implicit float->double conversion and a literal
|
||||
// with the fp64 suffix: this is what an application actually writes, and it is the part
|
||||
// that has to survive the conversion folding.
|
||||
double accumulated = uScalar * 2.0lf + 1.5;
|
||||
g_out[11] = float(accumulated);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr int kOutputSlots = 12;
|
||||
|
||||
class DoublePrecisionScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_program = CompileComputeProgram(kComputeSource);
|
||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||
|
||||
glGenBuffers(1, &m_output);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
const std::vector<float> zeroes(kOutputSlots, 0.0f);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, kOutputSlots * sizeof(float), zeroes.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_output);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_output != 0) glDeleteBuffers(1, &m_output);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::vector<float> Dispatch() {
|
||||
glUseProgram(m_program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<float> values(kOutputSlots, -1.0f);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kOutputSlots * sizeof(float), values.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
return values;
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
unsigned int m_output = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
TEST_F(DoublePrecisionScenario, ADoubleUniformReachesTheShaderAtFloatPrecision) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
|
||||
ASSERT_GE(scalar, 0);
|
||||
// 0.1 has no exact float (or double) representation, so this only passes if the
|
||||
// value really travelled through the demoted slot rather than being read out of
|
||||
// some other four bytes.
|
||||
glUniform1d(scalar, 0.1);
|
||||
glUseProgram(0);
|
||||
|
||||
const std::vector<float> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_FLOAT_EQ(values[0], static_cast<float>(0.1));
|
||||
EXPECT_FLOAT_EQ(values[11], static_cast<float>(static_cast<float>(0.1) * 2.0f + 1.5f))
|
||||
<< "arithmetic on the demoted value, including the folded fp64 literal";
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, EveryDoubleShapeLandsInItsOwnSlot) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
|
||||
const GLint vector = glGetUniformLocation(m_program, "uVector");
|
||||
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
|
||||
const GLint array0 = glGetUniformLocation(m_program, "uArray[0]");
|
||||
const GLint array2 = glGetUniformLocation(m_program, "uArray[2]");
|
||||
ASSERT_GE(scalar, 0);
|
||||
ASSERT_GE(vector, 0);
|
||||
ASSERT_GE(matrix, 0);
|
||||
ASSERT_GE(array0, 0);
|
||||
ASSERT_GE(array2, 0);
|
||||
|
||||
glUniform1d(scalar, 5.0);
|
||||
const GLdouble vectorValue[3] = {11.0, 12.0, 13.0};
|
||||
glUniform3dv(vector, 1, vectorValue);
|
||||
// Column-major, and every entry distinct so a transposed or mis-strided write
|
||||
// cannot land on a value that happens to match.
|
||||
GLdouble matrixValue[16] = {};
|
||||
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
|
||||
glUniformMatrix4dv(matrix, 1, GL_FALSE, matrixValue);
|
||||
const GLdouble arrayValue[3] = {71.0, 72.0, 73.0};
|
||||
glUniform1dv(array0, 3, arrayValue);
|
||||
glUseProgram(0);
|
||||
|
||||
const std::vector<float> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_FLOAT_EQ(values[0], 5.0f) << "scalar double";
|
||||
EXPECT_FLOAT_EQ(values[1], 11.0f) << "dvec3 .x";
|
||||
EXPECT_FLOAT_EQ(values[2], 12.0f) << "dvec3 .y";
|
||||
EXPECT_FLOAT_EQ(values[3], 13.0f) << "dvec3 .z";
|
||||
EXPECT_FLOAT_EQ(values[4], 100.0f) << "dmat4 [0][0]";
|
||||
EXPECT_FLOAT_EQ(values[5], 103.0f) << "dmat4 [0][3] - within the first column";
|
||||
EXPECT_FLOAT_EQ(values[6], 112.0f) << "dmat4 [3][0] - column stride";
|
||||
EXPECT_FLOAT_EQ(values[7], 115.0f) << "dmat4 [3][3]";
|
||||
EXPECT_FLOAT_EQ(values[8], 71.0f) << "double array element 0";
|
||||
EXPECT_FLOAT_EQ(values[9], 72.0f) << "double array element 1 - element stride";
|
||||
EXPECT_FLOAT_EQ(values[10], 73.0f) << "double array element 2";
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheTransposeFlagStillTransposes) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
|
||||
ASSERT_GE(matrix, 0);
|
||||
GLdouble matrixValue[16] = {};
|
||||
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
|
||||
glUniformMatrix4dv(matrix, 1, GL_TRUE, matrixValue);
|
||||
glUseProgram(0);
|
||||
|
||||
const std::vector<float> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
// Transposed, so [column][row] now reads the source's [row][column].
|
||||
EXPECT_FLOAT_EQ(values[4], 100.0f) << "dmat4 [0][0] is on the diagonal either way";
|
||||
EXPECT_FLOAT_EQ(values[5], 112.0f) << "dmat4 [0][3] after transpose";
|
||||
EXPECT_FLOAT_EQ(values[6], 103.0f) << "dmat4 [3][0] after transpose";
|
||||
EXPECT_FLOAT_EQ(values[7], 115.0f) << "dmat4 [3][3] is on the diagonal either way";
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheUniformIsStillReportedAsADouble) {
|
||||
if (!Ready()) return;
|
||||
// The demotion is an implementation detail of how the value is STORED. What the
|
||||
// shader source declared is what the application asked about, so the reflection
|
||||
// keeps answering GL_DOUBLE* - an application that switches on the type and calls
|
||||
// glUniform*d has to keep working, and it is the glUniform*d path that is correct
|
||||
// for these uniforms.
|
||||
struct Expectation {
|
||||
const char* name;
|
||||
GLenum type;
|
||||
GLint size;
|
||||
};
|
||||
const Expectation expectations[] = {
|
||||
{"uScalar", GL_DOUBLE, 1},
|
||||
{"uVector", GL_DOUBLE_VEC3, 1},
|
||||
{"uMatrix", GL_DOUBLE_MAT4, 1},
|
||||
{"uArray[0]", GL_DOUBLE, 3},
|
||||
};
|
||||
|
||||
GLint activeUniforms = 0;
|
||||
glGetProgramiv(m_program, GL_ACTIVE_UNIFORMS, &activeUniforms);
|
||||
ASSERT_GT(activeUniforms, 0);
|
||||
|
||||
for (const Expectation& expectation : expectations) {
|
||||
bool found = false;
|
||||
for (GLint index = 0; index < activeUniforms; ++index) {
|
||||
char name[128] = {};
|
||||
GLsizei length = 0;
|
||||
GLint size = 0;
|
||||
GLenum type = 0;
|
||||
glGetActiveUniform(m_program, static_cast<GLuint>(index), sizeof(name) - 1, &length, &size,
|
||||
&type, name);
|
||||
if (std::string(name, static_cast<size_t>(length)) != expectation.name) continue;
|
||||
found = true;
|
||||
EXPECT_EQ(type, expectation.type) << expectation.name;
|
||||
EXPECT_EQ(size, expectation.size) << expectation.name;
|
||||
break;
|
||||
}
|
||||
EXPECT_TRUE(found) << "glGetActiveUniform never reported " << expectation.name;
|
||||
}
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, GetUniformdvReadsBackWhatWasStored) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
|
||||
const GLint vector = glGetUniformLocation(m_program, "uVector");
|
||||
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
|
||||
ASSERT_GE(scalar, 0);
|
||||
ASSERT_GE(vector, 0);
|
||||
ASSERT_GE(matrix, 0);
|
||||
glUniform1d(scalar, 0.1);
|
||||
const GLdouble vectorValue[3] = {11.5, 12.5, 13.5};
|
||||
glUniform3dv(vector, 1, vectorValue);
|
||||
GLdouble matrixValue[16] = {};
|
||||
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
|
||||
glUniformMatrix4dv(matrix, 1, GL_FALSE, matrixValue);
|
||||
glUseProgram(0);
|
||||
|
||||
// The readback has to undo exactly what the write did - the same std140 column
|
||||
// padding, the same 4-byte components - or a dmat4 comes back with its columns
|
||||
// shifted and nothing else in the API would say so.
|
||||
GLdouble readScalar = 0.0;
|
||||
glGetUniformdv(m_program, scalar, &readScalar);
|
||||
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
|
||||
<< "the value is what a float can hold, not the double that was passed in";
|
||||
|
||||
GLdouble readVector[3] = {};
|
||||
glGetUniformdv(m_program, vector, readVector);
|
||||
EXPECT_DOUBLE_EQ(readVector[0], 11.5);
|
||||
EXPECT_DOUBLE_EQ(readVector[1], 12.5);
|
||||
EXPECT_DOUBLE_EQ(readVector[2], 13.5);
|
||||
|
||||
GLdouble readMatrix[16] = {};
|
||||
glGetUniformdv(m_program, matrix, readMatrix);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
|
||||
}
|
||||
|
||||
// The float query sees the same storage through the type it is actually stored as.
|
||||
GLfloat readFloat = 0.0f;
|
||||
glGetUniformfv(m_program, scalar, &readFloat);
|
||||
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, ADoubleUniformKeepsItsDeclaredInitializer) {
|
||||
if (!Ready()) return;
|
||||
// A declared initializer is seeded straight into the uniform shadow at link, and the
|
||||
// seeding used to skip 64-bit floats outright ("no 32-bit shadow encoding") - which
|
||||
// was true before the demotion and silently left every such uniform reading zero.
|
||||
const char* source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform double uSeeded = 2.5lf;
|
||||
uniform dvec3 uSeededVector = dvec3(4.0lf, 5.0lf, 6.0lf);
|
||||
layout(std430, binding = 0) buffer Output {
|
||||
float g_out[];
|
||||
};
|
||||
void main() {
|
||||
g_out[0] = float(uSeeded);
|
||||
g_out[1] = float(uSeededVector.x);
|
||||
g_out[2] = float(uSeededVector.y);
|
||||
g_out[3] = float(uSeededVector.z);
|
||||
}
|
||||
)";
|
||||
const GLuint program = CompileComputeProgram(source);
|
||||
ASSERT_NE(program, 0u) << m_buildLog;
|
||||
|
||||
glUseProgram(program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<float> values(4, -1.0f);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, 4 * sizeof(float), values.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
|
||||
EXPECT_FLOAT_EQ(values[0], 2.5f) << "scalar double initializer";
|
||||
EXPECT_FLOAT_EQ(values[1], 4.0f) << "dvec3 initializer .x";
|
||||
EXPECT_FLOAT_EQ(values[2], 5.0f) << "dvec3 initializer .y";
|
||||
EXPECT_FLOAT_EQ(values[3], 6.0f) << "dvec3 initializer .z";
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheFp64ExtensionIsNotAdvertised) {
|
||||
if (!Ready()) return;
|
||||
// The shader above compiled, linked and ran without the extension string, which is
|
||||
// the point: an application does not need GL_ARB_gpu_shader_fp64 advertised to USE
|
||||
// doubles here. What the string additionally promises is 64-bit precision, and that
|
||||
// is the one thing the demotion cannot deliver - so it stays off unless
|
||||
// MOBILEGL_ADVERTISE_FP64 asks for it, and an application that branches on the
|
||||
// string keeps taking its float path.
|
||||
GLint extensionCount = 0;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
|
||||
ASSERT_GT(extensionCount, 0);
|
||||
bool advertised = false;
|
||||
for (GLint i = 0; i < extensionCount; ++i) {
|
||||
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
|
||||
if (name != nullptr && std::string(name) == "GL_ARB_gpu_shader_fp64") advertised = true;
|
||||
}
|
||||
EXPECT_FALSE(advertised);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) {
|
||||
if (!Ready()) return;
|
||||
// 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
|
||||
// ones whose device lacks shaderFloat64. Declined loudly rather than accepted and
|
||||
// drawn as garbage; the matching POST row says the same thing at startup.
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
while (glGetError() != GL_NO_ERROR) {}
|
||||
|
||||
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
while (glGetError() != GL_NO_ERROR) {}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,348 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DrawParametersScenario.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
|
||||
//
|
||||
// gl_BaseVertex / gl_BaseInstance / gl_DrawID (GL_ARB_shader_draw_parameters),
|
||||
// read straight out of the shader that a draw command produced.
|
||||
//
|
||||
// Neither backend has these builtins for free, and each is wrong in its own way
|
||||
// when nobody watches:
|
||||
//
|
||||
// * DirectVulkan HAS a BaseVertex builtin, but Vulkan's carries the draw's
|
||||
// firstVertex on a NON-INDEXED draw where GL's is defined to be zero ("the
|
||||
// value passed to the baseVertex parameter, or zero for a command with no
|
||||
// such parameter"). Only the indexed meaning of the two agrees. Every
|
||||
// DrawArrays form therefore takes the ZeroBaseVertex program variant.
|
||||
// * DirectGLES has no such builtins at all: ESSL knows none of them, so the
|
||||
// transpiler demotes each one to a uniform the draw paths feed. A uniform
|
||||
// nobody writes keeps whatever the previous draw left in it - which is what
|
||||
// made gl_BaseVertex report a stale base vertex, and what made
|
||||
// gl_BaseInstance read an unbound storage buffer on a plain glDrawArrays.
|
||||
//
|
||||
// The shader paints the three values, so a draw that carries the wrong ones
|
||||
// paints the wrong colour rather than merely disagreeing with an expectation
|
||||
// somewhere. The framebuffer is cleared to WHITE and no case expects 255 in any
|
||||
// channel, so "the draw did not happen" can never be mistaken for a pass.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// #version 450: glslang only declares the ARB builtins from 440 up.
|
||||
//
|
||||
// Each value is painted at 8 units per count, not 1: the errors these builtins
|
||||
// actually have are OFF BY ONE (a sub-draw that never got its own gl_DrawID reads
|
||||
// the previous one's, a base vertex that arrives one command late), and at one unit
|
||||
// per count no readback tolerance can tell those from rounding.
|
||||
//
|
||||
// And biased by two counts, so that ZERO is not the clamp floor. Five of these cases
|
||||
// expect zero, and an unbiased encoding would let every negative value - the shape a
|
||||
// sign or rebase mistake produces - clamp to the same black and pass.
|
||||
constexpr const char* kVertexSource = R"(#version 450 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
layout(location = 0) in vec2 aPos;
|
||||
flat out vec3 vParams;
|
||||
void main() {
|
||||
vParams = (vec3(gl_BaseVertexARB, gl_BaseInstanceARB, gl_DrawIDARB) * 8.0 + 16.0) / 255.0;
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFragmentSource = R"(#version 450 core
|
||||
flat in vec3 vParams;
|
||||
out vec4 oColor;
|
||||
void main() {
|
||||
oColor = vec4(vParams, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
struct Vertex {
|
||||
float x, y;
|
||||
};
|
||||
|
||||
// 3 dummy vertices, then the left half of the viewport as two triangles,
|
||||
// then the right half. Nothing here is symmetric by accident:
|
||||
//
|
||||
// * the padding makes a draw that ignores `first` / baseVertex paint a
|
||||
// degenerate triangle (i.e. nothing) instead of the right picture;
|
||||
// * the two halves let one multi-draw show TWO different gl_DrawID
|
||||
// values in one readback.
|
||||
//
|
||||
// Indices 3..14 together cover the whole viewport, which is what the
|
||||
// single-draw cases use.
|
||||
constexpr int kPad = 3;
|
||||
constexpr int kLeftFirst = kPad; // 3
|
||||
constexpr int kRightFirst = kPad + 6; // 9
|
||||
constexpr int kHalfCount = 6;
|
||||
|
||||
std::vector<Vertex> SceneVertices() {
|
||||
std::vector<Vertex> vertices(static_cast<std::size_t>(kPad), Vertex{0.0f, 0.0f});
|
||||
const float bounds[2][2] = {{-1.0f, 0.0f}, {0.0f, 1.0f}};
|
||||
for (const auto& half : bounds) {
|
||||
const float x0 = half[0];
|
||||
const float x1 = half[1];
|
||||
vertices.push_back({x0, -1.0f});
|
||||
vertices.push_back({x1, -1.0f});
|
||||
vertices.push_back({x1, 1.0f});
|
||||
vertices.push_back({x0, -1.0f});
|
||||
vertices.push_back({x1, 1.0f});
|
||||
vertices.push_back({x0, 1.0f});
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
// GL's DrawArraysIndirectCommand / DrawElementsIndirectCommand, spelled out
|
||||
// so a test can write one without depending on a GL header's struct.
|
||||
struct ArraysCommand {
|
||||
std::uint32_t count, instanceCount, first, baseInstance;
|
||||
};
|
||||
struct ElementsCommand {
|
||||
std::uint32_t count, instanceCount, firstIndex;
|
||||
std::int32_t baseVertex;
|
||||
std::uint32_t baseInstance;
|
||||
};
|
||||
|
||||
class DrawParametersScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
|
||||
const std::vector<Vertex> vertices = SceneVertices();
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertices.size() * sizeof(Vertex)),
|
||||
vertices.data(), GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<const void*>(0));
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
for (GLuint* buffer : {&m_ebo, &m_indirect, &m_parameter, &m_vbo}) {
|
||||
if (*buffer != 0) glDeleteBuffers(1, buffer);
|
||||
*buffer = 0;
|
||||
}
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void FillBuffer(GLuint& name, GLenum target, const std::vector<T>& data) {
|
||||
if (name == 0) glGenBuffers(1, &name);
|
||||
glBindBuffer(target, name);
|
||||
glBufferData(target, static_cast<GLsizeiptr>(data.size() * sizeof(T)), data.data(), GL_STATIC_DRAW);
|
||||
}
|
||||
|
||||
// Clears to white, runs `draw` and reads the frame back.
|
||||
template <typename DrawFn>
|
||||
Image Render(DrawFn&& draw) {
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
||||
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
draw();
|
||||
return ReadPixels(HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
||||
}
|
||||
|
||||
// The three builtins as the shader saw them, at a point in one half of
|
||||
// the viewport. `half` is 0 for the left half and 1 for the right.
|
||||
struct DrawParams {
|
||||
int baseVertex = -1, baseInstance = -1, drawId = -1;
|
||||
};
|
||||
// Decodes the biased 8-units-per-count encoding back to the integer the
|
||||
// shader saw. Rounding to the nearest step absorbs any UNORM slop; adjacent
|
||||
// values stay eight units apart, so an off-by-one still reads as one, and a
|
||||
// negative value lands below the bias and decodes negative rather than
|
||||
// clamping into a legitimate zero.
|
||||
static DrawParams ParamsAt(const Image& image, int half) {
|
||||
const int x = image.Width() * (1 + 2 * half) / 4;
|
||||
const Rgba8 pixel = image.At(x, image.Height() / 2);
|
||||
const auto decode = [](std::uint8_t channel) {
|
||||
return (static_cast<int>(channel) - 16 + 4) / 8;
|
||||
};
|
||||
return {decode(pixel.r), decode(pixel.g), decode(pixel.b)};
|
||||
}
|
||||
|
||||
static void ExpectParams(const Image& image, int half, const DrawParams& expected,
|
||||
const std::string& what) {
|
||||
const DrawParams actual = ParamsAt(image, half);
|
||||
EXPECT_EQ(actual.baseVertex, expected.baseVertex)
|
||||
<< what << ": gl_BaseVertex (half " << half << ")";
|
||||
EXPECT_EQ(actual.baseInstance, expected.baseInstance)
|
||||
<< what << ": gl_BaseInstance (half " << half << ")";
|
||||
EXPECT_EQ(actual.drawId, expected.drawId) << what << ": gl_DrawID (half " << half << ")";
|
||||
}
|
||||
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_ebo = 0;
|
||||
GLuint m_indirect = 0;
|
||||
GLuint m_parameter = 0;
|
||||
};
|
||||
|
||||
// ---- the non-indexed forms: gl_BaseVertex is zero, `first` or not ----
|
||||
|
||||
// Vulkan's BaseVertex would answer 3 here (the draw's firstVertex); GL's
|
||||
// must answer 0, because glDrawArrays has no baseVertex parameter at all.
|
||||
TEST_F(DrawParametersScenario, DrawArraysReportsAZeroBaseVertexDespiteItsFirst) {
|
||||
if (!Ready()) return;
|
||||
const Image image = Render([&] { glDrawArrays(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount); });
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 0, 0}, "glDrawArrays(first=3)");
|
||||
ExpectParams(image, 1, {0, 0, 0}, "glDrawArrays(first=3)");
|
||||
}
|
||||
|
||||
TEST_F(DrawParametersScenario, DrawArraysInstancedBaseInstanceReportsItsBaseInstance) {
|
||||
if (!Ready()) return;
|
||||
const Image image = Render([&] {
|
||||
glDrawArraysInstancedBaseInstance(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount, 1, 5);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 5, 0}, "glDrawArraysInstancedBaseInstance(baseInstance=5)");
|
||||
}
|
||||
|
||||
// The base instance of one draw must not survive into the next one. This is
|
||||
// the shape that broke on DirectGLES: the emulation uniform is per-program
|
||||
// state, so a draw that never writes it inherits the last writer's value.
|
||||
TEST_F(DrawParametersScenario, APlainDrawAfterABaseInstancedOneSeesZeroAgain) {
|
||||
if (!Ready()) return;
|
||||
const Image image = Render([&] {
|
||||
glDrawArraysInstancedBaseInstance(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount, 1, 7);
|
||||
glDrawArrays(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 0, 0}, "plain glDrawArrays after a base-instanced draw");
|
||||
}
|
||||
|
||||
// ---- the indexed forms: gl_BaseVertex IS the base vertex ----
|
||||
|
||||
TEST_F(DrawParametersScenario, DrawElementsBaseVertexReportsItsBaseVertex) {
|
||||
if (!Ready()) return;
|
||||
std::vector<std::uint32_t> indices;
|
||||
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i);
|
||||
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
|
||||
|
||||
const Image image = Render([&] {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(0), kLeftFirst);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {kLeftFirst, 0, 0}, "glDrawElementsBaseVertex(basevertex=3)");
|
||||
ExpectParams(image, 1, {kLeftFirst, 0, 0}, "glDrawElementsBaseVertex(basevertex=3)");
|
||||
}
|
||||
|
||||
// ... and is zero again for the command that has none, including after one
|
||||
// that did: the same leak the base instance has, on the other builtin. The
|
||||
// preceding draw MUST carry a non-zero base vertex or this case proves nothing -
|
||||
// one index run reaches the geometry through the base vertex, the second through
|
||||
// its own indices, so the two draws paint the same picture with different
|
||||
// gl_BaseVertex and only the second one's value survives in the framebuffer.
|
||||
TEST_F(DrawParametersScenario, DrawElementsAfterABaseVertexDrawReportsZeroAgain) {
|
||||
if (!Ready()) return;
|
||||
std::vector<std::uint32_t> indices;
|
||||
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i);
|
||||
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i + kLeftFirst);
|
||||
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
|
||||
const auto rebasedRun = reinterpret_cast<const void*>(2 * kHalfCount * sizeof(std::uint32_t));
|
||||
|
||||
const Image image = Render([&] {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(0), kLeftFirst);
|
||||
glDrawElements(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT, rebasedRun);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 0, 0}, "glDrawElements after a base-vertex draw");
|
||||
ExpectParams(image, 1, {0, 0, 0}, "glDrawElements after a base-vertex draw");
|
||||
}
|
||||
|
||||
// ---- the multi-draw forms: one gl_DrawID per sub-draw ----
|
||||
|
||||
TEST_F(DrawParametersScenario, MultiDrawArraysNumbersItsSubDraws) {
|
||||
if (!Ready()) return;
|
||||
const GLint firsts[2] = {kLeftFirst, kRightFirst};
|
||||
const GLsizei counts[2] = {kHalfCount, kHalfCount};
|
||||
|
||||
const Image image = Render([&] { glMultiDrawArrays(GL_TRIANGLES, firsts, counts, 2); });
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 0, 0}, "glMultiDrawArrays sub-draw 0");
|
||||
ExpectParams(image, 1, {0, 0, 1}, "glMultiDrawArrays sub-draw 1");
|
||||
}
|
||||
|
||||
// Every field of an indexed indirect command at once: its own gl_DrawID, the
|
||||
// baseVertex word (which the CPU reads out of the command) and the
|
||||
// baseInstance word (which DirectGLES reads through a storage-buffer view of
|
||||
// the very same buffer).
|
||||
TEST_F(DrawParametersScenario, MultiDrawElementsIndirectCarriesEveryCommandsParameters) {
|
||||
if (!Ready()) return;
|
||||
std::vector<std::uint32_t> indices;
|
||||
for (std::uint32_t i = 0; i < kHalfCount; ++i) indices.push_back(i);
|
||||
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
|
||||
|
||||
const std::vector<ElementsCommand> commands = {
|
||||
{kHalfCount, 1, 0, kLeftFirst, 0},
|
||||
{kHalfCount, 1, 0, kRightFirst, 4},
|
||||
};
|
||||
FillBuffer(m_indirect, GL_DRAW_INDIRECT_BUFFER, commands);
|
||||
|
||||
const Image image = Render([&] {
|
||||
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, reinterpret_cast<const void*>(0), 2,
|
||||
sizeof(ElementsCommand));
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {kLeftFirst, 0, 0}, "indirect command 0");
|
||||
ExpectParams(image, 1, {kRightFirst, 4, 1}, "indirect command 1");
|
||||
}
|
||||
|
||||
// glMultiDrawArraysIndirectCount was missing from the DirectGLES backend
|
||||
// table entirely, so the frontend answered INVALID_OPERATION for every call
|
||||
// while GL_ARB_indirect_parameters was advertised. The parameter buffer here
|
||||
// holds a count SMALLER than maxdrawcount, so a path that ignores it draws a
|
||||
// third command over the top of the second and changes the right half.
|
||||
TEST_F(DrawParametersScenario, MultiDrawArraysIndirectCountObeysItsParameterBuffer) {
|
||||
if (!Ready()) return;
|
||||
const std::vector<ArraysCommand> commands = {
|
||||
{kHalfCount, 1, kLeftFirst, 0},
|
||||
{kHalfCount, 1, kRightFirst, 6},
|
||||
{kHalfCount, 1, kRightFirst, 9},
|
||||
};
|
||||
FillBuffer(m_indirect, GL_DRAW_INDIRECT_BUFFER, commands);
|
||||
const std::vector<std::uint32_t> parameters = {2};
|
||||
FillBuffer(m_parameter, GL_PARAMETER_BUFFER, parameters);
|
||||
|
||||
const Image image = Render([&] {
|
||||
glMultiDrawArraysIndirectCount(GL_TRIANGLES, reinterpret_cast<const void*>(0), 0, 3,
|
||||
sizeof(ArraysCommand));
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 0, 0}, "counted indirect command 0");
|
||||
ExpectParams(image, 1, {0, 6, 1}, "counted indirect command 1");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,674 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/VertexAttribBindingScenario.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
|
||||
//
|
||||
// ARB_vertex_attrib_binding: the separate format/binding state the GL 4.3 vertex
|
||||
// input model is made of, read back out of the draw that consumed it.
|
||||
//
|
||||
// Every scenario here captures the vertex shader's inputs with transform feedback
|
||||
// under GL_RASTERIZER_DISCARD, which is what the KHR-GL43.vertex_attrib_binding
|
||||
// cases do: the captured record IS the fetched vertex, so "the binding state did
|
||||
// not reach the draw" and "the draw fetched the wrong bytes" are distinguishable
|
||||
// from each other and from "the capture did not run" (the buffer is pre-filled
|
||||
// with a poison value).
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr float kPoison = -1234.0f;
|
||||
|
||||
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// A vertex-only capture program, exactly how the CTS builds one: the varying
|
||||
// names are declared before the link and the fragment stage is absent because
|
||||
// the draw runs under GL_RASTERIZER_DISCARD.
|
||||
GLuint BuildCaptureProgram(const std::string& vertexSource, const std::vector<const char*>& xfbVaryings,
|
||||
std::string* log) {
|
||||
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
|
||||
if (vertexShader == 0) return 0;
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
if (!xfbVaryings.empty()) {
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(xfbVaryings.size()), xfbVaryings.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vertexShader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// Four float inputs at locations 0..3, captured as four vec4s per vertex.
|
||||
// Locations the test does not feed keep their current-attribute value, which
|
||||
// every scenario sets to a known constant first.
|
||||
std::string CaptureVertexSource() {
|
||||
return R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_attrib0;
|
||||
layout(location = 1) in vec4 vs_in_attrib1;
|
||||
layout(location = 2) in vec4 vs_in_attrib2;
|
||||
layout(location = 3) in vec4 vs_in_attrib3;
|
||||
out StageData {
|
||||
vec4 attrib0;
|
||||
vec4 attrib1;
|
||||
vec4 attrib2;
|
||||
vec4 attrib3;
|
||||
} vs_out;
|
||||
void main() {
|
||||
vs_out.attrib0 = vs_in_attrib0;
|
||||
vs_out.attrib1 = vs_in_attrib1;
|
||||
vs_out.attrib2 = vs_in_attrib2;
|
||||
vs_out.attrib3 = vs_in_attrib3;
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
std::vector<const char*> CaptureVaryingNames() {
|
||||
return {"StageData.attrib0", "StageData.attrib1", "StageData.attrib2", "StageData.attrib3"};
|
||||
}
|
||||
|
||||
// Runs `vertexCount` x `instanceCount` points through the capture program and
|
||||
// returns the interleaved floats (16 per point: four vec4s).
|
||||
std::vector<float> CapturePoints(GLuint program, GLuint xfbBuffer, int vertexCount, int instanceCount) {
|
||||
const std::size_t floats = static_cast<std::size_t>(vertexCount) * instanceCount * 16;
|
||||
std::vector<float> poison(floats, kPoison);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(floats * sizeof(float)), poison.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(program);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArraysInstanced(GL_POINTS, 0, vertexCount, instanceCount);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> data(floats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(floats * sizeof(float)),
|
||||
data.data());
|
||||
glUseProgram(0);
|
||||
return data;
|
||||
}
|
||||
|
||||
// As CapturePoints, but through the baseInstance entry point, and on a capture buffer
|
||||
// of its own.
|
||||
//
|
||||
// Kept separate from CapturePoints rather than defaulting a parameter, for two
|
||||
// reasons. Every existing caller stays on the draw command that carries no
|
||||
// baseInstance at all, so the negative control is a DIFFERENT command rather than
|
||||
// the same one passed a zero. And baseInstance is the first thing here that needs
|
||||
// several captures in ONE test, which the shared helper cannot currently do: a
|
||||
// second capture into the same buffer object comes back empty on DirectVulkan
|
||||
// (respecifying a buffer that is bound to a transform-feedback binding point does
|
||||
// not reach that binding - reproduced with two plain CapturePoints calls, so it is
|
||||
// neither about baseInstance nor about this helper). A fresh buffer per capture
|
||||
// sidesteps it; without that, this scenario would be pinning that bug instead.
|
||||
std::vector<float> CaptureOwnBufferBaseInstance(GLuint program, int vertexCount, int instanceCount,
|
||||
GLuint baseInstance, bool useBaseInstanceCommand) {
|
||||
const std::size_t floats = static_cast<std::size_t>(vertexCount) * instanceCount * 16;
|
||||
std::vector<float> poison(floats, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(floats * sizeof(float)), poison.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(program);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
if (useBaseInstanceCommand) {
|
||||
glDrawArraysInstancedBaseInstance(GL_POINTS, 0, vertexCount, instanceCount, baseInstance);
|
||||
} else {
|
||||
glDrawArraysInstanced(GL_POINTS, 0, vertexCount, instanceCount);
|
||||
}
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> data(floats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(floats * sizeof(float)),
|
||||
data.data());
|
||||
glUseProgram(0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
return data;
|
||||
}
|
||||
|
||||
// point p, attribute a, component c
|
||||
float At(const std::vector<float>& data, int point, int attrib, int component) {
|
||||
const std::size_t index = static_cast<std::size_t>(point) * 16 + attrib * 4 + component;
|
||||
return index < data.size() ? data[index] : kPoison;
|
||||
}
|
||||
|
||||
void ResetCurrentAttribs() {
|
||||
for (GLuint i = 0; i < 4; ++i) {
|
||||
glVertexAttrib4f(i, 0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
::testing::AssertionResult Vec4Is(const std::vector<float>& data, int point, int attrib, float x, float y,
|
||||
float z, float w) {
|
||||
const float gx = At(data, point, attrib, 0);
|
||||
const float gy = At(data, point, attrib, 1);
|
||||
const float gz = At(data, point, attrib, 2);
|
||||
const float gw = At(data, point, attrib, 3);
|
||||
const float tolerance = 0.01f;
|
||||
auto close = [tolerance](float a, float b) { return (a - b) < tolerance && (b - a) < tolerance; };
|
||||
if (close(gx, x) && close(gy, y) && close(gz, z) && close(gw, w)) {
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
return ::testing::AssertionFailure()
|
||||
<< "point " << point << " attribute " << attrib << " is (" << gx << ", " << gy << ", " << gz << ", "
|
||||
<< gw << "), expected (" << x << ", " << y << ", " << z << ", " << w << ")";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class VertexAttribBindingScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_program = BuildCaptureProgram(CaptureVertexSource(), CaptureVaryingNames(), &m_log);
|
||||
ASSERT_NE(m_program, 0u) << "capture program did not link: " << m_log;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenBuffers(1, &m_xfbo);
|
||||
glBindVertexArray(m_vao);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindVertexArray(0);
|
||||
glDeleteVertexArrays(1, &m_vao);
|
||||
glDeleteBuffers(1, &m_xfbo);
|
||||
glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_xfbo = 0;
|
||||
std::string m_log;
|
||||
};
|
||||
|
||||
// glVertexAttribFormat + glBindVertexBuffer + glVertexAttribBinding, in the order
|
||||
// the CTS uses (buffer first, then format, then binding), must feed the draw.
|
||||
TEST_F(VertexAttribBindingScenario, FormatAndBindingFeedTheDraw) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
ResetCurrentAttribs();
|
||||
|
||||
const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
|
||||
GLuint vbo = 0;
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
glBindVertexBuffer(0, vbo, 0, 12);
|
||||
glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
glVertexAttribBinding(1, 0);
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 1, 1.0f, 2.0f, 3.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 1, 1, 4.0f, 5.0f, 6.0f, 1.0f));
|
||||
// An attribute nothing configured still reports its current value.
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 0, 0.0f, 0.0f, 0.0f, 0.0f));
|
||||
|
||||
glDisableVertexAttribArray(1);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
}
|
||||
|
||||
// The reverse order - format and binding declared before any buffer exists on the
|
||||
// binding point - has to resolve to the same thing once glBindVertexBuffer lands.
|
||||
TEST_F(VertexAttribBindingScenario, FormatBeforeBufferStillResolves) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
ResetCurrentAttribs();
|
||||
|
||||
const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
|
||||
GLuint vbo = 0;
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
glVertexAttribBinding(2, 3);
|
||||
glVertexAttribFormat(2, 2, GL_FLOAT, GL_FALSE, 4);
|
||||
glEnableVertexAttribArray(2);
|
||||
glBindVertexBuffer(3, vbo, 0, 12);
|
||||
|
||||
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 2, 2.0f, 3.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 1, 2, 5.0f, 6.0f, 0.0f, 1.0f));
|
||||
|
||||
glDisableVertexAttribArray(2);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.3.1: a binding point's stride is the byte distance between
|
||||
// consecutive elements, and zero means every vertex reads the SAME element. That
|
||||
// is the opposite of glVertexAttribPointer's stride 0, which means "tightly
|
||||
// packed" - the two spellings must not be collapsed into one another.
|
||||
TEST_F(VertexAttribBindingScenario, BindingStrideZeroRepeatsOneElement) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
ResetCurrentAttribs();
|
||||
|
||||
const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f};
|
||||
GLuint vbo = 0;
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
glVertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, 0);
|
||||
glVertexAttribBinding(0, 5);
|
||||
glBindVertexBuffer(5, vbo, 16, 0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 0, 5.0f, 6.0f, 7.0f, 8.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 1, 0, 5.0f, 6.0f, 7.0f, 8.0f));
|
||||
|
||||
glDisableVertexAttribArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
}
|
||||
|
||||
// The pointer API keeps its own meaning of stride 0 (tightly packed) even though
|
||||
// it is defined in terms of the binding model - the negative control for the
|
||||
// scenario above.
|
||||
TEST_F(VertexAttribBindingScenario, PointerStrideZeroStaysTightlyPacked) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
ResetCurrentAttribs();
|
||||
|
||||
const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f};
|
||||
GLuint vbo = 0;
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
|
||||
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 0, 1.0f, 2.0f, 3.0f, 4.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 1, 0, 5.0f, 6.0f, 7.0f, 8.0f));
|
||||
|
||||
glDisableVertexAttribArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
}
|
||||
|
||||
// glVertexBindingDivisor is per BINDING POINT: it has to reach every attribute
|
||||
// pointed at that binding, and the instance step must honour the divisor rather
|
||||
// than advancing once per instance.
|
||||
TEST_F(VertexAttribBindingScenario, BindingDivisorAppliesToEveryAttributeOnThePoint) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
ResetCurrentAttribs();
|
||||
|
||||
const float vertices[] = {10.0f, 20.0f, 30.0f, 40.0f};
|
||||
GLuint vbo = 0;
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0);
|
||||
glVertexAttribFormat(1, 1, GL_FLOAT, GL_FALSE, 4);
|
||||
glVertexAttribBinding(0, 4);
|
||||
glVertexAttribBinding(1, 4);
|
||||
glBindVertexBuffer(4, vbo, 0, 8);
|
||||
glVertexBindingDivisor(4, 2);
|
||||
glEnableVertexAttribArray(0);
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
// The divisor is per binding point, so it has to be visible on BOTH attributes
|
||||
// pointed at it - and this query is what separates "the frontend never resolved
|
||||
// it" from "the backend did not apply it".
|
||||
GLint divisor = -1;
|
||||
glGetVertexAttribiv(0, GL_VERTEX_ATTRIB_ARRAY_DIVISOR, &divisor);
|
||||
EXPECT_EQ(divisor, 2);
|
||||
divisor = -1;
|
||||
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_DIVISOR, &divisor);
|
||||
EXPECT_EQ(divisor, 2);
|
||||
|
||||
// 1 vertex x 4 instances, divisor 2: instances 0,1 read element 0 and
|
||||
// instances 2,3 read element 1.
|
||||
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 1, 4);
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 1, 0, 10.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 2, 0, 30.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 3, 0, 30.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 1, 20.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 2, 1, 40.0f, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
glDisableVertexAttribArray(0);
|
||||
glDisableVertexAttribArray(1);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
}
|
||||
|
||||
// baseInstance moves the ELEMENT the instanced arrays start at. DirectGLES has no
|
||||
// ES entry point that says so on the drivers we ship against (GL_EXT_base_instance
|
||||
// is absent on Adreno), so it folds the shift into the attribute's own offset - and
|
||||
// the thing that made this worth pinning is that the value used to reach the shader
|
||||
// uniform for gl_BaseInstance and NEVER the fetch, so a draw could report a base
|
||||
// instance it had not actually read from.
|
||||
//
|
||||
// The three draws are the point. Zero first as a negative control, so a backend that
|
||||
// simply ignored baseInstance could not pass on the middle draw alone; and zero AGAIN
|
||||
// last, because the shift is emitted into per-attribute state the VAO twin memoises -
|
||||
// leaving it applied would make every subsequent ordinary draw fetch from the wrong
|
||||
// element, which is a far worse bug than the one being fixed.
|
||||
TEST_F(VertexAttribBindingScenario, BaseInstanceMovesTheInstancedArraysStartElement) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
ResetCurrentAttribs();
|
||||
|
||||
const float instanceData[] = {10.0f, 20.0f, 30.0f, 40.0f};
|
||||
GLuint vbo = 0;
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(instanceData), instanceData, GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0);
|
||||
glVertexAttribBinding(0, 0);
|
||||
glBindVertexBuffer(0, vbo, 0, 4);
|
||||
glVertexBindingDivisor(0, 1);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
const std::vector<float> atZero = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, true);
|
||||
EXPECT_TRUE(Vec4Is(atZero, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(atZero, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
const std::vector<float> atTwo = CaptureOwnBufferBaseInstance(m_program, 1, 2, 2, true);
|
||||
EXPECT_TRUE(Vec4Is(atTwo, 0, 0, 30.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(atTwo, 1, 0, 40.0f, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
// Nothing about the vertex array changed between these two draws, so only a
|
||||
// backend that actively un-shifts on a baseInstance change gets back to 10/20.
|
||||
const std::vector<float> backToZero = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, true);
|
||||
EXPECT_TRUE(Vec4Is(backToZero, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(backToZero, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
// And a draw command with no baseInstance parameter at all must be unaffected by
|
||||
// the one that came before it.
|
||||
const std::vector<float> plain = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, false);
|
||||
EXPECT_TRUE(Vec4Is(plain, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(plain, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
glDisableVertexAttribArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
}
|
||||
|
||||
// baseInstance is defined against the instanced arrays only: an array with divisor 0
|
||||
// advances per VERTEX and its start element is "first", which baseInstance does not
|
||||
// touch. An emulation that shifted by offset without checking the divisor would move
|
||||
// this one too, and nothing in the case above would notice.
|
||||
TEST_F(VertexAttribBindingScenario, BaseInstanceLeavesPerVertexArraysWhereTheyWere) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
ResetCurrentAttribs();
|
||||
|
||||
const float perVertex[] = {1.0f, 2.0f, 3.0f, 4.0f};
|
||||
const float perInstance[] = {10.0f, 20.0f, 30.0f, 40.0f};
|
||||
GLuint buffers[2] = {0, 0};
|
||||
glGenBuffers(2, buffers);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(perVertex), perVertex, GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, buffers[1]);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(perInstance), perInstance, GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0);
|
||||
glVertexAttribBinding(0, 0);
|
||||
glBindVertexBuffer(0, buffers[0], 0, 4);
|
||||
glVertexBindingDivisor(0, 0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
glVertexAttribFormat(1, 1, GL_FLOAT, GL_FALSE, 0);
|
||||
glVertexAttribBinding(1, 1);
|
||||
glBindVertexBuffer(1, buffers[1], 0, 4);
|
||||
glVertexBindingDivisor(1, 1);
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
// 2 vertices x 2 instances, baseInstance 2. Points come out instance-major.
|
||||
const std::vector<float> data = CaptureOwnBufferBaseInstance(m_program, 2, 2, 2, true);
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 1, 0, 2.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 2, 0, 1.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 3, 0, 2.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 1, 30.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 1, 1, 30.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 2, 1, 40.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 3, 1, 40.0f, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
glDisableVertexAttribArray(0);
|
||||
glDisableVertexAttribArray(1);
|
||||
glDeleteBuffers(2, buffers);
|
||||
}
|
||||
|
||||
// Two attributes on one binding point at different relative offsets, plus a
|
||||
// binding offset: the fetch address is binding offset + relative offset, and the
|
||||
// relative offset must not leak into the binding's own offset.
|
||||
TEST_F(VertexAttribBindingScenario, RelativeOffsetComposesWithBindingOffset) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
ResetCurrentAttribs();
|
||||
|
||||
const float vertices[] = {0.0f, 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
|
||||
GLuint vbo = 0;
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0);
|
||||
glVertexAttribFormat(1, 1, GL_FLOAT, GL_FALSE, 8);
|
||||
glVertexAttribBinding(0, 1);
|
||||
glVertexAttribBinding(1, 1);
|
||||
glBindVertexBuffer(1, vbo, 8, 12);
|
||||
glEnableVertexAttribArray(0);
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 0, 1.0f, 2.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 0, 1, 3.0f, 0.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 1, 0, 4.0f, 5.0f, 0.0f, 1.0f));
|
||||
EXPECT_TRUE(Vec4Is(data, 1, 1, 6.0f, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
glDisableVertexAttribArray(0);
|
||||
glDisableVertexAttribArray(1);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
}
|
||||
|
||||
// The KHR-GL43.vertex_attrib_binding.basic-input* capture program verbatim: a
|
||||
// 16-element vec4 input ARRAY at location 0, copied element by element into a
|
||||
// 16-element array inside an output interface block, all 16 members captured.
|
||||
// Every one of the 17 basic-input* cases is built on it, so a backend that cannot
|
||||
// produce this program fails all of them with "the draw captured zeros" and no
|
||||
// other symptom.
|
||||
TEST_F(VertexAttribBindingScenario, InputArrayCaptureProgramFeedsTheDraw) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::string vs = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_attrib[16];
|
||||
out StageData {
|
||||
vec4 attrib[16];
|
||||
} vs_out;
|
||||
void main() {
|
||||
for (int i = 0; i < vs_in_attrib.length(); ++i) {
|
||||
vs_out.attrib[i] = vs_in_attrib[i];
|
||||
}
|
||||
}
|
||||
)";
|
||||
std::vector<std::string> names;
|
||||
for (int i = 0; i < 16; ++i) names.push_back("StageData.attrib[" + std::to_string(i) + "]");
|
||||
std::vector<const char*> varyings;
|
||||
for (const auto& n : names) varyings.push_back(n.c_str());
|
||||
|
||||
std::string log;
|
||||
const GLuint program = BuildCaptureProgram(vs, varyings, &log);
|
||||
ASSERT_NE(program, 0u) << "capture program did not link: " << log;
|
||||
|
||||
for (GLuint i = 0; i < 16; ++i) glVertexAttrib4f(i, 0.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
|
||||
GLuint vbo = 0;
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
glBindVertexBuffer(0, vbo, 0, 12);
|
||||
glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
glVertexAttribBinding(1, 0);
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
// 16 vec4s per point rather than the 4 the shared helper assumes.
|
||||
constexpr std::size_t kFloatsPerPoint = 64;
|
||||
std::vector<float> poison(kFloatsPerPoint * 2, kPoison);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_xfbo);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
|
||||
poison.data(), GL_DYNAMIC_DRAW);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(program);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 2);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> data(poison.size(), kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(data.size() * sizeof(float)), data.data());
|
||||
glUseProgram(0);
|
||||
|
||||
// Element 0 of the array has no enabled array behind it, so it must deliver the
|
||||
// current generic attribute value set above - including its w, which is 0 here and
|
||||
// NOT the 1 an unwritten vec4 input defaults to.
|
||||
EXPECT_FLOAT_EQ(data[0], 0.0f);
|
||||
EXPECT_FLOAT_EQ(data[3], 0.0f);
|
||||
|
||||
// attribute 1 of point 0 and of point 1.
|
||||
EXPECT_FLOAT_EQ(data[4], 1.0f);
|
||||
EXPECT_FLOAT_EQ(data[5], 2.0f);
|
||||
EXPECT_FLOAT_EQ(data[6], 3.0f);
|
||||
EXPECT_FLOAT_EQ(data[7], 1.0f);
|
||||
EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 4], 4.0f);
|
||||
EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 5], 5.0f);
|
||||
EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 6], 6.0f);
|
||||
EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 7], 1.0f);
|
||||
|
||||
glDisableVertexAttribArray(1);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
// Same program, but every one of the 16 elements is asked for a DIFFERENT current value.
|
||||
//
|
||||
// An input array occupies one location per element (GL 4.6 core 11.1.1), so `in vec4 a[16]`
|
||||
// at location 0 is active on 0..15 - and the whole location span is what a backend reads to
|
||||
// decide which attributes need their current value pushed. Reflection used to record the
|
||||
// span of the ELEMENT type only, so a 16-element array claimed exactly one location: every
|
||||
// element above the first silently read the (0,0,0,1) an unwritten input defaults to instead
|
||||
// of the value glVertexAttrib4f had set. The test above could not see it, because the only
|
||||
// element it reads a current value from is element 0 - the one location the array did claim.
|
||||
TEST_F(VertexAttribBindingScenario, EveryInputArrayElementGetsItsOwnCurrentValue) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::string vs = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_attrib[16];
|
||||
out StageData {
|
||||
vec4 attrib[16];
|
||||
} vs_out;
|
||||
void main() {
|
||||
for (int i = 0; i < vs_in_attrib.length(); ++i) {
|
||||
vs_out.attrib[i] = vs_in_attrib[i];
|
||||
}
|
||||
}
|
||||
)";
|
||||
std::vector<std::string> names;
|
||||
for (int i = 0; i < 16; ++i) names.push_back("StageData.attrib[" + std::to_string(i) + "]");
|
||||
std::vector<const char*> varyings;
|
||||
for (const auto& n : names) varyings.push_back(n.c_str());
|
||||
|
||||
std::string log;
|
||||
const GLuint program = BuildCaptureProgram(vs, varyings, &log);
|
||||
ASSERT_NE(program, 0u) << "capture program did not link: " << log;
|
||||
|
||||
// Distinct in every component, and never (0,0,0,1): the value an element that was
|
||||
// skipped would report has to be distinguishable from every value that was asked for.
|
||||
for (GLuint i = 0; i < 16; ++i) {
|
||||
const float base = static_cast<float>(i) + 1.0f;
|
||||
glVertexAttrib4f(i, base, base + 100.0f, base + 200.0f, base + 300.0f);
|
||||
}
|
||||
|
||||
constexpr std::size_t kFloatsPerPoint = 64;
|
||||
std::vector<float> poison(kFloatsPerPoint, kPoison);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_xfbo);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
|
||||
poison.data(), GL_DYNAMIC_DRAW);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(program);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> data(poison.size(), kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(data.size() * sizeof(float)), data.data());
|
||||
glUseProgram(0);
|
||||
|
||||
for (int element = 0; element < 16; ++element) {
|
||||
const float base = static_cast<float>(element) + 1.0f;
|
||||
EXPECT_FLOAT_EQ(data[element * 4 + 0], base) << "element " << element;
|
||||
EXPECT_FLOAT_EQ(data[element * 4 + 1], base + 100.0f) << "element " << element;
|
||||
EXPECT_FLOAT_EQ(data[element * 4 + 2], base + 200.0f) << "element " << element;
|
||||
EXPECT_FLOAT_EQ(data[element * 4 + 3], base + 300.0f) << "element " << element;
|
||||
}
|
||||
|
||||
for (GLuint i = 0; i < 16; ++i) glVertexAttrib4f(i, 0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -68,6 +68,8 @@ namespace {
|
||||
return type && type->getQualifier().builtIn != glslang::EbvNone;
|
||||
}
|
||||
|
||||
// Locations one ELEMENT of a vertex input occupies (GL 4.6 core 11.1.1): a matrix
|
||||
// takes one per column, everything else this backend can feed takes one.
|
||||
static int GetVertexInputLocationSpan(GLenum glType) {
|
||||
switch (glType) {
|
||||
case GL_FLOAT_MAT2:
|
||||
@@ -87,6 +89,26 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// How many elements an ARRAY vertex input has. glslang reflects such an input as ONE
|
||||
// record spelled "name[0]" carrying the ELEMENT's glDefineType and the array length,
|
||||
// so the type alone cannot say how many locations the declaration covers: GL 4.6 core
|
||||
// 11.1.1 gives an array one location per element (times the element's own span), and
|
||||
// `in vec4 a[16]` at location 0 therefore occupies 0..15, not 0. Missing that left
|
||||
// every location above the base with no recorded name or type, which is what the
|
||||
// backends read to decide whether an attribute is active at all.
|
||||
static MobileGL::Int GetVertexInputArrayElements(const glslang::TObjectReflection& input) {
|
||||
const glslang::TType* type = input.getType();
|
||||
if (type == nullptr || !type->isArray()) return 1;
|
||||
// An unsized input array has no span to compute; treat it as one element rather
|
||||
// than guessing, so it can only ever under-claim locations.
|
||||
if (!type->isSizedArray()) return 1;
|
||||
return std::max(1, type->getCumulativeArraySize());
|
||||
}
|
||||
|
||||
static MobileGL::Int GetVertexInputTotalLocationSpan(const glslang::TObjectReflection& input) {
|
||||
return GetVertexInputLocationSpan(input.glDefineType) * GetVertexInputArrayElements(input);
|
||||
}
|
||||
|
||||
static GLenum GetVertexInputLocationType(GLenum glType) {
|
||||
switch (glType) {
|
||||
case GL_FLOAT_MAT2:
|
||||
@@ -563,8 +585,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
// - SharedStd140UBO: a DECLARED uniform block is active even when no member is
|
||||
// ever read (reflected from the linker objects). PreprocessShaderSource coerces
|
||||
// every block to std140, so this covers all of them.
|
||||
// - IntermediateIO: GL_PROGRAM_INPUT is the input interface of the program's FIRST
|
||||
// stage and GL_PROGRAM_OUTPUT the output interface of its LAST one. Without this
|
||||
// glslang hardcodes those boundaries to vertex/fragment, so a separable program
|
||||
// made of one non-vertex stage has an empty input interface and one made of a
|
||||
// non-fragment stage an empty output interface
|
||||
// (KHR-GL43.program_interface_query.separate-programs-*).
|
||||
// - UnwrapIOBlocks: an inter-stage interface block enumerates as its MEMBERS -
|
||||
// "Color.r", and "gl_Position" for an anonymous gl_PerVertex - not as the block
|
||||
// instance. Only reachable through IntermediateIO: a vertex stage's inputs and a
|
||||
// fragment stage's outputs can never be blocks, so this is inert for a program
|
||||
// whose boundary stages are the hardcoded ones.
|
||||
if (!artifacts.program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix |
|
||||
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO)) {
|
||||
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO |
|
||||
EShReflectionIntermediateIO | EShReflectionUnwrapIOBlocks)) {
|
||||
artifacts.linkStatus = false;
|
||||
artifacts.infoLog = "Build reflection failed.";
|
||||
DeferLog(std::format("ProgramObject {}: DoReflection - buildReflection() returned false",
|
||||
@@ -852,14 +886,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
// ------------ attributes (vertex in) ---------------
|
||||
Int inCount = artifacts.program->getNumPipeInputs();
|
||||
// The pipe-input list is the input interface of the program's FIRST stage, which is only
|
||||
// the vertex attribute set when the program actually HAS a vertex stage. A separable
|
||||
// fragment/geometry/tessellation program reflects its own stage inputs here, and those are
|
||||
// varyings - registering them as vertex attributes would hand glGetActiveAttrib and the
|
||||
// attribute location table interstage varyings.
|
||||
Int inCount = artifacts.program->getIntermediate(EShLangVertex) != nullptr
|
||||
? artifacts.program->getNumPipeInputs()
|
||||
: 0;
|
||||
MGLOG_D("ProgramObject %u: Reflection - pipe input count (attributes) = %d", in.externalIndex, inCount);
|
||||
|
||||
Int maxLoc = -1;
|
||||
for (int i = 0; i < inCount; ++i) {
|
||||
Int loc = (Int)artifacts.program->getPipeInput(i).layoutLocation();
|
||||
if (loc >= 0 && loc != glslang::TQualifier::layoutLocationEnd) {
|
||||
const Int locationSpan = GetVertexInputLocationSpan(artifacts.program->getPipeInput(i).glDefineType);
|
||||
const Int locationSpan = GetVertexInputTotalLocationSpan(artifacts.program->getPipeInput(i));
|
||||
maxLoc = std::max(maxLoc, loc + locationSpan - 1);
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: Reflection - pipe input[%d] name='%s' layoutLocation=%d glType=%u",
|
||||
@@ -895,7 +936,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
(Int)ProgramObject::NormalizeBuiltinPipeInputName(inVar.name).length());
|
||||
|
||||
if (location >= 0 && location < (int)artifacts.attribs.size()) {
|
||||
const Int locationSpan = GetVertexInputLocationSpan(inVar.glDefineType);
|
||||
const Int locationSpan = GetVertexInputTotalLocationSpan(inVar);
|
||||
const GLenum locationType = GetVertexInputLocationType(inVar.glDefineType);
|
||||
for (Int locationOffset = 0; locationOffset < locationSpan; ++locationOffset) {
|
||||
const Int expandedLocation = location + locationOffset;
|
||||
@@ -951,6 +992,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
|
||||
if (!artifacts.program) return false;
|
||||
// The pipe-output list is the output interface of the program's LAST stage. Only a
|
||||
// fragment stage's outputs are color numbers indexed against GL_MAX_DRAW_BUFFERS; a
|
||||
// separable vertex/geometry/tessellation program's outputs are varyings, and holding
|
||||
// them to the draw-buffer range fails the link of every such program.
|
||||
if (artifacts.program->getIntermediate(EShLangFragment) == nullptr) return true;
|
||||
|
||||
UnorderedMap<Int, String> colorNumberOwners;
|
||||
const Int outputCount = artifacts.program->getNumPipeOutputs();
|
||||
|
||||
@@ -165,10 +165,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
const Int elements = init.arraySize;
|
||||
if (componentsPerElement <= 0 || elements <= 0) continue;
|
||||
|
||||
const Bool isFloat = init.basicType == glslang::EbtFloat || init.basicType == glslang::EbtFloat16;
|
||||
// EbtDouble belongs with the floats now, not with the skipped types: every 64-bit
|
||||
// float in a shader is narrowed to 32 bits before the module reaches a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass), so a `uniform double d = 1.5;` has exactly
|
||||
// the 32-bit shadow encoding a `uniform float` does - and glslang already folded its
|
||||
// value into floatValues, which is a vector<double> either way. Leaving it out meant
|
||||
// the initializer was silently dropped and the uniform came up zero.
|
||||
const Bool isFloat = init.basicType == glslang::EbtFloat ||
|
||||
init.basicType == glslang::EbtFloat16 ||
|
||||
init.basicType == glslang::EbtDouble;
|
||||
const Bool isInt = init.basicType == glslang::EbtInt || init.basicType == glslang::EbtUint ||
|
||||
init.basicType == glslang::EbtBool;
|
||||
// Anything else (fp64, 64-bit integers) has no 32-bit shadow encoding here, and a
|
||||
// Anything else (64-bit integers) has no 32-bit shadow encoding here, and a
|
||||
// half-written uniform is worse than an untouched one.
|
||||
if (!isFloat && !isInt) continue;
|
||||
const SizeT provided = isFloat ? init.floatValues.size() : init.intValues.size();
|
||||
|
||||
@@ -326,15 +326,22 @@ namespace MobileGL::MG_State::GLState {
|
||||
: kInvalidUniformOffset;
|
||||
}
|
||||
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
||||
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size:
|
||||
// std140 pads each column of a float matrix out to a vec4, so a mat3 spans 48 bytes
|
||||
// even though only 36 of them carry components. Anything reading or writing a whole
|
||||
// uniform's storage - a bounds check, a copy between two programs' shadows - wants
|
||||
// this rather than GetUniformSizesInBytes.
|
||||
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size,
|
||||
// for two reasons. std140 pads each column of a matrix out to a vec4, so a mat3 spans
|
||||
// 48 bytes even though only 36 of them carry components. And every 64-bit float in a
|
||||
// shader is narrowed to 32 bits before the module reaches a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting that
|
||||
// demoted module - so a `double` uniform occupies exactly what its float-typed twin
|
||||
// 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
|
||||
// programs' shadows - wants this rather than GetUniformSizesInBytes.
|
||||
static SizeT UniformStorageSpanInBytes(const glslang::TType* type, SizeT tightSize) {
|
||||
if (type != nullptr && type->isMatrix() && type->getBasicType() != glslang::EbtDouble) {
|
||||
if (type != nullptr && type->isMatrix()) {
|
||||
return static_cast<SizeT>(type->getMatrixCols()) * 4 * sizeof(Float);
|
||||
}
|
||||
if (type != nullptr && type->getBasicType() == glslang::EbtDouble) {
|
||||
return tightSize / 2;
|
||||
}
|
||||
return tightSize;
|
||||
}
|
||||
SizeT GetUniformStorageSpanInBytes(Uint location) const {
|
||||
|
||||
@@ -61,12 +61,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void VertexArrayObject::SetAttributeFormat(Uint index, int size, DataType type, Bool normalized, int stride,
|
||||
SizeT offset, Bool isInteger, Bool isBgra) {
|
||||
SizeT offset, Bool isInteger, Bool isBgra, int effectiveStride) {
|
||||
if (index >= MAX_VERTEX_ATTRIBS) return;
|
||||
if (size < 1 || size > 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
// See VertexAttribute::Stride: the resolved field carries the effective stride so that
|
||||
// a zero in it can only ever mean the binding model's "do not advance".
|
||||
const int resolvedStride = effectiveStride >= 0 ? effectiveStride : stride;
|
||||
|
||||
// The classic pointer-style API takes back full ownership of the resolved fields.
|
||||
m_attributeUsesBindingModel[index] = false;
|
||||
|
||||
@@ -77,7 +81,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_attributes[index].LegacyPointer = offset;
|
||||
|
||||
if (m_attributes[index].Size == size && m_attributes[index].Type == type &&
|
||||
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == stride &&
|
||||
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == resolvedStride &&
|
||||
m_attributes[index].Offset == offset && m_attributes[index].IsInteger == isInteger &&
|
||||
m_attributes[index].IsBgra == isBgra && !m_attributes[index].IsLong) {
|
||||
return;
|
||||
@@ -87,7 +91,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
attr.Size = size;
|
||||
attr.Type = type;
|
||||
attr.Normalized = normalized;
|
||||
attr.Stride = stride;
|
||||
attr.Stride = resolvedStride;
|
||||
attr.Offset = offset;
|
||||
attr.IsInteger = isInteger;
|
||||
attr.IsBgra = isBgra;
|
||||
|
||||
@@ -19,6 +19,14 @@ namespace MobileGL {
|
||||
int Size = 4;
|
||||
DataType Type = DataType::Float32;
|
||||
Bool Normalized = false;
|
||||
// The RESOLVED byte distance between consecutive elements, never the raw
|
||||
// glVertexAttrib*Pointer argument: a pointer call's stride 0 means "tightly
|
||||
// packed" and is resolved to the element size here, so a zero that survives
|
||||
// into this field can only have come from the binding model, where a zero
|
||||
// VERTEX_BINDING_STRIDE means the opposite - every vertex reads the SAME
|
||||
// element and the fetch address never advances (GL 4.6 core 10.3.1). Backends
|
||||
// consume this verbatim; collapsing 0 back into the element size is what made
|
||||
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the buffer.
|
||||
int Stride = 0;
|
||||
SizeT Offset = 0;
|
||||
Bool IsInteger = false;
|
||||
@@ -76,8 +84,12 @@ namespace MobileGL {
|
||||
void DisableAttribute(Uint index);
|
||||
Bool IsAttributeEnabled(Uint index) const;
|
||||
|
||||
// `stride` is the raw glVertexAttrib*Pointer argument, reported verbatim by
|
||||
// GL_VERTEX_ATTRIB_ARRAY_STRIDE. `effectiveStride` is what the fetch actually
|
||||
// advances by - the same value when the argument is non-zero, the tightly
|
||||
// packed element size when it is zero. Pass -1 to say the two are the same.
|
||||
void SetAttributeFormat(Uint index, int size, DataType type, Bool normalized, int stride, SizeT offset,
|
||||
Bool isInteger, Bool isBgra = false);
|
||||
Bool isInteger, Bool isBgra = false, int effectiveStride = -1);
|
||||
|
||||
void BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer);
|
||||
|
||||
|
||||
@@ -121,12 +121,6 @@ void main() { color = vec4(0, 1, 0, 1); }
|
||||
ExpectLinked(p);
|
||||
ClearErrors();
|
||||
|
||||
// KNOWN GAP, not an expectation: a separable FRAGMENT program's own inputs are not
|
||||
// in the reflection at all - glslang builds the "pipe input" list from the vertex
|
||||
// stage unless EShReflectionIntermediateIO is set, and setting that makes a
|
||||
// vertex-only separable program report its VS outputs as fragment outputs, which
|
||||
// fails ValidateFragmentOutputLocations and breaks glCreateShaderProgramv. So
|
||||
// GL_PROGRAM_INPUT is empty here until that validation is stage-aware.
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 1);
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 6);
|
||||
|
||||
@@ -804,6 +798,163 @@ void main(void) {
|
||||
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- uniform-block-types --
|
||||
// GL_REFERENCED_BY_*_SHADER is per block INSTANCE. An array of uniform blocks enumerates
|
||||
// one resource per element, and only the elements a stage actually dereferences are
|
||||
// referenced by it - declaring the array is not referencing every element.
|
||||
TEST_F(ProgramInterfaceTest, ArrayedUniformBlockReferencedByIsPerElement) {
|
||||
const char* vs = R"(#version 430
|
||||
in vec4 position;
|
||||
uniform SimpleBlock { mat3x2 a; mat4 b; vec4 c; };
|
||||
void main(void) {
|
||||
float tmp = a[0][1] * b[1][2] * c.x;
|
||||
gl_Position = position * tmp;
|
||||
}
|
||||
)";
|
||||
const char* fs = R"(#version 430
|
||||
uniform TrickyBlock { mat4 b; uint c; } e[2];
|
||||
out vec4 color;
|
||||
void main() { color = vec4(0, 1, 0, 1) * e[0].b[0][0]; }
|
||||
)";
|
||||
const GLuint p = MakeProgram(vs, fs);
|
||||
BindAttribLocation(p, 0, "position");
|
||||
BindFragDataLocation(p, 0, "color");
|
||||
LinkProgram(p);
|
||||
ExpectLinked(p);
|
||||
ClearErrors();
|
||||
|
||||
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES), 3);
|
||||
ExpectResource(p, GL_UNIFORM_BLOCK, "TrickyBlock[0]", "TrickyBlock[0]");
|
||||
ExpectResource(p, GL_UNIFORM_BLOCK, "TrickyBlock[1]", "TrickyBlock[1]");
|
||||
|
||||
const std::vector<GLenum> refProps = {GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER};
|
||||
EXPECT_EQ(PropsOf(p, GL_UNIFORM_BLOCK, "SimpleBlock", refProps), (std::vector<GLint>{1, 0}));
|
||||
EXPECT_EQ(PropsOf(p, GL_UNIFORM_BLOCK, "TrickyBlock[0]", refProps), (std::vector<GLint>{0, 1}));
|
||||
EXPECT_EQ(PropsOf(p, GL_UNIFORM_BLOCK, "TrickyBlock[1]", refProps), (std::vector<GLint>{0, 0}))
|
||||
<< "the unreferenced element of a block array must not inherit its sibling's stages";
|
||||
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The boundary of the rule above, and the case that caught it on device
|
||||
// (KHR-GL43.program_interface_query.ssb-types): a SHADER STORAGE block array's buffer
|
||||
// variables reflect under ONE subscript-free spelling shared by every element, so a union
|
||||
// over them credits element 0 and starves every other element - even the ones the shader
|
||||
// plainly reads. Storage blocks keep glslang's own mask, and both elements here must report
|
||||
// the fragment stage.
|
||||
TEST_F(ProgramInterfaceTest, ArrayedStorageBlockKeepsGlslangStagesForEveryElement) {
|
||||
const char* vs = R"(#version 430
|
||||
in vec4 position;
|
||||
void main(void) { gl_Position = position; }
|
||||
)";
|
||||
const char* fs = R"(#version 430
|
||||
layout(binding = 4) buffer SimpleStorage { vec4 a; } ss[2];
|
||||
out vec4 color;
|
||||
void main() { color = ss[0].a + ss[1].a; }
|
||||
)";
|
||||
const GLuint p = MakeProgram(vs, fs);
|
||||
BindAttribLocation(p, 0, "position");
|
||||
BindFragDataLocation(p, 0, "color");
|
||||
LinkProgram(p);
|
||||
ExpectLinked(p);
|
||||
ClearErrors();
|
||||
|
||||
const std::vector<GLenum> refProps = {GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER};
|
||||
for (const char* name : {"SimpleStorage[0]", "SimpleStorage[1]"}) {
|
||||
EXPECT_EQ(PropsOf(p, GL_SHADER_STORAGE_BLOCK, name, refProps), (std::vector<GLint>{0, 1}))
|
||||
<< "for " << name << ": a storage block the fragment stage reads must say so";
|
||||
}
|
||||
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- separate-programs-vertex --
|
||||
// A vertex-only separable program's OUTPUT interface is its own stage outputs, and an
|
||||
// inter-stage block enumerates as its members: "Color.r", and "gl_Position" for the
|
||||
// anonymous gl_PerVertex redeclaration - never the block instance name "vs_color".
|
||||
TEST_F(ProgramInterfaceTest, SeparableVertexProgramEnumeratesItsOwnOutputBlockMembers) {
|
||||
const char* vs = R"(#version 430 core
|
||||
layout(location = 0) in vec4 in_vertex;
|
||||
out Color { float r, g, b; vec4 iLikePie; } vs_color;
|
||||
out gl_PerVertex { vec4 gl_Position; };
|
||||
uniform float u;
|
||||
uniform vec4 v;
|
||||
void main() {
|
||||
gl_Position = in_vertex;
|
||||
vs_color.r = u; vs_color.g = 0.0; vs_color.b = 0.0; vs_color.iLikePie = v;
|
||||
}
|
||||
)";
|
||||
const GLuint p = CreateShaderProgramv(GL_VERTEX_SHADER, 1, &vs);
|
||||
ExpectLinked(p);
|
||||
ClearErrors();
|
||||
|
||||
// Exactly 5: the four Color members plus gl_Position. The anonymous gl_PerVertex
|
||||
// redeclaration drops gl_PointSize and gl_ClipDistance, which glslang keeps in the
|
||||
// block type as hidden members rather than erasing.
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 1);
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 10);
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 5);
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 15);
|
||||
|
||||
ExpectResource(p, GL_PROGRAM_INPUT, "in_vertex", "in_vertex");
|
||||
for (const char* name : {"Color.r", "Color.g", "Color.b", "Color.iLikePie", "gl_Position"}) {
|
||||
ExpectResource(p, GL_PROGRAM_OUTPUT, name, name);
|
||||
}
|
||||
EXPECT_EQ(GetProgramResourceIndex(p, GL_PROGRAM_OUTPUT, "vs_color"), GL_INVALID_INDEX)
|
||||
<< "the block instance is not the resource; its members are";
|
||||
|
||||
// A vertex-stage output has no color number, hence no index either, and it is not
|
||||
// per-patch. NAME_LENGTH/TYPE/ARRAY_SIZE come from the member, not the block.
|
||||
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "Color.iLikePie",
|
||||
{GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_REFERENCED_BY_COMPUTE_SHADER,
|
||||
GL_REFERENCED_BY_FRAGMENT_SHADER, GL_REFERENCED_BY_GEOMETRY_SHADER,
|
||||
GL_REFERENCED_BY_TESS_CONTROL_SHADER, GL_REFERENCED_BY_TESS_EVALUATION_SHADER,
|
||||
GL_REFERENCED_BY_VERTEX_SHADER, GL_IS_PER_PATCH, GL_LOCATION_INDEX}),
|
||||
(std::vector<GLint>{15, GL_FLOAT_VEC4, 1, 0, 0, 0, 0, 0, 1, 0, -1}));
|
||||
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- separate-programs-geometry --
|
||||
// Both boundaries are a middle stage here. The input block carries an instance name
|
||||
// (gl_in[]) so its members are prefixed with the BLOCK name, and the arrayed-ness of the
|
||||
// block itself is dropped: "gl_PerVertex.gl_Position", array size 1.
|
||||
TEST_F(ProgramInterfaceTest, SeparableGeometryProgramEnumeratesBothBlockBoundaries) {
|
||||
const char* gs = R"(#version 430
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
out gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; };
|
||||
in gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; } gl_in[];
|
||||
void main() {
|
||||
gl_Position = vec4(-1, 1, 0, 1); EmitVertex();
|
||||
gl_Position = gl_in[0].gl_Position; EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
const GLuint p = CreateShaderProgramv(GL_GEOMETRY_SHADER, 1, &gs);
|
||||
ExpectLinked(p);
|
||||
ClearErrors();
|
||||
|
||||
ExpectResource(p, GL_PROGRAM_INPUT, "gl_PerVertex.gl_Position", "gl_PerVertex.gl_Position");
|
||||
ExpectResource(p, GL_PROGRAM_OUTPUT, "gl_Position", "gl_Position");
|
||||
// A non-fragment stage's outputs are varyings: no color number, so no color index.
|
||||
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "gl_Position", {GL_LOCATION_INDEX}), (std::vector<GLint>{-1}));
|
||||
|
||||
const std::vector<GLenum> stageProps = {
|
||||
GL_NAME_LENGTH,
|
||||
GL_TYPE,
|
||||
GL_ARRAY_SIZE,
|
||||
GL_REFERENCED_BY_COMPUTE_SHADER,
|
||||
GL_REFERENCED_BY_FRAGMENT_SHADER,
|
||||
GL_REFERENCED_BY_GEOMETRY_SHADER,
|
||||
GL_REFERENCED_BY_TESS_CONTROL_SHADER,
|
||||
GL_REFERENCED_BY_TESS_EVALUATION_SHADER,
|
||||
GL_REFERENCED_BY_VERTEX_SHADER,
|
||||
GL_IS_PER_PATCH};
|
||||
EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "gl_PerVertex.gl_Position", stageProps),
|
||||
(std::vector<GLint>{25, GL_FLOAT_VEC4, 1, 0, 0, 1, 0, 0, 0, 0}));
|
||||
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "gl_Position", stageProps),
|
||||
(std::vector<GLint>{12, GL_FLOAT_VEC4, 1, 0, 0, 1, 0, 0, 0, 0}));
|
||||
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- separate-programs-fragment --
|
||||
TEST_F(ProgramInterfaceTest, SeparableFragmentProgramSeparatesUniformsFromBufferVariables) {
|
||||
const char* fs = R"(#version 430
|
||||
@@ -817,11 +968,12 @@ void main() { fs_color = vs_color + x + a; }
|
||||
ExpectLinked(p);
|
||||
ClearErrors();
|
||||
|
||||
// KNOWN GAP, not an expectation - the same one SimpleShaders documents: a separable
|
||||
// FRAGMENT program's own inputs are absent from the glslang reflection, so
|
||||
// GL_PROGRAM_INPUT is empty. Spec-correct values here would be 1 and 9 ("vs_color").
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 0);
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 0);
|
||||
// GL_PROGRAM_INPUT is the input interface of the program's FIRST stage, which for a
|
||||
// separable fragment program is the fragment stage: "vs_color", name length 9.
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 1);
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 9);
|
||||
EXPECT_EQ(GetProgramResourceIndex(p, GL_PROGRAM_INPUT, "vs_color"), 0u);
|
||||
EXPECT_EQ(ResourceName(p, GL_PROGRAM_INPUT, 0), "vs_color");
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 1);
|
||||
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 9);
|
||||
// The buffer variable is NOT a uniform, even though the frontend reflection keeps
|
||||
|
||||
@@ -346,8 +346,10 @@ void main() {
|
||||
|
||||
EXPECT_NE(rewritten.find("int instance = mg_ZeroBasedInstanceID + mg_BaseInstanceLowered;"),
|
||||
MobileGL::String::npos);
|
||||
EXPECT_NE(rewritten.find("#define mg_ZeroBasedInstanceID (gl_InstanceID - ((mg_BaseInstanceWordIndex >= 0) ? "
|
||||
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex)]) : 0))"),
|
||||
// One-based word index: zero is the "not an indirect draw" sentinel because that is
|
||||
// the value a GLSL uniform starts at and no draw path writes it before the first draw.
|
||||
EXPECT_NE(rewritten.find("#define mg_ZeroBasedInstanceID (gl_InstanceID - ((mg_BaseInstanceWordIndex > 0) ? "
|
||||
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : 0))"),
|
||||
MobileGL::String::npos);
|
||||
EXPECT_NE(rewritten.find(
|
||||
"layout(std430, binding = 12) readonly buffer mg_IndirectParams { highp uint mg_indirectWords[]; };"),
|
||||
@@ -356,6 +358,38 @@ void main() {
|
||||
EXPECT_EQ(CountOccurrences(rewritten, "gl_InstanceID"), 1u);
|
||||
}
|
||||
|
||||
// The sentinel itself, on the builtin it exists for. A zero-based index with a
|
||||
// negative "off" value made every NON-indirect draw of such a program read
|
||||
// mg_indirectWords[0] out of a storage buffer nothing had bound - the uniform starts
|
||||
// at zero and no non-indirect draw path writes it - which is where the CTS
|
||||
// shader_draw_parameters cases lost their geometry on Adreno. Pinned as text because
|
||||
// this contract lives in two places at once: the generated ESSL below and the +1 that
|
||||
// BackendProgramObjectImpl::SetBaseInstanceWordIndex applies.
|
||||
TEST(DirectGLESSanity, TheIndirectWordIndexIsOneBasedSoItsUnwrittenValueMeansNotIndirect) {
|
||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
||||
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
||||
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||
caps.MaxShaderStorageBufferBindings = 13;
|
||||
|
||||
const MobileGL::String source = R"(#version 310 es
|
||||
highp int mg_BaseInstanceLowered;
|
||||
void main() {
|
||||
gl_Position = vec4(float(mg_BaseInstanceLowered));
|
||||
}
|
||||
)";
|
||||
|
||||
const auto rewritten = MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms(
|
||||
source, GL_VERTEX_SHADER);
|
||||
|
||||
EXPECT_NE(rewritten.find("#define mg_BaseInstanceLowered ((mg_BaseInstanceWordIndex > 0) ? "
|
||||
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : mg_BaseInstance)"),
|
||||
MobileGL::String::npos)
|
||||
<< rewritten;
|
||||
// A zero-based form would spell either of these; neither may survive.
|
||||
EXPECT_EQ(rewritten.find("mg_BaseInstanceWordIndex >= 0"), MobileGL::String::npos);
|
||||
EXPECT_EQ(rewritten.find("uint(mg_BaseInstanceWordIndex)"), MobileGL::String::npos);
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) {
|
||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
||||
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
||||
|
||||
@@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.14)
|
||||
add_executable(
|
||||
SpirvPassTest
|
||||
SpirvPassTest.cpp
|
||||
DemoteFloat64Test.cpp
|
||||
FlattenXfbInterfaceBlocksTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(SpirvPassTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DemoteFloat64Test.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 <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::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
// The test-side reference walker, deliberately independent of the production code: a bug in
|
||||
// the pass must not be able to hide behind the same helper. Counts OpTypeFloat declarations of
|
||||
// a given width and collects the Offset literal of every OpMemberDecorate, in module order.
|
||||
constexpr Uint32 kSpirvHeaderWordCount = 5;
|
||||
constexpr Uint32 kOpTypeFloat = 22;
|
||||
constexpr Uint32 kOpName = 5;
|
||||
constexpr Uint32 kOpMemberDecorate = 72;
|
||||
constexpr Uint32 kOpFConvert = 115;
|
||||
constexpr Uint32 kOpCapability = 17;
|
||||
constexpr Uint32 kDecorationOffset = 35;
|
||||
constexpr Uint32 kCapabilityFloat64 = 10;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[i] >> 16;
|
||||
const Uint32 opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
visit(opcode, &spirv[i], wordCount);
|
||||
i += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 CountFloatTypesOfWidth(const Vector<Uint32>& spirv, Uint32 width) {
|
||||
Uint32 count = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpTypeFloat && wordCount >= 3 && words[2] == width) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
Uint32 CountFConverts(const Vector<Uint32>& spirv) {
|
||||
Uint32 count = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32*, Uint32) {
|
||||
if (opcode == kOpFConvert) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
Bool DeclaresFloat64Capability(const Vector<Uint32>& spirv) {
|
||||
Bool found = false;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpCapability && wordCount >= 2 && words[1] == kCapabilityFloat64) found = true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
// Byte offset of every member of the struct named `blockName`, in member order.
|
||||
Vector<Uint32> CollectOffsetsOf(const Vector<Uint32>& spirv, const String& blockName) {
|
||||
Uint32 structId = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != kOpName || wordCount < 3 || structId != 0) return;
|
||||
const char* text = reinterpret_cast<const char*>(&words[2]);
|
||||
const SizeT maxBytes = (wordCount - 2) * sizeof(Uint32);
|
||||
if (std::strncmp(text, blockName.c_str(), maxBytes) == 0) structId = words[1];
|
||||
});
|
||||
if (structId == 0) return {};
|
||||
|
||||
std::map<Uint32, Uint32> offsetByMember;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[1] == structId &&
|
||||
words[3] == kDecorationOffset) {
|
||||
offsetByMember[words[2]] = words[4];
|
||||
}
|
||||
});
|
||||
|
||||
Vector<Uint32> offsets;
|
||||
for (const auto& [member, offset] : offsetByMember) offsets.push_back(offset);
|
||||
return offsets;
|
||||
}
|
||||
|
||||
// Everything the production pipeline does to a source before the pass sees it.
|
||||
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;
|
||||
}
|
||||
|
||||
// A vertex shader that exercises every shape the pass has to handle at once: a block with
|
||||
// double / dvec2 / dvec3 / dvec4 / dmat4 members between two floats (so a shifted offset would
|
||||
// be visible), a default-block double uniform, a 64-bit vertex input, a double-typed array, an
|
||||
// implicit float->double conversion and an explicit double->float one.
|
||||
const char* kWideVertexSource = R"(#version 460 core
|
||||
layout(std140, binding = 0) uniform Blk {
|
||||
float a;
|
||||
double d;
|
||||
dvec2 v2;
|
||||
dvec3 v3;
|
||||
dvec4 v4;
|
||||
dmat4 m4;
|
||||
double arr[3];
|
||||
float z;
|
||||
};
|
||||
layout(location = 0) uniform double uScale;
|
||||
layout(location = 0) in dvec3 inPos;
|
||||
layout(location = 1) in vec3 inNormal;
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
double s = d * uScale + a;
|
||||
dvec3 p = inPos * v3 + v2.xyx + v4.xyz + dvec3(m4[0].xyz);
|
||||
s += p.x + p.y + p.z + arr[0] + arr[1] + arr[2] + z + 0.5lf;
|
||||
vOut = float(s) + inNormal.x;
|
||||
gl_Position = vec4(float(s));
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
class DemoteFloat64Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
ShaderCompiler::SetSpirvValidationEnabled(true);
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// The wrapper validates its OUTPUT on every run, so this covers every demotion the test
|
||||
// performed without any of them having to say so.
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
|
||||
<< "the demoted module did not survive spirv-val";
|
||||
}
|
||||
|
||||
Uint64 m_validationFailuresAtStart = 0;
|
||||
};
|
||||
|
||||
TEST_F(DemoteFloat64Test, DemotesEveryWidthAndDropsTheCapability) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
ASSERT_EQ(CountFloatTypesOfWidth(input, 64), 1u) << Disassemble(input);
|
||||
ASSERT_TRUE(DeclaresFloat64Capability(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(output, 64), 0u) << Disassemble(output);
|
||||
// And exactly one 32-bit float type survives: the merge has to happen, or spirv-val rejects
|
||||
// the second declaration.
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(output, 32), 1u) << Disassemble(output);
|
||||
EXPECT_FALSE(DeclaresFloat64Capability(output));
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, RederivesTheStd140LayoutOfADemotedUniformBlock) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(std140, binding = 0) uniform Blk {
|
||||
float a;
|
||||
double d;
|
||||
dvec2 v2;
|
||||
dvec3 v3;
|
||||
dvec4 v4;
|
||||
dmat4 m4;
|
||||
double arr[3];
|
||||
float z;
|
||||
};
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
vOut = float(d + v2.x + v3.y + v4.z + m4[2].w + arr[1] + a + z);
|
||||
gl_Position = vec4(vOut);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
// What glslang laid out for the 64-bit members, which is what an application computing
|
||||
// std140 by hand would also get.
|
||||
EXPECT_EQ(CollectOffsetsOf(input, "Blk"), (Vector<Uint32>{0, 8, 16, 32, 64, 96, 224, 272}))
|
||||
<< Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
// std140 for the demoted members: float at 4, vec2 at 8, vec3 at 16 (aligned like a vec4),
|
||||
// vec4 at 32, mat4 at 48 with a 16-byte column stride, the array at 112 with the std140
|
||||
// 16-byte element stride, and the trailing float at 160. This is what SPIRV-Cross has to be
|
||||
// able to re-derive for GLSL ES, which has no member layout(offset=) to fall back on.
|
||||
EXPECT_EQ(CollectOffsetsOf(output, "Blk"), (Vector<Uint32>{0, 4, 8, 16, 32, 48, 112, 160}))
|
||||
<< Disassemble(output);
|
||||
const String text = Disassemble(output);
|
||||
EXPECT_NE(text.find("MatrixStride 16"), String::npos) << text;
|
||||
EXPECT_NE(text.find("ArrayStride 16"), String::npos) << text;
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, RederivesTheStd430LayoutOfADemotedStorageBlock) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(std430, binding = 0) buffer Ssbo {
|
||||
double head;
|
||||
dvec4 wide;
|
||||
double tail[4];
|
||||
};
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
vOut = float(head + wide.w + tail[3]);
|
||||
gl_Position = vec4(vOut);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_EQ(CollectOffsetsOf(input, "Ssbo"), (Vector<Uint32>{0, 32, 64})) << Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
// std430, so the array packs at its element size rather than being rounded to 16: float at 0,
|
||||
// vec4 at 16, float[4] at 32 with a 4-byte stride. A storage block must NOT come out std140,
|
||||
// which is the whole reason the packing is chosen per storage class.
|
||||
EXPECT_EQ(CollectOffsetsOf(output, "Ssbo"), (Vector<Uint32>{0, 16, 32})) << Disassemble(output);
|
||||
EXPECT_NE(Disassemble(output).find("ArrayStride 4"), String::npos) << Disassemble(output);
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, LeavesTheLayoutOfABlockWithoutDoublesAlone) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(std140, binding = 0) uniform Blk {
|
||||
float a;
|
||||
vec3 v3;
|
||||
mat4 m4;
|
||||
};
|
||||
layout(std140, binding = 1) uniform Wide {
|
||||
float w;
|
||||
double d;
|
||||
};
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
vOut = float(a + v3.y + m4[1].z + float(d) + w);
|
||||
gl_Position = vec4(vOut);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> before = CollectOffsetsOf(input, "Blk");
|
||||
ASSERT_FALSE(before.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
// Only the block that actually narrowed is re-laid-out. Touching the other one would be
|
||||
// churn at best, and a disagreement with glslang's own layout at worst.
|
||||
EXPECT_EQ(CollectOffsetsOf(output, "Blk"), before) << Disassemble(output);
|
||||
EXPECT_EQ(CollectOffsetsOf(output, "Wide"), (Vector<Uint32>{0, 4})) << Disassemble(output);
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, FoldsTheConversionsThatBecameIdentities) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
ASSERT_GT(CountFConverts(input), 0u) << "the fixture no longer converts between the two widths";
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
// SPIR-V requires the two component widths of an OpFConvert to differ, so every one of them
|
||||
// has to be gone: both sides are 32 bits now.
|
||||
EXPECT_EQ(CountFConverts(output), 0u) << Disassemble(output);
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, NarrowsDoubleConstantsToTheirFloatValue) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(location = 0) out float outValue;
|
||||
void main() {
|
||||
double d = 0.5lf;
|
||||
outValue = float(d * 0.25lf);
|
||||
gl_Position = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
|
||||
// A 64-bit literal is two words wide and a 32-bit one is a single word, so a constant left
|
||||
// unconverted is not merely imprecise - it is an unparseable instruction. Disassembling both
|
||||
// values proves the re-encode produced the right number, not just the right width.
|
||||
const String text = Disassemble(output);
|
||||
EXPECT_NE(text.find("OpConstant %float 0.5"), String::npos) << text;
|
||||
EXPECT_NE(text.find("OpConstant %float 0.25"), String::npos) << text;
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, LeavesAModuleWithoutDoublesByteIdentical) {
|
||||
const String source = R"(#version 460 core
|
||||
layout(location = 0) in vec4 inPos;
|
||||
void main() { gl_Position = inPos; }
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
// The pass reports SuccessWithoutChange here, and SPIRV-Tools asserts (in assert-enabled
|
||||
// builds) that such a run round-trips byte-identically.
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, DeclinesAModuleThatBitcastsAcrossTheWidthBoundary) {
|
||||
// packDouble2x32 is defined only for a 64-bit result: there is no 32-bit answer to give, and
|
||||
// narrowing one side of the surrounding OpBitcast alone produces a module spirv-val rejects.
|
||||
// The contract is that such a module comes back untouched rather than broken.
|
||||
const String source = R"(#version 460 core
|
||||
#extension GL_ARB_gpu_shader_fp64 : require
|
||||
layout(location = 0) uniform uvec2 uPacked;
|
||||
layout(location = 0) out float outValue;
|
||||
void main() {
|
||||
double d = packDouble2x32(uPacked);
|
||||
outValue = float(d);
|
||||
gl_Position = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
ASSERT_EQ(CountFloatTypesOfWidth(input, 64), 1u) << Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
EXPECT_EQ(output, input) << Disassemble(output);
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(output));
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, ModuleDeclaresFloat64AnswersBothWays) {
|
||||
const Vector<Uint32> wide = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
|
||||
ASSERT_FALSE(wide.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(wide));
|
||||
|
||||
Vector<Uint32> demoted;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wide, demoted));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(demoted));
|
||||
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64({}));
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, TheSharedChainDemotesToo) {
|
||||
// Production never calls the pass on its own: it reaches it through the one chain every
|
||||
// module goes through at link, on both backends.
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(output)) << Disassemble(output);
|
||||
}
|
||||
|
||||
// The payoff on the Espryt path: SPIRV-Cross throws "FP64 not supported in ES profile" for every
|
||||
// one of these before demotion, so the program simply could not be transpiled at all.
|
||||
class DemoteFloat64EsslTest : public DemoteFloat64Test, public ::testing::WithParamInterface<const char*> {};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
Shapes, DemoteFloat64EsslTest,
|
||||
::testing::Values(
|
||||
// A double that never reaches an interface: locals and literals only.
|
||||
R"(#version 460 core
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
double s = 0.5lf;
|
||||
for (int i = 0; i < 3; ++i) s = s * 1.5lf + 0.25lf;
|
||||
vOut = float(s);
|
||||
gl_Position = vec4(float(s));
|
||||
}
|
||||
)",
|
||||
// A default-block double uniform: the glUniform*d path, and the block MobileGL lays out
|
||||
// itself.
|
||||
R"(#version 460 core
|
||||
layout(location = 0) uniform double uScale;
|
||||
layout(location = 1) uniform dvec3 uOffset;
|
||||
layout(location = 2) uniform dmat4 uTransform;
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
dvec3 p = uOffset * uScale + dvec3(uTransform[1].xyz);
|
||||
vOut = float(p.x + p.y + p.z);
|
||||
gl_Position = vec4(float(p.x));
|
||||
}
|
||||
)",
|
||||
// An application-declared std140 block whose members are 64-bit.
|
||||
R"(#version 460 core
|
||||
layout(std140, binding = 0) uniform Blk {
|
||||
float a;
|
||||
double d;
|
||||
dvec2 v2;
|
||||
dvec3 v3;
|
||||
dvec4 v4;
|
||||
dmat4 m4;
|
||||
double arr[3];
|
||||
float z;
|
||||
};
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
double s = d + v2.x + v3.y + v4.z + m4[2].w + arr[1] + a + z;
|
||||
vOut = float(s);
|
||||
gl_Position = vec4(float(s));
|
||||
}
|
||||
)",
|
||||
// A 64-bit vertex input, which is what glVertexAttribLFormat feeds.
|
||||
R"(#version 460 core
|
||||
layout(location = 0) in dvec3 inPos;
|
||||
layout(location = 2) in double inWeight;
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
vOut = float(inPos.x + inPos.y + inPos.z + inWeight);
|
||||
gl_Position = vec4(vOut);
|
||||
}
|
||||
)",
|
||||
// An std430 storage block, whose double members pack differently again.
|
||||
R"(#version 460 core
|
||||
layout(std430, binding = 0) buffer Ssbo {
|
||||
double head;
|
||||
dvec4 wide;
|
||||
double tail[4];
|
||||
};
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
double s = head + wide.w + tail[3];
|
||||
vOut = float(s);
|
||||
gl_Position = vec4(float(s));
|
||||
}
|
||||
)"));
|
||||
|
||||
TEST_P(DemoteFloat64EsslTest, TheDemotedModuleCanBeEmittedAsEssl) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, GetParam());
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
|
||||
SpvcSession session(output, SessionUsageBit::Transpile);
|
||||
spvc_compiler_options options;
|
||||
ASSERT_EQ(session.CreateOptions(&options), SPVC_SUCCESS);
|
||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
|
||||
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);
|
||||
ASSERT_EQ(session.SetOptions(options), SPVC_SUCCESS);
|
||||
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
ASSERT_TRUE(essl) << essl.error().log;
|
||||
EXPECT_NE(essl->find("#version 320 es"), String::npos) << *essl;
|
||||
// ESSL has no 64-bit float spelling at all, so any of these in the output is SPIRV-Cross
|
||||
// having emitted something no ES driver will compile.
|
||||
EXPECT_EQ(essl->find("double"), String::npos) << *essl;
|
||||
EXPECT_EQ(essl->find("dvec"), String::npos) << *essl;
|
||||
EXPECT_EQ(essl->find("dmat"), String::npos) << *essl;
|
||||
}
|
||||
|
||||
TEST_F(DemoteFloat64Test, RejectsGarbageInput) {
|
||||
const Vector<Uint32> notSpirv{0xdeadbeefu, 0u, 0u, 0u, 0u};
|
||||
Vector<Uint32> output;
|
||||
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output));
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FlattenXfbInterfaceBlocksTest.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 <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 KHR-GL43.vertex_attrib_binding.basic-input capture program's output side, verbatim:
|
||||
// a 16-element vec4 array inside a named output block, which is what the capture list
|
||||
// addresses member by member ("StageData.attrib[0]" ... "StageData.attrib[15]").
|
||||
const char* kCaptureVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_attrib;
|
||||
out StageData {
|
||||
vec4 attrib[16];
|
||||
} vs_out;
|
||||
void main() {
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
vs_out.attrib[i] = vs_in_attrib;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// Mixed member widths, so a member that claims the wrong number of locations moves every
|
||||
// member after it.
|
||||
// 440, because a location on the BLOCK is ARB_enhanced_layouts.
|
||||
const char* kMixedBlockVertexSource = R"(#version 440 core
|
||||
layout(location = 0) in vec4 vs_in_attrib;
|
||||
layout(location = 0) out StageData {
|
||||
vec4 first;
|
||||
vec2 second;
|
||||
mat4 third;
|
||||
vec4 fourth;
|
||||
} vs_out;
|
||||
void main() {
|
||||
vs_out.first = vs_in_attrib;
|
||||
vs_out.second = vs_in_attrib.xy;
|
||||
vs_out.third = mat4(vs_in_attrib.x);
|
||||
vs_out.fourth = vs_in_attrib;
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
class FlattenXfbInterfaceBlocksTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
ShaderCompiler::SetSpirvValidationEnabled(true);
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
|
||||
<< "the flattened module did not survive spirv-val";
|
||||
}
|
||||
|
||||
Uint64 m_validationFailuresAtStart = 0;
|
||||
};
|
||||
|
||||
TEST_F(FlattenXfbInterfaceBlocksTest, FlattensACapturedBlockIntoOneVariablePerMember) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_EQ(flattened, (std::set<String>{"StageData"}));
|
||||
|
||||
const String dis = Disassemble(output);
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
ASSERT_TRUE(tools.Validate(output)) << dis;
|
||||
EXPECT_NE(dis.find("StageData_attrib"), String::npos) << dis;
|
||||
// The block itself must have stopped being an interface variable, or the driver would see
|
||||
// both spellings of the same data.
|
||||
EXPECT_NE(dis.find("Private"), String::npos) << dis;
|
||||
}
|
||||
|
||||
// The declaration is the point of the whole exercise: the emitted ESSL has to declare a plain
|
||||
// output ARRAY, not an interface block, because that is the shape the Adreno driver can capture.
|
||||
// SPIR-V validation does NOT catch the difference - leaving the struct's Block decoration on the
|
||||
// demoted shadow produced a module that validated and emitted `StageData vs_out;` next to a block
|
||||
// declaration the driver rejected with a bare "'vs_out' : syntax error".
|
||||
TEST_F(FlattenXfbInterfaceBlocksTest, TheEmittedDeclarationIsAPlainArrayNotABlock) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
// Negative control: untouched, the block is emitted AS a block.
|
||||
const String before = Transpile(input);
|
||||
EXPECT_NE(before.find("out StageData"), String::npos) << before;
|
||||
EXPECT_EQ(before.find("StageData_attrib"), String::npos) << before;
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
|
||||
|
||||
const String after = Transpile(output);
|
||||
EXPECT_NE(after.find("StageData_attrib[16]"), String::npos) << after;
|
||||
EXPECT_EQ(after.find("out StageData"), String::npos)
|
||||
<< "the block must not still be declared as an output block:\n"
|
||||
<< after;
|
||||
}
|
||||
|
||||
// GL 4.6 core 11.1.2.1: consecutive members take consecutive locations, and a member takes as
|
||||
// many as its type needs. Getting a span wrong silently moves every member after it.
|
||||
TEST_F(FlattenXfbInterfaceBlocksTest, GivesEachMemberItsOwnConsecutiveLocations) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kMixedBlockVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String dis = Disassemble(output);
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
ASSERT_TRUE(tools.Validate(output)) << dis;
|
||||
EXPECT_NE(dis.find("OpDecorate %StageData_first Location 0"), String::npos) << dis;
|
||||
EXPECT_NE(dis.find("OpDecorate %StageData_second Location 1"), String::npos) << dis;
|
||||
EXPECT_NE(dis.find("OpDecorate %StageData_third Location 2"), String::npos) << dis;
|
||||
// mat4 takes four, so the member after it starts at 2 + 4.
|
||||
EXPECT_NE(dis.find("OpDecorate %StageData_fourth Location 6"), String::npos) << dis;
|
||||
}
|
||||
|
||||
// Nothing captures this block, so nothing may touch it: a shader that merely HAS an output
|
||||
// block must reach the driver exactly as it was.
|
||||
TEST_F(FlattenXfbInterfaceBlocksTest, LeavesABlockNoCaptureNamesAlone) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(
|
||||
ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"SomeOtherBlock"}, flattened, output));
|
||||
EXPECT_TRUE(flattened.empty());
|
||||
|
||||
const String after = Transpile(output);
|
||||
EXPECT_NE(after.find("out StageData"), String::npos) << after;
|
||||
EXPECT_EQ(after.find("StageData_attrib"), String::npos) << after;
|
||||
}
|
||||
|
||||
// An empty request must not even run the optimizer: every program without transform feedback
|
||||
// takes this path on every build.
|
||||
TEST_F(FlattenXfbInterfaceBlocksTest, DeclinesAnEmptyRequestWithoutRewriting) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
EXPECT_FALSE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {}, flattened, output));
|
||||
EXPECT_TRUE(flattened.empty());
|
||||
EXPECT_TRUE(output.empty());
|
||||
}
|
||||
|
||||
// The capture list has to follow the declaration exactly, and only for blocks that were
|
||||
// actually rewritten - a member of a block left alone keeps the application's spelling, and so
|
||||
// does a name with no block prefix at all (gl_Position, a plain varying).
|
||||
TEST_F(FlattenXfbInterfaceBlocksTest, RewritesOnlyTheCaptureNamesOfFlattenedBlocks) {
|
||||
String rewritten;
|
||||
EXPECT_TRUE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("StageData.attrib[0]", {"StageData"},
|
||||
rewritten));
|
||||
EXPECT_EQ(rewritten, "StageData_attrib[0]");
|
||||
|
||||
EXPECT_TRUE(
|
||||
ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("StageData.attrib", {"StageData"}, rewritten));
|
||||
EXPECT_EQ(rewritten, "StageData_attrib");
|
||||
|
||||
EXPECT_FALSE(
|
||||
ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("Other.member", {"StageData"}, rewritten));
|
||||
EXPECT_FALSE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("gl_Position", {"StageData"}, rewritten));
|
||||
EXPECT_FALSE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("vColor", {"StageData"}, rewritten));
|
||||
EXPECT_FALSE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(".leading", {"StageData"}, rewritten));
|
||||
}
|
||||
@@ -128,6 +128,134 @@ namespace {
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// KHR-GL44.multi_bind.errors_bind_textures / .errors_bind_image_textures / .errors_bind_samplers.
|
||||
// Both entry points were silent no-op stubs, so every row here answered GL_NO_ERROR.
|
||||
// errors_bind_samplers is in the list because that case checks the invalid-name rule by calling
|
||||
// glBindTextures with a sampler-name array - a name from the wrong namespace is simply not an
|
||||
// existing texture.
|
||||
TEST_F(NegativeApiErrorsTest, MultiBindTexturesRejectsBadRangesAndNames) {
|
||||
GLint maxUnits = 0;
|
||||
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxUnits);
|
||||
ASSERT_GT(maxUnits, 0);
|
||||
GLint maxImageUnits = 0;
|
||||
GetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
|
||||
GLuint texture = 0;
|
||||
GenTextures(1, &texture);
|
||||
BindTexture(GL_TEXTURE_2D, texture);
|
||||
TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
|
||||
|
||||
// Reserved by glGenTextures but never bound: not an object yet, so the multi-bind entry
|
||||
// points must refuse it instead of creating it the way glBindTexture would.
|
||||
GLuint reservedOnly = 0;
|
||||
GenTextures(1, &reservedOnly);
|
||||
ASSERT_NE(reservedOnly, 0u);
|
||||
ASSERT_EQ(IsTexture(reservedOnly), GL_FALSE);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint good[1] = {texture};
|
||||
const GLuint mixed[2] = {texture, reservedOnly};
|
||||
|
||||
std::vector<Row> rows = {
|
||||
{"glBindTextures with negative count", [&] { BindTextures(0, -1, good); }, GL_INVALID_VALUE},
|
||||
{"glBindTextures with first + count past the last unit",
|
||||
[&] { BindTextures(static_cast<GLuint>(maxUnits), 1, good); }, GL_INVALID_OPERATION},
|
||||
{"glBindTextures with a reserved-but-uncreated name", [&] { BindTextures(0, 2, mixed); },
|
||||
GL_INVALID_OPERATION},
|
||||
{"glBindImageTextures with negative count", [&] { BindImageTextures(0, -1, good); }, GL_INVALID_VALUE},
|
||||
};
|
||||
if (maxImageUnits > 0) {
|
||||
rows.push_back({"glBindImageTextures with first + count past the last image unit",
|
||||
[&] { BindImageTextures(static_cast<GLuint>(maxImageUnits), 1, good); },
|
||||
GL_INVALID_OPERATION});
|
||||
rows.push_back({"glBindImageTextures with a reserved-but-uncreated name",
|
||||
[&] { BindImageTextures(0, 2, mixed); }, GL_INVALID_OPERATION});
|
||||
}
|
||||
RunRows(rows);
|
||||
|
||||
// The loop semantics again: the good element at index 0 binds, the bad one does not.
|
||||
GLint bound = -1;
|
||||
GetIntegeri_v(GL_TEXTURE_BINDING_2D, 0, &bound);
|
||||
EXPECT_EQ(static_cast<GLuint>(bound), texture) << "a rejected element must not take the valid ones with it";
|
||||
GetIntegeri_v(GL_TEXTURE_BINDING_2D, 1, &bound);
|
||||
EXPECT_EQ(bound, 0) << "the rejected element must not have bound anything";
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// KHR-GL44.multi_bind.functional_bind_textures / .functional_bind_image_textures: the binding
|
||||
// has to land on the texture's OWN target - glBindTextures takes no target parameter - and
|
||||
// element zero has to unbind every target of its unit.
|
||||
TEST_F(NegativeApiErrorsTest, MultiBindTexturesBindsToTheTexturesOwnTarget) {
|
||||
GLuint textures[2] = {0, 0};
|
||||
GenTextures(2, textures);
|
||||
BindTexture(GL_TEXTURE_1D, textures[0]);
|
||||
TexStorage1D(GL_TEXTURE_1D, 1, GL_RGBA8, 4);
|
||||
BindTexture(GL_TEXTURE_3D, textures[1]);
|
||||
TexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 4, 4, 4);
|
||||
// Leave the active unit's slots clean so only the multi-bind result is under test.
|
||||
BindTexture(GL_TEXTURE_1D, 0);
|
||||
BindTexture(GL_TEXTURE_3D, 0);
|
||||
DrainErrors();
|
||||
|
||||
BindTextures(0, 2, textures);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint bound = -1;
|
||||
GetIntegeri_v(GL_TEXTURE_BINDING_1D, 0, &bound);
|
||||
EXPECT_EQ(static_cast<GLuint>(bound), textures[0]) << "a 1D texture must land on the unit's 1D slot";
|
||||
GetIntegeri_v(GL_TEXTURE_BINDING_3D, 0, &bound);
|
||||
EXPECT_EQ(bound, 0) << "no other target of the unit may be touched";
|
||||
GetIntegeri_v(GL_TEXTURE_BINDING_3D, 1, &bound);
|
||||
EXPECT_EQ(static_cast<GLuint>(bound), textures[1]) << "a 3D texture must land on the unit's 3D slot";
|
||||
|
||||
// A zero element - and a NULL array - unbind EVERY target of the unit, not just one.
|
||||
const GLuint zeros[1] = {0};
|
||||
BindTextures(0, 1, zeros);
|
||||
GetIntegeri_v(GL_TEXTURE_BINDING_1D, 0, &bound);
|
||||
EXPECT_EQ(bound, 0);
|
||||
BindTextures(1, 1, nullptr);
|
||||
GetIntegeri_v(GL_TEXTURE_BINDING_3D, 1, &bound);
|
||||
EXPECT_EQ(bound, 0) << "a NULL <textures> unbinds the range";
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint maxImageUnits = 0;
|
||||
GetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
if (maxImageUnits > 0) {
|
||||
// ARB_multi_bind fixes every glBindImageTexture parameter but the unit and the name:
|
||||
// level 0, layered, layer 0, READ_WRITE, and the texture's own internal format.
|
||||
BindImageTextures(0, 1, &textures[1]);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
GetIntegeri_v(GL_IMAGE_BINDING_NAME, 0, &bound);
|
||||
EXPECT_EQ(static_cast<GLuint>(bound), textures[1]);
|
||||
GetIntegeri_v(GL_IMAGE_BINDING_LEVEL, 0, &bound);
|
||||
EXPECT_EQ(bound, 0);
|
||||
GetIntegeri_v(GL_IMAGE_BINDING_LAYERED, 0, &bound);
|
||||
EXPECT_EQ(bound, GL_TRUE);
|
||||
GetIntegeri_v(GL_IMAGE_BINDING_ACCESS, 0, &bound);
|
||||
EXPECT_EQ(bound, GL_READ_WRITE);
|
||||
GetIntegeri_v(GL_IMAGE_BINDING_FORMAT, 0, &bound);
|
||||
EXPECT_EQ(bound, GL_RGBA8);
|
||||
|
||||
BindImageTextures(0, 1, nullptr);
|
||||
GetIntegeri_v(GL_IMAGE_BINDING_NAME, 0, &bound);
|
||||
EXPECT_EQ(bound, 0) << "a NULL <textures> resets the image unit";
|
||||
}
|
||||
|
||||
// Through the EXPORTED entry points, not just the GLImpl functions: both of these were
|
||||
// declared with the stub macro, so a working implementation that is never wired into
|
||||
// Definitions.cpp still answers GL_NO_ERROR and binds nothing.
|
||||
::glBindTextures(0, 1, &textures[0]);
|
||||
GetIntegeri_v(GL_TEXTURE_BINDING_1D, 0, &bound);
|
||||
EXPECT_EQ(static_cast<GLuint>(bound), textures[0]) << "glBindTextures is still exported as a no-op stub";
|
||||
if (maxImageUnits > 0) {
|
||||
::glBindImageTextures(0, 1, &textures[1]);
|
||||
GetIntegeri_v(GL_IMAGE_BINDING_NAME, 0, &bound);
|
||||
EXPECT_EQ(static_cast<GLuint>(bound), textures[1])
|
||||
<< "glBindImageTextures is still exported as a no-op stub";
|
||||
}
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
TEST_F(NegativeApiErrorsTest, BufferRangeOffsetAlignmentAppliesToTheBindingPoint) {
|
||||
GLint ssboAlignment = 0;
|
||||
GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment);
|
||||
|
||||
@@ -540,6 +540,10 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
INIT_GLES_FUNC_OPTIONAL(glMultiDrawArraysIndirectEXT)
|
||||
INIT_GLES_FUNC_OPTIONAL(glMultiDrawElementsIndirectEXT)
|
||||
INIT_GLES_FUNC_OPTIONAL(glMultiDrawElementsBaseVertexEXT)
|
||||
|
||||
INIT_GLES_FUNC_OPTIONAL(glDrawArraysInstancedBaseInstanceEXT)
|
||||
INIT_GLES_FUNC_OPTIONAL(glDrawElementsInstancedBaseInstanceEXT)
|
||||
INIT_GLES_FUNC_OPTIONAL(glDrawElementsInstancedBaseVertexBaseInstanceEXT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,6 +855,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
// Resolved into caps.TextureBufferSupport below, once the ES version is also known.
|
||||
Bool hasExtTextureBuffer = false;
|
||||
Bool hasOesTextureBuffer = false;
|
||||
// Combined with the three entry points below; DirectGLES emulates baseInstance when this
|
||||
// comes out false, so a stub pointer counting as support would silently break the draws.
|
||||
Bool hasBaseInstanceExtension = false;
|
||||
for (GLint i = 0; i < extCount; ++i) {
|
||||
const char* extension = (const char*)glesFuncs.glGetStringi(GL_EXTENSIONS, i);
|
||||
if (extension) {
|
||||
@@ -891,7 +898,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
hasOesTextureBuffer = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
|
||||
caps.SupportsBaseInstance = true;
|
||||
hasBaseInstanceExtension = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_disjoint_timer_query") == 0) {
|
||||
caps.SupportsDisjointTimerQuery = true;
|
||||
@@ -929,6 +936,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.SupportsMultiDrawElementsBaseVertex = hasDrawElementsBaseVertexExtension &&
|
||||
hasMultiDrawArraysExtension &&
|
||||
glesFuncs.glMultiDrawElementsBaseVertexEXT != nullptr;
|
||||
// All three, not any: DirectGLES picks native-vs-emulated once per draw entry point off
|
||||
// this single flag, so a driver that resolved only some of them must count as absent.
|
||||
caps.SupportsBaseInstance = hasBaseInstanceExtension &&
|
||||
glesFuncs.glDrawArraysInstancedBaseInstanceEXT != nullptr &&
|
||||
glesFuncs.glDrawElementsInstancedBaseInstanceEXT != nullptr &&
|
||||
glesFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT != nullptr;
|
||||
// Core from ES 3.2 on, so an extension string is not required there; below 3.2 the
|
||||
// extension is, and the pointer still has to have resolved either way.
|
||||
const Bool esAtLeast32 = caps.GLESVersion.Major > 3 ||
|
||||
@@ -963,6 +976,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
MGLOG_I(" draw elements base vertex (ES 3.2 core or EXT/OES_draw_elements_base_vertex): %s",
|
||||
caps.SupportsDrawElementsBaseVertex ? "yes" : "no");
|
||||
MGLOG_I(" compute shaders (ES 3.1 core): %s", caps.SupportsComputeShader ? "yes" : "no");
|
||||
MGLOG_I(" base instance (EXT_base_instance; emulated by attribute offsets when absent): %s",
|
||||
caps.SupportsBaseInstance ? "yes" : "no");
|
||||
|
||||
MGLOG_I("OpenGL ES capabilities:");
|
||||
glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment);
|
||||
|
||||
@@ -638,6 +638,17 @@ namespace MobileGL {
|
||||
GL_FUNC_TYPEDEF(void, glBruh)
|
||||
GL_FUNC_TYPEDEF(void, glMultiDrawElementsBaseVertexEXT, GLenum mode, const GLsizei* count, GLenum type,
|
||||
const void* const* indices, GLsizei drawcount, const GLint* basevertex)
|
||||
|
||||
// GL_EXT_base_instance. Where a driver has these, the "+ baseInstance" of the
|
||||
// instanced-array element index is the driver's job; where it does not, DirectGLES
|
||||
// folds it into the attribute offsets instead (VertexArrayImpl::BaseInstanceByteShift).
|
||||
GL_FUNC_TYPEDEF(void, glDrawArraysInstancedBaseInstanceEXT, GLenum mode, GLint first, GLsizei count,
|
||||
GLsizei instancecount, GLuint baseinstance)
|
||||
GL_FUNC_TYPEDEF(void, glDrawElementsInstancedBaseInstanceEXT, GLenum mode, GLsizei count, GLenum type,
|
||||
const void* indices, GLsizei instancecount, GLuint baseinstance)
|
||||
GL_FUNC_TYPEDEF(void, glDrawElementsInstancedBaseVertexBaseInstanceEXT, GLenum mode, GLsizei count,
|
||||
GLenum type, const void* indices, GLsizei instancecount, GLint basevertex,
|
||||
GLuint baseinstance)
|
||||
/*
|
||||
namespace Caps {
|
||||
struct GLESCaps {
|
||||
@@ -1034,6 +1045,10 @@ namespace MobileGL {
|
||||
GL_FUNC_DECL(glMultiDrawElementsIndirectEXT)
|
||||
GL_FUNC_DECL(glMultiDrawElementsBaseVertexEXT)
|
||||
|
||||
GL_FUNC_DECL(glDrawArraysInstancedBaseInstanceEXT)
|
||||
GL_FUNC_DECL(glDrawElementsInstancedBaseInstanceEXT)
|
||||
GL_FUNC_DECL(glDrawElementsInstancedBaseVertexBaseInstanceEXT)
|
||||
|
||||
GL_FUNC_DECL(glBruh)
|
||||
};
|
||||
|
||||
|
||||
@@ -46,6 +46,22 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
RankMobileGLReported = 5,
|
||||
};
|
||||
|
||||
// Both backends' fp64 rows end the same way, and the sentence they end with depends on
|
||||
// a config flag rather than on anything either backend probes: the demotion is what
|
||||
// makes doubles work, but GL_ARB_gpu_shader_fp64 promises the PRECISION the demotion
|
||||
// cannot deliver, so the string is opt-in and the row has to say which way it went.
|
||||
String AppendFp64AdvertisementNote(String detail) {
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
return Move(detail) +
|
||||
". GL_ARB_gpu_shader_fp64 IS advertised (MOBILEGL_ADVERTISE_FP64): an application "
|
||||
"that checks the string will believe it has 64-bit precision, and it does not";
|
||||
}
|
||||
return Move(detail) +
|
||||
". GL_ARB_gpu_shader_fp64 is not advertised, because the precision it promises is the "
|
||||
"one thing the demotion cannot provide; set MOBILEGL_ADVERTISE_FP64=1 to advertise it "
|
||||
"anyway";
|
||||
}
|
||||
|
||||
struct ReportBuilder {
|
||||
BackendPostReport report;
|
||||
Bool fatalFailed = false;
|
||||
@@ -336,8 +352,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Pass("GL_EXT_base_instance", "supported (native baseInstance draws)");
|
||||
} else {
|
||||
builder.Info("GL_EXT_base_instance",
|
||||
"not supported; no impact: the native indirect path deliberately does not "
|
||||
"rely on it (shader-side emulation handles baseInstance semantics)");
|
||||
"not supported; direct baseInstance draws are emulated by shifting the "
|
||||
"instanced arrays' attribute offsets, and gl_BaseInstance by a uniform. "
|
||||
"The one gap is an INDIRECT draw whose command carries a non-zero "
|
||||
"baseInstance and is executed natively: its vertex fetch is not shifted");
|
||||
}
|
||||
// Both multi-draw rows gate on the capability flags, not the entry-point pointers:
|
||||
// eglGetProcAddress may hand back a non-NULL stub for these on drivers without the
|
||||
@@ -484,14 +502,24 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Reported rather than probed: this one cannot come out any other way. OpenGL ES has no
|
||||
// double-precision vertex format and ESSL has no fp64 type, so there is no driver and no
|
||||
// extension that could make it work - the row exists so the loss is named at startup
|
||||
// instead of discovered as an unexplained GL_INVALID_OPERATION at draw setup.
|
||||
// Both rows are reported rather than probed: neither can come out any other way.
|
||||
// ESSL has no 64-bit float type at all, so no driver and no extension could change
|
||||
// either answer, and the rows exist so the two halves of the loss are named at
|
||||
// startup instead of discovered as a shader that will not compile or an
|
||||
// unexplained GL_INVALID_OPERATION at draw setup.
|
||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
||||
"demoted to fp32 - ESSL has no 64-bit float type, so every double / "
|
||||
"dvec / dmat in a shader is narrowed to 32 bits before transpilation "
|
||||
"(DemoteFloat64Pass). Such shaders COMPILE AND RUN, at single "
|
||||
"precision; a block containing a double is re-laid-out for the "
|
||||
"narrowed members, so an application that hard-codes std140 offsets "
|
||||
"computed for doubles must query them instead"));
|
||||
builder.Warn("64-bit vertex attributes",
|
||||
"not supported on any GLES driver (ES has no GL_DOUBLE vertex format and ESSL has "
|
||||
"no fp64 type); glVertexAttribLFormat / glVertexArrayAttribLFormat report "
|
||||
"GL_INVALID_OPERATION - use the Vulkan backend if the application needs them");
|
||||
"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); "
|
||||
"glVertexAttribLFormat / glVertexArrayAttribLFormat report "
|
||||
"GL_INVALID_OPERATION - feed the attribute with glVertexAttribPointer(GL_FLOAT), "
|
||||
"which a demoted dvec input reads correctly");
|
||||
if (glesFuncs.glPatchParameteri != nullptr) {
|
||||
builder.Pass("Tessellation patch parameters",
|
||||
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
|
||||
@@ -1843,14 +1871,25 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, so sampling "
|
||||
"one reads nothing and glFramebufferTextureLayer on one is declined");
|
||||
}
|
||||
if (features.shaderFloat64 == VK_TRUE) {
|
||||
builder.Pass("shaderFloat64",
|
||||
"GLSL double/dvec/dmat and 64-bit vertex attributes (glVertexAttribLFormat) supported");
|
||||
} else {
|
||||
builder.Warn("shaderFloat64",
|
||||
"unsupported; any shader declaring a double fails to create a shader module, and "
|
||||
"glVertexAttribLFormat reports GL_INVALID_OPERATION instead of feeding the attribute");
|
||||
}
|
||||
// Reported whichever way the device answers, because MobileGL no longer follows the
|
||||
// device here: every 64-bit float is narrowed to 32 bits before any module reaches this
|
||||
// backend (DemoteFloat64Pass), so the Float64 capability is never declared and a device
|
||||
// that HAS the feature gains nothing from it. The device's own answer is still worth
|
||||
// printing - it is the reason the demotion is unconditional.
|
||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
||||
format("demoted to fp32 (device shaderFloat64 = {}) - every double / dvec / "
|
||||
"dmat in a shader is narrowed to 32 bits before pipeline creation, so "
|
||||
"such shaders BUILD AND RUN at single precision on every device "
|
||||
"instead of failing to create a shader module on the ones without the "
|
||||
"feature. A block containing a double is re-laid-out for the narrowed "
|
||||
"members, so an application that hard-codes std140 offsets computed "
|
||||
"for doubles must query them instead",
|
||||
features.shaderFloat64 == VK_TRUE ? "supported" : "unsupported")));
|
||||
builder.Warn("64-bit vertex attributes",
|
||||
"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 "
|
||||
"anyway. glVertexAttribLFormat reports GL_INVALID_OPERATION - feed the attribute with "
|
||||
"glVertexAttribPointer(GL_FLOAT), which a demoted dvec input reads correctly");
|
||||
|
||||
Bool shaderDrawParameters = false;
|
||||
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
#include "SpirvPasses/RenameBuiltinShadowingFunctionsPass.h"
|
||||
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
||||
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
||||
#include "SpirvPasses/DemoteFloat64Pass.h"
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||
#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h"
|
||||
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
@@ -576,6 +580,37 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleDeclaresFloat64(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
// Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced
|
||||
// no SPIR-V is not a verdict about 64-bit floats, and parsing it would push a
|
||||
// spurious diagnostic through the message consumer.
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresFloat64"), spirv.data(),
|
||||
spirv.size());
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
for (const spvtools::opt::Instruction& type : context->types_values()) {
|
||||
if (type.opcode() == spv::Op::OpTypeFloat && type.NumInOperands() >= 1 &&
|
||||
type.GetSingleWordInOperand(0) == 64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
|
||||
|
||||
return RunOptimizerChecked("DemoteFloat64ToFloat32", optimizer, inputBinary, outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
@@ -614,6 +649,17 @@ namespace MobileGL {
|
||||
RenameBuiltinShadowingFunctionsPass::CreateRenameBuiltinShadowingFunctionsPass());
|
||||
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
||||
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
||||
// No mobile GPU has 64-bit floats: Adreno and Mali both report shaderFloat64 ==
|
||||
// VK_FALSE, and ESSL has no fp64 type for SPIRV-Cross to emit. Demoting here - in
|
||||
// the one chain every module goes through, on both backends, at link - is what
|
||||
// makes `double` compile at all, and makes it behave the SAME everywhere, which
|
||||
// matters because the GL frontend's uniform storage cannot be per-backend: the
|
||||
// glUniform*d shadow narrows to float unconditionally to match this. Runs last so
|
||||
// no earlier pass ever has to reason about a width it will not see in the output;
|
||||
// in particular it runs before the backends' PackDoubleVertexInputsPass, whose
|
||||
// OpBitcast this one would otherwise decline on. Costs one types_values() walk on
|
||||
// the overwhelming majority of modules, which declare no 64-bit float at all.
|
||||
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
|
||||
|
||||
return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
@@ -629,6 +675,36 @@ namespace MobileGL {
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(SplitArrayVertexInputsPass::CreateSplitArrayVertexInputsPass());
|
||||
|
||||
return RunOptimizerChecked("SplitArrayVertexInputsForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
|
||||
const std::set<String>& blockNames,
|
||||
std::set<String>& flattenedBlockNames,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
if (blockNames.empty()) return false;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(FlattenXfbInterfaceBlocksPass::CreateFlattenXfbInterfaceBlocksPass(
|
||||
blockNames, &flattenedBlockNames));
|
||||
|
||||
return RunOptimizerChecked("FlattenXfbInterfaceBlocksForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(
|
||||
const String& captureName, const std::set<String>& flattenedBlockNames, String& outName) {
|
||||
return FlattenXfbInterfaceBlocksPass::RewriteCaptureName(captureName, flattenedBlockNames,
|
||||
outName);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
@@ -758,6 +834,15 @@ namespace MobileGL {
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(ZeroBaseVertexPass::CreateZeroBaseVertexPass());
|
||||
|
||||
return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "glslang/TVarEntryInfo.h"
|
||||
#include "glslang/TMglGlslIoResolver.h"
|
||||
|
||||
#include <set>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
@@ -27,6 +29,30 @@ namespace MobileGL {
|
||||
// Only for backends without native draw-parameter support (DirectGLES).
|
||||
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Replaces an ARRAY vertex input with one input per element at consecutive
|
||||
// locations, seeding a Private copy of the array so indexed reads still work.
|
||||
// GLSL ES has no array vertex inputs and SPIRV-Cross refuses the whole module
|
||||
// rather than emulating them, so without this the stage never reaches the
|
||||
// driver. Only for the DirectGLES transpile path.
|
||||
static bool SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Replaces the named interface BLOCKS with one variable per member, named
|
||||
// "<Block>_<member>", shadowing the block itself so the body is untouched. The
|
||||
// Adreno ES driver silently captures NOTHING for a transform-feedback varying
|
||||
// named as a block member, so a capture list that names one has to be respelled
|
||||
// - and the declaration with it. `flattenedBlockNames` reports which blocks
|
||||
// this stage actually rewrote, which is what the capture list must follow.
|
||||
// Only for the DirectGLES transpile path.
|
||||
static bool FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
|
||||
const std::set<String>& blockNames,
|
||||
std::set<String>& flattenedBlockNames,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// The capture request "StageData.attrib[0]" as the pass above renamed it,
|
||||
// "StageData_attrib[0]", or false when it does not name a member of a block
|
||||
// that was flattened.
|
||||
static bool RewriteXfbCaptureNameForFlattenedBlock(const String& captureName,
|
||||
const std::set<String>& flattenedBlockNames,
|
||||
String& outName);
|
||||
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
||||
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
||||
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
||||
@@ -65,6 +91,12 @@ namespace MobileGL {
|
||||
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Builds the non-indexed-draw variant of a vertex shader: every gl_BaseVertex
|
||||
// read becomes zero, which is what GL defines for a command carrying no
|
||||
// baseVertex parameter while Vulkan's builtin would report firstVertex.
|
||||
// See ZeroBaseVertexPass.
|
||||
static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
|
||||
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
|
||||
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
|
||||
@@ -87,6 +119,12 @@ namespace MobileGL {
|
||||
// reinterpretation paths (for example, R32F storage accessed as r32ui).
|
||||
static bool UseUnformattedFloatStorageImagesForVulkan(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
|
||||
// Rewrites every 64-bit float in the module to a 32-bit one, preserving every
|
||||
// block offset and stride exactly (see DemoteFloat64Pass). Already part of
|
||||
// SanitizeAndOptimizeBinary, which is where production reaches it; exposed
|
||||
// separately so a test can drive the demotion on its own.
|
||||
static bool DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
static Result<String> DecompileShader(SpvcSession& session);
|
||||
|
||||
// Parses one trivial shader in each configuration the production path can
|
||||
@@ -146,6 +184,12 @@ namespace MobileGL {
|
||||
// check exists so that failure can be reported as the missing capability it is,
|
||||
// naming the shader, rather than as a driver info log nobody sees.
|
||||
static Bool ModuleDeclaresBufferTextureSampler(const Vector<Uint32>& spirv);
|
||||
|
||||
// True when the module still declares a 64-bit float type. After
|
||||
// SanitizeAndOptimizeBinary that can only mean DemoteFloat64Pass declined the
|
||||
// module (see its header for the two operations that make it decline), which is
|
||||
// what the backends report: no mobile driver can build such a module.
|
||||
static Bool ModuleDeclaresFloat64(const Vector<Uint32>& spirv);
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.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 "DemoteFloat64Pass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/latest_version_glsl_std_450_header.h"
|
||||
#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 <algorithm>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
constexpr Uint32 kFloat64Width = 64;
|
||||
constexpr Uint32 kFloat32Width = 32;
|
||||
|
||||
// OpTypeFloat <id> <Width>. SPIR-V 1.6 added an optional FP Encoding operand after
|
||||
// it; the width stays operand 0 either way, and an encoded (non-IEEE) float is not
|
||||
// something glslang can emit for `double`, so it is left alone by the width guard.
|
||||
Bool IsFloatTypeOfWidth(const Instruction& type, Uint32 width) {
|
||||
return type.opcode() == spv::Op::OpTypeFloat && type.NumInOperands() >= 1 &&
|
||||
type.GetSingleWordInOperand(0) == width;
|
||||
}
|
||||
|
||||
// Exactly the types spirv-val forbids a second declaration of. Aggregates - arrays
|
||||
// and structs - are excluded on purpose: the spec permits duplicates of those, and
|
||||
// merging them would take one of the two OpNames and one of the two ArrayStride /
|
||||
// Offset decoration sets with it.
|
||||
Bool IsDuplicableType(spv::Op opcode) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpTypeFloat:
|
||||
case spv::Op::OpTypeVector:
|
||||
case spv::Op::OpTypeMatrix:
|
||||
case spv::Op::OpTypePointer:
|
||||
case spv::Op::OpTypeFunction: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 RoundUp(Uint32 value, Uint32 alignment) {
|
||||
if (alignment == 0) return value;
|
||||
return (value + alignment - 1) / alignment * alignment;
|
||||
}
|
||||
|
||||
// Re-derives the std140 / std430 layout of a block whose members have just become
|
||||
// 32 bits wide, and writes it back as Offset / ArrayStride / MatrixStride.
|
||||
//
|
||||
// Why the layout is recomputed rather than preserved. Preserving it - leaving each
|
||||
// member at the byte offset glslang picked for the 64-bit type and letting the
|
||||
// freed 4 bytes become padding - keeps the application's byte layout intact and is
|
||||
// the obvious first choice, but it does not survive contact with the Espryt path:
|
||||
// SPIRV-Cross has to print an ESSL block, GLSL ES has no member `layout(offset=)`
|
||||
// (no ARB_enhanced_layouts), so it refuses any block whose declared offsets are not
|
||||
// exactly what std140 or std430 computes - "Buffer block cannot be expressed as any
|
||||
// of std430, std140, scalar". Every shader with a double in a block would fail to
|
||||
// transpile at all, which is the case this whole demotion exists to fix. Padding
|
||||
// members back in cannot rescue it either: a dmat4 member carries MatrixStride 32
|
||||
// and std140 demands 16 for the demoted mat4, and no amount of padding BETWEEN
|
||||
// members changes a stride INSIDE one.
|
||||
//
|
||||
// What recomputing costs: a block laid out for 64-bit members changes its
|
||||
// driver-visible byte layout, so an application that hard-codes std140 offsets
|
||||
// computed for doubles addresses the wrong bytes. Applications that query their
|
||||
// offsets are unaffected, and MobileGL's own default-uniform block is unaffected by
|
||||
// construction - the frontend builds its uniform routing by reflecting THIS module
|
||||
// (ProgramSpirvTask::BuildGlobalUboRouting), so glUniform*d writes wherever the
|
||||
// demoted shader reads.
|
||||
class BlockRelayout {
|
||||
public:
|
||||
BlockRelayout(IRContext* irContext, Bool std140)
|
||||
: m_irContext(irContext), m_std140(std140) {}
|
||||
|
||||
// Size and alignment of `typeId`, applying every stride decoration it implies
|
||||
// on the way down. Zero size means "not a type this layout knows how to
|
||||
// describe"; the caller then leaves the block alone rather than guessing.
|
||||
struct Extent {
|
||||
Uint32 size = 0;
|
||||
Uint32 alignment = 0;
|
||||
};
|
||||
|
||||
Extent Measure(Uint32 typeId) {
|
||||
const auto memo = m_extents.find(typeId);
|
||||
if (memo != m_extents.end()) return memo->second;
|
||||
|
||||
const Extent extent = MeasureUncached(typeId);
|
||||
m_extents.emplace(typeId, extent);
|
||||
return extent;
|
||||
}
|
||||
|
||||
private:
|
||||
Extent MeasureUncached(Uint32 typeId) {
|
||||
const Instruction* type = m_irContext->get_def_use_mgr()->GetDef(typeId);
|
||||
if (type == nullptr) return {};
|
||||
|
||||
switch (type->opcode()) {
|
||||
case spv::Op::OpTypeInt:
|
||||
case spv::Op::OpTypeFloat: {
|
||||
const Uint32 bytes = type->GetSingleWordInOperand(0) / 8;
|
||||
return {bytes, bytes};
|
||||
}
|
||||
case spv::Op::OpTypeBool: return {4, 4};
|
||||
case spv::Op::OpTypeVector: {
|
||||
const Extent component = Measure(type->GetSingleWordInOperand(0));
|
||||
if (component.size == 0) return {};
|
||||
const Uint32 count = type->GetSingleWordInOperand(1);
|
||||
// A three-component vector aligns like a four-component one.
|
||||
return {component.size * count,
|
||||
component.alignment * (count == 3 ? 4 : count)};
|
||||
}
|
||||
case spv::Op::OpTypeMatrix: {
|
||||
const Extent column = Measure(type->GetSingleWordInOperand(0));
|
||||
if (column.size == 0) return {};
|
||||
const Uint32 stride = MatrixOrArrayStride(column.alignment);
|
||||
return {stride * type->GetSingleWordInOperand(1), stride};
|
||||
}
|
||||
case spv::Op::OpTypeArray:
|
||||
case spv::Op::OpTypeRuntimeArray: {
|
||||
const Extent element = Measure(type->GetSingleWordInOperand(0));
|
||||
if (element.size == 0) return {};
|
||||
const Uint32 alignment = MatrixOrArrayStride(element.alignment);
|
||||
const Uint32 stride = RoundUp(element.size, alignment);
|
||||
SetTypeDecoration(typeId, spv::Decoration::ArrayStride, stride);
|
||||
if (type->opcode() == spv::Op::OpTypeRuntimeArray) {
|
||||
// An unsized array contributes its stride and nothing more; the
|
||||
// block's size is whatever the application bound.
|
||||
return {stride, alignment};
|
||||
}
|
||||
return {stride * ArrayLength(type->GetSingleWordInOperand(1)), alignment};
|
||||
}
|
||||
case spv::Op::OpTypeStruct: return MeasureStruct(*type);
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
|
||||
Extent MeasureStruct(const Instruction& structType) {
|
||||
Uint32 cursor = 0;
|
||||
Uint32 alignment = m_std140 ? 16u : 1u;
|
||||
for (Uint32 member = 0; member < structType.NumInOperands(); ++member) {
|
||||
const Uint32 memberTypeId = structType.GetSingleWordInOperand(member);
|
||||
const Extent extent = Measure(memberTypeId);
|
||||
if (extent.size == 0) return {};
|
||||
|
||||
const Uint32 offset = RoundUp(cursor, extent.alignment);
|
||||
SetMemberDecoration(structType.result_id(), member, spv::Decoration::Offset, offset);
|
||||
// A matrix member carries the stride between its columns on the MEMBER,
|
||||
// not on the type, so it has to be (re)stated here - including for a
|
||||
// matrix reached through an array.
|
||||
const Instruction* memberType =
|
||||
m_irContext->get_def_use_mgr()->GetDef(PeelArrays(memberTypeId));
|
||||
if (memberType != nullptr && memberType->opcode() == spv::Op::OpTypeMatrix) {
|
||||
const Extent column = Measure(memberType->GetSingleWordInOperand(0));
|
||||
SetMemberDecoration(structType.result_id(), member,
|
||||
spv::Decoration::MatrixStride,
|
||||
MatrixOrArrayStride(column.alignment));
|
||||
}
|
||||
|
||||
cursor = offset + extent.size;
|
||||
alignment = std::max(alignment, extent.alignment);
|
||||
}
|
||||
return {RoundUp(cursor, alignment), alignment};
|
||||
}
|
||||
|
||||
// std140 rounds every array and matrix stride up to a four-component vector.
|
||||
Uint32 MatrixOrArrayStride(Uint32 elementAlignment) const {
|
||||
return m_std140 ? RoundUp(elementAlignment, 16) : elementAlignment;
|
||||
}
|
||||
|
||||
Uint32 PeelArrays(Uint32 typeId) const {
|
||||
const Instruction* type = m_irContext->get_def_use_mgr()->GetDef(typeId);
|
||||
while (type != nullptr && (type->opcode() == spv::Op::OpTypeArray ||
|
||||
type->opcode() == spv::Op::OpTypeRuntimeArray)) {
|
||||
type = m_irContext->get_def_use_mgr()->GetDef(type->GetSingleWordInOperand(0));
|
||||
}
|
||||
return type != nullptr ? type->result_id() : 0;
|
||||
}
|
||||
|
||||
Uint32 ArrayLength(Uint32 lengthConstantId) const {
|
||||
const Instruction* length = m_irContext->get_def_use_mgr()->GetDef(lengthConstantId);
|
||||
if (length == nullptr || length->opcode() != spv::Op::OpConstant ||
|
||||
length->NumInOperands() < 1) {
|
||||
return 1;
|
||||
}
|
||||
return length->GetSingleWordInOperand(0);
|
||||
}
|
||||
|
||||
void SetTypeDecoration(Uint32 targetId, spv::Decoration decoration, Uint32 value) {
|
||||
for (Instruction& annotation : m_irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != targetId) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) != decoration) {
|
||||
continue;
|
||||
}
|
||||
annotation.SetInOperand(2, {value});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SetMemberDecoration(Uint32 structId, Uint32 member, spv::Decoration decoration,
|
||||
Uint32 value) {
|
||||
for (Instruction& annotation : m_irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpMemberDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != structId) continue;
|
||||
if (annotation.GetSingleWordInOperand(1) != member) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) != decoration) {
|
||||
continue;
|
||||
}
|
||||
annotation.SetInOperand(3, {value});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
IRContext* m_irContext = nullptr;
|
||||
Bool m_std140 = true;
|
||||
std::unordered_map<Uint32, Extent> m_extents;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status DemoteFloat64Pass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
|
||||
// --- 1. the leaf types -------------------------------------------------------
|
||||
std::vector<Instruction*> float64Types;
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (IsFloatTypeOfWidth(type, kFloat64Width)) {
|
||||
float64Types.push_back(&type);
|
||||
}
|
||||
}
|
||||
if (float64Types.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
std::unordered_set<Uint32> float64TypeIds;
|
||||
for (const Instruction* type : float64Types) {
|
||||
float64TypeIds.insert(type->result_id());
|
||||
}
|
||||
|
||||
// Every type a *value* can have that is 64-bit float underneath: the scalar itself
|
||||
// plus the vectors and matrices built from it. The types-and-values section is in
|
||||
// declaration order and SPIR-V forbids a type from forward-referencing another, so
|
||||
// one forward walk is already the complete transitive closure. Aggregates and
|
||||
// pointers are deliberately not included: no operand of the instructions checked
|
||||
// below is ever an aggregate or a pointer.
|
||||
std::unordered_set<Uint32> float64ValueTypeIds = float64TypeIds;
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (type.opcode() != spv::Op::OpTypeVector && type.opcode() != spv::Op::OpTypeMatrix) {
|
||||
continue;
|
||||
}
|
||||
if (float64ValueTypeIds.count(type.GetSingleWordInOperand(0)) != 0) {
|
||||
float64ValueTypeIds.insert(type.result_id());
|
||||
}
|
||||
}
|
||||
|
||||
// Every type that has a 64-bit float anywhere underneath it, which is exactly the
|
||||
// set of blocks whose layout has to be re-derived once the leaves narrow. Computed
|
||||
// now, before the rewrite makes a demoted float indistinguishable from one that was
|
||||
// always 32 bits; the same single forward walk is a complete closure.
|
||||
std::unordered_set<Uint32> wideTypeIds = float64TypeIds;
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
const auto contains = [&](Uint32 operand) {
|
||||
return wideTypeIds.count(type.GetSingleWordInOperand(operand)) != 0;
|
||||
};
|
||||
switch (type.opcode()) {
|
||||
case spv::Op::OpTypeVector:
|
||||
case spv::Op::OpTypeMatrix:
|
||||
case spv::Op::OpTypeArray:
|
||||
case spv::Op::OpTypeRuntimeArray:
|
||||
if (contains(0)) wideTypeIds.insert(type.result_id());
|
||||
break;
|
||||
case spv::Op::OpTypePointer:
|
||||
if (contains(1)) wideTypeIds.insert(type.result_id());
|
||||
break;
|
||||
case spv::Op::OpTypeStruct:
|
||||
for (Uint32 member = 0; member < type.NumInOperands(); ++member) {
|
||||
if (contains(member)) {
|
||||
wideTypeIds.insert(type.result_id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto valueIsFloat64 = [&](Uint32 id) {
|
||||
const Instruction* def = defUseMgr->GetDef(id);
|
||||
return def != nullptr && float64ValueTypeIds.count(def->type_id()) != 0;
|
||||
};
|
||||
|
||||
// --- 2. decline before touching anything ------------------------------------
|
||||
// These are the operations that mean "the 64 bits themselves", not "a wide float".
|
||||
// Narrowing one side of them produces a module spirv-val rejects, and rebuilding
|
||||
// the value would mean emulating fp64 in software. Bail out with the module
|
||||
// byte-identical instead; the caller still has its "module declares Float64"
|
||||
// diagnostic for it.
|
||||
Uint32 glslStd450SetId = 0;
|
||||
for (const Instruction& import : irContext->ext_inst_imports()) {
|
||||
if (import.GetInOperand(0).AsString() == "GLSL.std.450") {
|
||||
glslStd450SetId = import.result_id();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const char* declineReason = nullptr;
|
||||
irContext->module()->ForEachInst(
|
||||
[&](Instruction* inst) {
|
||||
if (declineReason != nullptr) return;
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpBitcast: {
|
||||
// "Total bit width of Result Type and Operand must match" - true
|
||||
// today, false the moment one of the two sides halves.
|
||||
const Bool resultIs64 = float64ValueTypeIds.count(inst->type_id()) != 0;
|
||||
const Bool operandIs64 = valueIsFloat64(inst->GetSingleWordInOperand(0));
|
||||
if (resultIs64 != operandIs64) {
|
||||
declineReason = "an OpBitcast across the 64-bit boundary "
|
||||
"(doubleBitsToUint64 / uint64BitsToDouble / "
|
||||
"packDouble2x32)";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case spv::Op::OpExtInst: {
|
||||
if (glslStd450SetId == 0 ||
|
||||
inst->GetSingleWordInOperand(0) != glslStd450SetId) {
|
||||
break;
|
||||
}
|
||||
const Uint32 extOpcode = inst->GetSingleWordInOperand(1);
|
||||
if (extOpcode == GLSLstd450PackDouble2x32 ||
|
||||
extOpcode == GLSLstd450UnpackDouble2x32) {
|
||||
declineReason = "GLSL.std.450 PackDouble2x32 / UnpackDouble2x32, "
|
||||
"which are defined only for a 64-bit float";
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
},
|
||||
/*run_on_debug_line_insts=*/false);
|
||||
|
||||
if (declineReason != nullptr) {
|
||||
MGLOG_D("DemoteFloat64Pass: declined - the module uses %s; its 64-bit floats are "
|
||||
"left in place",
|
||||
declineReason);
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// --- 3. re-encode the literals ----------------------------------------------
|
||||
// A 64-bit float constant carries two literal words and a 32-bit one carries a
|
||||
// single word, so the value has to be narrowed before the type changes underneath
|
||||
// it - afterwards there is no way left to tell how wide the literal was meant to
|
||||
// be. Composite constants hold <id>s, not literals, and need nothing.
|
||||
for (Instruction& value : irContext->types_values()) {
|
||||
if (value.opcode() != spv::Op::OpConstant && value.opcode() != spv::Op::OpSpecConstant) {
|
||||
continue;
|
||||
}
|
||||
if (float64TypeIds.count(value.type_id()) == 0) continue;
|
||||
if (value.NumInOperands() < 1) continue;
|
||||
const Operand& literal = value.GetInOperand(0);
|
||||
if (literal.words.size() != 2) continue;
|
||||
|
||||
const Uint64 bits =
|
||||
(static_cast<Uint64>(literal.words[1]) << 32) | static_cast<Uint64>(literal.words[0]);
|
||||
double wide = 0.0;
|
||||
std::memcpy(&wide, &bits, sizeof(wide));
|
||||
// Deliberately the ordinary narrowing conversion: a magnitude no float can
|
||||
// hold becomes an infinity, which is the same answer the demoted arithmetic
|
||||
// around it would produce.
|
||||
const float narrow = static_cast<float>(wide);
|
||||
Uint32 narrowedBits = 0;
|
||||
std::memcpy(&narrowedBits, &narrow, sizeof(narrowedBits));
|
||||
|
||||
Operand narrowedLiteral = literal;
|
||||
narrowedLiteral.words = {narrowedBits};
|
||||
value.SetInOperands({std::move(narrowedLiteral)});
|
||||
}
|
||||
|
||||
// --- 4. the demotion itself --------------------------------------------------
|
||||
// In place, so every composite type, every pointer, every struct member offset and
|
||||
// every debug name that referred to the 64-bit type keeps referring to the same
|
||||
// <id>. This is the whole reason the pass does not build parallel types.
|
||||
for (Instruction* type : float64Types) {
|
||||
type->SetInOperand(0, {kFloat32Width});
|
||||
}
|
||||
// The cached analysis::Type objects were built against the old widths.
|
||||
irContext->InvalidateAnalyses(IRContext::kAnalysisTypes);
|
||||
|
||||
// --- 5. the conversions that just became identities --------------------------
|
||||
// `float(someDouble)` and `double(someFloat)` are both OpFConvert, and SPIR-V
|
||||
// requires the two component widths to differ. Both sides are 32 bits now, so each
|
||||
// one is replaced by its operand. Walking in module order means a chain of them
|
||||
// resolves in a single sweep: by the time the second is reached its operand has
|
||||
// already been rewritten to the ultimate source.
|
||||
const auto componentWidth = [&](Uint32 typeId) -> Uint32 {
|
||||
const Instruction* def = defUseMgr->GetDef(typeId);
|
||||
if (def == nullptr) return 0;
|
||||
if (def->opcode() == spv::Op::OpTypeVector) {
|
||||
def = defUseMgr->GetDef(def->GetSingleWordInOperand(0));
|
||||
}
|
||||
if (def == nullptr || def->opcode() != spv::Op::OpTypeFloat) return 0;
|
||||
return def->GetSingleWordInOperand(0);
|
||||
};
|
||||
|
||||
std::vector<Instruction*> identityConversions;
|
||||
irContext->module()->ForEachInst(
|
||||
[&](Instruction* inst) {
|
||||
if (inst->opcode() != spv::Op::OpFConvert) return;
|
||||
const Instruction* operandDef = defUseMgr->GetDef(inst->GetSingleWordInOperand(0));
|
||||
if (operandDef == nullptr) return;
|
||||
const Uint32 resultWidth = componentWidth(inst->type_id());
|
||||
if (resultWidth == 0 || resultWidth != componentWidth(operandDef->type_id())) {
|
||||
return;
|
||||
}
|
||||
identityConversions.push_back(inst);
|
||||
},
|
||||
/*run_on_debug_line_insts=*/false);
|
||||
|
||||
for (Instruction* conversion : identityConversions) {
|
||||
irContext->ReplaceAllUsesWith(conversion->result_id(),
|
||||
conversion->GetSingleWordInOperand(0));
|
||||
irContext->KillInst(conversion);
|
||||
}
|
||||
|
||||
// --- 6. the capability -------------------------------------------------------
|
||||
std::vector<Instruction*> deadCapabilities;
|
||||
for (Instruction& capability : irContext->capabilities()) {
|
||||
if (static_cast<spv::Capability>(capability.GetSingleWordInOperand(0)) ==
|
||||
spv::Capability::Float64) {
|
||||
deadCapabilities.push_back(&capability);
|
||||
}
|
||||
}
|
||||
for (Instruction* capability : deadCapabilities) {
|
||||
irContext->KillInst(capability);
|
||||
}
|
||||
|
||||
// --- 7. merge what the rewrite made into a duplicate -------------------------
|
||||
// `double` and `float` are now the same declaration, and so is every vector,
|
||||
// matrix, pointer and function type spelled in terms of them. Walking the section
|
||||
// in declaration order and replacing each duplicate the moment it is found means
|
||||
// the later types are already canonical by the time they are keyed: SPIR-V forbids
|
||||
// a type from forward-referencing another, so every operand of the instruction
|
||||
// being looked at has been through this loop already.
|
||||
std::map<std::vector<Uint32>, Uint32> keptTypeByShape;
|
||||
for (Instruction* type = &*irContext->types_values_begin(); type != nullptr;) {
|
||||
Instruction* next = type->NextNode();
|
||||
if (!IsDuplicableType(type->opcode())) {
|
||||
type = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<Uint32> shape{static_cast<Uint32>(type->opcode())};
|
||||
for (Uint32 i = 0; i < type->NumInOperands(); ++i) {
|
||||
const Operand& operand = type->GetInOperand(i);
|
||||
shape.insert(shape.end(), operand.words.begin(), operand.words.end());
|
||||
}
|
||||
|
||||
const auto kept = keptTypeByShape.find(shape);
|
||||
if (kept == keptTypeByShape.end()) {
|
||||
keptTypeByShape.emplace(std::move(shape), type->result_id());
|
||||
type = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
irContext->KillNamesAndDecorates(type->result_id());
|
||||
irContext->ReplaceAllUsesWith(type->result_id(), kept->second);
|
||||
irContext->KillInst(type);
|
||||
type = next;
|
||||
}
|
||||
|
||||
// --- 8. re-derive the layout of every block that held a 64-bit float -----------
|
||||
// See BlockRelayout for why this is a recomputation and not a preservation. Only
|
||||
// blocks that actually narrowed are touched: relaying out an untouched block would
|
||||
// be pure churn, and would risk disagreeing with glslang over a layout that was
|
||||
// already correct.
|
||||
for (Instruction& variable : irContext->types_values()) {
|
||||
if (variable.opcode() != spv::Op::OpVariable) continue;
|
||||
const auto storageClass =
|
||||
static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0));
|
||||
if (storageClass != spv::StorageClass::Uniform &&
|
||||
storageClass != spv::StorageClass::StorageBuffer &&
|
||||
storageClass != spv::StorageClass::PushConstant) {
|
||||
continue;
|
||||
}
|
||||
const Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
|
||||
if (pointerType == nullptr) continue;
|
||||
|
||||
// An arrayed block (`uniform Blk { ... } blocks[4];`) is a pointer to an array
|
||||
// of the struct; the layout lives on the struct either way.
|
||||
const Instruction* blockType = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
|
||||
while (blockType != nullptr && (blockType->opcode() == spv::Op::OpTypeArray ||
|
||||
blockType->opcode() == spv::Op::OpTypeRuntimeArray)) {
|
||||
blockType = defUseMgr->GetDef(blockType->GetSingleWordInOperand(0));
|
||||
}
|
||||
if (blockType == nullptr || blockType->opcode() != spv::Op::OpTypeStruct) continue;
|
||||
if (wideTypeIds.count(blockType->result_id()) == 0) continue;
|
||||
|
||||
// A storage block packs std430, a uniform block std140. Before SPIR-V 1.3 a
|
||||
// storage block was a Uniform-storage variable whose struct carried
|
||||
// BufferBlock, so the decoration decides rather than the storage class alone.
|
||||
Bool isStorageBlock = storageClass == spv::StorageClass::StorageBuffer;
|
||||
for (const Instruction& annotation : irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != blockType->result_id()) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::BufferBlock) {
|
||||
isStorageBlock = true;
|
||||
}
|
||||
}
|
||||
|
||||
BlockRelayout relayout(irContext, /*std140=*/!isStorageBlock);
|
||||
if (relayout.Measure(blockType->result_id()).size == 0) {
|
||||
// A member shape the layout rules here do not describe. Leaving the block
|
||||
// at its 64-bit offsets keeps the module valid for Vulkan; SPIRV-Cross will
|
||||
// decline it for ESSL, which is the same outcome as before the demotion.
|
||||
MGLOG_D("DemoteFloat64Pass: block %%%u contains a member this pass cannot lay "
|
||||
"out; its 64-bit offsets are left in place",
|
||||
blockType->result_id());
|
||||
}
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken DemoteFloat64Pass::CreateDemoteFloat64Pass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<DemoteFloat64Pass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,84 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.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>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Rewrites every 64-bit float in the module to a 32-bit one: `OpTypeFloat 64` becomes
|
||||
// `OpTypeFloat 32` in place, so every vector, matrix, array, struct, pointer and
|
||||
// function type that named it keeps its <id> and every decoration attached to it, and
|
||||
// only the meaning of the leaf type changes. The double literals are re-encoded, the
|
||||
// now-width-preserving OpFConvert pairs collapse to their operand, and the Float64
|
||||
// capability goes away.
|
||||
//
|
||||
// Why demote at all: no mobile GPU has it. Adreno and Mali both report
|
||||
// VkPhysicalDeviceFeatures::shaderFloat64 == VK_FALSE (the Magma POST has a row for it),
|
||||
// so a module declaring Float64 cannot become a pipeline there; and ESSL has no 64-bit
|
||||
// float type at all, so SPIRV-Cross throws "FP64 not supported in ES profile" and the
|
||||
// Espryt path never even reaches the driver. Demotion is what makes `double` in an
|
||||
// application's GLSL compile and run everywhere, at fp32 precision.
|
||||
//
|
||||
// BLOCK LAYOUT IS RE-DERIVED, NOT PRESERVED, and that was not the first choice - see
|
||||
// BlockRelayout in the .cpp for the measurement that forced it. Preserving the 64-bit
|
||||
// offsets (float + 4 bytes of padding in each slot) keeps the application's byte layout
|
||||
// intact and is what a Vulkan-only implementation would do, but GLSL ES has no member
|
||||
// `layout(offset=)`, so SPIRV-Cross recomputes std140/std430 from the declared types
|
||||
// and refuses any block whose stated offsets disagree - "Buffer block cannot be
|
||||
// expressed as any of std430, std140, scalar". Every shader with a double in a block
|
||||
// would then fail to transpile for Espryt at all, which is the case this demotion
|
||||
// exists to fix. Nor can padding members rescue it: a dmat4 member carries
|
||||
// MatrixStride 32 and std140 requires 16 for the demoted mat4, and padding BETWEEN
|
||||
// members cannot change a stride INSIDE one.
|
||||
//
|
||||
// What re-deriving costs, stated plainly: a block that held 64-bit members changes its
|
||||
// driver-visible byte layout, so an application that hard-codes std140 offsets it
|
||||
// computed for doubles addresses the wrong bytes. Applications that query their offsets
|
||||
// are unaffected. MobileGL's own default-uniform block is unaffected by construction:
|
||||
// the frontend builds its uniform routing by reflecting the module this pass produced
|
||||
// (ProgramSpirvTask::BuildGlobalUboRouting), so glUniform*d - which narrows to float
|
||||
// for the same reason - writes exactly where the demoted shader reads. Blocks with no
|
||||
// 64-bit member anywhere are never touched.
|
||||
//
|
||||
// Declines (leaves the module byte-identical, so the caller's existing "this module
|
||||
// still declares Float64" failure path reports it) when the module contains an
|
||||
// operation whose validity depends on the operand really being 64 bits wide:
|
||||
// - OpBitcast across the boundary - packDouble2x32 / doubleBitsToUint64 and friends,
|
||||
// where SPIR-V requires both sides to have the same total bit width;
|
||||
// - GLSL.std.450 PackDouble2x32 / UnpackDouble2x32, which are defined only for a
|
||||
// 64-bit float result/operand.
|
||||
//
|
||||
// ORDERING: must run before PackDoubleVertexInputsPass, which introduces exactly the
|
||||
// OpBitcast this pass declines on. After demotion no 64-bit vertex input is left, so
|
||||
// that pass becomes a no-op rather than a conflict.
|
||||
//
|
||||
// The in-place rewrite creates duplicate type declarations by construction - a module
|
||||
// that had both `double` and `float` ends up with two `OpTypeFloat 32`, and spirv-val
|
||||
// rejects that ("Duplicate non-aggregate type declarations are not allowed") - so the
|
||||
// pass merges them itself afterwards. Deliberately NOT by registering spvtools'
|
||||
// RemoveDuplicates alongside it: that pass also merges structurally identical STRUCTS
|
||||
// and calls KillNamesAndDecorates on the loser, which would silently delete the OpName
|
||||
// of one of two distinct-but-identically-shaped interface blocks - and OpName is how
|
||||
// MobileGL resolves block and varying names. Only the non-aggregate types spirv-val
|
||||
// actually forbids duplicates of are merged here; arrays and structs are left alone,
|
||||
// which also keeps a `double[]`'s ArrayStride from being merged into a `float[]`'s.
|
||||
class DemoteFloat64Pass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-demote-float64"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateDemoteFloat64Pass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,636 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.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 "FlattenXfbInterfaceBlocksPass.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/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
#include "source/util/string_utils.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
// Decorations that describe how ONE member is interpolated or stored, and
|
||||
// therefore have to travel from the block member to the variable that replaces
|
||||
// it. Location is handled separately (it is computed per member); everything
|
||||
// else about a block - Block itself, member Offsets, builtin decorations - is
|
||||
// about the aggregate and stays behind on the shadow.
|
||||
Bool IsMemberDecorationToCarry(spv::Decoration decoration) {
|
||||
switch (decoration) {
|
||||
case spv::Decoration::RelaxedPrecision:
|
||||
case spv::Decoration::Flat:
|
||||
case spv::Decoration::NoPerspective:
|
||||
case spv::Decoration::Centroid:
|
||||
case spv::Decoration::Sample:
|
||||
case spv::Decoration::Invariant:
|
||||
case spv::Decoration::Patch:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Decorations that say something about a member's placement WITHIN the block's
|
||||
// location, which a free-standing variable cannot express the same way. Rather
|
||||
// than move a member to a place the consumer will not look for it, decline.
|
||||
Bool IsMemberDecorationThatBlocksFlattening(spv::Decoration decoration) {
|
||||
switch (decoration) {
|
||||
case spv::Decoration::Component:
|
||||
case spv::Decoration::XfbBuffer:
|
||||
case spv::Decoration::XfbStride:
|
||||
case spv::Decoration::Stream:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Every name the module already spells, so a synthesised "<Block>_<member>" that
|
||||
// would collide with one declines instead of emitting two declarations of the
|
||||
// same identifier (which the driver rejects, taking the whole program with it).
|
||||
std::unordered_set<String> CollectNames(IRContext& irContext) {
|
||||
std::unordered_set<String> names;
|
||||
for (auto& debugInst : irContext.debugs2()) {
|
||||
if (debugInst.opcode() != spv::Op::OpName) continue;
|
||||
names.insert(debugInst.GetInOperand(1).AsString());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
// Locations one value of `type` occupies (GL 4.6 core 11.1.2.1 / 15.2): a
|
||||
// matrix takes one per column, a double-precision vector wider than two takes
|
||||
// two, an array takes its element's span once per element. 0 means "this pass
|
||||
// cannot place it", which declines the whole block rather than guessing.
|
||||
Uint32 LocationSpan(const analysis::Type* type) {
|
||||
if (type == nullptr) return 0;
|
||||
if (type->AsFloat() != nullptr || type->AsInteger() != nullptr ||
|
||||
type->AsBool() != nullptr) {
|
||||
return 1u;
|
||||
}
|
||||
if (const auto* vector = type->AsVector()) {
|
||||
const auto* element = vector->element_type();
|
||||
if (element->AsFloat() == nullptr && element->AsInteger() == nullptr &&
|
||||
element->AsBool() == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
const auto* elementFloat = element->AsFloat();
|
||||
const Bool is64Bit = elementFloat != nullptr && elementFloat->width() == 64;
|
||||
return (is64Bit && vector->element_count() > 2) ? 2u : 1u;
|
||||
}
|
||||
if (const auto* matrix = type->AsMatrix()) {
|
||||
const Uint32 columnSpan = LocationSpan(matrix->element_type());
|
||||
return columnSpan == 0 ? 0 : columnSpan * matrix->element_count();
|
||||
}
|
||||
if (const auto* array = type->AsArray()) {
|
||||
const Uint32 elementSpan = LocationSpan(array->element_type());
|
||||
if (elementSpan == 0) return 0;
|
||||
// A runtime array has no span; a vertex-stage interface never has one.
|
||||
if (!array->length_info().words.empty() &&
|
||||
array->length_info().words[0] !=
|
||||
static_cast<Uint32>(analysis::Array::LengthInfo::kConstant)) {
|
||||
return 0;
|
||||
}
|
||||
// words[0] is the tag, words[1..] the constant value; only a
|
||||
// single-word length can be an array size here.
|
||||
if (array->length_info().words.size() != 2) return 0;
|
||||
const Uint32 count = array->length_info().words[1];
|
||||
return count == 0 ? 0 : elementSpan * count;
|
||||
}
|
||||
// Structs (a nested block member) carry their own layout rules and are not
|
||||
// worth guessing at: declining leaves the module exactly as it was.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The Location a variable carries, or false when it carries none.
|
||||
Bool FindLocationDecoration(IRContext& irContext, Uint32 variableId, Uint32& outLocation) {
|
||||
for (auto& annotation : irContext.annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != variableId) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
||||
spv::Decoration::Location) {
|
||||
continue;
|
||||
}
|
||||
outLocation = annotation.GetSingleWordInOperand(2);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The Location decorating member `member` of struct type `typeId`, if any.
|
||||
Bool FindMemberLocationDecoration(IRContext& irContext, Uint32 typeId, Uint32 member,
|
||||
Uint32& outLocation) {
|
||||
for (auto& annotation : irContext.annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpMemberDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != typeId) continue;
|
||||
if (annotation.GetSingleWordInOperand(1) != member) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) !=
|
||||
spv::Decoration::Location) {
|
||||
continue;
|
||||
}
|
||||
outLocation = annotation.GetSingleWordInOperand(3);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String FindName(IRContext& irContext, Uint32 id) {
|
||||
for (auto& debugInst : irContext.debugs2()) {
|
||||
if (debugInst.opcode() != spv::Op::OpName) continue;
|
||||
if (debugInst.GetSingleWordInOperand(0) != id) continue;
|
||||
return debugInst.GetInOperand(1).AsString();
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
String FindMemberName(IRContext& irContext, Uint32 typeId, Uint32 member) {
|
||||
for (auto& debugInst : irContext.debugs2()) {
|
||||
if (debugInst.opcode() != spv::Op::OpMemberName) continue;
|
||||
if (debugInst.GetSingleWordInOperand(0) != typeId) continue;
|
||||
if (debugInst.GetSingleWordInOperand(1) != member) continue;
|
||||
return debugInst.GetInOperand(2).AsString();
|
||||
}
|
||||
return String();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool FlattenXfbInterfaceBlocksPass::RewriteCaptureName(const String& captureName,
|
||||
const std::set<String>& flattenedBlockNames,
|
||||
String& outName) {
|
||||
const SizeT dot = captureName.find('.');
|
||||
if (dot == String::npos || dot == 0) return false;
|
||||
const String blockName = captureName.substr(0, dot);
|
||||
if (flattenedBlockNames.find(blockName) == flattenedBlockNames.end()) return false;
|
||||
outName = blockName + "_" + captureName.substr(dot + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status FlattenXfbInterfaceBlocksPass::Process() {
|
||||
if (m_blockNames.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
auto* irContext = context();
|
||||
auto entryPoints = irContext->module()->entry_points();
|
||||
if (entryPoints.begin() == entryPoints.end()) return Status::SuccessWithoutChange;
|
||||
Instruction* entryPoint = &*entryPoints.begin();
|
||||
|
||||
// A stage whose outputs are published somewhere other than the end of the entry
|
||||
// point cannot take the shadow-and-copy-out shape: a geometry shader's outputs
|
||||
// are captured by every OpEmitVertex, and a tessellation control shader's are
|
||||
// per-invocation slots of an arrayed interface. Copying at OpReturn would
|
||||
// publish once, at the end, which is silently the wrong data rather than a
|
||||
// failure - so those stages keep their blocks and the capture keeps its
|
||||
// spelling.
|
||||
const auto executionModel =
|
||||
static_cast<spv::ExecutionModel>(entryPoint->GetSingleWordInOperand(0));
|
||||
if (executionModel == spv::ExecutionModel::Geometry ||
|
||||
executionModel == spv::ExecutionModel::TessellationControl) {
|
||||
MGLOG_I("FlattenXfbInterfaceBlocksPass: execution model %u publishes outputs outside "
|
||||
"the entry point's return; leaving its blocks declared as blocks",
|
||||
static_cast<Uint32>(executionModel));
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
|
||||
struct Member {
|
||||
Uint32 typeId = 0;
|
||||
Uint32 interfacePointerTypeId = 0;
|
||||
Uint32 privatePointerTypeId = 0;
|
||||
Uint32 variableId = 0;
|
||||
Uint32 location = 0;
|
||||
Bool hasLocation = false;
|
||||
String name;
|
||||
};
|
||||
struct Target {
|
||||
Instruction* variable = nullptr;
|
||||
Uint32 structTypeId = 0;
|
||||
Uint32 privatePointerTypeId = 0;
|
||||
spv::StorageClass storageClass = spv::StorageClass::Output;
|
||||
String blockName;
|
||||
std::vector<Member> members;
|
||||
};
|
||||
std::vector<Target> targets;
|
||||
const std::unordered_set<String> existingNames = CollectNames(*irContext);
|
||||
|
||||
for (Instruction& inst : irContext->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||
const auto storageClass =
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0));
|
||||
if (storageClass != spv::StorageClass::Input &&
|
||||
storageClass != spv::StorageClass::Output) {
|
||||
continue;
|
||||
}
|
||||
Instruction* pointerType = defUseMgr->GetDef(inst.type_id());
|
||||
if (pointerType == nullptr) continue;
|
||||
const Uint32 pointeeTypeId = pointerType->GetSingleWordInOperand(1);
|
||||
const analysis::Type* pointeeType = typeMgr->GetType(pointeeTypeId);
|
||||
const auto* structType = pointeeType != nullptr ? pointeeType->AsStruct() : nullptr;
|
||||
if (structType == nullptr) continue;
|
||||
|
||||
const String blockName = FindName(*irContext, pointeeTypeId);
|
||||
if (blockName.empty() ||
|
||||
m_blockNames.find(blockName) == m_blockNames.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Target target;
|
||||
target.variable = &inst;
|
||||
target.structTypeId = pointeeTypeId;
|
||||
target.storageClass = storageClass;
|
||||
target.blockName = blockName;
|
||||
|
||||
Uint32 blockLocation = 0;
|
||||
const Bool hasBlockLocation =
|
||||
FindLocationDecoration(*irContext, inst.result_id(), blockLocation);
|
||||
|
||||
Bool usable = true;
|
||||
for (auto& annotation : irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpMemberDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != pointeeTypeId) continue;
|
||||
if (IsMemberDecorationThatBlocksFlattening(
|
||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)))) {
|
||||
usable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Uint32 runningLocation = blockLocation;
|
||||
const auto& memberTypes = structType->element_types();
|
||||
for (Uint32 memberIndex = 0; usable && memberIndex < memberTypes.size(); ++memberIndex) {
|
||||
Member member;
|
||||
member.typeId = typeMgr->GetId(memberTypes[memberIndex]);
|
||||
member.name = FindMemberName(*irContext, pointeeTypeId, memberIndex);
|
||||
if (member.typeId == 0 || member.name.empty() ||
|
||||
existingNames.count(blockName + "_" + member.name) != 0) {
|
||||
usable = false;
|
||||
break;
|
||||
}
|
||||
const Uint32 span = LocationSpan(memberTypes[memberIndex]);
|
||||
if (span == 0) {
|
||||
usable = false;
|
||||
break;
|
||||
}
|
||||
Uint32 memberLocation = 0;
|
||||
if (FindMemberLocationDecoration(*irContext, pointeeTypeId, memberIndex,
|
||||
memberLocation)) {
|
||||
member.location = memberLocation;
|
||||
member.hasLocation = true;
|
||||
} else if (hasBlockLocation) {
|
||||
member.location = runningLocation;
|
||||
member.hasLocation = true;
|
||||
}
|
||||
runningLocation += span;
|
||||
target.members.push_back(member);
|
||||
}
|
||||
if (!usable || target.members.empty()) {
|
||||
MGLOG_I("FlattenXfbInterfaceBlocksPass: block '%s' has a member this pass cannot "
|
||||
"place; leaving it declared as a block",
|
||||
blockName.c_str());
|
||||
continue;
|
||||
}
|
||||
targets.push_back(std::move(target));
|
||||
}
|
||||
|
||||
if (targets.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
// The entry point's first block, past the leading OpVariable run SPIR-V
|
||||
// requires to stay at the top of a function. Resolved BEFORE anything is
|
||||
// mutated: a decline after the rewrite has started would leave a half-converted
|
||||
// module behind, and the optimizer serialises whatever the module holds
|
||||
// regardless of the status this returns.
|
||||
const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||
spvtools::opt::Function* entryFunction = nullptr;
|
||||
for (auto& function : *irContext->module()) {
|
||||
if (function.result_id() == entryFunctionId) {
|
||||
entryFunction = &function;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entryFunction == nullptr || entryFunction->begin() == entryFunction->end()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
{
|
||||
auto& probeBlock = *entryFunction->begin();
|
||||
auto probe = probeBlock.begin();
|
||||
while (probe != probeBlock.end() && probe->opcode() == spv::Op::OpVariable) ++probe;
|
||||
if (probe == probeBlock.end()) return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// Demoting the block changes the STORAGE CLASS of every pointer derived from it,
|
||||
// and a derived pointer's own result type still says Input/Output - which is an
|
||||
// invalid module ("the result pointer storage class and base pointer storage
|
||||
// class in OpAccessChain do not match") that spirv-val rejects and a driver may
|
||||
// silently miscompile. Collected before anything is mutated so an unsupported
|
||||
// use can still decline the whole rewrite rather than leave the module broken.
|
||||
std::unordered_set<Uint32> derivedPointers;
|
||||
for (const auto& target : targets) {
|
||||
derivedPointers.insert(target.variable->result_id());
|
||||
}
|
||||
std::vector<Instruction*> pointersToRetype;
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& inst : block) {
|
||||
const spv::Op opcode = inst.opcode();
|
||||
const Bool indexes = opcode == spv::Op::OpAccessChain ||
|
||||
opcode == spv::Op::OpInBoundsAccessChain ||
|
||||
opcode == spv::Op::OpCopyObject;
|
||||
if (indexes) {
|
||||
if (inst.NumInOperands() > 0 &&
|
||||
derivedPointers.count(inst.GetSingleWordInOperand(0)) != 0) {
|
||||
derivedPointers.insert(inst.result_id());
|
||||
pointersToRetype.push_back(&inst);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Reads and writes are exactly what a shadowed block is for; the
|
||||
// pointer they use does not change shape.
|
||||
if (opcode == spv::Op::OpLoad || opcode == spv::Op::OpStore) continue;
|
||||
for (Uint32 i = 0; i < inst.NumInOperands(); ++i) {
|
||||
const Operand& operand = inst.GetInOperand(i);
|
||||
if (operand.type != SPV_OPERAND_TYPE_ID || operand.words.size() != 1) {
|
||||
continue;
|
||||
}
|
||||
if (derivedPointers.count(operand.words[0]) == 0) continue;
|
||||
MGLOG_I("FlattenXfbInterfaceBlocksPass: interface block %%%u reaches a "
|
||||
"SPIR-V opcode %u that this pass cannot follow; leaving it "
|
||||
"declared as a block",
|
||||
operand.words[0], static_cast<Uint32>(opcode));
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every type this pass names has to exist before the variables that name it.
|
||||
for (auto& target : targets) {
|
||||
target.privatePointerTypeId =
|
||||
typeMgr->FindPointerToType(target.structTypeId, spv::StorageClass::Private);
|
||||
for (auto& member : target.members) {
|
||||
member.interfacePointerTypeId =
|
||||
typeMgr->FindPointerToType(member.typeId, target.storageClass);
|
||||
member.privatePointerTypeId =
|
||||
typeMgr->FindPointerToType(member.typeId, spv::StorageClass::Private);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Operand> interfaceOperands;
|
||||
for (Uint32 i = 0; i < entryPoint->NumInOperands(); ++i) {
|
||||
interfaceOperands.push_back(entryPoint->GetInOperand(i));
|
||||
}
|
||||
|
||||
std::unordered_set<Uint32> strippedBlockStructTypes;
|
||||
for (auto& target : targets) {
|
||||
Instruction* variable = target.variable;
|
||||
const Uint32 oldVariableId = variable->result_id();
|
||||
|
||||
std::vector<std::unique_ptr<Instruction>> newDecorations;
|
||||
std::vector<std::unique_ptr<Instruction>> newNames;
|
||||
for (auto& member : target.members) {
|
||||
member.variableId = irContext->TakeNextId();
|
||||
irContext->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpVariable, member.interfacePointerTypeId, member.variableId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<Uint32>(target.storageClass)}}}));
|
||||
|
||||
// The name IS the contract: it is what the rewritten capture request
|
||||
// asks the driver for, and what makes the producer and consumer of a
|
||||
// flattened block still match each other by name.
|
||||
const String flatName = target.blockName + "_" + member.name;
|
||||
std::vector<Operand> nameOperands;
|
||||
nameOperands.push_back({SPV_OPERAND_TYPE_ID, {member.variableId}});
|
||||
nameOperands.push_back(
|
||||
{SPV_OPERAND_TYPE_LITERAL_STRING,
|
||||
spvtools::utils::MakeVector(flatName)});
|
||||
newNames.push_back(spvtools::MakeUnique<Instruction>(irContext, spv::Op::OpName, 0, 0,
|
||||
nameOperands));
|
||||
|
||||
if (member.hasLocation) {
|
||||
newDecorations.push_back(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpDecorate, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {member.variableId}},
|
||||
{SPV_OPERAND_TYPE_DECORATION,
|
||||
{static_cast<Uint32>(spv::Decoration::Location)}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {member.location}}}));
|
||||
}
|
||||
}
|
||||
|
||||
// Member decorations that describe the member (not the aggregate) move to
|
||||
// the variable that now carries it.
|
||||
for (auto& annotation : irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpMemberDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != target.structTypeId) continue;
|
||||
const Uint32 memberIndex = annotation.GetSingleWordInOperand(1);
|
||||
if (memberIndex >= target.members.size()) continue;
|
||||
const auto decoration =
|
||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2));
|
||||
if (!IsMemberDecorationToCarry(decoration)) continue;
|
||||
std::vector<Operand> operands;
|
||||
operands.push_back({SPV_OPERAND_TYPE_ID, {target.members[memberIndex].variableId}});
|
||||
for (Uint32 i = 2; i < annotation.NumInOperands(); ++i) {
|
||||
operands.push_back(annotation.GetInOperand(i));
|
||||
}
|
||||
newDecorations.push_back(
|
||||
spvtools::MakeUnique<Instruction>(irContext, spv::Op::OpDecorate, 0, 0, operands));
|
||||
}
|
||||
|
||||
// The block's own Location described where its members start and means
|
||||
// nothing on a Private shadow; leaving it would also make SPIRV-Cross print
|
||||
// a location for a variable that no longer has an interface.
|
||||
std::vector<Instruction*> deadDecorations;
|
||||
// A struct type shared by two flattened variables would otherwise have its
|
||||
// Block decoration killed twice - and the second kill is a use-after-free,
|
||||
// not a no-op.
|
||||
const Bool structTypeAlreadyStripped =
|
||||
strippedBlockStructTypes.count(target.structTypeId) != 0;
|
||||
strippedBlockStructTypes.insert(target.structTypeId);
|
||||
for (auto& annotation : irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||
const Uint32 target0 = annotation.GetSingleWordInOperand(0);
|
||||
if (target0 == oldVariableId) {
|
||||
deadDecorations.push_back(&annotation);
|
||||
continue;
|
||||
}
|
||||
if (structTypeAlreadyStripped) continue;
|
||||
// The struct type stops being an interface block the moment its only
|
||||
// interface variable becomes a Private shadow, and SPIRV-Cross prints a
|
||||
// Block-decorated struct as a BLOCK declaration - `out StageData {...}
|
||||
// vs_out;` for a variable that is no longer an output, which is not
|
||||
// ESSL and which the driver rejects with a bare syntax error on the
|
||||
// instance name. A block name is unique per stage, so nothing else can
|
||||
// still need this decoration.
|
||||
if (target0 == target.structTypeId &&
|
||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::Block) {
|
||||
deadDecorations.push_back(&annotation);
|
||||
}
|
||||
}
|
||||
for (auto* annotation : deadDecorations) {
|
||||
irContext->KillInst(annotation);
|
||||
}
|
||||
for (auto& decoration : newDecorations) {
|
||||
irContext->AddAnnotationInst(std::move(decoration));
|
||||
}
|
||||
for (auto& debugName : newNames) {
|
||||
irContext->AddDebug2Inst(std::move(debugName));
|
||||
}
|
||||
|
||||
// Demote the block to a Private shadow. Every OpAccessChain and OpStore the
|
||||
// body already performs on it stays valid and keeps its types; only the
|
||||
// storage class changed, and Private is the one storage class a pointer of
|
||||
// any shape may live in.
|
||||
variable->SetResultType(target.privatePointerTypeId);
|
||||
variable->SetInOperand(0, {static_cast<Uint32>(spv::StorageClass::Private)});
|
||||
variable->RemoveFromList();
|
||||
irContext->AddGlobalValue(std::unique_ptr<Instruction>(variable));
|
||||
|
||||
// SPIR-V 1.3 lists only Input/Output in the entry-point interface, and the
|
||||
// block is neither any more: it is replaced in place by its members.
|
||||
std::vector<Operand> rebuilt;
|
||||
for (Uint32 i = 0; i < interfaceOperands.size(); ++i) {
|
||||
const Operand& operand = interfaceOperands[i];
|
||||
if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID && operand.words.size() == 1 &&
|
||||
operand.words[0] == oldVariableId) {
|
||||
for (const auto& member : target.members) {
|
||||
rebuilt.push_back({SPV_OPERAND_TYPE_ID, {member.variableId}});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
rebuilt.push_back(operand);
|
||||
}
|
||||
interfaceOperands = std::move(rebuilt);
|
||||
}
|
||||
|
||||
entryPoint->SetInOperands(std::move(interfaceOperands));
|
||||
|
||||
// Index constants for the copy access chains. Appended after the variables
|
||||
// above, which is legal: nothing declared before them names them, and the only
|
||||
// instructions that do are the ones inserted into the function body below.
|
||||
auto* constMgr = irContext->get_constant_mgr();
|
||||
std::vector<Uint32> memberIndexConstants;
|
||||
Uint32 maxMembers = 0;
|
||||
for (const auto& target : targets) {
|
||||
maxMembers = std::max(maxMembers, static_cast<Uint32>(target.members.size()));
|
||||
}
|
||||
memberIndexConstants.resize(maxMembers, 0);
|
||||
for (Uint32 index = 0; index < maxMembers; ++index) {
|
||||
memberIndexConstants[index] = constMgr->GetUIntConstId(index);
|
||||
}
|
||||
|
||||
// Input: seed the shadow once, before any code that reads it. Output: publish
|
||||
// it at every exit, after all the code that writes it.
|
||||
auto& entryBlock = *entryFunction->begin();
|
||||
auto insertPoint = entryBlock.begin();
|
||||
while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) {
|
||||
++insertPoint;
|
||||
}
|
||||
if (insertPoint == entryBlock.end()) return Status::SuccessWithoutChange;
|
||||
|
||||
for (const auto& target : targets) {
|
||||
if (target.storageClass != spv::StorageClass::Input) continue;
|
||||
for (Uint32 memberIndex = 0; memberIndex < target.members.size(); ++memberIndex) {
|
||||
const auto& member = target.members[memberIndex];
|
||||
const Uint32 loadedId = irContext->TakeNextId();
|
||||
const Uint32 memberPointerId = irContext->TakeNextId();
|
||||
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpLoad, member.typeId, loadedId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {member.variableId}}}));
|
||||
++insertPoint;
|
||||
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpAccessChain, member.privatePointerTypeId, memberPointerId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {target.variable->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {memberIndexConstants[memberIndex]}}}));
|
||||
++insertPoint;
|
||||
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {memberPointerId}},
|
||||
{SPV_OPERAND_TYPE_ID, {loadedId}}}));
|
||||
++insertPoint;
|
||||
}
|
||||
}
|
||||
|
||||
// EVERY return, not just the last block: a shader with an early `return` would
|
||||
// otherwise publish nothing on that path.
|
||||
std::vector<Instruction*> returns;
|
||||
for (auto& block : *entryFunction) {
|
||||
Instruction* terminator = block.terminator();
|
||||
if (terminator != nullptr && terminator->opcode() == spv::Op::OpReturn) {
|
||||
returns.push_back(terminator);
|
||||
}
|
||||
}
|
||||
for (Instruction* returnInst : returns) {
|
||||
for (const auto& target : targets) {
|
||||
if (target.storageClass != spv::StorageClass::Output) continue;
|
||||
for (Uint32 memberIndex = 0; memberIndex < target.members.size(); ++memberIndex) {
|
||||
const auto& member = target.members[memberIndex];
|
||||
const Uint32 memberPointerId = irContext->TakeNextId();
|
||||
const Uint32 loadedId = irContext->TakeNextId();
|
||||
returnInst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpAccessChain, member.privatePointerTypeId,
|
||||
memberPointerId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {target.variable->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {memberIndexConstants[memberIndex]}}}));
|
||||
returnInst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpLoad, member.typeId, loadedId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {memberPointerId}}}));
|
||||
returnInst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {member.variableId}},
|
||||
{SPV_OPERAND_TYPE_ID, {loadedId}}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retype the derived pointers collected above. Done last, so the pointer types it
|
||||
// appends land after the variables.
|
||||
for (Instruction* pointer : pointersToRetype) {
|
||||
Instruction* resultType = defUseMgr->GetDef(pointer->type_id());
|
||||
if (resultType == nullptr || resultType->opcode() != spv::Op::OpTypePointer) continue;
|
||||
if (static_cast<spv::StorageClass>(resultType->GetSingleWordInOperand(0)) ==
|
||||
spv::StorageClass::Private) {
|
||||
continue;
|
||||
}
|
||||
pointer->SetResultType(typeMgr->FindPointerToType(resultType->GetSingleWordInOperand(1),
|
||||
spv::StorageClass::Private));
|
||||
}
|
||||
|
||||
if (m_flattenedBlockNames != nullptr) {
|
||||
for (const auto& target : targets) {
|
||||
m_flattenedBlockNames->insert(target.blockName);
|
||||
}
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken FlattenXfbInterfaceBlocksPass::CreateFlattenXfbInterfaceBlocksPass(
|
||||
const std::set<String>& blockNames, std::set<String>* flattenedBlockNames) {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
MakeUnique<FlattenXfbInterfaceBlocksPass>(blockNames, flattenedBlockNames));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,73 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.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 <set>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Replaces a named interface BLOCK whose members transform feedback captures with
|
||||
// one free-standing variable per member, named "<BlockName>_<MemberName>", and
|
||||
// demotes the block itself to a Private shadow that the entry point copies into
|
||||
// (Input) or out of (Output). The block's own body code is untouched: every
|
||||
// OpAccessChain into it - dynamically indexed ones included - keeps working
|
||||
// against the shadow.
|
||||
//
|
||||
// WHY. The Adreno ES driver accepts "BlockName.member" in
|
||||
// glTransformFeedbackVaryings, links, and reports the names straight back from
|
||||
// glGetTransformFeedbackVarying - and then captures NONE of them: the recorded
|
||||
// stream holds gl_Position in slot 0 and leaves the rest of every vertex's record
|
||||
// untouched. Proven on an Adreno 830 by writing a recognisable gl_Position into
|
||||
// the KHR-GL43.vertex_attrib_binding.basic-input capture program: the value landed
|
||||
// in the slot that had asked for StageData.attrib[0]. The same program with the
|
||||
// block flattened into a plain output array captures every member correctly, which
|
||||
// is what this pass produces. Requesting the members under the ESSL instance-name
|
||||
// spelling ("vs_out.attrib[0]") instead makes the driver fail the link outright, so
|
||||
// the capture list cannot be spelled around the defect - only the declaration can.
|
||||
//
|
||||
// The rename is deterministic and derived only from names that GLSL interface
|
||||
// matching already requires both sides of a stage boundary to agree on (block name
|
||||
// and member names), so running this pass over EVERY stage of a program with the
|
||||
// same block set keeps a producer and its consumer matched.
|
||||
//
|
||||
// DirectGLES only: Vulkan captures by xfb_offset, never by name.
|
||||
class FlattenXfbInterfaceBlocksPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
// `blockNames` are block TYPE names, i.e. the "StageData" of
|
||||
// "StageData.attrib[0]". `flattenedBlockNames` receives the subset this pass
|
||||
// actually rewrote, so the caller can rename exactly those capture requests
|
||||
// and leave the rest spelled as the application wrote them.
|
||||
FlattenXfbInterfaceBlocksPass(const std::set<String>& blockNames,
|
||||
std::set<String>* flattenedBlockNames)
|
||||
: m_blockNames(blockNames), m_flattenedBlockNames(flattenedBlockNames) {}
|
||||
|
||||
const char* name() const override { return "mobilegl-flatten-xfb-interface-blocks"; }
|
||||
Status Process() override;
|
||||
|
||||
// The member of `blockName` spelled by a capture request, as this pass names
|
||||
// it: "StageData.attrib[0]" -> "StageData_attrib[0]". Returns false when the
|
||||
// name does not address a member of a flattened block.
|
||||
static Bool RewriteCaptureName(const String& captureName, const std::set<String>& flattenedBlockNames,
|
||||
String& outName);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateFlattenXfbInterfaceBlocksPass(
|
||||
const std::set<String>& blockNames, std::set<String>* flattenedBlockNames);
|
||||
|
||||
private:
|
||||
std::set<String> m_blockNames;
|
||||
std::set<String>* m_flattenedBlockNames = nullptr;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,394 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.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 "SplitArrayVertexInputsPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#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/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
// How many attribute locations one element of the array consumes. GL 4.6 core
|
||||
// 11.1.1: a scalar or vector of up to four 32-bit components takes one; a
|
||||
// double-precision vector wider than two takes two; a matrix takes one per
|
||||
// column. Zero means "this pass will not touch it" - the module keeps its array
|
||||
// input and SPIRV-Cross will say so, which is a better outcome than a silently
|
||||
// mis-located split.
|
||||
Uint32 LocationsPerElement(const analysis::Type* type) {
|
||||
if (type == nullptr) return 0;
|
||||
// Any scalar takes one location, a 64-bit one included.
|
||||
if (type->AsFloat() != nullptr || type->AsInteger() != nullptr ||
|
||||
type->AsBool() != nullptr) {
|
||||
return 1u;
|
||||
}
|
||||
if (const auto* vector = type->AsVector()) {
|
||||
const auto* element = vector->element_type();
|
||||
const auto* elementFloat = element->AsFloat();
|
||||
const Bool is64Bit = elementFloat != nullptr && elementFloat->width() == 64;
|
||||
if (element->AsFloat() == nullptr && element->AsInteger() == nullptr &&
|
||||
element->AsBool() == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return (is64Bit && vector->element_count() > 2) ? 2u : 1u;
|
||||
}
|
||||
// Matrices, structs, images and nested arrays are left alone deliberately:
|
||||
// an array of them is vanishingly rare as a vertex input and each carries its
|
||||
// own location-assignment rule, so getting one wrong would corrupt every
|
||||
// attribute after it rather than fail loudly.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The Location a variable is decorated with, or `false` when it carries none.
|
||||
// A vertex input without an explicit location cannot be split: the split has to
|
||||
// name base+i, and inventing a base would collide with whatever the linker
|
||||
// assigned.
|
||||
Bool FindLocationDecoration(IRContext& irContext, Uint32 variableId, Uint32& outLocation) {
|
||||
for (auto& annotation : irContext.annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != variableId) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
||||
spv::Decoration::Location) {
|
||||
continue;
|
||||
}
|
||||
outLocation = annotation.GetSingleWordInOperand(2);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status SplitArrayVertexInputsPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto entryPoints = irContext->module()->entry_points();
|
||||
if (entryPoints.begin() == entryPoints.end()) return Status::SuccessWithoutChange;
|
||||
|
||||
Instruction* entryPoint = &*entryPoints.begin();
|
||||
if (static_cast<spv::ExecutionModel>(entryPoint->GetSingleWordInOperand(0)) !=
|
||||
spv::ExecutionModel::Vertex) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constMgr = irContext->get_constant_mgr();
|
||||
|
||||
struct Target {
|
||||
Instruction* variable = nullptr;
|
||||
Uint32 arrayTypeId = 0;
|
||||
Uint32 elementTypeId = 0;
|
||||
Uint32 elementCount = 0;
|
||||
Uint32 baseLocation = 0;
|
||||
Uint32 locationsPerElement = 1;
|
||||
Uint32 elementInputPointerTypeId = 0;
|
||||
Uint32 elementPrivatePointerTypeId = 0;
|
||||
Uint32 arrayPrivatePointerTypeId = 0;
|
||||
};
|
||||
std::vector<Target> targets;
|
||||
|
||||
for (Instruction& inst : irContext->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||
if (static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
Instruction* pointerType = defUseMgr->GetDef(inst.type_id());
|
||||
if (pointerType == nullptr) continue;
|
||||
const Uint32 pointeeTypeId = pointerType->GetSingleWordInOperand(1);
|
||||
const analysis::Type* pointeeType = typeMgr->GetType(pointeeTypeId);
|
||||
const auto* arrayType = pointeeType != nullptr ? pointeeType->AsArray() : nullptr;
|
||||
if (arrayType == nullptr) continue;
|
||||
|
||||
// A builtin input array (gl_ClipDistance and friends) is not an attribute and
|
||||
// has no location; SPIRV-Cross emits those itself.
|
||||
Uint32 baseLocation = 0;
|
||||
if (!FindLocationDecoration(*irContext, inst.result_id(), baseLocation)) continue;
|
||||
|
||||
const Uint32 elementTypeId = typeMgr->GetId(arrayType->element_type());
|
||||
const Uint32 locationsPerElement = LocationsPerElement(arrayType->element_type());
|
||||
if (elementTypeId == 0 || locationsPerElement == 0) {
|
||||
MGLOG_I("SplitArrayVertexInputsPass: vertex input %%%u is an array whose element "
|
||||
"type has no single-location mapping; leaving it declared as an array",
|
||||
inst.result_id());
|
||||
continue;
|
||||
}
|
||||
|
||||
// OpTypeArray's length is an id of an integer constant.
|
||||
Instruction* arrayTypeInst = defUseMgr->GetDef(pointeeTypeId);
|
||||
if (arrayTypeInst == nullptr || arrayTypeInst->NumInOperands() < 2) continue;
|
||||
const analysis::Constant* lengthConstant =
|
||||
constMgr->FindDeclaredConstant(arrayTypeInst->GetSingleWordInOperand(1));
|
||||
if (lengthConstant == nullptr || lengthConstant->AsIntConstant() == nullptr) continue;
|
||||
const Uint32 elementCount = lengthConstant->AsIntConstant()->GetU32();
|
||||
if (elementCount == 0) continue;
|
||||
|
||||
Target target;
|
||||
target.variable = &inst;
|
||||
target.arrayTypeId = pointeeTypeId;
|
||||
target.elementTypeId = elementTypeId;
|
||||
target.elementCount = elementCount;
|
||||
target.baseLocation = baseLocation;
|
||||
target.locationsPerElement = locationsPerElement;
|
||||
targets.push_back(target);
|
||||
}
|
||||
|
||||
if (targets.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
// Everything the demoted array's pointer flows into, in SSA order. Demoting the
|
||||
// variable changes the STORAGE CLASS of every pointer derived from it, and a
|
||||
// derived pointer's own result type still says Input - which is an invalid
|
||||
// module ("the result pointer storage class and base pointer storage class in
|
||||
// OpAccessChain do not match"), so each one has to be retyped too. Collected
|
||||
// before anything is mutated so an unsupported use can still decline the whole
|
||||
// rewrite rather than leave a half-converted module behind.
|
||||
std::unordered_set<Uint32> derivedPointers;
|
||||
for (const auto& target : targets) {
|
||||
derivedPointers.insert(target.variable->result_id());
|
||||
}
|
||||
std::vector<Instruction*> pointersToRetype;
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& inst : block) {
|
||||
const spv::Op opcode = inst.opcode();
|
||||
// A pointer this pass moved may only be loaded from or indexed into.
|
||||
// Anything else (handing it to a function, storing THROUGH it, casting
|
||||
// it) either cannot happen for a vertex input or would need the callee
|
||||
// rewritten as well, and silently getting that wrong is worse than
|
||||
// leaving the module for SPIRV-Cross to reject out loud.
|
||||
const Bool indexes =
|
||||
opcode == spv::Op::OpAccessChain ||
|
||||
opcode == spv::Op::OpInBoundsAccessChain ||
|
||||
opcode == spv::Op::OpCopyObject;
|
||||
if (indexes) {
|
||||
if (inst.NumInOperands() > 0 &&
|
||||
derivedPointers.count(inst.GetSingleWordInOperand(0)) != 0) {
|
||||
derivedPointers.insert(inst.result_id());
|
||||
pointersToRetype.push_back(&inst);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (opcode == spv::Op::OpLoad) continue; // reads are always fine
|
||||
for (Uint32 i = 0; i < inst.NumInOperands(); ++i) {
|
||||
const Operand& operand = inst.GetInOperand(i);
|
||||
if (operand.type != SPV_OPERAND_TYPE_ID || operand.words.size() != 1) {
|
||||
continue;
|
||||
}
|
||||
if (derivedPointers.count(operand.words[0]) == 0) continue;
|
||||
MGLOG_I("SplitArrayVertexInputsPass: array vertex input %%%u reaches a "
|
||||
"SPIR-V opcode %u that this pass cannot follow; leaving it "
|
||||
"declared as an "
|
||||
"array",
|
||||
operand.words[0], static_cast<Uint32>(opcode));
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Entry block insertion point: after the block's leading OpVariable run, which
|
||||
// SPIR-V requires to stay at the top of a function's first block.
|
||||
const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||
spvtools::opt::Function* entryFunction = nullptr;
|
||||
for (auto& function : *irContext->module()) {
|
||||
if (function.result_id() == entryFunctionId) {
|
||||
entryFunction = &function;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entryFunction == nullptr || entryFunction->begin() == entryFunction->end()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
auto& entryBlock = *entryFunction->begin();
|
||||
auto insertPoint = entryBlock.begin();
|
||||
while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) {
|
||||
++insertPoint;
|
||||
}
|
||||
if (insertPoint == entryBlock.end()) return Status::SuccessWithoutChange;
|
||||
|
||||
// Every type this pass names has to exist before the variables that name it: the
|
||||
// types-and-variables section is emitted in order and a forward reference to a
|
||||
// type is invalid SPIR-V. Same staging as PackDoubleVertexInputsPass.
|
||||
for (auto& target : targets) {
|
||||
target.elementInputPointerTypeId =
|
||||
typeMgr->FindPointerToType(target.elementTypeId, spv::StorageClass::Input);
|
||||
target.elementPrivatePointerTypeId =
|
||||
typeMgr->FindPointerToType(target.elementTypeId, spv::StorageClass::Private);
|
||||
target.arrayPrivatePointerTypeId =
|
||||
typeMgr->FindPointerToType(target.arrayTypeId, spv::StorageClass::Private);
|
||||
}
|
||||
|
||||
// Index constants for the seeding access chains, all of them before any variable.
|
||||
Uint32 maxElementCount = 0;
|
||||
for (const auto& target : targets) {
|
||||
maxElementCount = std::max(maxElementCount, target.elementCount);
|
||||
}
|
||||
std::vector<Uint32> indexConstantIds(maxElementCount, 0);
|
||||
for (Uint32 index = 0; index < maxElementCount; ++index) {
|
||||
indexConstantIds[index] = constMgr->GetUIntConstId(index);
|
||||
}
|
||||
|
||||
std::vector<Operand> interfaceOperands;
|
||||
for (Uint32 i = 0; i < entryPoint->NumInOperands(); ++i) {
|
||||
interfaceOperands.push_back(entryPoint->GetInOperand(i));
|
||||
}
|
||||
|
||||
for (const auto& target : targets) {
|
||||
Instruction* variable = target.variable;
|
||||
const Uint32 oldVariableId = variable->result_id();
|
||||
|
||||
// Collected, never added inline: AddAnnotationInst mutates the annotation
|
||||
// list this function is still walking below.
|
||||
std::vector<std::unique_ptr<Instruction>> newDecorations;
|
||||
std::vector<Uint32> elementVariableIds(target.elementCount, 0);
|
||||
for (Uint32 element = 0; element < target.elementCount; ++element) {
|
||||
const Uint32 elementVariableId = irContext->TakeNextId();
|
||||
elementVariableIds[element] = elementVariableId;
|
||||
irContext->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpVariable, target.elementInputPointerTypeId,
|
||||
elementVariableId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<Uint32>(spv::StorageClass::Input)}}}));
|
||||
newDecorations.push_back(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpDecorate, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {elementVariableId}},
|
||||
{SPV_OPERAND_TYPE_DECORATION,
|
||||
{static_cast<Uint32>(spv::Decoration::Location)}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER,
|
||||
{target.baseLocation + element * target.locationsPerElement}}}));
|
||||
}
|
||||
|
||||
// The array's own decorations move to the elements, except Location (each
|
||||
// element got its own above) and anything that only described the aggregate.
|
||||
// RelaxedPrecision is the one that MUST travel: dropping it changes the
|
||||
// declared precision of the input in the emitted ESSL.
|
||||
std::vector<Instruction*> deadDecorations;
|
||||
for (auto& annotation : irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != oldVariableId) continue;
|
||||
const auto decoration =
|
||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1));
|
||||
if (decoration == spv::Decoration::RelaxedPrecision ||
|
||||
decoration == spv::Decoration::Flat ||
|
||||
decoration == spv::Decoration::NoPerspective ||
|
||||
decoration == spv::Decoration::Centroid ||
|
||||
decoration == spv::Decoration::Sample) {
|
||||
for (const Uint32 elementVariableId : elementVariableIds) {
|
||||
std::vector<Operand> operands;
|
||||
operands.push_back({SPV_OPERAND_TYPE_ID, {elementVariableId}});
|
||||
for (Uint32 i = 1; i < annotation.NumInOperands(); ++i) {
|
||||
operands.push_back(annotation.GetInOperand(i));
|
||||
}
|
||||
newDecorations.push_back(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpDecorate, 0, 0, operands));
|
||||
}
|
||||
}
|
||||
deadDecorations.push_back(&annotation);
|
||||
}
|
||||
for (auto* annotation : deadDecorations) {
|
||||
irContext->KillInst(annotation);
|
||||
}
|
||||
for (auto& decoration : newDecorations) {
|
||||
irContext->AddAnnotationInst(std::move(decoration));
|
||||
}
|
||||
|
||||
// Demote the original to a Private global: every existing OpLoad and
|
||||
// OpAccessChain on it stays valid and keeps its array type, dynamic indices
|
||||
// included. Moved to the end of the section for the same reason as in
|
||||
// PackDoubleVertexInputsPass - a variable may not forward-reference its type.
|
||||
variable->SetResultType(target.arrayPrivatePointerTypeId);
|
||||
variable->SetInOperand(0, {static_cast<Uint32>(spv::StorageClass::Private)});
|
||||
variable->RemoveFromList();
|
||||
irContext->AddGlobalValue(std::unique_ptr<Instruction>(variable));
|
||||
|
||||
// SPIR-V 1.3 lists only Input/Output in the entry-point interface, and the
|
||||
// array is no longer an Input: replace it with the elements in place, so the
|
||||
// interface keeps one entry per live interface variable.
|
||||
std::vector<Operand> rebuilt;
|
||||
for (Uint32 i = 0; i < interfaceOperands.size(); ++i) {
|
||||
const Operand& operand = interfaceOperands[i];
|
||||
if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID &&
|
||||
operand.words.size() == 1 && operand.words[0] == oldVariableId) {
|
||||
for (const Uint32 elementVariableId : elementVariableIds) {
|
||||
rebuilt.push_back({SPV_OPERAND_TYPE_ID, {elementVariableId}});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
rebuilt.push_back(operand);
|
||||
}
|
||||
interfaceOperands = std::move(rebuilt);
|
||||
|
||||
// Seed the Private array once, at the top of the entry point, before any of
|
||||
// the code that reads it.
|
||||
for (Uint32 element = 0; element < target.elementCount; ++element) {
|
||||
const Uint32 loadedId = irContext->TakeNextId();
|
||||
const Uint32 elementPointerId = irContext->TakeNextId();
|
||||
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpLoad, target.elementTypeId, loadedId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {elementVariableIds[element]}}}));
|
||||
++insertPoint;
|
||||
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpAccessChain, target.elementPrivatePointerTypeId,
|
||||
elementPointerId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {oldVariableId}},
|
||||
{SPV_OPERAND_TYPE_ID, {indexConstantIds[element]}}}));
|
||||
++insertPoint;
|
||||
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {elementPointerId}},
|
||||
{SPV_OPERAND_TYPE_ID, {loadedId}}}));
|
||||
++insertPoint;
|
||||
}
|
||||
}
|
||||
|
||||
entryPoint->SetInOperands(std::move(interfaceOperands));
|
||||
|
||||
// Retype the derived pointers collected above. Done last, so the pointer types
|
||||
// it appends land after the variables (nothing in the types-and-variables
|
||||
// section names them - they are only ever the result type of an instruction in
|
||||
// a function body).
|
||||
for (Instruction* pointer : pointersToRetype) {
|
||||
Instruction* resultType = defUseMgr->GetDef(pointer->type_id());
|
||||
if (resultType == nullptr || resultType->opcode() != spv::Op::OpTypePointer) continue;
|
||||
if (static_cast<spv::StorageClass>(resultType->GetSingleWordInOperand(0)) ==
|
||||
spv::StorageClass::Private) {
|
||||
continue;
|
||||
}
|
||||
pointer->SetResultType(typeMgr->FindPointerToType(resultType->GetSingleWordInOperand(1),
|
||||
spv::StorageClass::Private));
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken SplitArrayVertexInputsPass::CreateSplitArrayVertexInputsPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<SplitArrayVertexInputsPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,52 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.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>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Replaces an ARRAY vertex input with one input variable per element, at
|
||||
// consecutive locations, and demotes the original array to a Private global
|
||||
// seeded once at the top of the entry point.
|
||||
//
|
||||
// GLSL ES has no array vertex inputs at all (GLSL ES 3.20 4.3.4: a vertex shader
|
||||
// input "cannot be ... arrays"), and SPIRV-Cross does not emulate the difference:
|
||||
// it refuses the whole module with "OpenGL ES doesn't support array input
|
||||
// variables in vertex shader". The stage then never reaches the driver, the
|
||||
// program links without a vertex shader, and every draw using it is a silent
|
||||
// no-op - which is how the entire KHR-GL43.vertex_attrib_binding.basic-input*
|
||||
// family (its capture program declares `in vec4 vs_in_attrib[16]`) failed on
|
||||
// DirectGLES with no symptom other than "the draw captured zeros".
|
||||
//
|
||||
// Desktop GL DOES allow the declaration, and it means exactly what the split
|
||||
// produces: element i of an input array consumes location base+i (GL 4.6 core
|
||||
// 11.1.1). So the split is a spelling change, not a semantic one - the same
|
||||
// vertex attributes feed the same components, and the frontend's reflection
|
||||
// (which the backends bind attributes from) is not involved.
|
||||
//
|
||||
// Downstream code is untouched on purpose: the original variable keeps its id and
|
||||
// its array type, so every OpAccessChain into it - including the DYNAMICALLY
|
||||
// indexed ones a `for` loop produces, which is precisely what an input array is
|
||||
// usually written for - stays valid against the Private copy.
|
||||
//
|
||||
// DirectGLES only. Vulkan takes array vertex inputs as they are.
|
||||
class SplitArrayVertexInputsPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-split-array-vertex-inputs"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateSplitArrayVertexInputsPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,149 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.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 "ZeroBaseVertexPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#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 <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// Returns the Input OpVariable decorated with |builtin|, or nullptr if none.
|
||||
Instruction* FindBuiltinInputVariable(IRContext* context, spv::BuiltIn builtin) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
for (auto& annotation : context->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
||||
spv::Decoration::BuiltIn) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != builtin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* variable = defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
|
||||
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
return variable;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status ZeroBaseVertexPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
|
||||
Instruction* baseVertexVar = FindBuiltinInputVariable(irContext, spv::BuiltIn::BaseVertex);
|
||||
if (baseVertexVar == nullptr) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t baseVertexVarId = baseVertexVar->result_id();
|
||||
|
||||
// Collect every load before mutating: rewriting invalidates the use list.
|
||||
//
|
||||
// Every OTHER kind of user is enumerated and refused rather than ignored. A read
|
||||
// that reaches the variable through a copied pointer or a pointer function
|
||||
// parameter would keep Vulkan's firstVertex while the pass still reported
|
||||
// success, i.e. a partial rewrite indistinguishable from a complete one. glslang
|
||||
// emits neither shape from GLSL today, so this fails closed on something that
|
||||
// cannot happen yet rather than silently half-doing it when it can.
|
||||
std::vector<Instruction*> baseVertexLoads;
|
||||
Bool sawUnexpectedUser = false;
|
||||
defUseMgr->ForEachUser(baseVertexVar, [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpLoad:
|
||||
if (user->GetSingleWordInOperand(0) == baseVertexVarId) {
|
||||
baseVertexLoads.push_back(user);
|
||||
} else {
|
||||
sawUnexpectedUser = true;
|
||||
}
|
||||
return;
|
||||
// Declarations of the variable, not reads of it.
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpDecorateId:
|
||||
case spv::Op::OpDecorateString:
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpEntryPoint:
|
||||
return;
|
||||
default:
|
||||
sawUnexpectedUser = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (sawUnexpectedUser) {
|
||||
return Status::Failure;
|
||||
}
|
||||
if (baseVertexLoads.empty()) {
|
||||
// Declared but never read - the variant is already the shader itself.
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// Materialize every zero constant BEFORE touching a single instruction, so the
|
||||
// constant/type managers are never consulted against a module this pass has
|
||||
// already half-rewritten - and so the rewrite loop below cannot fail partway
|
||||
// and leave one behind.
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
std::vector<uint32_t> zeroIds(baseVertexLoads.size(), 0);
|
||||
for (size_t i = 0; i < baseVertexLoads.size(); ++i) {
|
||||
// The zero is built from the LOAD's own type, because a shader may declare
|
||||
// the builtin as either int or uint.
|
||||
const uint32_t typeId = baseVertexLoads[i]->type_id();
|
||||
const spvtools::opt::analysis::Type* type = typeMgr->GetType(typeId);
|
||||
if (type == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
const spvtools::opt::analysis::Constant* zero = constantMgr->GetConstant(type, {0u});
|
||||
if (zero == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
const Instruction* zeroInst = constantMgr->GetDefiningInstruction(zero, typeId);
|
||||
if (zeroInst == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
zeroIds[i] = zeroInst->result_id();
|
||||
}
|
||||
|
||||
// `OpLoad %ty %res %baseVertex` becomes `OpCopyObject %ty %res %zero`. Keeping
|
||||
// %res makes every downstream use pick the zero up with no further rewriting.
|
||||
for (size_t i = 0; i < baseVertexLoads.size(); ++i) {
|
||||
baseVertexLoads[i]->SetOpcode(spv::Op::OpCopyObject);
|
||||
baseVertexLoads[i]->SetInOperands(Instruction::OperandList{
|
||||
{SPV_OPERAND_TYPE_ID, {zeroIds[i]}}});
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken ZeroBaseVertexPass::CreateZeroBaseVertexPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<ZeroBaseVertexPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,40 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.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>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// GL and Vulkan disagree about gl_BaseVertex on NON-INDEXED draws: GL defines it as
|
||||
// "the value passed to the baseVertex parameter, or zero for a command that has
|
||||
// none", so every DrawArrays form reads zero, while Vulkan's BaseVertex builtin
|
||||
// carries the draw's firstVertex there. (For indexed draws both mean the same thing,
|
||||
// GL's basevertex / Vulkan's vertexOffset, so those must keep the native builtin.)
|
||||
//
|
||||
// This pass produces the non-indexed variant of a vertex shader by replacing every
|
||||
// read of the BaseVertex builtin with a constant zero. The variable itself is left
|
||||
// declared - removing it would also have to reason about the DrawParameters
|
||||
// capability that a BaseInstance read in the same module still needs.
|
||||
//
|
||||
// Vulkan backend only, and only for the ZeroBaseVertex program variant: the
|
||||
// DirectGLES path has no BaseVertex builtin at all (see LowerDrawParametersPass).
|
||||
class ZeroBaseVertexPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "zero-base-vertex"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateZeroBaseVertexPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -159,6 +159,7 @@ MobileGL supports runtime configuration via environment variables.
|
||||
| `MOBILEGL_DISABLE_TIMERQUERY` | Disable GPU timer-query exposure and use. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_USE_ANGLE` | Load ANGLE EGL/GLES libraries. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_DISABLE_SUBGROUP` | Disable Vulkan shader subgroup support. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_ADVERTISE_FP64` | Advertise `GL_ARB_gpu_shader_fp64`. GLSL `double`/`dvec`/`dmat` compile and run either way - they are narrowed to 32 bits - so this only changes whether an application is told it has 64-bit precision, which it does not. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_MAGMA_R11G11B10F_FALLBACK` | Use Magma's R11G11B10F format fallback. | `0`, `1` | `0` |
|
||||
| `MOBILEGL_MAGMA_FRAMESINFLIGHT` | Set Magma frames in flight. | Integer `1`–`64` | `3` |
|
||||
| `MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER` | Avoid sampler mipmap minification filters. | `0`, `1` | `0` |
|
||||
|
||||
Reference in New Issue
Block a user