From d1293354d1eb91c4be0dd84cac1ebd3aeb245bf1 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 19 Oct 2026 02:53:01 -0400 Subject: [PATCH] [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 --- CMakeLists.txt | 1 + .../MG_Backend/DirectGLES/DirectGLESBind.cpp | 13 +- .../DirectGLES/DirectGLESInternal.h | 4 +- .../MG_Backend/DirectGLES/DirectGLESState.cpp | 6 +- .../DirectGLES/IntegerAttributePlan.h | 121 ++++++++++++ MobileGL/MG_Backend/DirectGLES/Managers.cpp | 1 + MobileGL/MG_Backend/DirectGLES/Managers.h | 22 +-- .../DirectGLES/ManagersPrograms.cpp | 62 ++++--- .../DirectGLES/ManagersVertexArrays.cpp | 28 ++- .../DirectGLES/NarrowIntegerSeeds.cpp | 175 ++++++++++++++++++ .../DirectGLES/NarrowIntegerSeeds.h | 80 ++++++++ MobileGL/MG_Pipe/PipeApply.h | 10 + .../MG_Test/Backend/DirectGLES/CMakeLists.txt | 11 ++ .../DirectGLES/IntegerAttributePlanTest.cpp | 114 ++++++++++++ .../DirectGLES/NarrowIntegerEsslTest.cpp | 144 ++++++++++++++ .../ShaderTranspiler/TranslationCacheTest.cpp | 5 + .../ShaderTranspiler/TranslationCache.cpp | 1 + .../ShaderTranspiler/TranslationCache.h | 4 + 18 files changed, 747 insertions(+), 55 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectGLES/IntegerAttributePlan.h create mode 100644 MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.cpp create mode 100644 MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.h create mode 100644 MobileGL/MG_Test/Backend/DirectGLES/IntegerAttributePlanTest.cpp create mode 100644 MobileGL/MG_Test/Backend/DirectGLES/NarrowIntegerEsslTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 36c17b87..b5b0ca06 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -529,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 diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESBind.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESBind.cpp index 9830e2bd..112f5789 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLESBind.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESBind.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. diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESInternal.h b/MobileGL/MG_Backend/DirectGLES/DirectGLESInternal.h index 41ef980a..26aaf60e 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLESInternal.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESInternal.h @@ -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. diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp index 99d047ef..3055972b 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp @@ -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 || diff --git a/MobileGL/MG_Backend/DirectGLES/IntegerAttributePlan.h b/MobileGL/MG_Backend/DirectGLES/IntegerAttributePlan.h new file mode 100644 index 00000000..540574e5 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/IntegerAttributePlan.h @@ -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 +#include + +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 diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 328c4c79..212a91dd 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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 "}) { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 365e8222..bf41a88c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -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 -// P13 W5: the record arm's deaths (the slot tables' handle refusals below), in every build. -#include +#include // 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& inputBlockRenames, const std::map& outputBlockRenames, Bool stripInputBlockLocations, Bool stripOutputBlockLocations, Int atomicCounterEsslBindingTop, - Bool enableSpirvValidation, String& outSource, + Bool narrowIntegerSeedsArmed, Bool enableSpirvValidation, String& outSource, std::set& outFlattenedXfbBlockNames, Vector& outAtomicCounterGlBindings, String& outError) const; @@ -3030,6 +3029,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector m_formatlessImageUnits; Uint64 m_imageUnitFormatSignature = 0; SamplerPassMemo m_samplerPassMemo; + NarrowIntegerSeedState m_narrowIntegerSeeds; }; extern Uint32 g_snormFallbackClampOutputMask; diff --git a/MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp b/MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp index 766e92ad..0dd3b267 100644 --- a/MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp +++ b/MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp @@ -888,8 +888,8 @@ namespace MobileGL::MG_Backend::DirectGLES { const UnorderedMap& storageBlockBindingOverrides, const std::map& inputBlockRenames, const std::map& outputBlockRenames, const Bool stripInputBlockLocations, const Bool stripOutputBlockLocations, - const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource, - std::set& outFlattenedXfbBlockNames, Vector& outAtomicCounterGlBindings, + const Int atomicCounterEsslBindingTop, const Bool narrowIntegerSeedsArmed, const Bool enableSpirvValidation, + String& outSource, std::set& outFlattenedXfbBlockNames, Vector& 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 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 flattenedXfbSpirv; if (!xfbCaptureBlockNames.empty()) { // Reported into a local first, and published only if the module is really @@ -1605,9 +1604,9 @@ 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(); + BeginNarrowIntegerSeedBuild(m_narrowIntegerSeeds); // GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was // cancelled (teardown) or whose optimizer run failed is fully linked and fully @@ -1636,9 +1635,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 +2043,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 +2069,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 +2285,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 +2648,12 @@ 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). + const IntegerVertexInputMasks integerInputs = IntegerVertexInputsOf(*record); + if (!record->Archive->Link.xfbVaryings.empty() && + (integerInputs.signedInputs | integerInputs.unsignedInputs)) + m_narrowIntegerSeeds.wanted = true; 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 +2774,7 @@ namespace MobileGL::MG_Backend::DirectGLES { binding.lastAssignedMsSampleMax = 0; m_samplerUniformBindings.push_back(binding); } + FinishNarrowIntegerSeedBuild(m_narrowIntegerSeeds, m_backendProgramId, m_backendProgramUsable); } void BackendProgramObjectImpl::Use() { diff --git a/MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp b/MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp index 24743b4d..45bafab6 100644 --- a/MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp +++ b/MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp @@ -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(attrib.Type)), attrib.Normalized ? GL_TRUE : GL_FALSE, 0); } else { - g_GLESFuncs.glVertexAttribIFormat(attribIndex, static_cast(attrib.Size), - MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)), - 0); + g_GLESFuncs.glVertexAttribIFormat(attribIndex, static_cast(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(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(attrib.Offset) + BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance); - const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)); + const GLenum glType = + IntegerAttribTypeForInput(MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)), + attrib.IsInteger && ((retypeHeld >> attribIndex) & 1u)); const GLint glSize = static_cast(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(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(stride), // See the pointer call above: the divisor'd // arrays' baseInstance shift is the pointer. diff --git a/MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.cpp b/MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.cpp new file mode 100644 index 00000000..e4305d73 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.cpp @@ -0,0 +1,175 @@ +// 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 + +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; + } + + 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(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(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(std::countr_zero(remaining)); + const auto& attrib = elements->Attributes[location]; + if (attrib.Enabled && attrib.IsInteger && !attrib.IsLong) + data.Note(location, static_cast(attrib.Type)); + } + auto* twin = ExistingProgramTwin(program); + return ResolveIntegerAttributePlan(inputs, data, twin == nullptr || !twin->NarrowIntegerSeeds().Refused()); + } + + Bool DropRefusedNarrowIntegerSeeds(IntegerAttributePlan& plan, MG_Pipe::MGPipeHandle program) { + if (!plan.NeedsSeeds()) return false; + auto* twin = ExistingProgramTwin(program); + if (twin == nullptr || !twin->NarrowIntegerSeeds().Refused()) return false; + plan = plan.WithoutSeeds(); + return true; + } + + 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 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* SeedNarrowIntegerVertexInputsForEssl(Bool armed, const Vector& spirv, + Vector& 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 diff --git a/MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.h b/MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.h new file mode 100644 index 00000000..83ac07de --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/NarrowIntegerSeeds.h @@ -0,0 +1,80 @@ +// 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 +#include +#include + +// 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 for the object, and it is rebuilt +// without the seeds. +namespace MobileGL::MG_Backend::DirectGLES { + struct NarrowIntegerSeedState { + Bool wanted = false; + Bool disabled = false; + // 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 that could not give the program working seeds. + Bool Refused() const { return disabled || (built && !available); } + }; + + // 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); + + // 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* SeedNarrowIntegerVertexInputsForEssl(Bool armed, const Vector& spirv, + Vector& 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 diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 1261ee78..de3553ba 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -405,6 +405,16 @@ namespace MobileGL::MG_Pipe { }; mutable Array 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 diff --git a/MobileGL/MG_Test/Backend/DirectGLES/CMakeLists.txt b/MobileGL/MG_Test/Backend/DirectGLES/CMakeLists.txt index 33839a83..1990d78e 100644 --- a/MobileGL/MG_Test/Backend/DirectGLES/CMakeLists.txt +++ b/MobileGL/MG_Test/Backend/DirectGLES/CMakeLists.txt @@ -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) diff --git a/MobileGL/MG_Test/Backend/DirectGLES/IntegerAttributePlanTest.cpp b/MobileGL/MG_Test/Backend/DirectGLES/IntegerAttributePlanTest.cpp new file mode 100644 index 00000000..201edb2c --- /dev/null +++ b/MobileGL/MG_Test/Backend/DirectGLES/IntegerAttributePlanTest.cpp @@ -0,0 +1,114 @@ +// 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 + +#include + +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::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(GL_FLOAT)); +} diff --git a/MobileGL/MG_Test/Backend/DirectGLES/NarrowIntegerEsslTest.cpp b/MobileGL/MG_Test/Backend/DirectGLES/NarrowIntegerEsslTest.cpp new file mode 100644 index 00000000..992c23ad --- /dev/null +++ b/MobileGL/MG_Test/Backend/DirectGLES/NarrowIntegerEsslTest.cpp @@ -0,0 +1,144 @@ +// 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. + +#include + +#include +#include +#include +#include +#include + +#include "Init.h" + +#include +#include + +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 CompileVertexModule() { + using namespace MG_Util::ShaderTranspiler; + Vector> shaders; + for (const auto& [stage, source] : + Vector>{{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 module = binary.value()[0]; + EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true)); + return module; + } + + String EmitEssl300(const Vector& 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{}; + } + + 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 module = CompileVertexModule(); + ASSERT_FALSE(module.empty()); + + Vector split; + ASSERT_TRUE(ShaderCompiler::SplitArrayVertexInputsForEssl(module, split, true)); + Vector 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(essl.find(String("uint ") + name), essl.find(String("highp uint ") + name) + 6) << 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"); +} diff --git a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp index f06e9bb2..ad6ba70f 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp @@ -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; diff --git a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp index 290f730c..f6a26fc7 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp @@ -208,6 +208,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler { builder.StringMap(inputs.outputBlockRenames ? *inputs.outputBlockRenames : kEmptyRenames); builder.Value(static_cast(inputs.stripInputBlockLocations)); builder.Value(static_cast(inputs.stripOutputBlockLocations)); + builder.Value(static_cast(inputs.narrowIntegerSeedsArmed)); static const Vector kEmptyWords; builder.Words(inputs.spirv ? *inputs.spirv : kEmptyWords); return MakeTranslationCacheKey(builder); diff --git a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h index 65235e72..7e705ffd 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h +++ b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h @@ -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