Compare commits

...
7 Commits
Author SHA1 Message Date
swung0x48 b5d64175a1 [Fix] (MG_IntegrationTest/Harness): list the canned narrow check unarmed
- The driver-watch control makes no GL call, so it never arms the comparator
- The verify-split arm proof needs it named, on both backends
2026-10-19 06:12:19 -04:00
swung0x48 1a53cbd31a [Test] (MG_IntegrationTest/Scenarios, MG_Test/Backend): run every narrow case
- the client-memory case runs on every arm, as the client array scenario does
- a capture program meets both signednesses in one capture without a relink
- a relink that turns an ivec4 input into uvec4 starts over
- the recreated program and array must reuse their names
- the ESSL check counts the width declarations
2026-10-19 04:01:07 -04:00
swung0x48 3d2e259080 [Fix] (MG_Backend/DirectGLES, MG_Test/Backend): re-arm seeds per link
- a failed armed build disables the seeds for that link only; a relink arms again
- a refusal applies to the link it was built for, so a relink's first draw expects seeds
- a relink without integer inputs drops the arming
- the program twin is looked up only for a plan that needs seeds
- the seed state resets after the unlinked-program return, not before it
2026-10-19 04:00:54 -04:00
swung0x48 d1293354d1 [Fix] (CMakeLists.txt, MG_Backend/DirectGLES, MG_Pipe, MG_Test/Backend, MG_Test/ShaderTranspiler, MG_Util/ShaderTranspiler): give integer attributes their GL value in Espryt
- every integer array reaches the driver with its input's signedness, at the pointer, zero-stride and client-memory call sites, so each fetch is one ES defines
- 8/16-bit data of the other signedness is widened back by the program's runtime-width seeds, armed on the first draw that needs them and set as two uniforms per draw
- capture programs with integer inputs are armed at their first build; arming is part of the ESSL translation key
- a build whose seeds cannot be verified keeps the driver's own value for narrow data; one that fails to link is rebuilt without them
- the program's integer input masks are memoized on its shader record
2026-10-19 02:53:01 -04:00
swung0x48 381124a0b6 [Feat] (CMakeLists.txt, MG_Test/ShaderTranspiler, MG_Util/ShaderTranspiler): seed narrow integer inputs from runtime widths
- ExtendNarrowIntegerVertexInputsPass gains a RuntimeWidths mode: every 32-bit integer input below location 32 is widened from two Private uint globals by shifts only
- ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl reports whether it rewrote the module
- the constant-mask mode is unchanged byte for byte
2026-10-19 02:32:21 -04:00
swung0x48 fd48928260 [Test] (MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios): check the array types Espryt hands the driver
- IntegerAttributeTypePeek wraps the ES draw entry points and flags an enabled array whose type has the other signedness from its input
- every narrow-integer case asserts it on DirectGLES monolith and in-process; elsewhere it logs why and keeps the value checks
- new cases: two programs on one array, a first build for matching data, one source built twice, relink, recreate, bound location, DSA, zero stride, client memory, 32-bit data, multi-draw, pipeline stage, type query
- red on DirectGLES until the driver gets the input's signedness
2026-10-19 02:23:35 -04:00
swung0x48 58b0550512 [Refactor] (MG_Backend/DirectVulkan, MG_Util/ShaderTranspiler): share the integer vertex input masks
- IntegerVertexInputMasks, the four narrow data masks and ResolveNarrowIntegerVertexInputs move to MG_Util so another backend can plan the same correction
- Magma keeps its VkFormat Note on a derived mask type; behaviour is unchanged
2026-10-19 02:10:58 -04:00
33 changed files with 2364 additions and 125 deletions
+2
View File
@@ -420,6 +420,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.cpp
@@ -528,6 +529,7 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectGLES/ManagersTextures.cpp
MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.cpp
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
@@ -398,12 +398,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// PrepareForDraw turns into BindBackendVAOId(0) exactly as a null frontend VAO did;
// logging about it would fire on every draw of a session that legitimately has no
// vertex-elements CSO yet.
void SyncCurrentVAOFromRecords(BackendVertexArrayObject* vaoTwin) {
void SyncCurrentVAOFromRecords(BackendVertexArrayObject* vaoTwin, const IntegerAttributePlan& plan) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!vaoTwin) return;
vaoTwin->SyncToBackendFromApplier();
vaoTwin->SyncToBackendFromApplier(plan);
}
// GL: a shader input whose generic attribute array is DISABLED reads that attribute's *current
@@ -910,7 +910,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, vaoConfigVersion, syncBit & DrawSyncBit::IndexBuffer,
syncBit & DrawSyncBit::IndirectBuffer);
VertexArrayImpl::SyncCurrentVAOFromRecords(vaoTwin);
// The draw's integer arrays and the program's narrow-integer seeds (NarrowIntegerSeeds.h).
IntegerAttributePlan integerPlan = ResolveIntegerAttributePlanForDraw(MG_Pipe::MGPipeApplier().DrawProgram);
VertexArrayImpl::SyncCurrentVAOFromRecords(vaoTwin, integerPlan);
TextureImpl::SyncNeccessaryTextures(textureKeys);
// A draw reads and writes through its image units too, so the unit bindings have to be
// as current as the sampled ones. Gated (see the sweep): a program with no image binding
@@ -927,7 +929,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// at all; the remaining uses of it below are the two callees' own, and both have a
// handle arm of their own. P5e (pa) finished it: the pull itself is gone on that arm and
// `currentProgram` is a null SharedPtr there, which this branch never reaches for.
PrgramImpl::SyncCurrentProgramByHandle(MG_Pipe::MGPipeApplier().DrawProgram, true);
PrgramImpl::SyncCurrentProgramByHandle(MG_Pipe::MGPipeApplier().DrawProgram, true, &integerPlan);
if (DropRefusedNarrowIntegerSeeds(integerPlan, MG_Pipe::MGPipeApplier().DrawProgram))
VertexArrayImpl::SyncCurrentVAOFromRecords(vaoTwin, integerPlan);
RenderStateImpl::SyncRenderState();
BindCurrentFBO(FramebufferTarget::Draw);
@@ -951,6 +955,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
BindCurrentTextures(textureKeys, currentProgram);
BindCurrentProgramWithResources(currentProgram, textureKeys, MG_Pipe::MGPipeApplier().DrawProgram);
PushNarrowIntegerWidthsForDraw(integerPlan, MG_Pipe::MGPipeApplier().DrawProgram);
// Last: opening the capture span needs the program current and the capture
// buffers bound, and ES rejects most binding changes once it is open.
@@ -387,7 +387,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_Pipe::MGPipeShaderCsoRecord* ResolveGlobalConstantsRecordForHandle(MG_Pipe::MGPipeHandle handle,
Uint uboSize);
void SyncCurrentProgramByHandle(MG_Pipe::MGPipeHandle cso, Bool forDraw);
// `plan` (a draw's) arms the program's narrow-integer seeds when it needs them.
void SyncCurrentProgramByHandle(MG_Pipe::MGPipeHandle cso, Bool forDraw,
const IntegerAttributePlan* plan = nullptr);
} // namespace PrgramImpl
// Defined with the surface registry, below.
@@ -1434,7 +1434,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// and the clamp masks, the broadcast count, ImageUnitFormatsStillMatch and the three
// patch clauses are unchanged - they are backend globals, fb's row and VALUE rows of the
// residual block, none of which this family owns.
void SyncCurrentProgramByHandle(MG_Pipe::MGPipeHandle cso, Bool forDraw) {
void SyncCurrentProgramByHandle(MG_Pipe::MGPipeHandle cso, Bool forDraw, const IntegerAttributePlan* plan) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
@@ -1468,8 +1468,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_lastUsedBackendProgramId = 0;
return;
}
// Sticky for the twin's lifetime: a draw that needs the seeds rebuilds the program once.
if (plan != nullptr && plan->NeedsSeeds()) twin->NarrowIntegerSeeds().wanted = true;
if (!twin->GetBackendProgramId() || twin->GetSyncedShaderCsoSerial() != record->Serial ||
twin->GetSyncedBindingsSerial() != record->BindingsSerial ||
twin->NarrowIntegerSeeds().Stale() || twin->GetSyncedBindingsSerial() != record->BindingsSerial ||
twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount ||
@@ -0,0 +1,121 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/IntegerAttributePlan.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h>
namespace MobileGL::MG_Backend::DirectGLES {
using MG_Util::ShaderTranspiler::IntegerVertexInputMasks;
// What one draw does with its integer vertex arrays. ES defines an integer attribute's value
// only when the array's type has the input's signedness, so every integer array feeding an
// integer input of the other signedness is ISSUED with the input's signedness (retypeMask).
// For 32-bit data that is the same bits, which is GL's value. For 8/16-bit data the driver
// then extends by the input's rule, and the program's seeds (ExtendNarrowIntegerVertexInputs-
// Pass's RuntimeWidths mode) widen the low 8 or 16 bits again by the data's rule: widths8 and
// widths16 are the locations to widen, and they are the two uniforms the draw sets.
struct IntegerAttributePlan {
Uint32 retypeMask = 0;
// Integer arrays feeding integer inputs: the only locations whose issued type is
// observable, so a VAO keeps whatever it holds for every other location.
Uint32 careMask = 0;
Uint32 widths8 = 0;
Uint32 widths16 = 0;
constexpr Bool NeedsSeeds() const { return (widths8 | widths16) != 0; }
// The plan for a program without working seeds: a narrow array keeps the application's
// type (the value the driver gives today) rather than a retype nothing widens back.
constexpr IntegerAttributePlan WithoutSeeds() const {
IntegerAttributePlan plan = *this;
plan.retypeMask &= ~(widths8 | widths16);
plan.widths8 = 0;
plan.widths16 = 0;
return plan;
}
constexpr Bool operator==(const IntegerAttributePlan&) const = default;
};
// The locations whose integer array data is of each width and signedness.
struct IntegerAttributeDataMasks {
MG_Util::ShaderTranspiler::NarrowIntegerAttributeMasks narrow;
Uint32 unsigned32 = 0;
Uint32 signed32 = 0;
constexpr void Note(Uint32 location, DataType type) {
if (location >= 32) return;
const Uint32 bit = 1u << location;
switch (type) {
case DataType::Uint8:
narrow.unsigned8 |= bit;
break;
case DataType::Uint16:
narrow.unsigned16 |= bit;
break;
case DataType::Uint32:
unsigned32 |= bit;
break;
case DataType::Int8:
narrow.signed8 |= bit;
break;
case DataType::Int16:
narrow.signed16 |= bit;
break;
case DataType::Int32:
signed32 |= bit;
break;
default:
break;
}
}
constexpr Uint32 Unsigned() const { return narrow.unsigned8 | narrow.unsigned16 | unsigned32; }
constexpr Uint32 Signed() const { return narrow.signed8 | narrow.signed16 | signed32; }
};
// `seedsUsable` is false once the program's armed build could not give it working seeds.
constexpr IntegerAttributePlan ResolveIntegerAttributePlan(const IntegerVertexInputMasks& inputs,
const IntegerAttributeDataMasks& data,
Bool seedsUsable) {
IntegerAttributePlan plan;
plan.careMask = (inputs.signedInputs | inputs.unsignedInputs) & (data.Unsigned() | data.Signed());
plan.retypeMask = (inputs.signedInputs & data.Unsigned()) | (inputs.unsignedInputs & data.Signed());
const auto narrow = MG_Util::ShaderTranspiler::ResolveNarrowIntegerVertexInputs(inputs, data.narrow);
plan.widths8 = narrow.zeroExtend8 | narrow.signExtend8;
plan.widths16 = narrow.zeroExtend16 | narrow.signExtend16;
return seedsUsable ? plan : plan.WithoutSeeds();
}
// The array type to issue: the application's, or its counterpart of the other signedness.
constexpr GLenum IntegerAttribTypeForInput(GLenum applicationType, Bool retype) {
if (!retype) return applicationType;
switch (applicationType) {
case GL_BYTE:
return GL_UNSIGNED_BYTE;
case GL_UNSIGNED_BYTE:
return GL_BYTE;
case GL_SHORT:
return GL_UNSIGNED_SHORT;
case GL_UNSIGNED_SHORT:
return GL_SHORT;
case GL_INT:
return GL_UNSIGNED_INT;
case GL_UNSIGNED_INT:
return GL_INT;
default:
return applicationType;
}
}
// What a vertex array issues after this plan: the plan's choice wherever it can be observed,
// what it already holds everywhere else, so a program switch that only changes which
// locations are read does not re-issue any pointer.
constexpr Uint32 HeldIntegerRetype(Uint32 held, const IntegerAttributePlan& plan) {
return (held & ~plan.careMask) | (plan.retypeMask & plan.careMask);
}
} // namespace MobileGL::MG_Backend::DirectGLES
@@ -374,6 +374,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (shaderType != GL_VERTEX_SHADER) {
return source;
}
source = PromoteNarrowIntegerWidthGlobals(std::move(source));
for (const char* name : {DRAW_ID_UNIFORM_NAME, BASE_VERTEX_UNIFORM_NAME}) {
for (const char* declPrefix :
{"highp int ", "mediump int ", "lowp int ", "int ", "highp uint ", "mediump uint ", "uint "}) {
+11 -11
View File
@@ -30,8 +30,8 @@
// so a forward declaration would not do. Push-only, like everything else P4a adds to this
// header, so the pull build's include graph is unchanged (D-P).
#include <MG_Pipe/PipeApply.h>
// P13 W5: the record arm's deaths (the slot tables' handle refusals below), in every build.
#include <MG_Pipe/PipeSessionFail.h>
#include <MG_Pipe/PipeSessionFail.h> // P13 W5: the slot tables' handle refusals, in every build
#include "NarrowIntegerSeeds.h"
namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
@@ -1230,9 +1230,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// above, so VertexArrayImpl::SyncCurrentVAOFromRecords calls this ENTRY directly
// instead of passing a null SharedPtr into a function whose signature promises one.
// CONTRACT-P5E §4.1's rule ("the object parameter is deleted from the transport
// overload, not defaulted to null") is what this obeys. The body is unchanged and
// was already record-only - it is this family's existence proof (scout S1 §2).
void SyncToBackendFromApplier();
// overload, not defaulted to null") is what this obeys. `plan`: IntegerAttributePlan.h.
void SyncToBackendFromApplier(const IntegerAttributePlan& plan);
// P14: the applier's buffer-set serials move whenever ANY VAO's set is published, so
// switching between two VAOs moves them although neither twin's buffers changed. Called
// before the draw's syncs: when the window this twin last emitted is the applier's
@@ -1455,12 +1454,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// one monotone Uint64, no wrap, nothing to patch. This is the Track H re-key
// ARCHITECTURE.md 9.5 counts.
Uint64 m_syncedIndexSerial = 0;
// 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.
// Byte shift baked into the instanced arrays' offsets by the baseInstance emulation:
// draw state, not VAO state, kept because the next sync has to correct what was EMITTED.
Uint32 m_syncedFetchBaseInstance = 0;
// The integer arrays last issued with the other signedness (HeldIntegerRetype).
Uint32 m_syncedRetypeMask = 0;
// BufferImpl::g_bufferBackendIdGeneration as of this twin's last emit. A
// mismatch means some live buffer's driver id was re-minted since; the ids
// baked into the driver VAO's attribute/element bindings may be dead even
@@ -2847,6 +2845,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// shader failed to transpile or compile, or the link itself failed). Use()
// must not leave the previously bound program current in that case.
Bool IsBackendProgramUsable() const { return m_backendProgramUsable; }
NarrowIntegerSeedState& NarrowIntegerSeeds() { return m_narrowIntegerSeeds; }
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
@@ -2971,7 +2970,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const std::map<String, String>& inputBlockRenames,
const std::map<String, String>& outputBlockRenames, Bool stripInputBlockLocations,
Bool stripOutputBlockLocations, Int atomicCounterEsslBindingTop,
Bool enableSpirvValidation, String& outSource,
Bool narrowIntegerSeedsArmed, Bool enableSpirvValidation, String& outSource,
std::set<String>& outFlattenedXfbBlockNames,
Vector<Int>& outAtomicCounterGlBindings, String& outError) const;
@@ -3030,6 +3029,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<Int> m_formatlessImageUnits;
Uint64 m_imageUnitFormatSignature = 0;
SamplerPassMemo m_samplerPassMemo;
NarrowIntegerSeedState m_narrowIntegerSeeds;
};
extern Uint32 g_snormFallbackClampOutputMask;
@@ -888,8 +888,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
const std::map<String, String>& inputBlockRenames, const std::map<String, String>& outputBlockRenames,
const Bool stripInputBlockLocations, const Bool stripOutputBlockLocations,
const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource,
std::set<String>& outFlattenedXfbBlockNames, Vector<Int>& outAtomicCounterGlBindings,
const Int atomicCounterEsslBindingTop, const Bool narrowIntegerSeedsArmed, const Bool enableSpirvValidation,
String& outSource, std::set<String>& outFlattenedXfbBlockNames, Vector<Int>& outAtomicCounterGlBindings,
String& outError) const {
// ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to
// plain globals (mg_*) before handing the module to SPIRV-Cross.
@@ -1050,24 +1050,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl(
*effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) &&
!splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) {
// Only when the pass ACTUALLY split something. The optimizer hands back a
// re-serialised copy either way, and adopting that copy for every vertex
// shader would put every one of them through a round trip they do not need
// - which is not free: it cost the create-indirect retrace 0.15 SSIM the
// first time this gate was missing (the original capture; the case is now
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024).
// Only when the pass ACTUALLY split something: the optimizer hands back a
// re-serialised copy either way, and that round trip is not free - it cost the
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024 retrace 0.15 SSIM.
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 case is now
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024). 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.
// An armed vertex stage's narrow-integer seeds (NarrowIntegerSeeds.h), after the split
// and adopted only when the pass rewrote the module. Keyed by narrowIntegerSeedsArmed.
Vector<unsigned int> narrowSeededSpirv;
effectiveSpirv =
SeedNarrowIntegerVertexInputsForEssl(narrowIntegerSeedsArmed && glShaderType == GL_VERTEX_SHADER,
*effectiveSpirv, narrowSeededSpirv, enableSpirvValidation);
// Adopt the rewritten module only when THIS stage actually had one of the blocks, for
// the array-input split's reason above. The report is per stage, not cumulative: a
// fragment shader consuming a block the vertex stage reported must still take its own
// rewrite or the two stages stop matching.
Vector<unsigned int> flattenedXfbSpirv;
if (!xfbCaptureBlockNames.empty()) {
// Reported into a local first, and published only if the module is really
@@ -1605,8 +1604,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Recorded before either early return below, so Use() can always name the GL
// program a no-op draw belongs to - including the "linked but not drawable" exit.
// Before either early return below, so Use() can name the GL program of a no-op draw.
m_frontendProgramId = src.GetExternalIndex();
// GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was
@@ -1620,6 +1618,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
src.GetExternalIndex());
return;
}
// After the return above, which keeps the previous driver program and the widths it holds.
BeginNarrowIntegerSeedBuild(m_narrowIntegerSeeds);
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u", src.GetExternalIndex(),
m_backendProgramId);
@@ -1636,9 +1636,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// this build current - the draw path compares the signature and rebuilds on a change.
const auto& storageBlockBindingOverrides = src.GetShaderStorageBlockBindingOverrides();
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(src);
// Rebuilt by the transpile loop below, one entry per atomic-counter block it finds.
// The top is snapshotted here so every stage of this program - and the draw path
// reading it afterwards - resolves the same slot for the same GL binding.
// Rebuilt by the transpile loop below, one entry per atomic-counter block; the top is
// snapshotted so every stage and the draw path resolve one slot per GL binding.
m_atomicCounterGlBindings.clear();
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
// Re-established by AttachPassthroughTessControlStage below when this program needs
@@ -2045,6 +2044,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
esslKeyInputs.stripInputBlockLocations = stripInputBlockLocations;
esslKeyInputs.stripOutputBlockLocations = stripOutputBlockLocations;
esslKeyInputs.enableSpirvValidation = enableSpirvValidation;
esslKeyInputs.narrowIntegerSeedsArmed = glShaderType == GL_VERTEX_SHADER && m_narrowIntegerSeeds.built;
auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache();
MG_Util::ShaderTranspiler::TranslationCacheKey esslCacheKey;
@@ -2070,9 +2070,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames, imageFormatBake,
storageBlockBindingOverrides, inputBlockRenames, outputBlockRenames,
stripInputBlockLocations, stripOutputBlockLocations,
m_atomicCounterEsslBindingTop, enableSpirvValidation, source,
stageFlattenedXfbBlockNames, stageAtomicCounterGlBindings,
transpileError)) {
m_atomicCounterEsslBindingTop, esslKeyInputs.narrowIntegerSeedsArmed,
enableSpirvValidation, source, stageFlattenedXfbBlockNames,
stageAtomicCounterGlBindings, transpileError)) {
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics
// below: one line per failing stage is bounded by program count and
// naming the stage is the entire diagnostic value. A stage that never
@@ -2286,14 +2286,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
pos = source.find(findStr, pos);
}
NoteNarrowIntegerSeedSource(m_narrowIntegerSeeds, source, glShaderType);
const char* sourceCStr = source.c_str();
MGLOG_D("Setting shader source for backend shader ID: %u\nsrc:\n%s", backendShaderId, sourceCStr);
g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr);
g_GLESFuncs.glCompileShader(backendShaderId);
// GL_FALSE, not GL_TRUE: an unwritten out-param must read as "compile failed"
// and take the diagnostic path, never as a silent success that attaches an
// uncompiled shader.
// GL_FALSE, not GL_TRUE: an unwritten out-param must read as "compile failed", never
// as a silent success that attaches an uncompiled shader.
GLint compileStatus = GL_FALSE;
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus);
if (compileStatus == GL_FALSE) {
@@ -2649,6 +2649,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_backendProgramUsable = false;
return;
}
// A capture program with integer inputs is armed at its first build, so arming never
// relinks a program in the middle of a capture (NarrowIntegerSeeds.h).
PrepareNarrowIntegerSeedBuild(m_narrowIntegerSeeds, *record);
SyncToBackendFromSource(ProgramArchiveSource::FromRecord(cso, *record));
// The two server-owned keys the draw path's clean condition reads instead of
// GetLinkVersion()/GetImageUnitVersion(). BindingsSerial is the second because a
@@ -2769,6 +2772,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
binding.lastAssignedMsSampleMax = 0;
m_samplerUniformBindings.push_back(binding);
}
FinishNarrowIntegerSeedBuild(m_narrowIntegerSeeds, m_backendProgramId, m_backendProgramUsable);
}
void BackendProgramObjectImpl::Use() {
@@ -422,7 +422,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// SyncZeroStrideAttribute on the wire views: the one spelling that can carry a resolved
// stride of zero, which glVertexAttribPointer's zero means the opposite of.
inline Bool SyncZeroStrideAttributeByHandle(Uint attribIndex, const MGPVertexAttribWire& attrib,
const MG_Pipe::MGPVertexBuffer& binding) {
const MG_Pipe::MGPVertexBuffer& binding, GLenum issuedType) {
if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) {
MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
return false;
@@ -438,9 +438,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)),
attrib.Normalized ? GL_TRUE : GL_FALSE, 0);
} else {
g_GLESFuncs.glVertexAttribIFormat(attribIndex, static_cast<GLint>(attrib.Size),
MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)),
0);
g_GLESFuncs.glVertexAttribIFormat(attribIndex, static_cast<GLint>(attrib.Size), issuedType, 0);
}
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
// The resolved offset goes on the binding point, not into a relative offset (the
@@ -506,7 +504,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// as the record of what was last
// EMITTED, which is what the next
// sync has to correct
void BackendVertexArrayObject::SyncToBackendFromApplier() {
void BackendVertexArrayObject::SyncToBackendFromApplier(const IntegerAttributePlan& plan) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
@@ -545,7 +543,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// same whichever side resolved it first - it is idempotent.
const Uint32 fetchBaseInstance = BackendUsesNativeBaseInstance() ? 0u : st.VertexFetchBaseInstance;
const Bool baseInstanceDirty = m_syncedFetchBaseInstance != fetchBaseInstance;
const Bool emitAttributes = attributesDirty || baseInstanceDirty || m_hasConvertedAttribute;
// The integer arrays to issue with the other signedness; only a pointer whose type flips
// is re-issued, through the shadow below.
const Uint32 retypeHeld = HeldIntegerRetype(m_syncedRetypeMask, plan);
const Bool emitAttributes =
attributesDirty || baseInstanceDirty || m_hasConvertedAttribute || retypeHeld != m_syncedRetypeMask;
if (!emitAttributes && !indexBufferDirty) {
return;
}
@@ -649,7 +651,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The binding API rewrote this attribute's format and binding: whatever the
// shadow said about its pointer no longer holds.
m_hostPointer[attribIndex].valid = false;
if (!SyncZeroStrideAttributeByHandle(attribIndex, attrib, *binding)) {
const GLenum issuedType =
IntegerAttribTypeForInput(MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)),
attrib.IsInteger && ((retypeHeld >> attribIndex) & 1u));
if (!SyncZeroStrideAttributeByHandle(attribIndex, attrib, *binding, issuedType)) {
continue;
}
// No shift here on purpose: a zero stride never advances. Binding index ==
@@ -674,7 +679,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT fetchOffset = static_cast<SizeT>(attrib.Offset) +
BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance);
const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type));
const GLenum glType =
IntegerAttribTypeForInput(MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)),
attrib.IsInteger && ((retypeHeld >> attribIndex) & 1u));
const GLint glSize = static_cast<GLint>(attrib.Size);
// The pointer the driver VAO already holds for this attribute - the buffer it
@@ -759,6 +766,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
if (emitAttributes) {
m_syncedFetchBaseInstance = fetchBaseInstance;
m_syncedRetypeMask = retypeHeld;
m_lastWalkWasPlain = walkWasPlain;
}
m_syncedBufferIdGeneration = currentBufferIdGeneration;
@@ -923,7 +931,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
reinterpret_cast<const void*>(BaseInstanceByteShift(attrib, fetchBaseInstance)));
} else {
g_GLESFuncs.glVertexAttribIPointer(
attribIndex, attrib.Size, MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
attribIndex, attrib.Size,
IntegerAttribTypeForInput(MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
((m_syncedRetypeMask >> attribIndex) & 1u) != 0),
static_cast<GLsizei>(stride),
// See the pointer call above: the divisor'd
// arrays' baseInstance shift is the pointer.
@@ -0,0 +1,189 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.cpp
// Copyright (c) 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 "NarrowIntegerSeeds.h"
#include "DirectGLESInternal.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
namespace MobileGL::MG_Backend::DirectGLES {
namespace {
using MG_Util::ShaderTranspiler::kNarrowIntegerWidths16GlobalName;
using MG_Util::ShaderTranspiler::kNarrowIntegerWidths8GlobalName;
// The twin a handle already has, without creating one: a program that was never built has
// no seeds to refuse.
PrgramImpl::BackendProgramObjectImpl* ExistingProgramTwin(MG_Pipe::MGPipeHandle program) {
auto* slot = PrgramImpl::g_backendProgramObjects.FindByHandle(program);
return slot != nullptr ? slot->get() : nullptr;
}
// Whether the program's build for `record`'s link refused the seeds; a link not built yet
// is presumed to take them.
Bool SeedsRefused(MG_Pipe::MGPipeHandle program, const MG_Pipe::MGPipeShaderCsoRecord& record) {
auto* twin = ExistingProgramTwin(program);
return twin != nullptr && twin->NarrowIntegerSeeds().RefusedAt(record.Serial);
}
String UniformDeclaration(const char* name) {
return String("uniform highp uint ") + name + ";";
}
} // namespace
IntegerVertexInputMasks IntegerVertexInputsOf(const MG_Pipe::MGPipeShaderCsoRecord& record) {
auto& memo = record.BackendIntegerInputs;
if (!memo.Valid || memo.Serial != record.Serial) {
IntegerVertexInputMasks inputs;
if (record.Archive) {
// Indexed by location, an empty name being no active input: the same reading
// SyncCurrentVertexAttributeValues makes of these two vectors.
const auto& link = record.Archive->Link;
const SizeT count = std::min<SizeT>(std::min(link.attribs.size(), link.attribTypes.size()), 32);
for (SizeT location = 0; location < count; ++location) {
if (!link.attribs[location].empty())
inputs.Note(static_cast<Uint32>(location), link.attribTypes[location]);
}
}
memo.SignedInputs = inputs.signedInputs;
memo.UnsignedInputs = inputs.unsignedInputs;
memo.Serial = record.Serial;
memo.Valid = true;
}
IntegerVertexInputMasks inputs;
inputs.signedInputs = memo.SignedInputs;
inputs.unsignedInputs = memo.UnsignedInputs;
return inputs;
}
IntegerAttributePlan ResolveIntegerAttributePlanForDraw(MG_Pipe::MGPipeHandle program) {
if (MG_Pipe::MGPipeHandleIsNull(program)) return {};
const MG_Pipe::MGPipeShaderCsoRecord* const record = PipeShaderCsoRecordForHandle(program);
if (record == nullptr || !record->Archive) return {};
const IntegerVertexInputMasks inputs = IntegerVertexInputsOf(*record);
Uint32 remaining = inputs.signedInputs | inputs.unsignedInputs;
if (remaining == 0) return {};
const auto* elements = VertexArrayImpl::BoundVertexElementsRecord(MG_Pipe::MGPipeApplier());
if (elements == nullptr) return {};
IntegerAttributeDataMasks data;
for (; remaining != 0; remaining &= remaining - 1) {
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
const auto& attrib = elements->Attributes[location];
if (attrib.Enabled && attrib.IsInteger && !attrib.IsLong)
data.Note(location, static_cast<DataType>(attrib.Type));
}
// The twin is looked up only for a plan that needs seeds; most integer draws need none.
const IntegerAttributePlan plan = ResolveIntegerAttributePlan(inputs, data, true);
return plan.NeedsSeeds() && SeedsRefused(program, *record) ? plan.WithoutSeeds() : plan;
}
Bool DropRefusedNarrowIntegerSeeds(IntegerAttributePlan& plan, MG_Pipe::MGPipeHandle program) {
if (!plan.NeedsSeeds()) return false;
const MG_Pipe::MGPipeShaderCsoRecord* const record = PipeShaderCsoRecordForHandle(program);
if (record == nullptr || !SeedsRefused(program, *record)) return false;
plan = plan.WithoutSeeds();
return true;
}
void PrepareNarrowIntegerSeedBuild(NarrowIntegerSeedState& state, const MG_Pipe::MGPipeShaderCsoRecord& record) {
const IntegerVertexInputMasks inputs = IntegerVertexInputsOf(record);
state.PrepareBuild(record.Serial, (inputs.signedInputs | inputs.unsignedInputs) != 0,
record.Archive && !record.Archive->Link.xfbVaryings.empty());
}
void PushNarrowIntegerWidthsForDraw(const IntegerAttributePlan& plan, MG_Pipe::MGPipeHandle program) {
// Only the twin this draw's sync stashed, and only while it is the program Use() bound: a
// uniform call with program 0, or on another program, is an error or a wrong write.
if (PrgramImpl::g_currentDrawProgramHandle != program) return;
auto* twin = PrgramImpl::g_currentDrawBackendProgram;
if (twin == nullptr || !twin->IsBackendProgramUsable() ||
PrgramImpl::g_lastUsedBackendProgramId != twin->GetBackendProgramId())
return;
NarrowIntegerSeedState& state = twin->NarrowIntegerSeeds();
if (!state.available) return;
if (state.pushed8 != plan.widths8) {
g_GLESFuncs.glUniform1ui(state.widths8Location, plan.widths8);
state.pushed8 = plan.widths8;
}
if (state.pushed16 != plan.widths16) {
g_GLESFuncs.glUniform1ui(state.widths16Location, plan.widths16);
state.pushed16 = plan.widths16;
}
}
void BeginNarrowIntegerSeedBuild(NarrowIntegerSeedState& state) {
state.built = state.Target();
state.declared = false;
state.available = false;
state.widths8Location = -1;
state.widths16Location = -1;
state.pushed8 = 0;
state.pushed16 = 0;
}
void NoteNarrowIntegerSeedSource(NarrowIntegerSeedState& state, const String& source, GLenum shaderType) {
if (!state.built || shaderType != GL_VERTEX_SHADER) return;
state.declared = source.find(UniformDeclaration(kNarrowIntegerWidths8GlobalName)) != String::npos &&
source.find(UniformDeclaration(kNarrowIntegerWidths16GlobalName)) != String::npos;
}
void FinishNarrowIntegerSeedBuild(NarrowIntegerSeedState& state, GLuint program, Bool programUsable) {
if (!state.built) return;
if (!programUsable) {
// The seeds may be why the build failed; the next build of this link goes without them.
state.disabled = true;
MGLOG_E_ONCE("DirectGLES: a program built with narrow integer seeds did not link; it is rebuilt "
"without them, and its 8/16-bit integer attributes keep the driver's own value");
return;
}
if (state.declared) {
state.widths8Location = g_GLESFuncs.glGetUniformLocation(program, kNarrowIntegerWidths8GlobalName);
state.widths16Location = g_GLESFuncs.glGetUniformLocation(program, kNarrowIntegerWidths16GlobalName);
}
state.available = state.widths8Location >= 0 && state.widths16Location >= 0;
if (!state.available) {
MGLOG_E_ONCE("DirectGLES: a program built with narrow integer seeds has no width uniforms (%s); its "
"8/16-bit integer attributes keep the driver's own value",
state.declared ? "the link dropped them" : "the vertex stage does not declare them");
}
}
const Vector<unsigned int>* SeedNarrowIntegerVertexInputsForEssl(Bool armed, const Vector<unsigned int>& spirv,
Vector<unsigned int>& storage,
Bool enableSpirvValidation) {
if (!armed) return &spirv;
Bool changed = false;
if (!MG_Util::ShaderTranspiler::ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(spirv, storage, changed,
enableSpirvValidation)) {
MGLOG_E_ONCE("DirectGLES: the narrow integer seed pass declined a vertex stage; it keeps the driver's "
"own value for 8/16-bit integer attributes");
return &spirv;
}
return changed && !storage.empty() ? &storage : &spirv;
}
String PromoteNarrowIntegerWidthGlobals(String source) {
for (const char* name : {kNarrowIntegerWidths8GlobalName, kNarrowIntegerWidths16GlobalName}) {
// The zero-initialised Private global is printed with or without a precision qualifier;
// only a declaration starting its own line is the global itself.
for (const char* prefix : {"highp uint ", "mediump uint ", "lowp uint ", "uint "}) {
Bool promoted = false;
for (const char* initializer : {" = 0u;", ";"}) {
const String declaration = String(prefix) + name + initializer;
const SizeT pos = source.find(declaration);
if (pos == String::npos || (pos != 0 && source[pos - 1] != '\n')) continue;
source.replace(pos, declaration.size(), UniformDeclaration(name));
promoted = true;
break;
}
if (promoted) break;
}
}
return source;
}
} // namespace MobileGL::MG_Backend::DirectGLES
@@ -0,0 +1,97 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.h
// Copyright (c) 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 "IntegerAttributePlan.h"
#include <Includes.h>
#include <MG_Pipe/MGPipeHandles.h>
#include <MG_Pipe/PipeApply.h>
// The program half of IntegerAttributePlan: the two width uniforms a program carries once some
// draw has fed it 8/16-bit data of the other signedness, and the per-draw plan that drives them.
//
// A program is built with the seeds only when it needs them ("armed"), so a program that never
// meets such data keeps its ESSL byte for byte. Arming is sticky for the program object's
// lifetime, so data alternating in signedness rebuilds it at most once and from then on moves two
// uniforms. A program that captures transform feedback and has integer inputs is armed at its
// first build, so arming never relinks a program in the middle of a capture.
//
// Every failure falls back to the values the driver gives today: a build whose ESSL lacks either
// uniform, or whose link dropped one, is "refused" and its draws keep the application's type for
// 8/16-bit data; a build that fails outright disables arming until the application relinks the
// object, and it is rebuilt without the seeds. A relink is presumed to arm until its build says no.
namespace MobileGL::MG_Backend::DirectGLES {
struct NarrowIntegerSeedState {
Bool wanted = false;
Bool disabled = false;
// The record Serial (the application's link) the last build was for.
Uint64 buildSerial = 0;
// What the last build ran with and what it verified.
Bool built = false;
Bool declared = false;
Bool available = false;
Int widths8Location = -1;
Int widths16Location = -1;
// The values the linked program holds; a link resets them to 0.
Uint32 pushed8 = 0;
Uint32 pushed16 = 0;
Bool Target() const { return wanted && !disabled; }
// The program's next build has to change its arming.
Bool Stale() const { return Target() != built; }
// An armed build of the link at `serial` that could not give the program working seeds.
Bool RefusedAt(Uint64 serial) const { return serial == buildSerial && (disabled || (built && !available)); }
// Before a build of the link at `serial`: a failure of an earlier link says nothing about
// this one, a link without integer inputs has nothing to arm, and a capturing one is armed.
void PrepareBuild(Uint64 serial, Bool hasIntegerInputs, Bool captures) {
if (serial != buildSerial) {
disabled = false;
buildSerial = serial;
}
if (!hasIntegerInputs)
wanted = false;
else if (captures)
wanted = true;
}
};
// The program's 32-bit int and uint vertex inputs, memoized on the record.
IntegerVertexInputMasks IntegerVertexInputsOf(const MG_Pipe::MGPipeShaderCsoRecord& record);
// The draw's plan, from the program's record and the bound vertex-elements record. All zero for
// a program without integer inputs, which costs two loads.
IntegerAttributePlan ResolveIntegerAttributePlanForDraw(MG_Pipe::MGPipeHandle program);
// After the program sync: true when this draw's build refused the seeds the plan counted on,
// in which case the plan has dropped them and the vertex array must be synced again.
Bool DropRefusedNarrowIntegerSeeds(IntegerAttributePlan& plan, MG_Pipe::MGPipeHandle program);
// After Use(): the two widths on the program the draw runs, when they differ from what it holds.
void PushNarrowIntegerWidthsForDraw(const IntegerAttributePlan& plan, MG_Pipe::MGPipeHandle program);
// PrepareBuild from the record the build is for.
void PrepareNarrowIntegerSeedBuild(NarrowIntegerSeedState& state, const MG_Pipe::MGPipeShaderCsoRecord& record);
// The build's three steps: reset at its start, note each stage's final ESSL, verify after the link.
void BeginNarrowIntegerSeedBuild(NarrowIntegerSeedState& state);
void NoteNarrowIntegerSeedSource(NarrowIntegerSeedState& state, const String& source, GLenum shaderType);
void FinishNarrowIntegerSeedBuild(NarrowIntegerSeedState& state, GLuint program, Bool programUsable);
// The armed vertex stage's SPIR-V step (after the array-input split): `spirv` itself unless
// the pass rewrote it into `storage`.
const Vector<unsigned int>* SeedNarrowIntegerVertexInputsForEssl(Bool armed, const Vector<unsigned int>& spirv,
Vector<unsigned int>& storage,
Bool enableSpirvValidation);
// The two width globals of a seeded vertex stage, as `uniform highp uint`.
String PromoteNarrowIntegerWidthGlobals(String source);
namespace VertexArrayImpl {
const MG_Pipe::MGPipeVertexElementsRecord* BoundVertexElementsRecord(const MG_Pipe::MGPipeApplierState& st);
} // namespace VertexArrayImpl
} // namespace MobileGL::MG_Backend::DirectGLES
@@ -14,7 +14,9 @@
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
using MG_Util::ShaderTranspiler::IntegerVertexInputMasks;
using MG_Util::ShaderTranspiler::NarrowIntegerVertexInputs;
using MG_Util::ShaderTranspiler::ResolveNarrowIntegerVertexInputs;
// Which vertex inputs need the ExtendNarrowIntegerVertexInputs program variant, decided from
// two halves that are each computed once and cached with the object they describe.
@@ -58,15 +60,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// The vertex-input half: the locations whose attribute DATA is narrow integer, by width and
// signedness. Filled from the source formats when a vertex input layout is built, so a draw
// reads four words instead of walking its attributes.
struct NarrowIntegerAttributeMasks {
Uint32 unsigned8 = 0;
Uint32 unsigned16 = 0;
Uint32 signed8 = 0;
Uint32 signed16 = 0;
// The vertex-input half, filled from the source formats when a vertex input layout is built,
// so a draw reads four words instead of walking its attributes.
struct NarrowIntegerAttributeMasks : MG_Util::ShaderTranspiler::NarrowIntegerAttributeMasks {
constexpr void Note(Uint32 location, VkFormat format) {
if (location >= 32) return;
const Uint32 bit = 1u << location;
@@ -88,46 +84,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
};
// The program half: the locations whose shader input is a 32-bit int or uint scalar/vector.
// Both masks are zero for a program without integer vertex inputs, which is what keeps the
// common draw at four AND instructions.
struct IntegerVertexInputMasks {
Uint32 signedInputs = 0;
Uint32 unsignedInputs = 0;
constexpr void Note(Uint32 location, GLenum shaderInputType) {
if (location >= 32) return;
const Uint32 bit = 1u << location;
switch (shaderInputType) {
case GL_INT:
case GL_INT_VEC2:
case GL_INT_VEC3:
case GL_INT_VEC4:
signedInputs |= bit;
break;
case GL_UNSIGNED_INT:
case GL_UNSIGNED_INT_VEC2:
case GL_UNSIGNED_INT_VEC3:
case GL_UNSIGNED_INT_VEC4:
unsignedInputs |= bit;
break;
default:
break;
}
}
};
// Unsigned narrow data into an int input is fetched sign-extended and must be zero-extended;
// signed narrow data into a uint input is fetched zero-extended and must be sign-extended.
// Matching signedness, 32-bit data and float inputs need nothing.
constexpr NarrowIntegerVertexInputs ResolveNarrowIntegerVertexInputs(const IntegerVertexInputMasks& inputs,
const NarrowIntegerAttributeMasks& data) {
NarrowIntegerVertexInputs result;
result.zeroExtend8 = inputs.signedInputs & data.unsigned8;
result.zeroExtend16 = inputs.signedInputs & data.unsigned16;
result.signExtend8 = inputs.unsignedInputs & data.signed8;
result.signExtend16 = inputs.unsignedInputs & data.signed16;
return result;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -53,6 +53,7 @@ add_executable(MobileGLIntegrationTest
Harness/BackendCapsPeek.cpp
Harness/PipeSlotPeek.cpp
Harness/PipeApplyPeek.cpp
Harness/IntegerAttributeTypePeek.cpp
Harness/P4aSeamPeek.cpp
Harness/P4aFinalFixPeek.cpp
Harness/PersistentMapPeek.cpp
@@ -0,0 +1,279 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/IntegerAttributeTypePeek.cpp
// Copyright (c) 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 "IntegerAttributeTypePeek.h"
#include <cstdio>
#include <cstring>
#include <vector>
#if !defined(__ANDROID__)
#include <Config.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/Server/ServerLoop.h>
#endif
#define MGITEST_INTEGER_ATTRIBUTE_TYPE_PEEK_LIVE 1
#endif
namespace MGITest {
bool IntegerAttributeTypeFitsInput(unsigned inputType, bool arrayInteger, unsigned arrayType) {
switch (inputType) {
case 0x1404: // GL_INT
case 0x8B53: // GL_INT_VEC2
case 0x8B54: // GL_INT_VEC3
case 0x8B55: // GL_INT_VEC4
return arrayInteger && (arrayType == 0x1400 || arrayType == 0x1402 || arrayType == 0x1404);
case 0x1405: // GL_UNSIGNED_INT
case 0x8DC6: // GL_UNSIGNED_INT_VEC2
case 0x8DC7: // GL_UNSIGNED_INT_VEC3
case 0x8DC8: // GL_UNSIGNED_INT_VEC4
return arrayInteger && (arrayType == 0x1401 || arrayType == 0x1403 || arrayType == 0x1405);
default:
return !arrayInteger;
}
}
#if defined(MGITEST_INTEGER_ATTRIBUTE_TYPE_PEEK_LIVE)
namespace {
namespace MGB = MobileGL::MG_Backend::DirectGLES;
// Written and read only on the thread that issues Espryt's driver calls: the test thread
// in monolith, the apply thread under the in-process transport (every access there goes
// through a probe on that thread).
IntegerAttributeTypeTally g_tally;
bool g_watching = false;
void NoteViolation(GLint program, const char* name, GLint location, GLenum inputType, GLint arrayInteger,
GLint arrayType) {
++g_tally.violations;
if (!g_tally.firstViolation.empty()) return;
char text[256];
std::snprintf(text, sizeof(text),
"draw %u: driver program %d input '%s' (type 0x%04x) at location %d reads an array "
"declared %s with type 0x%04x",
g_tally.draws, static_cast<int>(program), name, static_cast<unsigned>(inputType),
static_cast<int>(location), arrayInteger ? "integer" : "float",
static_cast<unsigned>(arrayType));
g_tally.firstViolation = text;
}
// Every enabled array an active input of the current driver program reads.
void InspectDraw() {
++g_tally.draws;
auto& gl = MGB::g_GLESFuncs;
GLint program = 0;
gl.glGetIntegerv(GL_CURRENT_PROGRAM, &program);
if (program == 0) return;
GLint count = 0;
GLint maxLength = 0;
gl.glGetProgramiv(static_cast<GLuint>(program), GL_ACTIVE_ATTRIBUTES, &count);
gl.glGetProgramiv(static_cast<GLuint>(program), GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &maxLength);
std::vector<char> name(static_cast<std::size_t>(maxLength > 0 ? maxLength : 0) + 1, '\0');
bool violated = false;
for (GLint index = 0; index < count && !violated; ++index) {
GLint size = 0;
GLenum type = 0;
gl.glGetActiveAttrib(static_cast<GLuint>(program), static_cast<GLuint>(index),
static_cast<GLsizei>(name.size()), nullptr, &size, &type, name.data());
if (std::strncmp(name.data(), "gl_", 3) == 0) continue;
const GLint location = gl.glGetAttribLocation(static_cast<GLuint>(program), name.data());
if (location < 0) continue;
GLint enabled = 0;
GLint arrayInteger = 0;
GLint arrayType = 0;
gl.glGetVertexAttribiv(static_cast<GLuint>(location), GL_VERTEX_ATTRIB_ARRAY_ENABLED, &enabled);
if (!enabled) continue;
gl.glGetVertexAttribiv(static_cast<GLuint>(location), GL_VERTEX_ATTRIB_ARRAY_INTEGER, &arrayInteger);
gl.glGetVertexAttribiv(static_cast<GLuint>(location), GL_VERTEX_ATTRIB_ARRAY_TYPE, &arrayType);
if (IntegerAttributeTypeFitsInput(type, arrayInteger != 0, static_cast<unsigned>(arrayType))) continue;
NoteViolation(program, name.data(), location, type, arrayInteger, arrayType);
violated = true;
}
}
// One hook per driver entry point: the original is kept beside it and called after the
// inspection, with the arguments untouched.
#define MGITEST_HOOK(name, before) \
struct Hook_##name { \
static inline decltype(MGB::g_GLESFuncs.name) original = nullptr; \
template <typename... Args> \
static void Call(Args... args) { \
before(); \
original(args...); \
} \
static void Install() { \
if (original != nullptr || MGB::g_GLESFuncs.name == nullptr) return; \
original = MGB::g_GLESFuncs.name; \
MGB::g_GLESFuncs.name = &Call; \
} \
static void Remove() { \
if (original == nullptr) return; \
MGB::g_GLESFuncs.name = original; \
original = nullptr; \
} \
};
void CountLink() {
++g_tally.links;
}
MGITEST_HOOK(glDrawArrays, InspectDraw)
MGITEST_HOOK(glDrawElements, InspectDraw)
MGITEST_HOOK(glDrawRangeElements, InspectDraw)
MGITEST_HOOK(glDrawArraysInstanced, InspectDraw)
MGITEST_HOOK(glDrawElementsInstanced, InspectDraw)
MGITEST_HOOK(glDrawArraysIndirect, InspectDraw)
MGITEST_HOOK(glDrawElementsIndirect, InspectDraw)
MGITEST_HOOK(glDrawElementsBaseVertex, InspectDraw)
MGITEST_HOOK(glDrawRangeElementsBaseVertex, InspectDraw)
MGITEST_HOOK(glDrawElementsInstancedBaseVertex, InspectDraw)
MGITEST_HOOK(glMultiDrawArraysIndirectEXT, InspectDraw)
MGITEST_HOOK(glMultiDrawElementsIndirectEXT, InspectDraw)
MGITEST_HOOK(glMultiDrawElementsBaseVertexEXT, InspectDraw)
MGITEST_HOOK(glDrawArraysInstancedBaseInstanceEXT, InspectDraw)
MGITEST_HOOK(glDrawElementsInstancedBaseInstanceEXT, InspectDraw)
MGITEST_HOOK(glDrawElementsInstancedBaseVertexBaseInstanceEXT, InspectDraw)
MGITEST_HOOK(glLinkProgram, CountLink)
#undef MGITEST_HOOK
#define MGITEST_EACH_HOOK(action) \
Hook_glDrawArrays::action(); \
Hook_glDrawElements::action(); \
Hook_glDrawRangeElements::action(); \
Hook_glDrawArraysInstanced::action(); \
Hook_glDrawElementsInstanced::action(); \
Hook_glDrawArraysIndirect::action(); \
Hook_glDrawElementsIndirect::action(); \
Hook_glDrawElementsBaseVertex::action(); \
Hook_glDrawRangeElementsBaseVertex::action(); \
Hook_glDrawElementsInstancedBaseVertex::action(); \
Hook_glMultiDrawArraysIndirectEXT::action(); \
Hook_glMultiDrawElementsIndirectEXT::action(); \
Hook_glMultiDrawElementsBaseVertexEXT::action(); \
Hook_glDrawArraysInstancedBaseInstanceEXT::action(); \
Hook_glDrawElementsInstancedBaseInstanceEXT::action(); \
Hook_glDrawElementsInstancedBaseVertexBaseInstanceEXT::action(); \
Hook_glLinkProgram::action();
void StartOnDriverThread() {
g_tally = {};
if (!g_watching) {
MGITEST_EACH_HOOK(Install)
}
g_watching = true;
}
void StopOnDriverThread() {
if (g_watching) {
MGITEST_EACH_HOOK(Remove)
}
g_watching = false;
}
#undef MGITEST_EACH_HOOK
// "Is Espryt the backend running": on any other backend no ES entry point was resolved.
bool EsprytIsRunning() {
return MGB::g_GLESFuncs.glDrawArrays != nullptr || Hook_glDrawArrays::original != nullptr;
}
#if MOBILEGL_BUILD_DISAGGREGATED
bool InProcess() {
return MobileGL::MG_Config::Transport == MobileGL::MG_Config::TransportMode::InProcess;
}
// Runs `body` on the apply thread once every record published so far has been applied.
template <typename Body>
bool OnApplyThread(Body body) {
auto* session = MobileGL::MG_Remote::Client::ClientSession::Active();
if (session == nullptr) return false;
if (session->WaitForApplied(session->LastPublishedSeq(), 30000) !=
MobileGL::MG_Remote::Transport::SessionWait::Reached)
return false;
const auto result = MobileGL::MG_Remote::Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void* opaque) -> MobileGLResult {
(*static_cast<Body*>(opaque))();
return MOBILEGL_OK;
},
&body);
return result == MOBILEGL_OK;
}
#endif
} // namespace
bool StartIntegerAttributeTypeWatch(std::string* whyNot) {
std::string reason;
bool started = false;
if (MobileGL::MG_Config::Transport == MobileGL::MG_Config::TransportMode::Monolith) {
if (EsprytIsRunning()) {
StartOnDriverThread();
started = true;
} else {
reason = "the backend running is not DirectGLES";
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
else if (InProcess()) {
bool espryt = false;
const bool reached = OnApplyThread([&]() {
espryt = EsprytIsRunning();
if (espryt) StartOnDriverThread();
});
started = reached && espryt;
if (!started)
reason = reached ? "the backend running is not DirectGLES" : "the apply thread did not answer";
}
#endif
else {
reason = "the server runs in another process";
}
if (!started && whyNot != nullptr) *whyNot = reason;
return started;
}
bool ReadIntegerAttributeTypeWatch(IntegerAttributeTypeTally* out) {
if (out == nullptr) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
if (InProcess()) {
bool watching = false;
if (!OnApplyThread([&]() {
watching = g_watching;
*out = g_tally;
}))
return false;
return watching;
}
#endif
if (MobileGL::MG_Config::Transport != MobileGL::MG_Config::TransportMode::Monolith || !g_watching) return false;
*out = g_tally;
return true;
}
void StopIntegerAttributeTypeWatch() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (InProcess()) {
OnApplyThread([]() { StopOnDriverThread(); });
return;
}
#endif
if (MobileGL::MG_Config::Transport == MobileGL::MG_Config::TransportMode::Monolith) StopOnDriverThread();
}
#else
bool StartIntegerAttributeTypeWatch(std::string* whyNot) {
if (whyNot != nullptr) *whyNot = "Espryt's driver table is not reachable in this build";
return false;
}
bool ReadIntegerAttributeTypeWatch(IntegerAttributeTypeTally*) {
return false;
}
void StopIntegerAttributeTypeWatch() {}
#endif
} // namespace MGITest
@@ -0,0 +1,49 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/IntegerAttributeTypePeek.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// What Espryt hands the ES driver for each integer vertex input, checked at every draw the driver
// receives. ES defines an integer attribute's value only when the array's type has the input's
// signedness (BYTE/SHORT/INT for int, the unsigned three for uint) and a float input only from a
// float array; anything else is undefined, and a driver that sign-extends GL_UNSIGNED_BYTE data
// into an ivec4 input is within its rights. Values alone cannot show this on a driver that happens
// to zero-extend, so the watch reads the driver's own vertex array state at each draw.
//
// A SEPARATE TRANSLATION UNIT for PipeApplyPeek.h's reason: it includes Espryt's own headers,
// which may not meet a scenario's GL headers. Every entry point returns false where it cannot
// look - Android, a backend other than DirectGLES, and a server in another process - and a caller
// that gets false has learned nothing.
#pragma once
#include <string>
namespace MGITest {
struct IntegerAttributeTypeTally {
// Draws the driver received while the watch was on, and how many of them had at least one
// enabled array whose type the program's input cannot read with a defined value.
unsigned draws = 0;
unsigned violations = 0;
// glLinkProgram calls the driver received, for the cases that bound relinks.
unsigned links = 0;
// The first violation, spelled out for the failure message.
std::string firstViolation;
};
// Starts a fresh tally. False where the watch cannot run; `whyNot` then names the reason.
bool StartIntegerAttributeTypeWatch(std::string* whyNot);
// The tally so far, once every draw issued before the call has reached the driver.
bool ReadIntegerAttributeTypeWatch(IntegerAttributeTypeTally* out);
// Puts the driver's entry points back. Harmless when no watch is running.
void StopIntegerAttributeTypeWatch();
// The rule the watch applies to one enabled array: whether an input of GL type `inputType`
// reads an array declared (`arrayInteger`, `arrayType`) with a value ES defines.
bool IntegerAttributeTypeFitsInput(unsigned inputType, bool arrayInteger, unsigned arrayType);
} // namespace MGITest
@@ -151,3 +151,12 @@ DirectGLES.VerifySplit.ScenarioIsolationScenario.ACaseLeavesItsDeletedProgramCur
DirectGLES.VerifySplit.ScenarioIsolationScenario.TheNextCaseStartsWithNoProgramCurrentAndNoErrorPending
DirectVulkan.VerifySplit.ScenarioIsolationScenario.ACaseLeavesItsDeletedProgramCurrentAndAnErrorUnread
DirectVulkan.VerifySplit.ScenarioIsolationScenario.TheNextCaseStartsWithNoProgramCurrentAndNoErrorPending
# --------------------------------------------------------------------------------------------
# CANNED-INPUT CHECKS OF A HARNESS RULE - no GL call at all (2)
# NarrowIntegerAttributeScenario's driver-watch control feeds the array-type rule fixed type
# pairs and checks its verdicts. It makes no GL call, so no fill point is reached and the
# arming line is honestly absent.
# --------------------------------------------------------------------------------------------
DirectGLES.VerifySplit.NarrowIntegerAttributeScenario.TheDriverWatchRuleRejectsAnArrayOfTheOtherSignedness
DirectVulkan.VerifySplit.NarrowIntegerAttributeScenario.TheDriverWatchRuleRejectsAnArrayOfTheOtherSignedness
@@ -21,12 +21,20 @@
// data, so a pipeline or program cached for one array, or the variant the previous draw settled
// on, must never be handed to the other.
// On DirectGLES every case also watches the driver itself (IntegerAttributeTypePeek.h): an array
// whose type has the other signedness from its input reads a value ES leaves undefined, so a case
// can show the right values on one driver and still render black on another. Where the watch
// cannot look it says so and the value checks still run.
#include <cstddef>
#include <cstring>
#include <functional>
#include <iostream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/IntegerAttributeTypePeek.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
@@ -67,12 +75,14 @@ void main()
}
)";
// A capture program: the vertex stage's flat outputs `varyings`, interleaved.
GLuint BuildCaptureProgram(const char* vertexSource, const std::vector<const char*>& varyings,
std::string* log) {
// A program of `vertexSource` and `fragmentSource` capturing the vertex stage's flat outputs
// `varyings` (none: no capture), interleaved; `beforeLink` runs on it just before the link.
GLuint BuildProgram(const char* vertexSource, const char* fragmentSource,
const std::vector<const char*>& varyings, std::string* log,
const std::function<void(GLuint)>& beforeLink = {}) {
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
if (vertexShader == 0) return 0;
const GLuint fragmentShader = CompileShader(GL_FRAGMENT_SHADER, kFragmentSource, log);
const GLuint fragmentShader = CompileShader(GL_FRAGMENT_SHADER, fragmentSource, log);
if (fragmentShader == 0) {
glDeleteShader(vertexShader);
return 0;
@@ -80,8 +90,11 @@ void main()
const GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
GL_INTERLEAVED_ATTRIBS);
if (!varyings.empty()) {
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
GL_INTERLEAVED_ATTRIBS);
}
if (beforeLink) beforeLink(program);
glLinkProgram(program);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
@@ -99,6 +112,12 @@ void main()
return program;
}
// A capture program: the vertex stage's flat outputs `varyings`, interleaved.
GLuint BuildCaptureProgram(const char* vertexSource, const std::vector<const char*>& varyings, std::string* log,
const std::function<void(GLuint)>& beforeLink = {}) {
return BuildProgram(vertexSource, kFragmentSource, varyings, log, beforeLink);
}
// Every pairing the correction covers, plus two that must keep their value untouched:
// matching signedness (location 2) and 32-bit data (location 6).
const char* const kEveryPairingVertexSource = R"(#version 410 core
@@ -133,6 +152,88 @@ void main()
}
)";
const char* const kSingleUintInputVertexSource = R"(#version 410 core
layout(location = 0) in uvec4 value;
flat out uvec4 captured;
void main()
{
captured = value;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
// No layout qualifier: the location comes from glBindAttribLocation.
const char* const kUnplacedInputVertexSource = R"(#version 410 core
in ivec4 value;
flat out ivec4 captured;
void main()
{
captured = value;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
const char* const kWideAndMatchedVertexSource = R"(#version 410 core
layout(location = 0) in ivec2 uintAsInt;
layout(location = 1) in uvec2 ushortAsUint;
flat out ivec4 captured;
void main()
{
captured = ivec4(uintAsInt, ivec2(ushortAsUint));
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
// A program without a capture list, read back through a 2x1 RGBA32I target instead, so its
// first build is not one that captures. Vertex i lands on pixel i.
const char* const kRenderVertexSource = R"(#version 410 core
layout(location = 0) in ivec4 value;
flat out ivec4 passed;
void main()
{
passed = value;
gl_Position = vec4(gl_VertexID == 0 ? -0.5 : 0.5, 0.0, 0.0, 1.0);
}
)";
const char* const kRenderFragmentSource = R"(#version 410 core
flat in ivec4 passed;
layout(location = 0) out ivec4 color;
void main()
{
color = passed;
}
)";
const char* const kSeparableVertexSource = R"(#version 410 core
layout(location = 0) in ivec4 value;
layout(location = 0) flat out ivec4 passed;
out gl_PerVertex { vec4 gl_Position; };
void main()
{
passed = value;
gl_Position = vec4(gl_VertexID == 0 ? -0.5 : 0.5, 0.0, 0.0, 1.0);
}
)";
const char* const kSeparableFragmentSource = R"(#version 410 core
layout(location = 0) flat in ivec4 passed;
layout(location = 0) out ivec4 color;
void main()
{
color = passed;
}
)";
// The same four bytes as GL_BYTE and as GL_UNSIGNED_BYTE, and the words each reaches an
// input with: sign-extended from GL_BYTE, zero-extended from GL_UNSIGNED_BYTE, whatever the
// input's own signedness.
const GLubyte kBytes[4] = {0xFF, 0x80, 5, 127};
const std::vector<GLuint> kBytesAsSigned = {0xFFFFFFFFu, 0xFFFFFF80u, 5u, 127u};
const std::vector<GLuint> kBytesAsUnsigned = {255u, 128u, 5u, 127u};
const GLubyte kLightBytes[4] = {240, 200, 128, 7};
const std::vector<GLuint> kLightWords = {240u, 200u, 128u, 7u};
// One vertex of kEveryPairingVertexSource's per-vertex data, tightly packed.
struct PerVertex {
GLubyte ubyteAsInt[4];
@@ -153,10 +254,20 @@ void main()
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureWords * sizeof(GLuint), nullptr, GL_DYNAMIC_READ);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_capture);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "capture buffer setup";
std::string whyNot;
m_watching = StartIntegerAttributeTypeWatch(&whyNot);
if (!m_watching) {
// A log line and a property, never a skip: the value checks below still run.
std::cout << "[itest] integer attribute type watch off: " << whyNot << std::endl;
RecordProperty("integer_attribute_type_watch", whyNot);
}
}
void TearDown() override {
if (!Ready()) return;
if (m_watching) StopIntegerAttributeTypeWatch();
if (m_target != 0) glDeleteFramebuffers(1, &m_target);
if (m_targetTexture != 0) glDeleteTextures(1, &m_targetTexture);
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
glBindVertexArray(0);
@@ -188,7 +299,8 @@ void main()
}
// One point drawn under GL_RASTERIZER_DISCARD; the first `words` captured words.
std::vector<GLuint> Capture(GLuint program, GLuint vao, std::size_t words) {
std::vector<GLuint> Capture(GLuint program, GLuint vao, std::size_t words,
const std::function<void()>& draw = {}) {
const std::vector<GLuint> poison(kCaptureWords, kPoison);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_capture);
glBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kCaptureWords * sizeof(GLuint), poison.data());
@@ -196,7 +308,10 @@ void main()
glBindVertexArray(vao);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArraysInstanced(GL_POINTS, 0, 1, 1);
if (draw)
draw();
else
glDrawArraysInstanced(GL_POINTS, 0, 1, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<GLuint> result(words, kPoison);
@@ -211,7 +326,74 @@ void main()
return result;
}
// `points` points drawn into a 2x1 RGBA32I target with `program` (0: the bound pipeline),
// by `draw` or one glDrawArrays; the four words its fragment stage wrote for each.
std::vector<GLuint> Render(GLuint program, GLuint vao, int points = 1,
const std::function<void()>& draw = {}) {
if (m_target == 0) {
glGenTextures(1, &m_targetTexture);
glBindTexture(GL_TEXTURE_2D, m_targetTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32I, 2, 1);
glBindTexture(GL_TEXTURE_2D, 0);
glGenFramebuffers(1, &m_target);
glBindFramebuffer(GL_FRAMEBUFFER, m_target);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_targetTexture, 0);
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
}
glBindFramebuffer(GL_FRAMEBUFFER, m_target);
glViewport(0, 0, 2, 1);
const GLint poison[4] = {static_cast<GLint>(kPoison), static_cast<GLint>(kPoison),
static_cast<GLint>(kPoison), static_cast<GLint>(kPoison)};
glClearBufferiv(GL_COLOR, 0, poison);
glUseProgram(program);
glBindVertexArray(vao);
if (draw)
draw();
else
glDrawArrays(GL_POINTS, 0, 1);
GLint pixels[8] = {};
glReadPixels(0, 0, 2, 1, GL_RGBA_INTEGER, GL_INT, pixels);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return std::vector<GLuint>(pixels, pixels + 4 * points);
}
// A vertex array whose location 0 reads `bytes` as four `type` components.
GLuint MakeByteArray(const GLubyte (&bytes)[4], GLenum type) {
const GLuint vao = MakeVertexArray();
MakeBuffer(bytes, sizeof(bytes));
glVertexAttribIPointer(0, 4, type, 0, nullptr);
glEnableVertexAttribArray(0);
return vao;
}
// The driver's view of every draw so far; null where the watch is off.
bool ReadWatch(IntegerAttributeTypeTally* tally) {
if (!m_watching) return false;
glFinish();
const bool read = ReadIntegerAttributeTypeWatch(tally);
EXPECT_TRUE(read) << "the integer attribute type watch started but could not be read";
return read;
}
// Every array a draw so far handed the driver has the signedness of the input it feeds.
void ExpectDriverArraysFitTheirInputs() {
IntegerAttributeTypeTally tally;
if (!ReadWatch(&tally)) return;
RecordProperty("integer_attribute_type_watch_draws", static_cast<int>(tally.draws));
EXPECT_GT(tally.draws, 0u) << "the watch saw no draw, so it checked nothing";
EXPECT_EQ(tally.violations, 0u) << tally.firstViolation;
}
// Driver links so far, or -1 where the watch is off.
long LinksSoFar() {
IntegerAttributeTypeTally tally;
return ReadWatch(&tally) ? static_cast<long>(tally.links) : -1;
}
static constexpr std::size_t kCaptureWords = 16;
bool m_watching = false;
GLuint m_target = 0;
GLuint m_targetTexture = 0;
GLuint m_capture = 0;
std::vector<GLuint> m_programs;
std::vector<GLuint> m_vaos;
@@ -258,6 +440,7 @@ void main()
};
EXPECT_EQ(captured, expected);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
TEST_F(NarrowIntegerAttributeScenario, OneProgramFollowsTheSignednessOfEachVertexArray) {
@@ -288,6 +471,406 @@ void main()
EXPECT_EQ(Capture(program, signedArray, 4), asSigned) << "GL_BYTE again";
EXPECT_EQ(Capture(program, unsignedArray, 4), asUnsigned) << "GL_UNSIGNED_BYTE again";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// The negative control for the driver watch, on canned input: the rule must call an
// unsigned array into an int input wrong, and every matching pairing right.
TEST_F(NarrowIntegerAttributeScenario, TheDriverWatchRuleRejectsAnArrayOfTheOtherSignedness) {
EXPECT_FALSE(IntegerAttributeTypeFitsInput(GL_INT_VEC4, true, GL_UNSIGNED_BYTE));
EXPECT_FALSE(IntegerAttributeTypeFitsInput(GL_UNSIGNED_INT_VEC2, true, GL_SHORT));
EXPECT_FALSE(IntegerAttributeTypeFitsInput(GL_INT, true, GL_UNSIGNED_INT));
EXPECT_FALSE(IntegerAttributeTypeFitsInput(GL_INT_VEC4, false, GL_FLOAT));
EXPECT_FALSE(IntegerAttributeTypeFitsInput(GL_FLOAT_VEC4, true, GL_BYTE));
EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_INT_VEC4, true, GL_BYTE));
EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_INT_VEC2, true, GL_SHORT));
EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_INT, true, GL_INT));
EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_UNSIGNED_INT_VEC4, true, GL_UNSIGNED_BYTE));
EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_UNSIGNED_INT_VEC3, true, GL_UNSIGNED_SHORT));
EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_UNSIGNED_INT, true, GL_UNSIGNED_INT));
EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_FLOAT_VEC4, false, GL_UNSIGNED_BYTE));
}
// One array read in turn by an int and a uint program: each switch changes which of the two
// signednesses the array has to be issued with, and the second round must need no relink.
TEST_F(NarrowIntegerAttributeScenario, AnIntAndAUintProgramTakeTurnsOnOneArray) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint intProgram = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log);
ASSERT_NE(intProgram, 0u) << log;
m_programs.push_back(intProgram);
const GLuint uintProgram = BuildCaptureProgram(kSingleUintInputVertexSource, {"captured"}, &log);
ASSERT_NE(uintProgram, 0u) << log;
m_programs.push_back(uintProgram);
const GLuint signedArray = MakeByteArray(kBytes, GL_BYTE);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
EXPECT_EQ(Capture(intProgram, signedArray, 4), kBytesAsSigned) << "int first";
EXPECT_EQ(Capture(uintProgram, signedArray, 4), kBytesAsSigned) << "then uint";
const long linksAfterFirstRound = LinksSoFar();
for (int round = 0; round < 2; ++round) {
EXPECT_EQ(Capture(intProgram, signedArray, 4), kBytesAsSigned) << "int, round " << round;
EXPECT_EQ(Capture(uintProgram, signedArray, 4), kBytesAsSigned) << "uint, round " << round;
}
EXPECT_EQ(LinksSoFar(), linksAfterFirstRound) << "switching programs relinked one";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// A program that never captures is built for the data it first meets. Fed data of the other
// signedness afterwards it may be rebuilt once, and every value - before, during and after
// the switch - is still GL's.
TEST_F(NarrowIntegerAttributeScenario, AProgramFirstDrawnWithMatchingDataKeepsEveryValue) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint program = BuildProgram(kRenderVertexSource, kRenderFragmentSource, {}, &log);
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
const GLuint signedArray = MakeByteArray(kBytes, GL_BYTE);
const GLuint unsignedArray = MakeByteArray(kBytes, GL_UNSIGNED_BYTE);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
EXPECT_EQ(Render(program, signedArray), kBytesAsSigned) << "matching data first";
const long linksAfterFirstDraw = LinksSoFar();
EXPECT_EQ(Render(program, unsignedArray), kBytesAsUnsigned) << "then the other signedness";
EXPECT_EQ(Render(program, signedArray), kBytesAsSigned) << "matching data again";
EXPECT_EQ(Render(program, unsignedArray), kBytesAsUnsigned) << "the other signedness again";
if (linksAfterFirstDraw >= 0) EXPECT_LE(LinksSoFar(), linksAfterFirstDraw + 1) << "rebuilt more than once";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// Two programs from one source, the first built for matching data only and the second for
// data of the other signedness. Whatever the backend shares between the two builds must not
// hand the second one the first one's shader.
TEST_F(NarrowIntegerAttributeScenario, TwoProgramsFromOneSourceKeepTheirOwnBuilds) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint first = BuildProgram(kRenderVertexSource, kRenderFragmentSource, {}, &log);
ASSERT_NE(first, 0u) << log;
m_programs.push_back(first);
const GLuint second = BuildProgram(kRenderVertexSource, kRenderFragmentSource, {}, &log);
ASSERT_NE(second, 0u) << log;
m_programs.push_back(second);
const GLuint signedArray = MakeByteArray(kBytes, GL_BYTE);
const GLuint unsignedArray = MakeByteArray(kBytes, GL_UNSIGNED_BYTE);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
EXPECT_EQ(Render(first, signedArray), kBytesAsSigned) << "the first program, matching data";
EXPECT_EQ(Render(second, unsignedArray), kBytesAsUnsigned) << "the second, the other signedness";
EXPECT_EQ(Render(first, signedArray), kBytesAsSigned) << "the first again";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// A relink between two draws of the same data: whatever per-program state the correction
// keeps has to survive the program being rebuilt underneath it.
TEST_F(NarrowIntegerAttributeScenario, ARelinkBetweenDrawsKeepsTheValues) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log);
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
const GLuint unsignedArray = MakeByteArray(kLightBytes, GL_UNSIGNED_BYTE);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
EXPECT_EQ(Capture(program, unsignedArray, 4), kLightWords) << "before the relink";
glLinkProgram(program);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
ASSERT_EQ(status, GL_TRUE);
EXPECT_EQ(Capture(program, unsignedArray, 4), kLightWords) << "after the relink";
EXPECT_EQ(Capture(program, unsignedArray, 4), kLightWords) << "and once more";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// A program and a vertex array deleted and created again, most likely under the same names,
// with the signedness of input and data swapped: nothing learned about the first pair may
// be believed for the second.
TEST_F(NarrowIntegerAttributeScenario, AProgramAndAnArrayCreatedAgainStartOver) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint intProgram = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log);
ASSERT_NE(intProgram, 0u) << log;
GLuint unsignedArray = 0;
glGenVertexArrays(1, &unsignedArray);
glBindVertexArray(unsignedArray);
MakeBuffer(kLightBytes, sizeof(kLightBytes));
glVertexAttribIPointer(0, 4, GL_UNSIGNED_BYTE, 0, nullptr);
glEnableVertexAttribArray(0);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "first pair setup";
EXPECT_EQ(Capture(intProgram, unsignedArray, 4), kLightWords) << "the first pair";
glUseProgram(0);
glBindVertexArray(0);
glDeleteProgram(intProgram);
glDeleteVertexArrays(1, &unsignedArray);
const GLuint uintProgram = BuildCaptureProgram(kSingleUintInputVertexSource, {"captured"}, &log);
ASSERT_NE(uintProgram, 0u) << log;
m_programs.push_back(uintProgram);
const GLuint signedArray = MakeByteArray(kBytes, GL_BYTE);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "second pair setup";
// The case is about the reused names; a fresh name would check nothing it is for.
EXPECT_EQ(uintProgram, intProgram) << "the program name was not reused";
EXPECT_EQ(signedArray, unsignedArray) << "the vertex array name was not reused";
EXPECT_EQ(Capture(uintProgram, signedArray, 4), kBytesAsSigned) << "the second pair";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// The input's location comes from glBindAttribLocation rather than the shader.
TEST_F(NarrowIntegerAttributeScenario, ABoundAttributeLocationIsCorrected) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint program = BuildCaptureProgram(kUnplacedInputVertexSource, {"captured"}, &log,
[](GLuint p) { glBindAttribLocation(p, 5, "value"); });
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
ASSERT_EQ(glGetAttribLocation(program, "value"), 5);
const GLuint vao = MakeVertexArray();
MakeBuffer(kLightBytes, sizeof(kLightBytes));
glVertexAttribIPointer(5, 4, GL_UNSIGNED_BYTE, 0, nullptr);
glEnableVertexAttribArray(5);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
EXPECT_EQ(Capture(program, vao, 4), kLightWords);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// The array is described through the direct-state-access entry points.
TEST_F(NarrowIntegerAttributeScenario, ADirectStateAccessArrayIsCorrected) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log);
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
GLuint vao = 0;
glCreateVertexArrays(1, &vao);
m_vaos.push_back(vao);
GLuint buffer = 0;
glCreateBuffers(1, &buffer);
m_buffers.push_back(buffer);
glNamedBufferData(buffer, sizeof(kLightBytes), kLightBytes, GL_STATIC_DRAW);
glVertexArrayVertexBuffer(vao, 0, buffer, 0, 4);
glVertexArrayAttribIFormat(vao, 0, 4, GL_UNSIGNED_BYTE, 0);
glVertexArrayAttribBinding(vao, 0, 0);
glEnableVertexArrayAttrib(vao, 0);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
EXPECT_EQ(Capture(program, vao, 4), kLightWords);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// A binding of stride zero, which never advances: two points read the same element.
TEST_F(NarrowIntegerAttributeScenario, AZeroStrideBindingIsCorrected) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log);
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
const GLuint vao = MakeVertexArray();
const GLuint buffer = MakeBuffer(kLightBytes, sizeof(kLightBytes));
glVertexAttribIFormat(0, 4, GL_UNSIGNED_BYTE, 0);
glVertexAttribBinding(0, 0);
glBindVertexBuffer(0, buffer, 0, 0);
glEnableVertexAttribArray(0);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
std::vector<GLuint> expected = kLightWords;
expected.insert(expected.end(), kLightWords.begin(), kLightWords.end());
EXPECT_EQ(Capture(program, vao, 8, [] { glDrawArrays(GL_POINTS, 0, 2); }), expected);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// An array in the application's own memory, as ClientVertexArrayScenario draws one on
// every arm.
TEST_F(NarrowIntegerAttributeScenario, AClientMemoryArrayIsCorrected) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log);
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
const GLuint vao = MakeVertexArray();
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribIPointer(0, 4, GL_UNSIGNED_BYTE, 0, kLightBytes);
glEnableVertexAttribArray(0);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
EXPECT_EQ(Capture(program, vao, 4, [] { glDrawArrays(GL_POINTS, 0, 1); }), kLightWords);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// 32-bit data of the other signedness is the same bits, and 16-bit data of the input's own
// signedness is untouched; both still reach the driver with the input's signedness.
TEST_F(NarrowIntegerAttributeScenario, WideDataAndMatchingDataKeepTheirBits) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint program = BuildCaptureProgram(kWideAndMatchedVertexSource, {"captured"}, &log);
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
const GLuint wide[2] = {0xFFFFFFF0u, 7u};
const GLushort matched[2] = {65000, 300};
const GLuint vao = MakeVertexArray();
MakeBuffer(wide, sizeof(wide));
glVertexAttribIPointer(0, 2, GL_UNSIGNED_INT, 0, nullptr);
MakeBuffer(matched, sizeof(matched));
glVertexAttribIPointer(1, 2, GL_UNSIGNED_SHORT, 0, nullptr);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
const std::vector<GLuint> expected = {0xFFFFFFF0u, 7u, 65000u, 300u};
EXPECT_EQ(Capture(program, vao, 4), expected);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// Two points through one glMultiDrawArrays, read back rather than captured.
TEST_F(NarrowIntegerAttributeScenario, AMultiDrawIsCorrected) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint program = BuildProgram(kRenderVertexSource, kRenderFragmentSource, {}, &log);
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
const GLubyte points[8] = {240, 200, 128, 7, 250, 129, 1, 255};
const GLuint vao = MakeVertexArray();
MakeBuffer(points, sizeof(points));
glVertexAttribIPointer(0, 4, GL_UNSIGNED_BYTE, 0, nullptr);
glEnableVertexAttribArray(0);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
const std::vector<GLuint> expected = {240u, 200u, 128u, 7u, 250u, 129u, 1u, 255u};
EXPECT_EQ(Render(program, vao, 2,
[] {
const GLint firsts[2] = {0, 1};
const GLsizei counts[2] = {1, 1};
glMultiDrawArrays(GL_POINTS, firsts, counts, 2);
}),
expected);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// The integer input belongs to a separable vertex program bound through a pipeline.
TEST_F(NarrowIntegerAttributeScenario, AProgramPipelineVertexStageIsCorrected) {
if (!Ready()) GTEST_SKIP();
const GLuint vertexStage = glCreateShaderProgramv(GL_VERTEX_SHADER, 1, &kSeparableVertexSource);
const GLuint fragmentStage = glCreateShaderProgramv(GL_FRAGMENT_SHADER, 1, &kSeparableFragmentSource);
ASSERT_NE(vertexStage, 0u);
ASSERT_NE(fragmentStage, 0u);
m_programs.push_back(vertexStage);
m_programs.push_back(fragmentStage);
GLint linked = GL_FALSE;
glGetProgramiv(vertexStage, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE);
glGetProgramiv(fragmentStage, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE);
GLuint pipeline = 0;
glGenProgramPipelines(1, &pipeline);
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vertexStage);
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fragmentStage);
const GLuint unsignedArray = MakeByteArray(kLightBytes, GL_UNSIGNED_BYTE);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "pipeline setup";
glUseProgram(0);
glBindProgramPipeline(pipeline);
EXPECT_EQ(Render(0, unsignedArray), kLightWords);
glBindProgramPipeline(0);
glDeleteProgramPipelines(1, &pipeline);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// Whatever the driver is handed, the application still reads back the type it declared.
TEST_F(NarrowIntegerAttributeScenario, TheArrayTypeQueryStillReportsTheDeclaredType) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log);
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
const GLuint unsignedArray = MakeByteArray(kLightBytes, GL_UNSIGNED_BYTE);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
EXPECT_EQ(Capture(program, unsignedArray, 4), kLightWords);
glBindVertexArray(unsignedArray);
GLint type = 0;
GLint integer = 0;
glGetVertexAttribiv(0, GL_VERTEX_ATTRIB_ARRAY_TYPE, &type);
glGetVertexAttribiv(0, GL_VERTEX_ATTRIB_ARRAY_INTEGER, &integer);
EXPECT_EQ(type, GL_UNSIGNED_BYTE);
EXPECT_EQ(integer, GL_TRUE);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// A capture program's first draws, matching data then the other signedness, inside one
// capture: the program has to be built for both before the capture opens, since nothing
// may relink it while it captures.
TEST_F(NarrowIntegerAttributeScenario, ACaptureProgramMeetsBothSignednessesInOneCapture) {
if (!Ready()) GTEST_SKIP();
const long linksBefore = LinksSoFar();
std::string log;
const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log);
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
const GLuint signedArray = MakeByteArray(kBytes, GL_BYTE);
const GLuint unsignedArray = MakeByteArray(kLightBytes, GL_UNSIGNED_BYTE);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
std::vector<GLuint> expected = kBytesAsSigned;
expected.insert(expected.end(), kLightWords.begin(), kLightWords.end());
EXPECT_EQ(Capture(program, signedArray, 8,
[unsignedArray] {
glDrawArrays(GL_POINTS, 0, 1);
glBindVertexArray(unsignedArray);
glDrawArrays(GL_POINTS, 0, 1);
}),
expected);
if (linksBefore >= 0) EXPECT_LE(LinksSoFar(), linksBefore + 1) << "the program was relinked";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
// The application relinks the same program object with its input at location 0 turned
// from ivec4 into uvec4: what was known about the old link's inputs no longer holds.
TEST_F(NarrowIntegerAttributeScenario, ARelinkThatChangesTheInputsSignednessStartsOver) {
if (!Ready()) GTEST_SKIP();
std::string log;
const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log);
ASSERT_NE(program, 0u) << log;
m_programs.push_back(program);
const GLuint unsignedArray = MakeByteArray(kLightBytes, GL_UNSIGNED_BYTE);
const GLuint signedArray = MakeByteArray(kBytes, GL_BYTE);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
EXPECT_EQ(Capture(program, unsignedArray, 4), kLightWords) << "the ivec4 link";
GLuint attached[4] = {};
GLsizei attachedCount = 0;
glGetAttachedShaders(program, 4, &attachedCount, attached);
for (GLsizei i = 0; i < attachedCount; ++i)
glDetachShader(program, attached[i]);
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, kSingleUintInputVertexSource, &log);
ASSERT_NE(vertexShader, 0u) << log;
const GLuint fragmentShader = CompileShader(GL_FRAGMENT_SHADER, kFragmentSource, &log);
ASSERT_NE(fragmentShader, 0u) << log;
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
ASSERT_EQ(status, GL_TRUE);
EXPECT_EQ(Capture(program, signedArray, 4), kBytesAsSigned) << "the uvec4 link, GL_BYTE data";
EXPECT_EQ(Capture(program, unsignedArray, 4), kLightWords) << "the uvec4 link, matching data";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
ExpectDriverArraysFitTheirInputs();
}
} // namespace
} // namespace MGITest
+10
View File
@@ -405,6 +405,16 @@ namespace MobileGL::MG_Pipe {
};
mutable Array<BackendHashMemoEntry, 4> BackendHashMemo{};
mutable Uint32 BackendHashMemoNext = 0;
// A backend's own derivation from the archive: the locations of its 32-bit int and uint
// vertex inputs, valid while Serial holds the value it was taken at. It dies with the
// record like the memo above, and nothing in MG_Pipe reads it.
struct BackendIntegerInputsMemo {
Uint64 Serial = 0;
Uint32 SignedInputs = 0;
Uint32 UnsignedInputs = 0;
Bool Valid = false;
};
mutable BackendIntegerInputsMemo BackendIntegerInputs{};
};
// The vertex-elements CSO as the applier holds it: the unpacked blob, both views, plus
@@ -74,6 +74,17 @@ add_executable(SubsystemDepsTest SubsystemDepsTest.cpp)
target_include_directories(SubsystemDepsTest PRIVATE ${MGL_ROOT}/include ${MGL_ROOT}/MobileGL)
target_link_libraries(SubsystemDepsTest PRIVATE MobileGLTestMain ${LINK_LIBRARIES})
gtest_discover_tests(SubsystemDepsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
# DirectGLES's narrow integer attributes: the per-draw plan, and an armed vertex stage's ESSL.
add_executable(IntegerAttributePlanTest IntegerAttributePlanTest.cpp)
target_include_directories(IntegerAttributePlanTest PRIVATE ${MGL_ROOT}/include ${MGL_ROOT}/MobileGL)
target_link_libraries(IntegerAttributePlanTest PRIVATE MobileGLTestMain ${LINK_LIBRARIES})
gtest_discover_tests(IntegerAttributePlanTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(NarrowIntegerEsslTest NarrowIntegerEsslTest.cpp)
target_include_directories(NarrowIntegerEsslTest PRIVATE ${MGL_ROOT}/include ${MGL_ROOT}/MobileGL)
target_link_libraries(NarrowIntegerEsslTest PRIVATE MobileGLTestMain glslang-default-resource-limits ${LINK_LIBRARIES})
gtest_discover_tests(NarrowIntegerEsslTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(BaseInstanceInjectionTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(ViewportIndexRoutingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -0,0 +1,149 @@
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/IntegerAttributePlanTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// What DirectGLES does with each (array data type, shader input type) pair. ES defines an integer
// attribute only when the array has the input's signedness, so every integer array is issued with
// it; 8/16-bit data of the other signedness also needs the program's seeds to put GL's value back,
// and without working seeds such an array keeps the application's type instead.
#include <gtest/gtest.h>
#include <MG_Backend/DirectGLES/IntegerAttributePlan.h>
#include <MG_Backend/DirectGLES/NarrowIntegerSeeds.h>
using namespace MobileGL;
using MobileGL::MG_Backend::DirectGLES::HeldIntegerRetype;
using MobileGL::MG_Backend::DirectGLES::IntegerAttribTypeForInput;
using MobileGL::MG_Backend::DirectGLES::IntegerAttributeDataMasks;
using MobileGL::MG_Backend::DirectGLES::IntegerAttributePlan;
using MobileGL::MG_Backend::DirectGLES::IntegerVertexInputMasks;
using MobileGL::MG_Backend::DirectGLES::NarrowIntegerSeedState;
using MobileGL::MG_Backend::DirectGLES::ResolveIntegerAttributePlan;
namespace {
struct DataCase {
DataType type;
GLenum glType;
Uint32 bits;
Bool isSigned;
};
constexpr DataCase kData[] = {
{DataType::Int8, GL_BYTE, 8, true}, {DataType::Uint8, GL_UNSIGNED_BYTE, 8, false},
{DataType::Int16, GL_SHORT, 16, true}, {DataType::Uint16, GL_UNSIGNED_SHORT, 16, false},
{DataType::Int32, GL_INT, 32, true}, {DataType::Uint32, GL_UNSIGNED_INT, 32, false},
};
Bool IsSignedGLType(GLenum type) {
return type == GL_BYTE || type == GL_SHORT || type == GL_INT;
}
IntegerAttributePlan PlanFor(DataType data, GLenum input, Uint32 location, Bool seedsUsable = true) {
IntegerVertexInputMasks inputs;
inputs.Note(location, input);
IntegerAttributeDataMasks masks;
masks.Note(location, data);
return ResolveIntegerAttributePlan(inputs, masks, seedsUsable);
}
} // namespace
TEST(IntegerAttributePlan, EveryIntegerArrayIsIssuedWithItsInputsSignedness) {
constexpr Uint32 location = 3;
constexpr Uint32 bit = 1u << location;
for (const DataCase& data : kData) {
for (const GLenum input : {GL_INT_VEC4, GL_UNSIGNED_INT_VEC2}) {
const Bool signedInput = input == GL_INT_VEC4;
const IntegerAttributePlan plan = PlanFor(data.type, input, location);
const Bool mismatch = signedInput != data.isSigned;
SCOPED_TRACE(testing::Message() << "data 0x" << std::hex << data.glType << " input 0x" << input);
EXPECT_EQ(plan.careMask, bit);
EXPECT_EQ(plan.retypeMask, mismatch ? bit : 0u);
EXPECT_EQ(IsSignedGLType(IntegerAttribTypeForInput(data.glType, (plan.retypeMask & bit) != 0)),
signedInput);
// Only 8/16-bit data of the other signedness needs the seeds; 32-bit is the same bits.
EXPECT_EQ(plan.widths8, mismatch && data.bits == 8 ? bit : 0u);
EXPECT_EQ(plan.widths16, mismatch && data.bits == 16 ? bit : 0u);
EXPECT_EQ(plan.widths8 & plan.widths16, 0u);
}
}
}
TEST(IntegerAttributePlan, AFloatInputLeavesItsArrayAlone) {
for (const DataCase& data : kData) {
const IntegerAttributePlan plan = PlanFor(data.type, GL_FLOAT_VEC4, 5);
EXPECT_EQ(plan, IntegerAttributePlan{}) << std::hex << data.glType;
}
// ...and an integer input fed float data has no integer array to issue.
EXPECT_EQ(PlanFor(DataType::Float32, GL_INT_VEC4, 5), IntegerAttributePlan{});
}
TEST(IntegerAttributePlan, WithoutWorkingSeedsANarrowArrayKeepsTheApplicationsType) {
for (const DataCase& data : kData) {
const GLenum input = data.isSigned ? GL_UNSIGNED_INT : GL_INT;
const IntegerAttributePlan plan = PlanFor(data.type, input, 7, false);
EXPECT_FALSE(plan.NeedsSeeds()) << std::hex << data.glType;
EXPECT_EQ(plan.retypeMask, data.bits == 32 ? (1u << 7) : 0u) << std::hex << data.glType;
EXPECT_EQ(plan, PlanFor(data.type, input, 7).WithoutSeeds());
}
}
TEST(IntegerAttributePlan, LocationsStopAt31) {
EXPECT_EQ(PlanFor(DataType::Uint8, GL_INT, 31).widths8, 1u << 31);
EXPECT_EQ(PlanFor(DataType::Uint8, GL_INT, 32), IntegerAttributePlan{});
}
TEST(IntegerAttributePlan, AVertexArrayKeepsWhatItHoldsWhereNothingCanSeeIt) {
IntegerAttributePlan plan;
plan.careMask = 0b0110u;
plan.retypeMask = 0b0100u;
// Bit 0 is held and unobservable: kept. Bit 1 is observable and wanted plain: cleared.
EXPECT_EQ(HeldIntegerRetype(0b0011u, plan), 0b0101u);
EXPECT_EQ(HeldIntegerRetype(0u, IntegerAttributePlan{}), 0u);
}
TEST(IntegerAttributePlan, TheIssuedTypeOnlyEverSwapsSignedness) {
for (const DataCase& data : kData) {
EXPECT_EQ(IntegerAttribTypeForInput(data.glType, false), data.glType);
const GLenum swapped = IntegerAttribTypeForInput(data.glType, true);
EXPECT_NE(IsSignedGLType(swapped), data.isSigned);
EXPECT_EQ(IntegerAttribTypeForInput(swapped, true), data.glType);
}
EXPECT_EQ(IntegerAttribTypeForInput(GL_FLOAT, true), static_cast<GLenum>(GL_FLOAT));
}
// A program's arming across builds: a failed armed build is retried without the seeds for that
// link only, and an application relink starts over from what the new link declares.
TEST(NarrowIntegerSeedState, AFailedArmedBuildLastsUntilTheNextLink) {
using MobileGL::MG_Backend::DirectGLES::BeginNarrowIntegerSeedBuild;
using MobileGL::MG_Backend::DirectGLES::FinishNarrowIntegerSeedBuild;
NarrowIntegerSeedState state;
state.PrepareBuild(1, true, false);
EXPECT_FALSE(state.wanted) << "a program that does not capture is armed by a draw, not a build";
state.wanted = true;
BeginNarrowIntegerSeedBuild(state);
FinishNarrowIntegerSeedBuild(state, 0, false);
EXPECT_TRUE(state.RefusedAt(1));
EXPECT_TRUE(state.Stale()) << "the failed link is rebuilt without the seeds";
state.PrepareBuild(1, true, false);
BeginNarrowIntegerSeedBuild(state);
EXPECT_FALSE(state.built);
EXPECT_TRUE(state.RefusedAt(1));
EXPECT_FALSE(state.Stale());
EXPECT_FALSE(state.RefusedAt(2)) << "a link not built yet is presumed to take the seeds";
state.PrepareBuild(2, true, false);
EXPECT_FALSE(state.RefusedAt(2));
EXPECT_TRUE(state.Target()) << "a relink arms again";
EXPECT_TRUE(state.Stale());
state.PrepareBuild(3, false, false);
EXPECT_FALSE(state.wanted) << "a link without integer inputs has nothing to arm";
EXPECT_FALSE(state.Target());
state.PrepareBuild(4, true, true);
EXPECT_TRUE(state.Target()) << "a capturing link is armed at its build";
}
@@ -0,0 +1,157 @@
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/NarrowIntegerEsslTest.cpp
// Copyright (c) 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
//
// A vertex stage armed with the narrow-integer seeds, through the same steps the DirectGLES build
// takes: the array-input split, the RuntimeWidths pass, ESSL 3.00 emission and the uniform
// promotion. The two widths have to come out as `uniform highp uint` (anything else is read
// uninitialised or at too low a precision) and the result has to be ESSL 3.00 a compiler accepts.
// The emission here uses plain ESSL 3.00 options, not the build's whole pass chain; the
// integration lanes run that chain (NarrowIntegerAttributeScenario).
#include <gtest/gtest.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include "Init.h"
#include <glslang/Public/ResourceLimits.h>
#include <glslang/Public/ShaderLang.h>
using namespace MobileGL;
using MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms;
using MobileGL::MG_Backend::DirectGLES::PromoteNarrowIntegerWidthGlobals;
using MobileGL::MG_Util::ShaderTranspiler::kNarrowIntegerWidths16GlobalName;
using MobileGL::MG_Util::ShaderTranspiler::kNarrowIntegerWidths8GlobalName;
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
namespace {
constexpr const char* kVertex = R"(#version 330 core
layout(location = 0) in vec2 pos;
layout(location = 1) in ivec4 lightAndData;
layout(location = 2) in uvec2 texc;
layout(location = 3) in int packed[2];
flat out ivec4 a;
flat out uvec4 b;
void main() {
a = lightAndData + ivec4(packed[0], packed[1], 0, 0);
b = uvec4(texc, 0u, 0u);
gl_Position = vec4(pos, 0.0, 1.0);
}
)";
constexpr const char* kFragment = R"(#version 330 core
flat in ivec4 a;
flat in uvec4 b;
out vec4 color;
void main() { color = vec4(a) + vec4(b); }
)";
Vector<Uint32> CompileVertexModule() {
using namespace MG_Util::ShaderTranspiler;
Vector<SharedPtr<glslang::TShader>> shaders;
for (const auto& [stage, source] :
Vector<Pair<GLenum, const char*>>{{GL_VERTEX_SHADER, kVertex}, {GL_FRAGMENT_SHADER, kFragment}}) {
ShaderAttrib attrib{.shaderType = stage, .sourceStr = source};
auto shader = ShaderCompiler::CompileShader(attrib);
EXPECT_TRUE(shader) << (shader ? String{} : shader.error().log);
if (!shader) return {};
shaders.push_back(shader.value());
}
ProgramAttrib programAttrib{.shaders = shaders};
auto program = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(program) << (program ? String{} : program.error().log);
if (!program) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER, GL_FRAGMENT_SHADER},
.program = *program.value()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binary) << (binary ? String{} : binary.error().log);
if (!binary || binary->size() != 2) return {};
Vector<Uint32> module = binary.value()[0];
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
return module;
}
String EmitEssl300(const Vector<Uint32>& spirv) {
SpvcSession session(spirv, SessionUsageBit::Transpile);
spvc_compiler_options options;
session.CreateOptions(&options);
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 300);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
session.SetOptions(options);
const char* result = nullptr;
session.Compile(&result);
EXPECT_NE(result, nullptr) << (session.GetLastErrorString() ? session.GetLastErrorString() : "");
return result != nullptr ? String(result) : String{};
}
// Declarations of `name` as a uint, whatever their qualifiers or initialiser.
SizeT CountUintDeclarations(const String& source, const char* name) {
SizeT count = 0;
for (const char* suffix : {";", " ="}) {
const String declaration = String("uint ") + name + suffix;
for (SizeT pos = source.find(declaration); pos != String::npos; pos = source.find(declaration, pos + 1))
++count;
}
return count;
}
String CompileErrorAsEssl300Vertex(const String& source) {
glslang::TShader shader(EShLangVertex);
const char* strings[] = {source.c_str()};
shader.setStrings(strings, 1);
if (shader.parse(GetDefaultResources(), 300, EEsProfile, false, false, EShMsgDefault)) return {};
return shader.getInfoLog();
}
} // namespace
TEST(NarrowIntegerEssl, ArmedVertexStageDeclaresBothWidthsAsHighpUniformsInValidEssl300) {
MobileGL::Initialize();
const Vector<Uint32> module = CompileVertexModule();
ASSERT_FALSE(module.empty());
Vector<Uint32> split;
ASSERT_TRUE(ShaderCompiler::SplitArrayVertexInputsForEssl(module, split, true));
Vector<Uint32> seeded;
Bool changed = false;
ASSERT_TRUE(
ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(split.empty() ? module : split, seeded, changed, true));
ASSERT_TRUE(changed);
const String emitted = EmitEssl300(seeded);
ASSERT_FALSE(emitted.empty());
const String essl = PromoteDrawParameterGlobalsToUniforms(emitted, GL_VERTEX_SHADER);
for (const char* name : {kNarrowIntegerWidths8GlobalName, kNarrowIntegerWidths16GlobalName}) {
EXPECT_NE(essl.find(String("uniform highp uint ") + name + ";"), String::npos) << name << "\n" << essl;
// Exactly one declaration, and it is the uniform.
EXPECT_EQ(CountUintDeclarations(essl, name), 1u) << name << "\n" << essl;
}
for (const char* bitfield : {"bitfieldExtract", "bitfieldInsert"})
EXPECT_EQ(essl.find(bitfield), String::npos) << essl;
EXPECT_EQ(CompileErrorAsEssl300Vertex(essl), String{}) << essl;
}
TEST(NarrowIntegerEssl, OnlyALineStartingDeclarationIsPromoted) {
const String promoted = PromoteNarrowIntegerWidthGlobals("#version 300 es\n"
"uint mg_NarrowIntegerWidths8 = 0u;\n"
"highp uint mg_NarrowIntegerWidths16;\n"
"void main() { uint x = mg_NarrowIntegerWidths8; }\n");
EXPECT_EQ(promoted, "#version 300 es\n"
"uniform highp uint mg_NarrowIntegerWidths8;\n"
"uniform highp uint mg_NarrowIntegerWidths16;\n"
"void main() { uint x = mg_NarrowIntegerWidths8; }\n");
// Already a uniform: untouched, and a mediump spelling is promoted to highp all the same.
EXPECT_EQ(PromoteNarrowIntegerWidthGlobals(promoted), promoted);
EXPECT_EQ(PromoteNarrowIntegerWidthGlobals("mediump uint mg_NarrowIntegerWidths16;\n"),
"uniform highp uint mg_NarrowIntegerWidths16;\n");
}
@@ -36,6 +36,7 @@ add_executable(
ZeroLayerOutputTest.cpp
FullCoverageSampleMaskTest.cpp
ExtendNarrowIntegerVertexInputsTest.cpp
ExtendNarrowIntegerVertexInputsRuntimeTest.cpp
RelaxedOpaqueStructTest.cpp
ShadingRateEsslTest.cpp
)
@@ -0,0 +1,274 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/ExtendNarrowIntegerVertexInputsRuntimeTest.cpp
// Copyright (c) 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
//
// ExtendNarrowIntegerVertexInputsPass's RuntimeWidths mode, the one a backend uses when it cannot
// vary the program per draw. Every 32-bit integer input below location 32 is seeded from the two
// width globals, with shifts only - no bitfield instruction, which ESSL 3.00 cannot spell - and the
// module must stay valid, keep its reflected inputs and leave a module without integer inputs alone.
#include <gtest/gtest.h>
#include <algorithm>
#include <tuple>
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
#include <spirv_reflect.h>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::kNarrowIntegerWidths16GlobalName;
using MobileGL::MG_Util::ShaderTranspiler::kNarrowIntegerWidths8GlobalName;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr const char* kVertex = R"(#version 330 core
layout(location = 0) in vec2 pos;
layout(location = 1) in ivec4 lightAndData;
layout(location = 2) in uvec2 texc;
layout(location = 3) in int single;
layout(location = 4) in uint other;
layout(location = 5) in int packed[2];
flat out ivec4 a;
flat out uvec4 b;
void main() {
a = lightAndData + ivec4(single, packed[0], packed[1], 0);
b = uvec4(texc, other, 0u);
gl_Position = vec4(pos, 0.0, 1.0);
}
)";
constexpr const char* kFloatOnlyVertex = R"(#version 330 core
layout(location = 0) in vec2 pos;
void main() { gl_Position = vec4(pos, 0.0, 1.0); }
)";
constexpr const char* kFragment = R"(#version 330 core
flat in ivec4 a;
flat in uvec4 b;
out vec4 color;
void main() { color = vec4(a) + vec4(b); }
)";
constexpr const char* kPlainFragment = R"(#version 330 core
out vec4 color;
void main() { color = vec4(1.0); }
)";
// Index 0 is the vertex module, 1 the fragment module.
Vector<Vector<Uint32>> CompileProgram(const char* vertex, const char* fragment) {
using namespace MG_Util::ShaderTranspiler;
Vector<SharedPtr<glslang::TShader>> shaders;
for (const auto& [stage, source] :
Vector<Pair<GLenum, const char*>>{{GL_VERTEX_SHADER, vertex}, {GL_FRAGMENT_SHADER, fragment}}) {
ShaderAttrib attrib{.shaderType = stage, .sourceStr = source};
auto shader = ShaderCompiler::CompileShader(attrib);
EXPECT_TRUE(shader) << (shader ? String{} : shader.error().log);
if (!shader) return {};
shaders.push_back(shader.value());
}
ProgramAttrib programAttrib{.shaders = shaders};
auto program = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(program) << (program ? String{} : program.error().log);
if (!program) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER, GL_FRAGMENT_SHADER},
.program = *program.value()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binary) << (binary ? String{} : binary.error().log);
if (!binary || binary->size() != 2) return {};
Vector<Vector<Uint32>> modules = binary.value();
for (auto& module : modules)
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
return modules;
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
EXPECT_TRUE(tools.Disassemble(spirv, &text));
return text;
}
String ValidationError(const Vector<Uint32>& spirv, spv_target_env env = SPV_ENV_VULKAN_1_1) {
spvtools::SpirvTools tools(env);
String error;
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
error += message != nullptr ? message : "";
});
return tools.Validate(spirv) ? String{} : (error.empty() ? String{"invalid"} : error);
}
SizeT CountOccurrences(const String& text, const String& needle) {
SizeT count = 0;
for (SizeT at = text.find(needle); at != String::npos; at = text.find(needle, at + needle.size()))
++count;
return count;
}
Vector<String> LinesWith(const String& text, const String& needle) {
Vector<String> lines;
SizeT begin = 0;
while (begin < text.size()) {
SizeT end = text.find('\n', begin);
if (end == String::npos) end = text.size();
String line = text.substr(begin, end - begin);
if (line.find(needle) != String::npos) lines.push_back(line);
begin = end + 1;
}
return lines;
}
Vector<std::tuple<Uint32, Uint32, Uint32>> ReflectInputs(const Vector<Uint32>& spirv) {
SpvReflectShaderModule module{};
EXPECT_EQ(spvReflectCreateShaderModule(spirv.size() * sizeof(Uint32), spirv.data(), &module),
SPV_REFLECT_RESULT_SUCCESS);
Uint32 count = 0;
EXPECT_EQ(spvReflectEnumerateInputVariables(&module, &count, nullptr), SPV_REFLECT_RESULT_SUCCESS);
Vector<SpvReflectInterfaceVariable*> inputs(count);
EXPECT_EQ(spvReflectEnumerateInputVariables(&module, &count, inputs.data()), SPV_REFLECT_RESULT_SUCCESS);
Vector<std::tuple<Uint32, Uint32, Uint32>> result;
for (const auto* input : inputs) {
if (input->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) continue;
result.emplace_back(input->location, input->numeric.vector.component_count,
static_cast<Uint32>(input->type_description->type_flags));
}
spvReflectDestroyShaderModule(&module);
std::sort(result.begin(), result.end());
return result;
}
// Moves every `OpDecorate %id Location from` to `to`, in the binary itself.
void MoveLocation(Vector<Uint32>& spirv, Uint32 from, Uint32 to) {
for (SizeT word = 5; word < spirv.size();) {
const Uint32 count = spirv[word] >> 16;
const Uint32 opcode = spirv[word] & 0xFFFFu;
if (count == 0) break;
if (opcode == 71u /* OpDecorate */ && count == 4 && spirv[word + 2] == 30u /* Location */ &&
spirv[word + 3] == from)
spirv[word + 3] = to;
word += count;
}
}
} // namespace
TEST(ExtendNarrowIntegerVertexInputsRuntimeWidths, SeedsEveryIntegerInputFromTheTwoWidths) {
MobileGL::Initialize();
const auto modules = CompileProgram(kVertex, kFragment);
ASSERT_EQ(modules.size(), 2u);
const String original = Disassemble(modules[0]);
Vector<Uint32> output;
Bool changed = false;
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(modules[0], output, changed, true));
EXPECT_TRUE(changed);
EXPECT_EQ(ValidationError(output), String{});
const String text = Disassemble(output);
// The two globals: named, Private, initialised to 0 and never stored.
for (const char* name : {kNarrowIntegerWidths8GlobalName, kNarrowIntegerWidths16GlobalName}) {
const String id = String("%") + name;
EXPECT_NE(text.find(id + " = OpVariable %_ptr_Private_uint Private %uint_0"), String::npos) << name << "\n"
<< text;
EXPECT_EQ(CountOccurrences(text, String("OpStore ") + id + " "), 0u) << text;
EXPECT_EQ(CountOccurrences(text, String("OpLoad %uint ") + id), 1u) << text;
}
// Every integer input is a seeded Private copy now; the float input is untouched.
for (const char* name : {"%lightAndData", "%texc", "%single", "%other", "%packed"}) {
EXPECT_NE(text.find(String(name) + " = OpVariable %_ptr_Private_"), String::npos) << name << "\n" << text;
EXPECT_EQ(CountOccurrences(text, String("OpStore ") + name + " "), 1u) << name << "\n" << text;
}
EXPECT_NE(text.find("%pos = OpVariable %_ptr_Input_v2float Input"), String::npos) << text;
// Six seeded values (the ivec4, the uvec2, the int, the uint and two array elements): a left
// shift each, an arithmetic right shift for the two uint ones, a logical one for the four int
// ones, and the shift amount splatted for the two vectors. No bitfield instruction at all.
EXPECT_EQ(CountOccurrences(text, "OpShiftLeftLogical") - CountOccurrences(original, "OpShiftLeftLogical"), 6u)
<< text;
EXPECT_EQ(CountOccurrences(text, "OpShiftRightArithmetic %v2uint") +
CountOccurrences(text, "OpShiftRightArithmetic %uint"),
2u)
<< text;
EXPECT_EQ(CountOccurrences(text, "OpShiftLeftLogical %v4int"), 1u) << text;
EXPECT_EQ(CountOccurrences(text, "OpShiftRightLogical %v4int"), 1u) << text;
EXPECT_EQ(CountOccurrences(text, "OpShiftRightLogical %int"), 3u) << text;
for (const char* splat : {"OpCompositeConstruct %v4uint", "OpCompositeConstruct %v2uint"})
EXPECT_EQ(CountOccurrences(text, splat) - CountOccurrences(original, splat), 1u) << splat << "\n" << text;
EXPECT_EQ(CountOccurrences(text, "OpBitFieldSExtract"), 0u) << text;
EXPECT_EQ(CountOccurrences(text, "OpBitFieldUExtract"), 0u) << text;
EXPECT_NE(text.find("%uint_24"), String::npos) << text;
EXPECT_NE(text.find("%uint_16"), String::npos) << text;
EXPECT_EQ(ReflectInputs(output), ReflectInputs(modules[0]));
}
// A width bit exists only for locations 0-31, and shifting a 32-bit value by 32 or more is
// undefined, so an input from location 32 up keeps its fetched value and its Input variable.
TEST(ExtendNarrowIntegerVertexInputsRuntimeWidths, LeavesLocationsFrom32UpAlone) {
MobileGL::Initialize();
auto modules = CompileProgram(kVertex, kFragment);
ASSERT_EQ(modules.size(), 2u);
Vector<Uint32> input = modules[0];
MoveLocation(input, 1, 40); // lightAndData
MoveLocation(input, 3, 33); // single
Vector<Uint32> output;
Bool changed = false;
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(input, output, changed, true));
EXPECT_TRUE(changed);
const String text = Disassemble(output);
EXPECT_NE(text.find("%lightAndData = OpVariable %_ptr_Input_v4int Input"), String::npos) << text;
EXPECT_NE(text.find("%single = OpVariable %_ptr_Input_int Input"), String::npos) << text;
EXPECT_NE(text.find("%texc = OpVariable %_ptr_Private_"), String::npos) << text;
// Every integer input above the bits: nothing to seed, so nothing changes.
for (const Uint32 location : {2u, 4u, 5u, 6u})
MoveLocation(input, location, 50 + location);
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(input, output, changed, true));
EXPECT_FALSE(changed);
EXPECT_EQ(output, input);
}
TEST(ExtendNarrowIntegerVertexInputsRuntimeWidths, LeavesAModuleWithoutIntegerInputsAlone) {
MobileGL::Initialize();
const auto plain = CompileProgram(kFloatOnlyVertex, kPlainFragment);
ASSERT_EQ(plain.size(), 2u);
Vector<Uint32> output;
Bool changed = true;
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(plain[0], output, changed, true));
EXPECT_FALSE(changed);
EXPECT_EQ(output, plain[0]);
const auto modules = CompileProgram(kVertex, kFragment);
ASSERT_EQ(modules.size(), 2u);
changed = true;
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(modules[1], output, changed, true));
EXPECT_FALSE(changed) << "a fragment module has no vertex inputs";
EXPECT_EQ(output, modules[1]);
}
// From SPIR-V 1.4 an entry point lists every global it uses, so the two width globals have to
// join the interface beside the new Inputs and the Private copies.
TEST(ExtendNarrowIntegerVertexInputsRuntimeWidths, ListsTheWidthGlobalsInASpirv14Interface) {
MobileGL::Initialize();
const auto modules = CompileProgram(kVertex, kFragment);
ASSERT_EQ(modules.size(), 2u);
Vector<Uint32> input = modules[0];
input[1] = 0x00010400u;
ASSERT_EQ(ValidationError(input, SPV_ENV_VULKAN_1_2), String{});
Vector<Uint32> output;
Bool changed = false;
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(input, output, changed, false));
EXPECT_TRUE(changed);
EXPECT_EQ(ValidationError(output, SPV_ENV_VULKAN_1_2), String{});
const auto entryPoint = LinesWith(Disassemble(output), "OpEntryPoint");
ASSERT_EQ(entryPoint.size(), 1u);
for (const char* name : {kNarrowIntegerWidths8GlobalName, kNarrowIntegerWidths16GlobalName, "lightAndData", "texc"})
EXPECT_NE(entryPoint[0].find(String("%") + name), String::npos) << name << "\n" << entryPoint[0];
}
@@ -1095,6 +1095,11 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) {
v.stripOutputBlockLocations = true;
variants.emplace_back("stripOutputBlockLocations", BuildEsslTranslationKey(v));
}
{ // arms the narrow-integer seeds of a vertex stage (ExtendNarrowIntegerVertexInputsForEssl)
EsslTranslationKeyInputs v = base;
v.narrowIntegerSeedsArmed = true;
variants.emplace_back("narrowIntegerSeedsArmed", BuildEsslTranslationKey(v));
}
{
EsslTranslationKeyInputs v = base;
v.enableSpirvValidation = true;
@@ -13,10 +13,10 @@ namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// The vertex input locations whose integer value a vertex module has to widen itself,
// one bit per location in each mask, and how. Every desktop GL driver hands an integer
// input the source integer converted to 32 bits by the SOURCE type's signedness and
// then reinterpreted as the declared int/uint type. A Vulkan fetch whose format has to
// carry the input's signedness instead extends by the wrong rule for 8/16-bit data:
// one bit per location in each mask, and how. GL hands an integer input the source
// integer converted to 32 bits by the SOURCE type's signedness and then reinterpreted
// as the declared int/uint type. A backend whose fetch has to carry the input's
// signedness instead extends 8/16-bit data by the wrong rule:
// - zeroExtend*: an int input fed unsigned bytes/shorts, fetched sign-extended, is
// masked back to its low 8/16 bits;
// - signExtend*: a uint input fed signed bytes/shorts, fetched zero-extended, has its
@@ -33,6 +33,64 @@ namespace MobileGL {
};
// Hashed as raw bytes by the program cache, so it must stay free of padding.
static_assert(sizeof(NarrowIntegerVertexInputs) == 4 * sizeof(Uint32));
// The two Private uint globals ExtendNarrowIntegerVertexInputsPass's RuntimeWidths mode
// reads each location's data width from: bit L of the first is set for 8-bit data at
// location L, of the second for 16-bit data. A backend running that mode turns them into
// uniforms and sets them per draw; 0, the value after a link, leaves every input as fetched.
inline constexpr const char* kNarrowIntegerWidths8GlobalName = "mg_NarrowIntegerWidths8";
inline constexpr const char* kNarrowIntegerWidths16GlobalName = "mg_NarrowIntegerWidths16";
// The vertex-input half: the locations whose attribute DATA is narrow integer, by width
// and signedness. Each backend fills it from its own description of the data.
struct NarrowIntegerAttributeMasks {
Uint32 unsigned8 = 0;
Uint32 unsigned16 = 0;
Uint32 signed8 = 0;
Uint32 signed16 = 0;
};
// The program half: the locations whose shader input is a 32-bit int or uint scalar/vector.
// Both masks are zero for a program without integer vertex inputs, which is what keeps the
// common draw at four AND instructions.
struct IntegerVertexInputMasks {
Uint32 signedInputs = 0;
Uint32 unsignedInputs = 0;
constexpr void Note(Uint32 location, GLenum shaderInputType) {
if (location >= 32) return;
const Uint32 bit = 1u << location;
switch (shaderInputType) {
case GL_INT:
case GL_INT_VEC2:
case GL_INT_VEC3:
case GL_INT_VEC4:
signedInputs |= bit;
break;
case GL_UNSIGNED_INT:
case GL_UNSIGNED_INT_VEC2:
case GL_UNSIGNED_INT_VEC3:
case GL_UNSIGNED_INT_VEC4:
unsignedInputs |= bit;
break;
default:
break;
}
}
};
// Unsigned narrow data into an int input is fetched sign-extended and must be zero-extended;
// signed narrow data into a uint input is fetched zero-extended and must be sign-extended.
// Matching signedness, 32-bit data and float inputs need nothing.
constexpr NarrowIntegerVertexInputs ResolveNarrowIntegerVertexInputs(
const IntegerVertexInputMasks& inputs, const NarrowIntegerAttributeMasks& data) {
NarrowIntegerVertexInputs result;
result.zeroExtend8 = inputs.signedInputs & data.unsigned8;
result.zeroExtend16 = inputs.signedInputs & data.unsigned16;
result.signExtend8 = inputs.unsignedInputs & data.signed8;
result.signExtend16 = inputs.unsignedInputs & data.signed16;
return result;
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -1998,6 +1998,19 @@ namespace MobileGL {
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary, Bool& changed,
const bool enableSpirvValidation) {
using namespace spvtools;
changed = false;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(ExtendNarrowIntegerVertexInputsPass::CreateRuntimeWidthsPass(&changed));
const bool ok = RunOptimizerChecked("ExtendNarrowIntegerVertexInputsForEssl", optimizer, inputBinary,
outputBinary, true, enableSpirvValidation);
changed = changed && ok;
return ok;
}
bool ShaderCompiler::LowerPixelCenterIntegerForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
@@ -351,6 +351,12 @@ namespace MobileGL {
Vector<uint32_t>& outputBinary,
const NarrowIntegerVertexInputs& inputs,
bool enableSpirvValidation = false);
// The same correction with each location's data width read at run time from the two
// kNarrowIntegerWidths*GlobalName globals, for a backend that cannot vary the program
// per draw. `changed` reports whether the module had an integer input to seed.
static bool ExtendNarrowIntegerVertexInputsForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary, Bool& changed,
bool enableSpirvValidation = false);
// Drops `layout(pixel_center_integer)` from a fragment module, which the Vulkan
// environment forbids, and shifts every gl_FragCoord.xy read by -0.5 to keep its
// values. See LowerPixelCenterIntegerPass.
@@ -7,6 +7,7 @@
// End of Source File Header
#include "ExtendNarrowIntegerVertexInputsPass.h"
#include "NarrowIntegerRuntimeWidths.h"
#include "spirv.hpp"
#include "source/opt/constants.h"
@@ -38,6 +39,8 @@ namespace MobileGL {
struct Extension {
Uint32 bits = 0; // 0: leave the value alone
Bool sign = false;
// The width is read at run time (RuntimeWidths); `bits` is then only non-zero.
Bool runtime = false;
};
Extension ExtensionAt(const NarrowIntegerVertexInputs& inputs, Uint32 location) {
@@ -50,6 +53,17 @@ namespace MobileGL {
return {};
}
// Every input below location 32 (a width bit exists for it), extended by its own rule.
Extension RuntimeExtensionAt(Uint32 location, Bool unsignedInput) {
if (location >= kMaxLocations) return {};
return {32, unsignedInput, true};
}
Bool IsUnsignedInteger(const analysis::Type* type) {
if (const auto* vector = type->AsVector()) type = vector->element_type();
return !type->AsInteger()->IsSigned();
}
// True for a 32-bit integer scalar or vector: the only shapes a 32-bit integer
// vertex attribute can feed, and the only ones the bit operations below accept.
Bool IsInteger32ScalarOrVector(const analysis::Type* type) {
@@ -72,6 +86,8 @@ namespace MobileGL {
// of a sign-extension; 0 where the element is left alone.
std::vector<Uint32> operandIds;
Uint32 privatePointerTypeId = 0;
Uint32 baseLocation = 0;
NarrowIntegerRuntimeWidths::ShiftType shiftType;
// Access chains rooted at the variable (chains of chains included), each with
// the Private pointer type its result takes once the variable is Private.
std::vector<std::pair<Instruction*, Uint32>> accessChains;
@@ -92,7 +108,8 @@ namespace MobileGL {
} // namespace
spvtools::opt::Pass::Status ExtendNarrowIntegerVertexInputsPass::Process() {
if (!m_inputs.Any()) return Status::SuccessWithoutChange;
const Bool runtime = m_mode == Mode::RuntimeWidths;
if (!runtime && !m_inputs.Any()) return Status::SuccessWithoutChange;
auto* irContext = context();
auto entryPoints = irContext->module()->entry_points();
@@ -141,11 +158,15 @@ namespace MobileGL {
target.arrayLength = static_cast<Uint32>(length->GetZeroExtendedValue());
elementCount = target.arrayLength;
}
if (!IsInteger32ScalarOrVector(typeMgr->GetType(target.elementTypeId))) continue;
const analysis::Type* elementType = typeMgr->GetType(target.elementTypeId);
if (!IsInteger32ScalarOrVector(elementType)) continue;
target.baseLocation = location->second;
Bool any = false;
for (Uint32 element = 0; element < elementCount; ++element) {
target.extensions.push_back(ExtensionAt(m_inputs, location->second + element));
target.extensions.push_back(
runtime ? RuntimeExtensionAt(location->second + element, IsUnsignedInteger(elementType))
: ExtensionAt(m_inputs, location->second + element));
any = any || target.extensions.back().bits != 0;
}
if (any) targets.push_back(std::move(target));
@@ -186,11 +207,19 @@ namespace MobileGL {
constant ? constantMgr->GetDefiningInstruction(constant, typeId) : nullptr;
return inst ? inst->result_id() : 0;
};
NarrowIntegerRuntimeWidths::Ids runtimeIds;
if (runtime && !NarrowIntegerRuntimeWidths::MaterializeTypes(irContext, runtimeIds))
return Status::Failure;
Uint32 zeroOffsetId = 0;
for (auto& target : targets) {
for (const Extension& extension : target.extensions) {
for (SizeT element = 0; element < target.extensions.size(); ++element) {
const Extension& extension = target.extensions[element];
Uint32 operandId = 0;
if (extension.bits != 0) {
if (extension.runtime) {
// The location, which selects the element's bit in the two widths.
operandId = constantMgr->GetUIntConstId(target.baseLocation + static_cast<Uint32>(element));
if (operandId == 0) return Status::Failure;
} else if (extension.bits != 0) {
operandId = extension.sign ? constantMgr->GetUIntConstId(extension.bits)
: constantId(target.elementTypeId, (1u << extension.bits) - 1u);
if (operandId == 0) return Status::Failure;
@@ -201,6 +230,11 @@ namespace MobileGL {
}
target.operandIds.push_back(operandId);
}
if (runtime) {
target.shiftType = NarrowIntegerRuntimeWidths::MaterializeShiftType(irContext, runtimeIds,
target.elementTypeId);
if (target.shiftType.typeId == 0) return Status::Failure;
}
target.privatePointerTypeId =
typeMgr->FindPointerToType(target.pointeeTypeId, spv::StorageClass::Private);
if (target.privatePointerTypeId == 0) return Status::Failure;
@@ -269,9 +303,20 @@ namespace MobileGL {
++insertPoint;
return resultId;
};
const NarrowIntegerRuntimeWidths::Emitter emitter{irContext, &insertPoint, nextId};
if (runtime) {
if (!NarrowIntegerRuntimeWidths::DeclareGlobals(irContext, runtimeIds, nextId))
return Status::Failure;
NarrowIntegerRuntimeWidths::LoadWidths(emitter, runtimeIds);
}
const auto extend = [&](const Target& target, SizeT element, Uint32 valueId) -> Uint32 {
const Extension& extension = target.extensions[element];
if (extension.bits == 0) return valueId;
if (extension.runtime) {
return NarrowIntegerRuntimeWidths::Extend(emitter, runtimeIds, target.elementTypeId,
target.shiftType, target.operandIds[element],
extension.sign, valueId);
}
if (!extension.sign) {
return insert(spv::Op::OpBitwiseAnd, target.elementTypeId,
{valueId, target.operandIds[element]});
@@ -345,9 +390,14 @@ namespace MobileGL {
}
insert(spv::Op::OpStore, 0, {privateId, correctedId});
}
if (runtime && interfaceListsPrivate) {
entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {runtimeIds.widths8Id}});
entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {runtimeIds.widths16Id}});
}
if (outOfIds) return Status::Failure;
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
if (m_outChanged != nullptr) *m_outChanged = true;
return Status::SuccessWithChange;
}
@@ -355,6 +405,11 @@ namespace MobileGL {
CreateExtendNarrowIntegerVertexInputsPass(const NarrowIntegerVertexInputs& inputs) {
return spvtools::Optimizer::PassToken(MakeUnique<ExtendNarrowIntegerVertexInputsPass>(inputs));
}
spvtools::Optimizer::PassToken ExtendNarrowIntegerVertexInputsPass::CreateRuntimeWidthsPass(
Bool* outChanged) {
return spvtools::Optimizer::PassToken(MakeUnique<ExtendNarrowIntegerVertexInputsPass>(outChanged));
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -26,20 +26,33 @@ namespace MobileGL {
// element (element i sits at location base + i). Inputs at other locations, and modules
// that are not vertex shaders, are left untouched.
//
// DirectVulkan only: the Vulkan fetch format must match the input's signedness, which is
// what moves the extension rule away from the source data's own.
// ConstantMasks bakes the listed locations into the module. RuntimeWidths seeds every
// 32-bit integer input below location 32 from two Private uint globals instead (see
// kNarrowIntegerWidths8GlobalName), so one module serves every data width; a width bit of
// 0 leaves the value as fetched.
class ExtendNarrowIntegerVertexInputsPass : public spvtools::opt::Pass {
public:
enum class Mode : Uint8 {
ConstantMasks,
RuntimeWidths,
};
explicit ExtendNarrowIntegerVertexInputsPass(const NarrowIntegerVertexInputs& inputs)
: m_inputs(inputs) {}
// RuntimeWidths; `outChanged` (optional) reports whether the module was rewritten.
explicit ExtendNarrowIntegerVertexInputsPass(Bool* outChanged)
: m_mode(Mode::RuntimeWidths), m_outChanged(outChanged) {}
const char* name() const override { return "mobilegl-extend-narrow-integer-vertex-inputs"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateExtendNarrowIntegerVertexInputsPass(
const NarrowIntegerVertexInputs& inputs);
static spvtools::Optimizer::PassToken CreateRuntimeWidthsPass(Bool* outChanged);
private:
NarrowIntegerVertexInputs m_inputs;
Mode m_mode = Mode::ConstantMasks;
Bool* m_outChanged = nullptr;
};
} // namespace ShaderTranspiler
} // namespace MG_Util
@@ -0,0 +1,108 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.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 "NarrowIntegerRuntimeWidths.h"
#include "spirv.hpp"
#include "source/opt/constants.h"
#include "source/opt/instruction.h"
#include "source/opt/module.h"
#include "source/opt/type_manager.h"
#include "source/util/make_unique.h"
#include <MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h>
namespace MobileGL::MG_Util::ShaderTranspiler::NarrowIntegerRuntimeWidths {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
Bool AddNamedGlobal(IRContext* context, const Ids& ids, Uint32 id, const char* name) {
if (id == 0) return false;
context->AddGlobalValue(spvtools::MakeUnique<Instruction>(
context, spv::Op::OpVariable, ids.privatePointerTypeId, id,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<Uint32>(spv::StorageClass::Private)}},
{SPV_OPERAND_TYPE_ID, {ids.zeroId}}}));
context->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
context, spv::Op::OpName, 0, 0,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {id}},
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
return true;
}
} // namespace
Bool MaterializeTypes(IRContext* context, Ids& ids) {
auto* typeMgr = context->get_type_mgr();
auto* constantMgr = context->get_constant_mgr();
ids.uintTypeId = typeMgr->GetUIntTypeId();
ids.zeroId = constantMgr->GetUIntConstId(0);
ids.oneId = constantMgr->GetUIntConstId(1);
ids.byteShiftId = constantMgr->GetUIntConstId(24);
ids.shortShiftId = constantMgr->GetUIntConstId(16);
ids.privatePointerTypeId = typeMgr->FindPointerToType(ids.uintTypeId, spv::StorageClass::Private);
return ids.uintTypeId != 0 && ids.zeroId != 0 && ids.oneId != 0 && ids.byteShiftId != 0 &&
ids.shortShiftId != 0 && ids.privatePointerTypeId != 0;
}
ShiftType MaterializeShiftType(IRContext* context, const Ids& ids, Uint32 elementTypeId) {
auto* typeMgr = context->get_type_mgr();
const auto* type = typeMgr->GetType(elementTypeId);
if (type == nullptr) return {};
if (const auto* vector = type->AsVector()) {
const Uint32 components = vector->element_count();
return {typeMgr->GetUIntVectorTypeId(components), components};
}
return {ids.uintTypeId, 1};
}
Bool DeclareGlobals(IRContext* context, Ids& ids, const std::function<Uint32()>& nextId) {
ids.widths8Id = nextId();
ids.widths16Id = nextId();
return AddNamedGlobal(context, ids, ids.widths8Id, kNarrowIntegerWidths8GlobalName) &&
AddNamedGlobal(context, ids, ids.widths16Id, kNarrowIntegerWidths16GlobalName);
}
Uint32 Emitter::Emit(spv::Op opcode, Uint32 typeId, const Vector<Uint32>& operands) const {
const Uint32 resultId = typeId != 0 ? nextId() : 0;
Instruction::OperandList operandList;
for (const Uint32 id : operands)
operandList.push_back({SPV_OPERAND_TYPE_ID, {id}});
*insertPoint = insertPoint->InsertBefore(
spvtools::MakeUnique<Instruction>(context, opcode, typeId, resultId, operandList));
++*insertPoint;
return resultId;
}
void LoadWidths(const Emitter& emitter, Ids& ids) {
ids.widths8ValueId = emitter.Emit(spv::Op::OpLoad, ids.uintTypeId, {ids.widths8Id});
ids.widths16ValueId = emitter.Emit(spv::Op::OpLoad, ids.uintTypeId, {ids.widths16Id});
}
Uint32 Extend(const Emitter& emitter, const Ids& ids, Uint32 elementTypeId, const ShiftType& shiftType,
Uint32 locationId, Bool signExtend, Uint32 valueId) {
// s = ((w8 >> L) & 1) * 24 + ((w16 >> L) & 1) * 16; one location has one data type, so at
// most one of the two bits is set and s is 0, 16 or 24.
const auto widthShift = [&](Uint32 widthsValueId, Uint32 shiftId) {
const Uint32 shifted =
emitter.Emit(spv::Op::OpShiftRightLogical, ids.uintTypeId, {widthsValueId, locationId});
const Uint32 bit = emitter.Emit(spv::Op::OpBitwiseAnd, ids.uintTypeId, {shifted, ids.oneId});
return emitter.Emit(spv::Op::OpIMul, ids.uintTypeId, {bit, shiftId});
};
const Uint32 byteShift = widthShift(ids.widths8ValueId, ids.byteShiftId);
const Uint32 shortShift = widthShift(ids.widths16ValueId, ids.shortShiftId);
Uint32 shift = emitter.Emit(spv::Op::OpIAdd, ids.uintTypeId, {byteShift, shortShift});
if (shiftType.components > 1)
shift = emitter.Emit(spv::Op::OpCompositeConstruct, shiftType.typeId,
Vector<Uint32>(shiftType.components, shift));
const Uint32 high = emitter.Emit(spv::Op::OpShiftLeftLogical, elementTypeId, {valueId, shift});
return emitter.Emit(signExtend ? spv::Op::OpShiftRightArithmetic : spv::Op::OpShiftRightLogical, elementTypeId,
{high, shift});
}
} // namespace MobileGL::MG_Util::ShaderTranspiler::NarrowIntegerRuntimeWidths
@@ -0,0 +1,66 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.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/instruction_list.h"
#include "source/opt/ir_context.h"
#include <Includes.h>
#include <functional>
// The RuntimeWidths half of ExtendNarrowIntegerVertexInputsPass. The data width of each location
// is draw state, so the seed reads it from two uint globals - bit L of the first set for 8-bit
// data at location L, of the second for 16-bit data - and widens the fetched value by the input's
// own rule from that many low bits: s = 24 or 16 (0 where neither bit is set) and
// int input: value = (value << s) >>> s (zero-extension)
// uint input: value = (value << s) >> s (sign-extension)
// Only shifts, AND, multiply and add, which ESSL 3.00 can spell; no bitfield instruction.
namespace MobileGL::MG_Util::ShaderTranspiler::NarrowIntegerRuntimeWidths {
struct Ids {
Uint32 uintTypeId = 0;
Uint32 zeroId = 0;
Uint32 oneId = 0;
Uint32 byteShiftId = 0; // 24
Uint32 shortShiftId = 0; // 16
Uint32 privatePointerTypeId = 0;
Uint32 widths8Id = 0;
Uint32 widths16Id = 0;
Uint32 widths8ValueId = 0;
Uint32 widths16ValueId = 0;
};
// The shift-amount type of one input: its uint vector, or uint for a scalar input.
struct ShiftType {
Uint32 typeId = 0;
Uint32 components = 1;
};
// Types and constants first: they have to precede every variable the pass adds or moves.
Bool MaterializeTypes(spvtools::opt::IRContext* context, Ids& ids);
ShiftType MaterializeShiftType(spvtools::opt::IRContext* context, const Ids& ids, Uint32 elementTypeId);
// The two named Private globals, initialised to 0 so an unset width reads as "as fetched".
Bool DeclareGlobals(spvtools::opt::IRContext* context, Ids& ids, const std::function<Uint32()>& nextId);
// Instructions go in before `insertPoint`, which then points past them.
struct Emitter {
spvtools::opt::IRContext* context = nullptr;
spvtools::opt::InstructionList::iterator* insertPoint = nullptr;
std::function<Uint32()> nextId;
Uint32 Emit(spv::Op opcode, Uint32 typeId, const Vector<Uint32>& operands) const;
};
// Loads both widths once, at the top of the entry point.
void LoadWidths(const Emitter& emitter, Ids& ids);
// `valueId` (of `elementTypeId`, the input's type) widened per the bits at `locationId`.
Uint32 Extend(const Emitter& emitter, const Ids& ids, Uint32 elementTypeId, const ShiftType& shiftType,
Uint32 locationId, Bool signExtend, Uint32 valueId);
} // namespace MobileGL::MG_Util::ShaderTranspiler::NarrowIntegerRuntimeWidths
@@ -208,6 +208,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
builder.StringMap(inputs.outputBlockRenames ? *inputs.outputBlockRenames : kEmptyRenames);
builder.Value(static_cast<Uint8>(inputs.stripInputBlockLocations));
builder.Value(static_cast<Uint8>(inputs.stripOutputBlockLocations));
builder.Value(static_cast<Uint8>(inputs.narrowIntegerSeedsArmed));
static const Vector<Uint32> kEmptyWords;
builder.Words(inputs.spirv ? *inputs.spirv : kEmptyWords);
return MakeTranslationCacheKey(builder);
@@ -670,6 +670,10 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// other end is in a different program must keep its location.
Bool stripInputBlockLocations = false;
Bool stripOutputBlockLocations = false;
// Whether this (vertex) stage carries the narrow-integer seeds - a per-program arming, set
// only once some draw fed the program 8/16-bit data of the other signedness, which changes
// the emitted ESSL of the same module.
Bool narrowIntegerSeedsArmed = false;
// The top of the reserved storage-block window atomic-counter blocks are moved into
// (`top - N` for GL binding N). Derived from the driver's