mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 10:03:23 +09:00
Compare commits
17
Commits
c4eb620982
...
cleanup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5d64175a1 | ||
|
|
1a53cbd31a | ||
|
|
3d2e259080 | ||
|
|
d1293354d1 | ||
|
|
381124a0b6 | ||
|
|
fd48928260 | ||
|
|
58b0550512 | ||
|
|
044a3f1c7c | ||
|
|
78c21544d8 | ||
|
|
5a48bd80b1 | ||
|
|
2093d845b8 | ||
|
|
cc0c2aa766 | ||
|
|
3528628681 | ||
|
|
fe60711c24 | ||
|
|
c56c7e76c8 | ||
|
|
0c2f8df727 | ||
|
|
b4b2ccfe7b |
@@ -400,6 +400,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripCompatPerVertexMembersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
||||
@@ -410,6 +411,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RetainGeometryOutputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
|
||||
@@ -417,6 +419,8 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/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
|
||||
@@ -525,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
|
||||
|
||||
@@ -1832,8 +1832,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// GL_MAX_TEXTURE_IMAGE_UNITS must never exceed host-side fixed arrays sized off it (e.g.
|
||||
// Minecraft's 128-entry Blaze3D GlStateManager.TEXTURES[], iterated by Iris), and the combined
|
||||
// limit must stay within our texture-unit state array capacity.
|
||||
//
|
||||
// The fragment limit is raised to the desktop value (32) even where the ES driver counts 16
|
||||
// per stage: Iris sizes its WHOLE-PROGRAM unit budget off this query (vertex and fragment
|
||||
// samplers share it, and it refuses the next sampler as soon as the budget is full), so 16
|
||||
// leaves shader packs such as iterationT without a pipeline at all. Unit NUMBERS up to the
|
||||
// driver's combined limit are legal in every stage, and a stage that really samples more
|
||||
// textures than the driver allows still fails at link with the driver's own log.
|
||||
m_dynamicParameters.MaxTextureImageUnits =
|
||||
std::min(m_GLESCapabilities.MaxTextureImageUnits,
|
||||
std::min(std::max(m_GLESCapabilities.MaxTextureImageUnits, m_GLESCapabilities.MaxCombinedTextureImageUnits),
|
||||
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS));
|
||||
m_dynamicParameters.MaxVertexTextureImageUnits =
|
||||
std::min(m_GLESCapabilities.MaxVertexTextureImageUnits,
|
||||
|
||||
@@ -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 "}) {
|
||||
|
||||
@@ -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
|
||||
@@ -53,24 +53,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The view has no lifetime of its own, so the memo lives in the server record it views
|
||||
// (MGPipeShaderCsoRecord::BackendHashMemo), never in the client's program object: an entry
|
||||
// answers only while the record's Serial and BindingsSerial are the ones it was taken at.
|
||||
Bool GetBackendHashMemo(Uint flags, Uint64& hash) const {
|
||||
for (const auto& entry : m_record->BackendHashMemo) {
|
||||
if (entry.Valid && entry.Key == flags && entry.Serial == m_record->Serial &&
|
||||
entry.BindingsSerial == m_record->BindingsSerial) {
|
||||
hash = entry.Hash;
|
||||
return true;
|
||||
}
|
||||
using BackendHashMemoEntry = MG_Pipe::MGPipeShaderCsoRecord::BackendHashMemoEntry;
|
||||
BackendHashMemoEntry* FindBackendHashMemo(Uint key) const {
|
||||
for (auto& entry : m_record->BackendHashMemo) {
|
||||
if (entry.Valid && entry.Key == key && entry.Serial == m_record->Serial &&
|
||||
entry.BindingsSerial == m_record->BindingsSerial)
|
||||
return &entry;
|
||||
}
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
|
||||
// Takes the next slot round robin; its LastVariantHash starts as the plain `hash`.
|
||||
BackendHashMemoEntry& SetBackendHashMemo(Uint key, Uint64 hash) const {
|
||||
auto& memo = m_record->BackendHashMemo;
|
||||
auto& entry = memo[m_record->BackendHashMemoNext++ % memo.size()];
|
||||
entry.Serial = m_record->Serial;
|
||||
entry.BindingsSerial = m_record->BindingsSerial;
|
||||
entry.Hash = hash;
|
||||
entry.Key = flags;
|
||||
entry.LastVariantHash = hash;
|
||||
entry.Key = key;
|
||||
entry.Valid = true;
|
||||
return entry;
|
||||
}
|
||||
Uint64 GetBackendStateVersion() const { return m_record->Serial; }
|
||||
Uint64 GetBlockBindingVersion() const { return m_record->BindingsSerial; }
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/NarrowIntegerVertexFormats.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 "../VkIncludes.h"
|
||||
#include <MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h>
|
||||
|
||||
#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.
|
||||
//
|
||||
// The pipeline gives an integer attribute the fetch format of the shader input's signedness
|
||||
// (VUID-VkGraphicsPipelineCreateInfo-Input-08733). For 32-bit data that swap only renames the
|
||||
// bits; for 8/16-bit data it changes the extension rule, so 240 in GL_UNSIGNED_BYTE data
|
||||
// reaches an `int` input as -16. These are exactly the (data, input) pairs that need it.
|
||||
enum class NarrowIntegerVertexFormat : Uint8 {
|
||||
None = 0,
|
||||
Unsigned8,
|
||||
Unsigned16,
|
||||
Signed8,
|
||||
Signed16,
|
||||
};
|
||||
|
||||
constexpr NarrowIntegerVertexFormat ClassifyNarrowIntegerVertexFormat(VkFormat format) {
|
||||
switch (format) {
|
||||
case VK_FORMAT_R8_UINT:
|
||||
case VK_FORMAT_R8G8_UINT:
|
||||
case VK_FORMAT_R8G8B8_UINT:
|
||||
case VK_FORMAT_R8G8B8A8_UINT:
|
||||
return NarrowIntegerVertexFormat::Unsigned8;
|
||||
case VK_FORMAT_R16_UINT:
|
||||
case VK_FORMAT_R16G16_UINT:
|
||||
case VK_FORMAT_R16G16B16_UINT:
|
||||
case VK_FORMAT_R16G16B16A16_UINT:
|
||||
return NarrowIntegerVertexFormat::Unsigned16;
|
||||
case VK_FORMAT_R8_SINT:
|
||||
case VK_FORMAT_R8G8_SINT:
|
||||
case VK_FORMAT_R8G8B8_SINT:
|
||||
case VK_FORMAT_R8G8B8A8_SINT:
|
||||
return NarrowIntegerVertexFormat::Signed8;
|
||||
case VK_FORMAT_R16_SINT:
|
||||
case VK_FORMAT_R16G16_SINT:
|
||||
case VK_FORMAT_R16G16B16_SINT:
|
||||
case VK_FORMAT_R16G16B16A16_SINT:
|
||||
return NarrowIntegerVertexFormat::Signed16;
|
||||
default:
|
||||
return NarrowIntegerVertexFormat::None;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
switch (ClassifyNarrowIntegerVertexFormat(format)) {
|
||||
case NarrowIntegerVertexFormat::Unsigned8:
|
||||
unsigned8 |= bit;
|
||||
break;
|
||||
case NarrowIntegerVertexFormat::Unsigned16:
|
||||
unsigned16 |= bit;
|
||||
break;
|
||||
case NarrowIntegerVertexFormat::Signed8:
|
||||
signed8 |= bit;
|
||||
break;
|
||||
case NarrowIntegerVertexFormat::Signed16:
|
||||
signed16 |= bit;
|
||||
break;
|
||||
case NarrowIntegerVertexFormat::None:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <spirv-tools/libspirv.h>
|
||||
#include <spirv-tools/optimizer.hpp>
|
||||
@@ -914,6 +915,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Where a per-vertex fix-up of `entryPoint`'s outputs goes: in front of every emit a
|
||||
// geometry shader can execute - EmitVertex and EmitStreamVertex alike, in the entry
|
||||
// function or in any function it calls, since the front end does not inline helpers - and,
|
||||
// for the other stages, in front of each return from the entry function itself, as a
|
||||
// helper's return does not end the invocation. Collected before anything is inserted.
|
||||
Vector<spvtools::opt::Instruction*> CollectPerVertexFixupSites(spvtools::opt::IRContext* context,
|
||||
const spvtools::opt::Instruction& entryPoint) {
|
||||
Vector<spvtools::opt::Instruction*> sites;
|
||||
const Uint32 entryFunctionId = entryPoint.GetSingleWordInOperand(1);
|
||||
if (context->GetFunction(entryFunctionId) == nullptr) return sites;
|
||||
const auto collect = [&sites](spvtools::opt::Function& function, auto isSite) {
|
||||
function.ForEachInst([&](spvtools::opt::Instruction* inst) {
|
||||
if (isSite(inst->opcode())) sites.push_back(inst);
|
||||
});
|
||||
};
|
||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
|
||||
spv::ExecutionModel::Geometry) {
|
||||
collect(*context->GetFunction(entryFunctionId), [](spv::Op op) { return op == spv::Op::OpReturn; });
|
||||
return sites;
|
||||
}
|
||||
std::unordered_set<Uint32> callTree;
|
||||
context->CollectCallTreeFromRoots(entryFunctionId, &callTree);
|
||||
for (auto& function : *context->module()) {
|
||||
if (callTree.count(function.result_id()) == 0) continue;
|
||||
collect(function,
|
||||
[](spv::Op op) { return op == spv::Op::OpEmitVertex || op == spv::Op::OpEmitStreamVertex; });
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
|
||||
// Is `pointerId` the position target itself, or an access chain rooted at it?
|
||||
Bool PointerReachesPositionTarget(spvtools::opt::IRContext* context, Uint32 pointerId,
|
||||
const PositionTargetInfo& target) {
|
||||
@@ -1095,22 +1126,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* function = context()->GetFunction(entryPoint.GetSingleWordInOperand(1));
|
||||
if (!function) continue;
|
||||
|
||||
Bool modifiedThisEntryPoint = false;
|
||||
for (auto& bb : *function) {
|
||||
for (auto instIter = bb.begin(); instIter != bb.end(); ++instIter) {
|
||||
auto* inst = &*instIter;
|
||||
const Bool needsFixup =
|
||||
(model == spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpEmitVertex) ||
|
||||
(model != spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpReturn);
|
||||
if (!needsFixup) continue;
|
||||
|
||||
modifiedThisEntryPoint |=
|
||||
InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap,
|
||||
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
|
||||
}
|
||||
for (auto* site : CollectPerVertexFixupSites(context(), entryPoint)) {
|
||||
modifiedThisEntryPoint |=
|
||||
InsertPositionFixup(context(), site, target, halfConstId, doYFlip, doZRemap,
|
||||
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
|
||||
}
|
||||
// Per entry point, and only for one this pass actually injected into: the
|
||||
// injected load/store is a static use of the position variable, so the
|
||||
@@ -1364,8 +1384,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Decorates the module's captured varyings for VK_EXT_transform_feedback:
|
||||
// user outputs get XfbBuffer/XfbStride/Offset directly; a captured
|
||||
// gl_Position (a gl_PerVertex member) is mirrored into a dedicated output
|
||||
// variable copied before every OpReturn, BEFORE the position fixup runs,
|
||||
// so the captured value is the shader's own (pre-remap) gl_Position.
|
||||
// variable copied at every CollectPerVertexFixupSites site, BEFORE the position
|
||||
// fixup runs, so the captured value is the shader's own (pre-remap) gl_Position.
|
||||
class XfbCaptureDecoratePass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
struct CapturedVarying {
|
||||
@@ -1393,7 +1413,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto entryPointIter = get_module()->entry_points().begin();
|
||||
if (entryPointIter == get_module()->entry_points().end()) return Status::SuccessWithoutChange;
|
||||
spvtools::opt::Instruction* entryPoint = &*entryPointIter;
|
||||
const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||
|
||||
// Name -> result id map from the debug section.
|
||||
std::unordered_map<std::string, Uint32> idsByName;
|
||||
@@ -1542,14 +1561,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (needsPositionMirror) {
|
||||
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint, spv::BuiltIn::Position,
|
||||
IsVec4Float32, "gl_Position", positionBufferIndex,
|
||||
positionOffset, decorateForXfb);
|
||||
modified |= MirrorPerVertexBuiltInForCapture(*entryPoint, spv::BuiltIn::Position, IsVec4Float32,
|
||||
"gl_Position", positionBufferIndex, positionOffset,
|
||||
decorateForXfb);
|
||||
}
|
||||
if (needsPointSizeMirror) {
|
||||
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint, spv::BuiltIn::PointSize,
|
||||
IsFloat32Scalar, "gl_PointSize", pointSizeBufferIndex,
|
||||
pointSizeOffset, decorateForXfb);
|
||||
modified |= MirrorPerVertexBuiltInForCapture(*entryPoint, spv::BuiltIn::PointSize, IsFloat32Scalar,
|
||||
"gl_PointSize", pointSizeBufferIndex, pointSizeOffset,
|
||||
decorateForXfb);
|
||||
}
|
||||
|
||||
if (!modified) return Status::SuccessWithoutChange;
|
||||
@@ -1592,11 +1611,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// built-in is looked up and what type it has, so one injector serves both. Anything
|
||||
// else in gl_PerVertex would need its own type check before it could be added here.
|
||||
template <typename DecorateFn>
|
||||
Bool MirrorPerVertexBuiltInForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||
spv::BuiltIn builtIn, BuiltInTypeCheckFn typeCheck,
|
||||
const char* glslName, Uint32 bufferIndex, Uint32 offsetBytes,
|
||||
Bool MirrorPerVertexBuiltInForCapture(spvtools::opt::Instruction& entryPoint, spv::BuiltIn builtIn,
|
||||
BuiltInTypeCheckFn typeCheck, const char* glslName,
|
||||
Uint32 bufferIndex, Uint32 offsetBytes,
|
||||
const DecorateFn& decorateForXfb) {
|
||||
const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0);
|
||||
using namespace spvtools::opt;
|
||||
PositionTargetInfo target{};
|
||||
if (!FindBuiltInTarget(context(), builtIn, typeCheck, &target)) {
|
||||
@@ -1640,27 +1658,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
decorateForXfb(mirrorVariableId, bufferIndex, offsetBytes);
|
||||
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {mirrorVariableId}});
|
||||
|
||||
auto* function = context()->GetFunction(entryFunctionId);
|
||||
if (function == nullptr) return false;
|
||||
const auto model = static_cast<spv::ExecutionModel>(entryPointModel);
|
||||
// Geometry stages capture per emitted vertex; other stages at return.
|
||||
Bool injected = false;
|
||||
for (auto& block : *function) {
|
||||
for (auto instIter = block.begin(); instIter != block.end(); ++instIter) {
|
||||
// Geometry stages capture per emitted vertex; other stages at return.
|
||||
const Bool isInjectionSite = model == spv::ExecutionModel::Geometry
|
||||
? instIter->opcode() == spv::Op::OpEmitVertex
|
||||
: instIter->opcode() == spv::Op::OpReturn;
|
||||
if (!isInjectionSite) continue;
|
||||
InstructionBuilder builder(context(), &*instIter, IRContext::kAnalysisNone);
|
||||
const Uint32 memberIndexId = builder.GetUintConstantId(target.memberIndex);
|
||||
auto* access =
|
||||
builder.AddAccessChain(target.vectorPtrTypeId, target.variableId, {memberIndexId});
|
||||
if (access == nullptr) return injected;
|
||||
auto* value = builder.AddLoad(target.vectorTypeId, access->result_id());
|
||||
if (value == nullptr) return injected;
|
||||
builder.AddStore(mirrorVariableId, value->result_id());
|
||||
injected = true;
|
||||
}
|
||||
for (auto* site : CollectPerVertexFixupSites(context(), entryPoint)) {
|
||||
InstructionBuilder builder(context(), site, IRContext::kAnalysisNone);
|
||||
const Uint32 memberIndexId = builder.GetUintConstantId(target.memberIndex);
|
||||
auto* access = builder.AddAccessChain(target.vectorPtrTypeId, target.variableId, {memberIndexId});
|
||||
if (access == nullptr) return injected;
|
||||
auto* value = builder.AddLoad(target.vectorTypeId, access->result_id());
|
||||
if (value == nullptr) return injected;
|
||||
builder.AddStore(mirrorVariableId, value->result_id());
|
||||
injected = true;
|
||||
}
|
||||
// The mirror was listed on the entry point above, but the loop just added a READ
|
||||
// of the SOURCE block through an access chain, and the interface rule covers
|
||||
@@ -1951,36 +1959,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
return success;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
ProgramFactory::CompileOptionFlags transformFlags) {
|
||||
if (input.empty()) {
|
||||
output.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!transformFlags) {
|
||||
output = input;
|
||||
return true;
|
||||
}
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||
optimizer.SetMessageConsumer(
|
||||
[](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
|
||||
MGLOG_E("Vulkan: position fix pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(CreateGlToVulkanPositionFixPass(transformFlags));
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
if (!success) {
|
||||
MGLOG_E("Vulkan: failed to run GL->Vulkan position fix pass");
|
||||
output = input;
|
||||
}
|
||||
return success;
|
||||
Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
ProgramFactory::CompileOptionFlags transformFlags) {
|
||||
if (input.empty()) {
|
||||
output.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!transformFlags) {
|
||||
output = input;
|
||||
return true;
|
||||
}
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
|
||||
MGLOG_E("Vulkan: position fix pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(CreateGlToVulkanPositionFixPass(transformFlags));
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
if (!success) {
|
||||
MGLOG_E("Vulkan: failed to run GL->Vulkan position fix pass");
|
||||
output = input;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
namespace {
|
||||
ShaderStage PickClipFixupStage(const Vector<ShaderStage>& stages) {
|
||||
Bool hasGeometry = false;
|
||||
Bool hasTessEval = false;
|
||||
@@ -2727,6 +2736,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkProgramObject& entry) const {
|
||||
entry.activeVertexInputLocationMask = 0;
|
||||
entry.vertexInputTypes.fill(0);
|
||||
entry.integerVertexInputs = {};
|
||||
entry.readsBaseVertexBuiltin = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
@@ -2793,6 +2803,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
entry.activeVertexInputLocationMask |= (1u << expandedLocation);
|
||||
entry.vertexInputTypes[expandedLocation] = locationType;
|
||||
entry.integerVertexInputs.Note(expandedLocation, locationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3544,8 +3555,79 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
++m_cacheStructureEpoch;
|
||||
}
|
||||
|
||||
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(const MagmaProgramSource& program,
|
||||
CompileOptionFlags flags) {
|
||||
namespace {
|
||||
// The vertex stage's own transforms, in the order they have to run: the ZeroBaseVertex
|
||||
// variant, the unconditional 64-bit input packing, then the narrow-integer variant.
|
||||
void TransformVertexStageForVulkan(Vector<Uint>& module, ProgramFactory::CompileOptionFlags flags,
|
||||
const NarrowIntegerVertexInputs& narrowIntegerInputs,
|
||||
Bool enableSpirvValidation, Uint programIndex) {
|
||||
// The non-indexed variant of a vertex stage that reads gl_BaseVertex: GL wants zero
|
||||
// there, Vulkan's builtin would hand it the draw's firstVertex. Requested per draw
|
||||
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
|
||||
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex
|
||||
// and for the baseVertex word of an indexed indirect command.
|
||||
if (flags & ProgramFactory::CompileOptionBit::ZeroBaseVertex) {
|
||||
Vector<Uint> zeroedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(module, zeroedSpirv,
|
||||
enableSpirvValidation)) {
|
||||
module = std::move(zeroedSpirv);
|
||||
} else {
|
||||
// Failing open keeps the native builtin, which is the pre-fix behavior:
|
||||
// gl_BaseVertex reads firstVertex on a DrawArrays instead of zero.
|
||||
MGLOG_E("ProgramFactory: failed to zero gl_BaseVertex for program %u; non-indexed "
|
||||
"draws will read the draw's first vertex from it instead of zero",
|
||||
programIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// A 64-bit vertex input has to arrive as its 32-bit word pair: VK_FORMAT_R64*_SFLOAT is
|
||||
// optional and lavapipe advertises none of them at all. The pass is unconditional so it
|
||||
// always agrees with the Float64 case in VertexInputStateFactory::ToVkVertexFormat, and
|
||||
// ReflectVertexInputs then sees an ordinary uvec2/uvec4 input.
|
||||
//
|
||||
// Failure here is not recoverable and must not be swallowed: ToVkVertexFormat has already
|
||||
// committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring
|
||||
// `in double` would reconcile to Unknown and build a pipeline with a UINT format under a
|
||||
// double input - garbage with no diagnostic anywhere.
|
||||
{
|
||||
Vector<Uint> packedSpirv;
|
||||
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
||||
module, packedSpirv, enableSpirvValidation);
|
||||
MOBILEGL_ASSERT(packOk,
|
||||
"ProgramFactory: 64-bit vertex input packing failed for program %u; the "
|
||||
"vertex-input format and the shader input type now disagree",
|
||||
programIndex);
|
||||
if (packOk) {
|
||||
module = std::move(packedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to pack 64-bit vertex inputs for program %u; "
|
||||
"double-typed vertex attributes will be fetched as uint32 words and not "
|
||||
"reinterpreted",
|
||||
programIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// The narrow-integer variant: the pipeline fetches these attributes with the format of
|
||||
// the input's signedness, and this restores the extension rule of the data's own (see
|
||||
// ResolveNarrowIntegerVertexInputs). After the 64-bit packing so it sees the inputs in
|
||||
// the shape the pipeline's vertex input state and ReflectVertexInputs describe.
|
||||
if (narrowIntegerInputs.Any()) {
|
||||
Vector<Uint> extendedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(
|
||||
module, extendedSpirv, narrowIntegerInputs, enableSpirvValidation)) {
|
||||
module = std::move(extendedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to extend narrow integer vertex inputs for program %u; "
|
||||
"8/16-bit integer attributes read through an input of the other signedness keep "
|
||||
"the fetch format's extension",
|
||||
programIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
MagmaProgramSource::BackendHashMemoEntry& ProgramFactory::ProgramHashMemo(const MagmaProgramSource& program,
|
||||
CompileOptionFlags flags) const {
|
||||
// Hashing the full SPIR-V of every stage is far too expensive to repeat per draw;
|
||||
// reuse the program's memoized hash while its backend state version is unchanged.
|
||||
// The memo keys on the flags word, which ComputeHash is no longer a pure function of:
|
||||
@@ -3561,16 +3643,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint memoKey = (flags & CompileOptionBit::FragCoordYFlip)
|
||||
? (flags.GetRaw() | (m_defaultFramebufferHeight << 16))
|
||||
: flags.GetRaw();
|
||||
HashType hash = 0;
|
||||
if (!program.GetBackendHashMemo(memoKey, hash)) {
|
||||
hash = ComputeHash(program, flags);
|
||||
program.SetBackendHashMemo(memoKey, hash);
|
||||
if (auto* memo = program.FindBackendHashMemo(memoKey)) return *memo;
|
||||
return program.SetBackendHashMemo(memoKey, ComputeHash(program, flags));
|
||||
}
|
||||
|
||||
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(
|
||||
const MagmaProgramSource& program, CompileOptionFlags flags,
|
||||
const NarrowIntegerVertexInputs& narrowIntegerInputs) {
|
||||
auto& memo = ProgramHashMemo(program, flags);
|
||||
// Mixed in after the memo, which keys on the flags word alone: the memo keeps answering
|
||||
// for the plain program, and the variant costs one 16-byte hash on top of it.
|
||||
HashType hash = memo.Hash;
|
||||
if (narrowIntegerInputs.Any()) hash = MixNarrowIntegerVertexInputs(hash, narrowIntegerInputs);
|
||||
memo.LastVariantHash = hash;
|
||||
return FindOrBuildProgram(program, flags, narrowIntegerInputs, hash);
|
||||
}
|
||||
|
||||
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateDrawProgram(const MagmaProgramSource& program,
|
||||
CompileOptionFlags flags) {
|
||||
auto& memo = ProgramHashMemo(program, flags);
|
||||
if (memo.LastVariantHash != memo.Hash) {
|
||||
const auto it = m_cache.find(memo.LastVariantHash);
|
||||
if (it != m_cache.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return it->second;
|
||||
}
|
||||
// The variant aged out, and its hash alone cannot rebuild it: start from the plain
|
||||
// program, and the draw settles on its variant again.
|
||||
memo.LastVariantHash = memo.Hash;
|
||||
}
|
||||
return FindOrBuildProgram(program, flags, {}, memo.Hash);
|
||||
}
|
||||
|
||||
const ProgramFactory::VkProgramObject& ProgramFactory::FindOrBuildProgram(
|
||||
const MagmaProgramSource& program, CompileOptionFlags flags,
|
||||
const NarrowIntegerVertexInputs& narrowIntegerInputs, HashType hash) {
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
// Every draw/dispatch funnels through this lookup (the renderer memos only
|
||||
// skip re-hashing, never the factory lookup), so an actively-used entry is
|
||||
// stamped at least once per frame boundary and can never be aged out while
|
||||
// Every draw/dispatch funnels through this lookup or GetOrCreateDrawProgram's (the
|
||||
// renderer memos only skip re-hashing, never the factory lookup), so an actively-used
|
||||
// entry is stamped at least once per frame boundary and can never be aged out while
|
||||
// any in-flight command buffer still references it.
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return it->second;
|
||||
@@ -3581,6 +3693,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
++m_cacheStructureEpoch;
|
||||
auto& entry = m_cache[hash];
|
||||
entry.hash = hash;
|
||||
entry.narrowIntegerInputs = narrowIntegerInputs;
|
||||
entry.lastUsedFrame = m_frameCounter;
|
||||
// The EXECUTABLE's stage list, not GetAttachedShaders(): `spirv` is a link artifact with
|
||||
// one module per linked stage, while the attach list is live and grows on
|
||||
@@ -3818,50 +3931,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// The non-indexed variant of a vertex stage that reads gl_BaseVertex: GL wants zero
|
||||
// there, Vulkan's builtin would hand it the draw's firstVertex. Requested per draw
|
||||
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
|
||||
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex
|
||||
// and for the baseVertex word of an indexed indirect command.
|
||||
if (stages[i] == ShaderStage::Vertex && (flags & CompileOptionBit::ZeroBaseVertex)) {
|
||||
Vector<Uint> zeroedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i], zeroedSpirv,
|
||||
enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(zeroedSpirv);
|
||||
} else {
|
||||
// Failing open keeps the native builtin, which is the pre-fix behavior:
|
||||
// gl_BaseVertex reads firstVertex on a DrawArrays instead of zero.
|
||||
MGLOG_E("ProgramFactory: failed to zero gl_BaseVertex for program %u; non-indexed "
|
||||
"draws will read the draw's first vertex from it instead of zero",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
// A 64-bit vertex input has to arrive as its 32-bit word pair: VK_FORMAT_R64*_SFLOAT is
|
||||
// optional and lavapipe advertises none of them at all. The pass is unconditional so it
|
||||
// always agrees with the Float64 case in VertexInputStateFactory::ToVkVertexFormat, and
|
||||
// ReflectVertexInputs below then sees an ordinary uvec2/uvec4 input.
|
||||
//
|
||||
// Failure here is not recoverable and must not be swallowed: ToVkVertexFormat has already
|
||||
// committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring
|
||||
// `in double` would reconcile to Unknown and build a pipeline with a UINT format under a
|
||||
// double input - garbage with no diagnostic anywhere.
|
||||
if (stages[i] == ShaderStage::Vertex) {
|
||||
Vector<Uint> packedSpirv;
|
||||
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
||||
moduleSpirvs[i], packedSpirv, enableSpirvValidation);
|
||||
MOBILEGL_ASSERT(packOk,
|
||||
"ProgramFactory: 64-bit vertex input packing failed for program %u; the "
|
||||
"vertex-input format and the shader input type now disagree",
|
||||
program.GetExternalIndex());
|
||||
if (packOk) {
|
||||
moduleSpirvs[i] = std::move(packedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to pack 64-bit vertex inputs for program %u; "
|
||||
"double-typed vertex attributes will be fetched as uint32 words and not "
|
||||
"reinterpreted",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
TransformVertexStageForVulkan(moduleSpirvs[i], flags, narrowIntegerInputs, enableSpirvValidation,
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
|
||||
// When Vulkan can legally access storage images without a statically declared
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "../MagmaSession.h"
|
||||
#include "PipelineFactory.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "NarrowIntegerVertexFormats.h"
|
||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
|
||||
@@ -200,6 +201,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Int globalUboBinding = -1;
|
||||
Uint32 activeVertexInputLocationMask = 0;
|
||||
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
|
||||
// The locations of vertexInputTypes that are int / uint, for the per-draw narrow-integer
|
||||
// variant decision (ResolveNarrowIntegerVertexInputs). Identical in every variant.
|
||||
IntegerVertexInputMasks integerVertexInputs;
|
||||
// The narrow-integer correction this entry's vertex stage was built with (none for the
|
||||
// plain program): what a draw compares its own resolved set against.
|
||||
NarrowIntegerVertexInputs narrowIntegerInputs;
|
||||
Uint32 activeFragmentOutputLocationMask = 0;
|
||||
Array<GLenum, kMaxVertexInputLocations> fragmentOutputTypes{};
|
||||
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
|
||||
@@ -328,6 +335,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
integerVertexInputs = other.integerVertexInputs;
|
||||
narrowIntegerInputs = other.narrowIntegerInputs;
|
||||
activeFragmentOutputLocationMask = other.activeFragmentOutputLocationMask;
|
||||
fragmentOutputTypes = other.fragmentOutputTypes;
|
||||
rasterizationProducerStage = other.rasterizationProducerStage;
|
||||
@@ -351,6 +360,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.pointSizeCapabilityUnsupported = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.integerVertexInputs = {};
|
||||
other.narrowIntegerInputs = {};
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
other.rasterizationProducerStage = ShaderStage::Unknown;
|
||||
other.producerOutputComponentCount = 0;
|
||||
@@ -402,6 +413,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
integerVertexInputs = other.integerVertexInputs;
|
||||
narrowIntegerInputs = other.narrowIntegerInputs;
|
||||
activeFragmentOutputLocationMask = other.activeFragmentOutputLocationMask;
|
||||
fragmentOutputTypes = other.fragmentOutputTypes;
|
||||
rasterizationProducerStage = other.rasterizationProducerStage;
|
||||
@@ -425,6 +438,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.pointSizeCapabilityUnsupported = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.integerVertexInputs = {};
|
||||
other.narrowIntegerInputs = {};
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
other.rasterizationProducerStage = ShaderStage::Unknown;
|
||||
other.producerOutputComponentCount = 0;
|
||||
@@ -518,7 +533,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ProgramFactory(const ProgramFactory&) = delete;
|
||||
|
||||
HashType ComputeHash(const MagmaProgramSource& program, CompileOptionFlags flags) const;
|
||||
const VkProgramObject& GetOrCreateProgram(const MagmaProgramSource& program, CompileOptionFlags flags);
|
||||
// `narrowIntegerInputs` selects the variant whose vertex stage widens narrow integer
|
||||
// attribute data GL's way (ExtendNarrowIntegerVertexInputsPass). It is draw-time state like
|
||||
// the flags, and the draw path asks for it only when ResolveNarrowIntegerVertexInputs finds
|
||||
// a location that needs it, so every other program keeps a single entry. The variant
|
||||
// returned is remembered per flags for GetOrCreateDrawProgram.
|
||||
const VkProgramObject& GetOrCreateProgram(const MagmaProgramSource& program, CompileOptionFlags flags,
|
||||
const NarrowIntegerVertexInputs& narrowIntegerInputs = {});
|
||||
// A draw's first lookup: the variant GetOrCreateProgram last returned for these flags, plain
|
||||
// or narrow-integer. Every variant reflects the same vertex inputs and builtins, which is all
|
||||
// a draw reads before it knows the variant it needs, so a program whose draws keep needing
|
||||
// one variant costs a single lookup and its plain entry, never drawn with, ages out.
|
||||
const VkProgramObject& GetOrCreateDrawProgram(const MagmaProgramSource& program, CompileOptionFlags flags);
|
||||
// The cache identity of a narrow-integer variant: the hash of the same program and flags
|
||||
// without it, mixed with the corrected locations. Content in, content out, so the variant's
|
||||
// pipelines key apart from the plain program's through programHash alone.
|
||||
static HashType MixNarrowIntegerVertexInputs(HashType hash, const NarrowIntegerVertexInputs& inputs) {
|
||||
return XXH64(&inputs, sizeof(inputs), hash);
|
||||
}
|
||||
|
||||
// The default framebuffer's current image height, baked as a literal into every
|
||||
// FragCoordYFlip variant (there is no push-constant or specialization channel here, and
|
||||
@@ -654,6 +686,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
HashType hash = 0;
|
||||
};
|
||||
|
||||
// The program's memo slot for `flags`, its plain hash computed on a miss.
|
||||
MagmaProgramSource::BackendHashMemoEntry& ProgramHashMemo(const MagmaProgramSource& program,
|
||||
CompileOptionFlags flags) const;
|
||||
// The cache entry under `hash`, built as that variant of `program` when absent.
|
||||
const VkProgramObject& FindOrBuildProgram(const MagmaProgramSource& program, CompileOptionFlags flags,
|
||||
const NarrowIntegerVertexInputs& narrowIntegerInputs, HashType hash);
|
||||
|
||||
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
|
||||
// `stages` is ALWAYS ProgramObject::GetLinkedShaderStages() - one entry per module of
|
||||
// `spirv`, at the same index. Taking the stages rather than the shader objects is what
|
||||
@@ -726,4 +765,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Private copy whose y is (framebufferHeight - y). Fails open (output = input, false). Exposed
|
||||
// so a unit case can validate the bytes it hands the driver.
|
||||
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output, Uint32 framebufferHeight);
|
||||
|
||||
// The clip-space fix-up of the last pre-rasterization stage: gl_Position is rewritten in place
|
||||
// per `transformFlags` before every vertex the stage produces. Fails open (output = input,
|
||||
// false). Exposed, like the transform above, so a unit case can validate its bytes.
|
||||
Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
ProgramFactory::CompileOptionFlags transformFlags);
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -214,6 +214,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
out.bindings.push_back(
|
||||
{binding, stride, buffer.Divisor ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX});
|
||||
out.attributes.push_back({location, binding, format, 0});
|
||||
out.narrowIntegerAttributes.Note(location, format);
|
||||
out.bindingAttributeLocations.push_back(location);
|
||||
out.bindingBaseOffsets.push_back(static_cast<SizeT>(offset));
|
||||
out.bindingConversions.push_back(conversion);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "Config.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "NarrowIntegerVertexFormats.h"
|
||||
#include "VertexInputStateBuilder.h"
|
||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
||||
#include <Includes.h>
|
||||
@@ -63,6 +64,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
||||
// three times per draw, so it is baked once at build time.
|
||||
Uint32 attributeLocationMask = 0;
|
||||
// The locations of `attributes` whose source data is 8/16-bit integer, by width and
|
||||
// signedness - the vertex-input half of the narrow-integer program variant decision
|
||||
// (NarrowIntegerVertexFormats.h). A pure function of `attributes`, so `layoutHash`
|
||||
// already covers it.
|
||||
NarrowIntegerAttributeMasks narrowIntegerAttributes;
|
||||
// Per-binding glVertexAttribDivisor values other than 1. Vulkan's instance input
|
||||
// rate advances once per instance and nothing else, so anything else has to be
|
||||
// stated through VK_EXT_vertex_attribute_divisor. Empty when every instanced
|
||||
|
||||
@@ -918,6 +918,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint32 vertexInputAttribMask = vis.attributeLocationMask;
|
||||
const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask;
|
||||
const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask;
|
||||
// The fetch format takes the shader input's signedness (VUID-VkGraphicsPipelineCreateInfo-
|
||||
// Input-08733). That renames the bits of 32-bit data, but extends 8/16-bit data by the
|
||||
// input's rule instead of the data's; the program variant SetupWireDraw picks for exactly
|
||||
// those attributes (ResolveNarrowIntegerVertexInputs) puts GL's value back in the shader.
|
||||
auto& patchedAttributes = m_patchedAttributesScratch;
|
||||
patchedAttributes.assign(vis.attributes.begin(), vis.attributes.end());
|
||||
Bool hasPatchedVertexAttributes = false;
|
||||
|
||||
@@ -734,11 +734,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_transformFeedbackFeatureEnabled && MG_Pipe::gPipeInputs->IsTransformFeedbackActive() &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0)
|
||||
flags |= ProgramFactory::CompileOptionBit::XfbCapture;
|
||||
const auto* programObject = &m_programFactory->GetOrCreateProgram(program, flags);
|
||||
// The variant this program's last draw with these flags settled on: what the draw reads
|
||||
// from it before the narrow-integer pick below is the same in every variant.
|
||||
const auto* programObject = &m_programFactory->GetOrCreateDrawProgram(program, flags);
|
||||
if (!(aspects & DrawSetupAspect::IndexBuffer) && programObject->readsBaseVertexBuiltin) {
|
||||
flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
|
||||
programObject = &m_programFactory->GetOrCreateProgram(program, flags);
|
||||
programObject = &m_programFactory->GetOrCreateDrawProgram(program, flags);
|
||||
}
|
||||
const auto vaoHandle = state.BoundVertexElements;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(vaoHandle)) MagmaWireFatal("vertex-elements-record");
|
||||
if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record");
|
||||
const auto& vao = state.VertexElementsCsos[vaoHandle.Slot];
|
||||
if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation");
|
||||
// P7 wave 2 package C, CONTRACT-P7 §3.2: THE `vertex-layout` ROW IS RETIRED AND SPLIT.
|
||||
//
|
||||
// A false is now ONE reason and only one - `buffer-window`, an enabled attribute the
|
||||
// program reads whose slot lies outside the window set_vertex_buffers published. That
|
||||
// is a disagreement about what crossed, not about what this device can express, so it
|
||||
// keeps a named Fatal through the hook (MGPipeSessionFail -> Session::Fail, which is
|
||||
// what rule I asks of a refusal that must stay a refusal). It is the check that caught
|
||||
// the Redmi VAO hash collision fixed in 376c04be, which is why it is not softened.
|
||||
// The P7 marker is gone from the detail string with the abort: this is a permanent
|
||||
// protocol error, not a shape the split path has yet to migrate, so it must not keep
|
||||
// counting against the census of unmigrated refusals (scripts/ci/fatal_census.py
|
||||
// counts the marker literally, which is also why no comment in this package spells it).
|
||||
// P14: resolved through the record's own memo (ResolveWireVertexInput), except in a nested
|
||||
// preparation, which builds into its own scratch rather than under the outer draw's feet.
|
||||
const VertexInputStateFactory::BackendVertexInputState* resolvedInput = nullptr;
|
||||
if (&scratch == &m_wireDrawScratch) {
|
||||
resolvedInput = m_vertexInputStateFactory->ResolveWireVertexInput(
|
||||
vao, state, programObject->activeVertexInputLocationMask);
|
||||
} else if (m_vertexInputStateFactory->BuildWireVertexInput(
|
||||
vao, state, programObject->activeVertexInputLocationMask, scratch.input)) {
|
||||
resolvedInput = &scratch.input;
|
||||
}
|
||||
if (resolvedInput == nullptr) MagmaWireFatal("vertex-layout-buffer-window");
|
||||
const auto& input = *resolvedInput;
|
||||
// The narrow-integer variant (NarrowIntegerVertexFormats.h), chosen from two masks cached on
|
||||
// the program entry and on the resolved layout: a program without integer vertex inputs pays
|
||||
// four ANDs and a compare here. Every variant reflects the same inputs, so the layout
|
||||
// resolved above with the entry's active mask is the chosen variant's too. A second lookup
|
||||
// only when this draw needs another variant than the last one with these flags.
|
||||
const NarrowIntegerVertexInputs narrowIntegerInputs =
|
||||
ResolveNarrowIntegerVertexInputs(programObject->integerVertexInputs, input.narrowIntegerAttributes);
|
||||
if (narrowIntegerInputs != programObject->narrowIntegerInputs)
|
||||
programObject = &m_programFactory->GetOrCreateProgram(program, flags, narrowIntegerInputs);
|
||||
if (programObject->invalidSpirvDeclined) {
|
||||
MGL_WIRE_DECLINE_AT(ProgramInvalidSpirv,
|
||||
"a module of the program fails SPIR-V validation and was never built");
|
||||
@@ -776,35 +816,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
(void)ResolveWireImage(*fbo, fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
}
|
||||
m_textureManager->FlushPendingUploads();
|
||||
const auto vaoHandle = state.BoundVertexElements;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(vaoHandle)) MagmaWireFatal("vertex-elements-record");
|
||||
if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record");
|
||||
const auto& vao = state.VertexElementsCsos[vaoHandle.Slot];
|
||||
if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation");
|
||||
// P7 wave 2 package C, CONTRACT-P7 §3.2: THE `vertex-layout` ROW IS RETIRED AND SPLIT.
|
||||
//
|
||||
// A false is now ONE reason and only one - `buffer-window`, an enabled attribute the
|
||||
// program reads whose slot lies outside the window set_vertex_buffers published. That
|
||||
// is a disagreement about what crossed, not about what this device can express, so it
|
||||
// keeps a named Fatal through the hook (MGPipeSessionFail -> Session::Fail, which is
|
||||
// what rule I asks of a refusal that must stay a refusal). It is the check that caught
|
||||
// the Redmi VAO hash collision fixed in 376c04be, which is why it is not softened.
|
||||
// The P7 marker is gone from the detail string with the abort: this is a permanent
|
||||
// protocol error, not a shape the split path has yet to migrate, so it must not keep
|
||||
// counting against the census of unmigrated refusals (scripts/ci/fatal_census.py
|
||||
// counts the marker literally, which is also why no comment in this package spells it).
|
||||
// P14: resolved through the record's own memo (ResolveWireVertexInput), except in a nested
|
||||
// preparation, which builds into its own scratch rather than under the outer draw's feet.
|
||||
const VertexInputStateFactory::BackendVertexInputState* resolvedInput = nullptr;
|
||||
if (&scratch == &m_wireDrawScratch) {
|
||||
resolvedInput =
|
||||
m_vertexInputStateFactory->ResolveWireVertexInput(vao, state, programObj.activeVertexInputLocationMask);
|
||||
} else if (m_vertexInputStateFactory->BuildWireVertexInput(vao, state, programObj.activeVertexInputLocationMask,
|
||||
scratch.input)) {
|
||||
resolvedInput = &scratch.input;
|
||||
}
|
||||
if (resolvedInput == nullptr) MagmaWireFatal("vertex-layout-buffer-window");
|
||||
const auto& input = *resolvedInput;
|
||||
// The seven device/format reasons are now MASKED OUT of the vertex input state, and
|
||||
// this is the monolith arm's pre-flight for them, statement for statement
|
||||
// (VulkanRenderer.cpp's SetupDraw: "Fail loudly rather than render wrong pixels"). An
|
||||
|
||||
@@ -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
|
||||
@@ -147,6 +148,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/TessellationXfbCaptureScenario.cpp
|
||||
Scenarios/PointSizeDemotionScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/NarrowIntegerAttributeScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/CopyImagePacked16Scenario.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
|
||||
|
||||
@@ -90,6 +90,7 @@ set(MGL_P8A_AMBIENT_CASES
|
||||
IterationRPScratchFixScenario.*
|
||||
LayeredAttachmentBarrierScenario.*
|
||||
LayeredAttachmentShapeScenario.*
|
||||
NarrowIntegerAttributeScenario.*
|
||||
NonCoreImageFormatScenario.*
|
||||
OrientationScenario.*
|
||||
P4aFinalFixScenario.*
|
||||
|
||||
@@ -0,0 +1,876 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/NarrowIntegerAttributeScenario.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
|
||||
//
|
||||
// 8/16-bit integer attribute data read through an int/uint input of the OTHER signedness.
|
||||
//
|
||||
// Every desktop GL driver hands such an input the source integer converted to 32 bits by the
|
||||
// SOURCE type's signedness, then reinterpreted as the declared type: GL_UNSIGNED_BYTE 240 reaches
|
||||
// an `int` input as 240, GL_BYTE -1 reaches a `uint` input as 0xFFFFFFFF. Sodium 0.5 relies on it
|
||||
// (GL_UNSIGNED_BYTE lightmap data into `in ivec4 a_LightAndData`), and DirectVulkan once lost it:
|
||||
// its fetch format has to carry the input's signedness, which re-extends narrow data by the
|
||||
// input's rule, and every lightmap value from 128 up came out negative - dark terrain under
|
||||
// shaderpacks. The values are captured with transform feedback so every bit is compared.
|
||||
//
|
||||
// The second case draws one program over two vertex arrays that differ only in the signedness
|
||||
// of the data, in both orders and each twice in a row: the correction is chosen per draw from the
|
||||
// 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
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
constexpr GLuint kPoison = 0xDEADBEEFu;
|
||||
|
||||
GLuint CompileShader(GLenum type, const char* source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
const char* const kFragmentSource = R"(#version 410 core
|
||||
out vec4 color;
|
||||
void main()
|
||||
{
|
||||
color = vec4(1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// 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, fragmentSource, log);
|
||||
if (fragmentShader == 0) {
|
||||
glDeleteShader(vertexShader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glAttachShader(program, fragmentShader);
|
||||
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);
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// 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
|
||||
layout(location = 0) in ivec4 ubyteAsInt;
|
||||
layout(location = 1) in uvec4 byteAsUint;
|
||||
layout(location = 2) in uvec2 ushortAsUint;
|
||||
layout(location = 3) in ivec2 ushortAsInt;
|
||||
layout(location = 4) in uvec2 shortAsUint;
|
||||
layout(location = 5) in ivec2 instancedUbyteAsInt;
|
||||
layout(location = 6) in uint intAsUint;
|
||||
flat out ivec4 capturedA;
|
||||
flat out uvec4 capturedB;
|
||||
flat out ivec4 capturedC;
|
||||
flat out uvec4 capturedD;
|
||||
void main()
|
||||
{
|
||||
capturedA = ubyteAsInt;
|
||||
capturedB = byteAsUint;
|
||||
capturedC = ivec4(ushortAsInt, instancedUbyteAsInt);
|
||||
capturedD = uvec4(shortAsUint, ushortAsUint.x, intAsUint);
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kSingleInputVertexSource = R"(#version 410 core
|
||||
layout(location = 0) 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 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];
|
||||
GLbyte byteAsUint[4];
|
||||
GLushort ushortAsUint[2];
|
||||
GLushort ushortAsInt[2];
|
||||
GLshort shortAsUint[2];
|
||||
GLint intAsUint;
|
||||
};
|
||||
|
||||
class NarrowIntegerAttributeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenBuffers(1, &m_capture);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_capture);
|
||||
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);
|
||||
for (const GLuint program : m_programs)
|
||||
glDeleteProgram(program);
|
||||
for (const GLuint vao : m_vaos)
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
for (const GLuint buffer : m_buffers)
|
||||
glDeleteBuffers(1, &buffer);
|
||||
if (m_capture != 0) glDeleteBuffers(1, &m_capture);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
GLuint MakeBuffer(const void* data, GLsizeiptr size) {
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
|
||||
m_buffers.push_back(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
GLuint MakeVertexArray() {
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
m_vaos.push_back(vao);
|
||||
return vao;
|
||||
}
|
||||
|
||||
// One point drawn under GL_RASTERIZER_DISCARD; the first `words` captured 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());
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(vao);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
if (draw)
|
||||
draw();
|
||||
else
|
||||
glDrawArraysInstanced(GL_POINTS, 0, 1, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
std::vector<GLuint> result(words, kPoison);
|
||||
const void* mapped =
|
||||
glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, words * sizeof(GLuint), GL_MAP_READ_BIT);
|
||||
if (mapped == nullptr) {
|
||||
ADD_FAILURE() << "glMapBufferRange returned null";
|
||||
return result;
|
||||
}
|
||||
std::memcpy(result.data(), mapped, words * sizeof(GLuint));
|
||||
glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
|
||||
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;
|
||||
std::vector<GLuint> m_buffers;
|
||||
};
|
||||
|
||||
TEST_F(NarrowIntegerAttributeScenario, EveryNarrowTypeKeepsItsOwnValueInAnInputOfTheOtherSignedness) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
std::string log;
|
||||
const GLuint program = BuildCaptureProgram(kEveryPairingVertexSource,
|
||||
{"capturedA", "capturedB", "capturedC", "capturedD"}, &log);
|
||||
ASSERT_NE(program, 0u) << log;
|
||||
m_programs.push_back(program);
|
||||
|
||||
const PerVertex vertex{{240, 200, 128, 7}, {-1, 5, -128, 127}, {65000, 300},
|
||||
{65000, 40000}, {-2, 1000}, -1};
|
||||
const GLubyte instance[4] = {250, 129, 0, 0};
|
||||
const GLuint vao = MakeVertexArray();
|
||||
MakeBuffer(&vertex, sizeof(vertex));
|
||||
const auto offset = [](std::size_t bytes) { return reinterpret_cast<const void*>(bytes); };
|
||||
constexpr GLsizei stride = sizeof(PerVertex);
|
||||
glVertexAttribIPointer(0, 4, GL_UNSIGNED_BYTE, stride, offset(offsetof(PerVertex, ubyteAsInt)));
|
||||
glVertexAttribIPointer(1, 4, GL_BYTE, stride, offset(offsetof(PerVertex, byteAsUint)));
|
||||
glVertexAttribIPointer(2, 2, GL_UNSIGNED_SHORT, stride, offset(offsetof(PerVertex, ushortAsUint)));
|
||||
glVertexAttribIPointer(3, 2, GL_UNSIGNED_SHORT, stride, offset(offsetof(PerVertex, ushortAsInt)));
|
||||
glVertexAttribIPointer(4, 2, GL_SHORT, stride, offset(offsetof(PerVertex, shortAsUint)));
|
||||
glVertexAttribIPointer(6, 1, GL_INT, stride, offset(offsetof(PerVertex, intAsUint)));
|
||||
MakeBuffer(instance, sizeof(instance));
|
||||
glVertexAttribIPointer(5, 2, GL_UNSIGNED_BYTE, 4, nullptr);
|
||||
glVertexAttribDivisor(5, 1);
|
||||
for (GLuint location = 0; location <= 6; ++location)
|
||||
glEnableVertexAttribArray(location);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
|
||||
|
||||
const std::vector<GLuint> captured = Capture(program, vao, 16);
|
||||
const std::vector<GLuint> expected = {
|
||||
240u, 200u, 128u, 7u, // GL_UNSIGNED_BYTE -> ivec4: zero-extended
|
||||
0xFFFFFFFFu, 5u, 0xFFFFFF80u, 127u, // GL_BYTE -> uvec4: sign-extended
|
||||
65000u, 40000u, // GL_UNSIGNED_SHORT -> ivec2: zero-extended
|
||||
250u, 129u, // instanced GL_UNSIGNED_BYTE -> ivec2
|
||||
0xFFFFFFFEu, 1000u, // GL_SHORT -> uvec2: sign-extended
|
||||
65000u, // GL_UNSIGNED_SHORT -> uvec2: already right
|
||||
0xFFFFFFFFu, // 32-bit GL_INT -> uint: the same bits
|
||||
};
|
||||
EXPECT_EQ(captured, expected);
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
ExpectDriverArraysFitTheirInputs();
|
||||
}
|
||||
|
||||
TEST_F(NarrowIntegerAttributeScenario, OneProgramFollowsTheSignednessOfEachVertexArray) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
std::string log;
|
||||
const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log);
|
||||
ASSERT_NE(program, 0u) << log;
|
||||
m_programs.push_back(program);
|
||||
|
||||
// The same four bytes, once as GL_BYTE and once as GL_UNSIGNED_BYTE.
|
||||
const GLubyte bytes[4] = {0xFF, 0x80, 5, 127};
|
||||
const GLuint signedArray = MakeVertexArray();
|
||||
MakeBuffer(bytes, sizeof(bytes));
|
||||
glVertexAttribIPointer(0, 4, GL_BYTE, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
const GLuint unsignedArray = MakeVertexArray();
|
||||
MakeBuffer(bytes, sizeof(bytes));
|
||||
glVertexAttribIPointer(0, 4, GL_UNSIGNED_BYTE, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup";
|
||||
|
||||
const std::vector<GLuint> asSigned = {0xFFFFFFFFu, 0xFFFFFF80u, 5u, 127u};
|
||||
const std::vector<GLuint> asUnsigned = {255u, 128u, 5u, 127u};
|
||||
EXPECT_EQ(Capture(program, signedArray, 4), asSigned) << "GL_BYTE first";
|
||||
EXPECT_EQ(Capture(program, signedArray, 4), asSigned) << "GL_BYTE twice in a row";
|
||||
EXPECT_EQ(Capture(program, unsignedArray, 4), asUnsigned) << "then GL_UNSIGNED_BYTE";
|
||||
EXPECT_EQ(Capture(program, unsignedArray, 4), asUnsigned) << "GL_UNSIGNED_BYTE twice in a row";
|
||||
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
|
||||
@@ -396,11 +396,25 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 Serial = 0;
|
||||
Uint64 BindingsSerial = 0;
|
||||
Uint64 Hash = 0;
|
||||
// The hash of the variant of this program the factory last handed out for this key:
|
||||
// Hash itself, or a variant keyed on draw-time vertex data. Derived from Hash, so it
|
||||
// is valid exactly as long.
|
||||
Uint64 LastVariantHash = 0;
|
||||
Uint32 Key = 0;
|
||||
Bool Valid = false;
|
||||
};
|
||||
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
|
||||
|
||||
@@ -57,6 +57,11 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
add_executable(CapabilityLimitsTest CapabilityLimitsTest.cpp)
|
||||
target_include_directories(CapabilityLimitsTest PRIVATE ${MGL_ROOT}/include ${MGL_ROOT}/MobileGL)
|
||||
target_link_libraries(CapabilityLimitsTest PRIVATE MobileGLTestMain ${LINK_LIBRARIES})
|
||||
gtest_discover_tests(CapabilityLimitsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
add_executable(ContextEpochTest ContextEpochTest.cpp)
|
||||
target_include_directories(ContextEpochTest PRIVATE ${MGL_ROOT}/include ${MGL_ROOT}/MobileGL)
|
||||
target_link_libraries(ContextEpochTest PRIVATE MobileGLTestMain ${LINK_LIBRARIES})
|
||||
@@ -69,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,43 @@
|
||||
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/CapabilityLimitsTest.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 <gtest/gtest.h>
|
||||
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
||||
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
|
||||
// Iris budgets every program's samplers (vertex and fragment together) against
|
||||
// GL_MAX_TEXTURE_IMAGE_UNITS, so an ES driver's 16-per-stage answer leaves shader packs without a
|
||||
// pipeline. The fragment limit reports the desktop value whenever the driver's combined limit can
|
||||
// number that many units, and never more than the combined limit or the per-stage cap.
|
||||
TEST(DirectGLESCapabilityLimits, AdvertisesDesktopFragmentTextureUnitsWhenCombinedLimitAllows) {
|
||||
constexpr Int kPerStageCap = MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS;
|
||||
MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
|
||||
|
||||
MG_External::GLESCapabilities adreno;
|
||||
adreno.MaxTextureImageUnits = 16;
|
||||
adreno.MaxVertexTextureImageUnits = 16;
|
||||
adreno.MaxCombinedTextureImageUnits = 96;
|
||||
backend.ApplyGLESCapabilitiesForTesting(adreno);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxTextureImageUnits, kPerStageCap);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxVertexTextureImageUnits, 16);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedTextureImageUnits, 96);
|
||||
|
||||
MG_External::GLESCapabilities narrowCombined;
|
||||
narrowCombined.MaxTextureImageUnits = 16;
|
||||
narrowCombined.MaxCombinedTextureImageUnits = 20;
|
||||
backend.ApplyGLESCapabilitiesForTesting(narrowCombined);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxTextureImageUnits, 20);
|
||||
|
||||
MG_External::GLESCapabilities wide;
|
||||
wide.MaxTextureImageUnits = 64;
|
||||
wide.MaxCombinedTextureImageUnits = 192;
|
||||
backend.ApplyGLESCapabilitiesForTesting(wide);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxTextureImageUnits, kPerStageCap);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -14,6 +14,7 @@ add_executable(
|
||||
PassthroughTessControlTest.cpp
|
||||
ViewportIndexReflectionTest.cpp
|
||||
DriverModuleScreenTest.cpp
|
||||
NarrowIntegerVertexFormatsTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(PipelineQuirkTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// MobileGL - MobileGL/MG_Test/Pipeline/NarrowIntegerVertexFormatsTest.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
|
||||
//
|
||||
// Which vertex inputs take Magma's narrow-integer program variant. The pipeline fetches an integer
|
||||
// attribute with the format of the shader input's signedness, which re-extends 8/16-bit data by the
|
||||
// wrong rule exactly when the data and the input disagree in signedness; every other pairing -
|
||||
// 32-bit data, matching signedness, float data or float inputs - must keep today's program.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <MG_Backend/DirectVulkan/Renderer/NarrowIntegerVertexFormats.h>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectVulkan::ClassifyNarrowIntegerVertexFormat;
|
||||
using MobileGL::MG_Backend::DirectVulkan::IntegerVertexInputMasks;
|
||||
using MobileGL::MG_Backend::DirectVulkan::MagmaProgramSource;
|
||||
using MobileGL::MG_Backend::DirectVulkan::NarrowIntegerAttributeMasks;
|
||||
using MobileGL::MG_Backend::DirectVulkan::NarrowIntegerVertexFormat;
|
||||
using MobileGL::MG_Backend::DirectVulkan::NarrowIntegerVertexInputs;
|
||||
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||
using MobileGL::MG_Backend::DirectVulkan::ResolveNarrowIntegerVertexInputs;
|
||||
|
||||
namespace {
|
||||
// The correction one (data format, shader input type) pair gets, alone at `location`.
|
||||
NarrowIntegerVertexInputs Resolve(VkFormat format, GLenum inputType, Uint32 location = 3) {
|
||||
IntegerVertexInputMasks inputs;
|
||||
inputs.Note(location, inputType);
|
||||
NarrowIntegerAttributeMasks data;
|
||||
data.Note(location, format);
|
||||
return ResolveNarrowIntegerVertexInputs(inputs, data);
|
||||
}
|
||||
|
||||
NarrowIntegerVertexInputs Only(Uint32 NarrowIntegerVertexInputs::* field, Uint32 location = 3) {
|
||||
NarrowIntegerVertexInputs expected;
|
||||
expected.*field = 1u << location;
|
||||
return expected;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(NarrowIntegerVertexFormats, ClassifiesExactlyThe8And16BitIntegerFormats) {
|
||||
for (const VkFormat format :
|
||||
{VK_FORMAT_R8_UINT, VK_FORMAT_R8G8_UINT, VK_FORMAT_R8G8B8_UINT, VK_FORMAT_R8G8B8A8_UINT})
|
||||
EXPECT_EQ(ClassifyNarrowIntegerVertexFormat(format), NarrowIntegerVertexFormat::Unsigned8) << format;
|
||||
for (const VkFormat format :
|
||||
{VK_FORMAT_R16_UINT, VK_FORMAT_R16G16_UINT, VK_FORMAT_R16G16B16_UINT, VK_FORMAT_R16G16B16A16_UINT})
|
||||
EXPECT_EQ(ClassifyNarrowIntegerVertexFormat(format), NarrowIntegerVertexFormat::Unsigned16) << format;
|
||||
for (const VkFormat format :
|
||||
{VK_FORMAT_R8_SINT, VK_FORMAT_R8G8_SINT, VK_FORMAT_R8G8B8_SINT, VK_FORMAT_R8G8B8A8_SINT})
|
||||
EXPECT_EQ(ClassifyNarrowIntegerVertexFormat(format), NarrowIntegerVertexFormat::Signed8) << format;
|
||||
for (const VkFormat format :
|
||||
{VK_FORMAT_R16_SINT, VK_FORMAT_R16G16_SINT, VK_FORMAT_R16G16B16_SINT, VK_FORMAT_R16G16B16A16_SINT})
|
||||
EXPECT_EQ(ClassifyNarrowIntegerVertexFormat(format), NarrowIntegerVertexFormat::Signed16) << format;
|
||||
// 32-bit integers, float/normalized/scaled data and packed or reversed formats never qualify.
|
||||
for (const VkFormat format :
|
||||
{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32B32A32_UINT, VK_FORMAT_R32_SINT, VK_FORMAT_R32G32B32A32_SINT,
|
||||
VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SNORM, VK_FORMAT_R8G8B8A8_USCALED, VK_FORMAT_R16G16_SSCALED,
|
||||
VK_FORMAT_R16_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_A2B10G10R10_UINT_PACK32,
|
||||
VK_FORMAT_A2B10G10R10_SNORM_PACK32, VK_FORMAT_UNDEFINED})
|
||||
EXPECT_EQ(ClassifyNarrowIntegerVertexFormat(format), NarrowIntegerVertexFormat::None) << format;
|
||||
}
|
||||
|
||||
TEST(NarrowIntegerVertexFormats, CorrectsOnlyTheSignednessMismatches) {
|
||||
// Sodium 0.5's a_LightAndData: GL_UNSIGNED_BYTE x4 into `in ivec4`.
|
||||
EXPECT_EQ(Resolve(VK_FORMAT_R8G8B8A8_UINT, GL_INT_VEC4), Only(&NarrowIntegerVertexInputs::zeroExtend8));
|
||||
EXPECT_EQ(Resolve(VK_FORMAT_R8_UINT, GL_INT), Only(&NarrowIntegerVertexInputs::zeroExtend8));
|
||||
EXPECT_EQ(Resolve(VK_FORMAT_R16G16_UINT, GL_INT_VEC2), Only(&NarrowIntegerVertexInputs::zeroExtend16));
|
||||
EXPECT_EQ(Resolve(VK_FORMAT_R16G16B16_UINT, GL_INT_VEC3), Only(&NarrowIntegerVertexInputs::zeroExtend16));
|
||||
EXPECT_EQ(Resolve(VK_FORMAT_R8G8B8A8_SINT, GL_UNSIGNED_INT_VEC4), Only(&NarrowIntegerVertexInputs::signExtend8));
|
||||
EXPECT_EQ(Resolve(VK_FORMAT_R8G8_SINT, GL_UNSIGNED_INT_VEC2), Only(&NarrowIntegerVertexInputs::signExtend8));
|
||||
EXPECT_EQ(Resolve(VK_FORMAT_R16_SINT, GL_UNSIGNED_INT), Only(&NarrowIntegerVertexInputs::signExtend16));
|
||||
|
||||
// Matching signedness: the fetch already extends by the data's rule.
|
||||
EXPECT_FALSE(Resolve(VK_FORMAT_R8G8B8A8_UINT, GL_UNSIGNED_INT_VEC4).Any());
|
||||
EXPECT_FALSE(Resolve(VK_FORMAT_R16_SINT, GL_INT).Any());
|
||||
// 32-bit data: the format swap renames bits, which is GL's reinterpretation already.
|
||||
EXPECT_FALSE(Resolve(VK_FORMAT_R32_UINT, GL_INT).Any());
|
||||
EXPECT_FALSE(Resolve(VK_FORMAT_R32G32B32A32_SINT, GL_UNSIGNED_INT_VEC4).Any());
|
||||
// Float data into an integer input and integer data into a float input are not this rule.
|
||||
EXPECT_FALSE(Resolve(VK_FORMAT_R8G8B8A8_USCALED, GL_INT_VEC4).Any());
|
||||
EXPECT_FALSE(Resolve(VK_FORMAT_R8_UNORM, GL_UNSIGNED_INT).Any());
|
||||
EXPECT_FALSE(Resolve(VK_FORMAT_R8G8B8A8_UINT, GL_FLOAT_VEC4).Any());
|
||||
EXPECT_FALSE(Resolve(VK_FORMAT_R16_SINT, GL_FLOAT).Any());
|
||||
}
|
||||
|
||||
// Locations are independent bits, a location the program does not read as an integer is ignored
|
||||
// even when its data is narrow, and the top location still fits.
|
||||
TEST(NarrowIntegerVertexFormats, KeepsLocationsApart) {
|
||||
IntegerVertexInputMasks inputs;
|
||||
inputs.Note(0, GL_FLOAT_VEC3);
|
||||
inputs.Note(1, GL_INT_VEC4);
|
||||
inputs.Note(2, GL_UNSIGNED_INT_VEC2);
|
||||
inputs.Note(5, GL_UNSIGNED_INT_VEC4);
|
||||
inputs.Note(31, GL_INT);
|
||||
inputs.Note(32, GL_INT); // beyond the masks: ignored, not wrapped
|
||||
EXPECT_EQ(inputs.signedInputs, (1u << 1) | (1u << 31));
|
||||
EXPECT_EQ(inputs.unsignedInputs, (1u << 2) | (1u << 5));
|
||||
|
||||
NarrowIntegerAttributeMasks data;
|
||||
data.Note(0, VK_FORMAT_R8G8B8A8_UINT); // a float input: not corrected
|
||||
data.Note(1, VK_FORMAT_R8G8B8A8_UINT);
|
||||
data.Note(2, VK_FORMAT_R16G16_UINT); // matching signedness
|
||||
data.Note(5, VK_FORMAT_R8G8B8A8_SINT);
|
||||
data.Note(7, VK_FORMAT_R16_SINT); // no shader input
|
||||
data.Note(31, VK_FORMAT_R16_UINT);
|
||||
data.Note(40, VK_FORMAT_R8_UINT);
|
||||
|
||||
const NarrowIntegerVertexInputs resolved = ResolveNarrowIntegerVertexInputs(inputs, data);
|
||||
EXPECT_EQ(resolved.zeroExtend8, 1u << 1);
|
||||
EXPECT_EQ(resolved.zeroExtend16, 1u << 31);
|
||||
EXPECT_EQ(resolved.signExtend8, 1u << 5);
|
||||
EXPECT_EQ(resolved.signExtend16, 0u);
|
||||
|
||||
// A program without integer inputs resolves to nothing whatever the data is.
|
||||
EXPECT_FALSE(ResolveNarrowIntegerVertexInputs(IntegerVertexInputMasks{}, data).Any());
|
||||
}
|
||||
|
||||
// The variant's program-cache identity: distinct from the plain program and from every other
|
||||
// correction set, and a pure function of its inputs.
|
||||
TEST(NarrowIntegerVertexFormats, VariantHashKeysEveryCorrectionApart) {
|
||||
constexpr ProgramFactory::HashType kBase = 0x0123456789abcdefull;
|
||||
const NarrowIntegerVertexInputs sodium = Only(&NarrowIntegerVertexInputs::zeroExtend8, 3);
|
||||
const auto hash = ProgramFactory::MixNarrowIntegerVertexInputs(kBase, sodium);
|
||||
EXPECT_NE(hash, kBase);
|
||||
EXPECT_EQ(ProgramFactory::MixNarrowIntegerVertexInputs(kBase, sodium), hash);
|
||||
EXPECT_NE(ProgramFactory::MixNarrowIntegerVertexInputs(kBase + 1, sodium), hash);
|
||||
for (const auto field : {&NarrowIntegerVertexInputs::zeroExtend16, &NarrowIntegerVertexInputs::signExtend8,
|
||||
&NarrowIntegerVertexInputs::signExtend16})
|
||||
EXPECT_NE(ProgramFactory::MixNarrowIntegerVertexInputs(kBase, Only(field, 3)), hash);
|
||||
EXPECT_NE(ProgramFactory::MixNarrowIntegerVertexInputs(kBase, Only(&NarrowIntegerVertexInputs::zeroExtend8, 4)),
|
||||
hash);
|
||||
}
|
||||
|
||||
// The program half and the entry's own correction live on the cache entry, and the cache's
|
||||
// robin-hood insert moves entries around: a mask left behind by a move would pick another
|
||||
// program's variant, and a correction left behind would make a draw take a variant for the plain
|
||||
// program or the reverse.
|
||||
TEST(NarrowIntegerVertexFormats, IntegerInputMasksTravelWithTheProgramEntry) {
|
||||
const NarrowIntegerVertexInputs sodium = Only(&NarrowIntegerVertexInputs::zeroExtend8, 3);
|
||||
ProgramFactory::VkProgramObject source;
|
||||
source.integerVertexInputs.signedInputs = 1u << 3;
|
||||
source.integerVertexInputs.unsignedInputs = 1u << 7;
|
||||
source.narrowIntegerInputs = sodium;
|
||||
|
||||
ProgramFactory::VkProgramObject moved(std::move(source));
|
||||
EXPECT_EQ(moved.integerVertexInputs.signedInputs, 1u << 3);
|
||||
EXPECT_EQ(moved.integerVertexInputs.unsignedInputs, 1u << 7);
|
||||
EXPECT_EQ(moved.narrowIntegerInputs, sodium);
|
||||
EXPECT_EQ(source.integerVertexInputs.signedInputs, 0u);
|
||||
EXPECT_EQ(source.integerVertexInputs.unsignedInputs, 0u);
|
||||
EXPECT_FALSE(source.narrowIntegerInputs.Any());
|
||||
|
||||
ProgramFactory::VkProgramObject assigned;
|
||||
assigned = std::move(moved);
|
||||
EXPECT_EQ(assigned.integerVertexInputs.signedInputs, 1u << 3);
|
||||
EXPECT_EQ(assigned.integerVertexInputs.unsignedInputs, 1u << 7);
|
||||
EXPECT_EQ(assigned.narrowIntegerInputs, sodium);
|
||||
EXPECT_EQ(moved.integerVertexInputs.signedInputs, 0u);
|
||||
EXPECT_EQ(moved.integerVertexInputs.unsignedInputs, 0u);
|
||||
EXPECT_FALSE(moved.narrowIntegerInputs.Any());
|
||||
}
|
||||
|
||||
// A draw starts from the variant the program's last lookup with the same flags returned, which
|
||||
// the program record keeps beside the plain hash it derives from. A fresh slot must point at the
|
||||
// plain program, and the variant must be forgotten with the hash: after a relink or a binding
|
||||
// change the same flags answer from a fresh slot.
|
||||
TEST(NarrowIntegerVertexFormats, TheLastVariantIsRememberedOnlyWithItsPlainHash) {
|
||||
MG_Pipe::MGPipeShaderCsoRecord record{};
|
||||
record.Archive = MakeShared<MG_State::GLState::ProgramArchive>();
|
||||
record.Serial = 4;
|
||||
record.BindingsSerial = 9;
|
||||
const MagmaProgramSource source({5, 1}, record);
|
||||
constexpr Uint kFlags = 0x21;
|
||||
|
||||
EXPECT_EQ(source.FindBackendHashMemo(kFlags), nullptr);
|
||||
auto& slot = source.SetBackendHashMemo(kFlags, 0xabcull);
|
||||
EXPECT_EQ(slot.LastVariantHash, 0xabcull) << "a fresh slot starts at the plain program";
|
||||
slot.LastVariantHash = 0xdefull;
|
||||
const auto* found = source.FindBackendHashMemo(kFlags);
|
||||
ASSERT_EQ(found, &slot);
|
||||
EXPECT_EQ(found->Hash, 0xabcull);
|
||||
EXPECT_EQ(found->LastVariantHash, 0xdefull);
|
||||
EXPECT_EQ(source.FindBackendHashMemo(kFlags + 1), nullptr) << "other flags have their own slot";
|
||||
|
||||
record.Serial = 5; // a relink
|
||||
EXPECT_EQ(source.FindBackendHashMemo(kFlags), nullptr);
|
||||
EXPECT_EQ(source.SetBackendHashMemo(kFlags, 0x123ull).LastVariantHash, 0x123ull);
|
||||
ASSERT_NE(source.FindBackendHashMemo(kFlags), nullptr);
|
||||
record.BindingsSerial = 10; // glUniformBlockBinding and friends
|
||||
EXPECT_EQ(source.FindBackendHashMemo(kFlags), nullptr);
|
||||
}
|
||||
@@ -20,8 +20,11 @@ add_executable(
|
||||
FlattenXfbInterfaceBlocksTest.cpp
|
||||
UniquifyIoBlockNamesTest.cpp
|
||||
StripIoBlockLocationsTest.cpp
|
||||
StripCompatPerVertexMembersTest.cpp
|
||||
LowerViewportIndexTest.cpp
|
||||
DeferGeometryEmitsTest.cpp
|
||||
RetainGeometryOutputsTest.cpp
|
||||
PositionFixupSitesTest.cpp
|
||||
ClampMultisampleFetchTest.cpp
|
||||
LegalizeResourceArrayIndexTest.cpp
|
||||
FlattenAtomicCounterBlockTest.cpp
|
||||
@@ -32,6 +35,8 @@ add_executable(
|
||||
PixelCenterIntegerTest.cpp
|
||||
ZeroLayerOutputTest.cpp
|
||||
FullCoverageSampleMaskTest.cpp
|
||||
ExtendNarrowIntegerVertexInputsTest.cpp
|
||||
ExtendNarrowIntegerVertexInputsRuntimeTest.cpp
|
||||
RelaxedOpaqueStructTest.cpp
|
||||
ShadingRateEsslTest.cpp
|
||||
)
|
||||
@@ -45,6 +50,7 @@ target_include_directories(SpirvPassTest PRIVATE
|
||||
target_link_libraries(
|
||||
SpirvPassTest PRIVATE
|
||||
MobileGLTestMain
|
||||
glslang-default-resource-limits
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/ExtendNarrowIntegerVertexInputsTest.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
|
||||
//
|
||||
// Magma fetches an integer attribute with the format of the shader input's signedness, so 8/16-bit
|
||||
// data reaches an input of the other signedness extended by the wrong rule (Sodium's UNSIGNED_BYTE
|
||||
// a_LightAndData read as `ivec4` turned 240 into -16). ExtendNarrowIntegerVertexInputsPass puts
|
||||
// GL's value back. What has to hold: the module stays valid, exactly the listed locations get a
|
||||
// zero-extension mask or a sign-extension, an array input is corrected element by element, the
|
||||
// reflected vertex inputs do not move, and everything else is left byte-for-byte 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::NarrowIntegerVertexInputs;
|
||||
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 uvec4 sbyte;
|
||||
layout(location = 4) in int shortAsInt;
|
||||
layout(location = 5) in int packed[3];
|
||||
layout(location = 8) in uint sshort;
|
||||
flat out ivec4 a;
|
||||
flat out uvec4 b;
|
||||
void main() {
|
||||
a = lightAndData + ivec4(shortAsInt) + ivec4(packed[0], packed[1], packed[2], 0);
|
||||
b = uvec4(texc, sshort, 0u) + sbyte;
|
||||
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); }
|
||||
)";
|
||||
|
||||
// Index 0 is the vertex module, 1 the fragment module (stages come back sorted).
|
||||
Vector<Vector<Uint32>> CompileProgram() {
|
||||
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<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;
|
||||
}
|
||||
|
||||
// The lines of `text` holding `opcode`, for checking an instruction's operands.
|
||||
Vector<String> LinesWith(const String& text, const String& opcode) {
|
||||
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(opcode) != String::npos) lines.push_back(line);
|
||||
begin = end + 1;
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
// (location, component count, numeric flags) of every non-builtin input, sorted: what the
|
||||
// program factory reflects the vertex inputs from.
|
||||
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;
|
||||
}
|
||||
|
||||
// Sodium's shape plus one of each other kind: UNSIGNED_BYTE into ivec4 (1), BYTE into uvec4 (3),
|
||||
// UNSIGNED_SHORT into int (4), SHORT into uint (8), and UNSIGNED_BYTE into the middle element
|
||||
// of an int array (location 6 = packed[1]).
|
||||
NarrowIntegerVertexInputs SodiumLikeInputs() {
|
||||
NarrowIntegerVertexInputs inputs;
|
||||
inputs.zeroExtend8 = (1u << 1) | (1u << 6);
|
||||
inputs.signExtend8 = 1u << 3;
|
||||
inputs.zeroExtend16 = 1u << 4;
|
||||
inputs.signExtend16 = 1u << 8;
|
||||
return inputs;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(ExtendNarrowIntegerVertexInputs, ExtendsExactlyTheListedLocations) {
|
||||
MobileGL::Initialize();
|
||||
const auto modules = CompileProgram();
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
const Vector<Uint32>& input = modules[0];
|
||||
const String original = Disassemble(input);
|
||||
ASSERT_EQ(CountOccurrences(original, "OpBitwiseAnd"), 0u) << original;
|
||||
ASSERT_EQ(CountOccurrences(original, "OpBitFieldSExtract"), 0u) << original;
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(input, output, SodiumLikeInputs(), true));
|
||||
EXPECT_EQ(ValidationError(output), String{});
|
||||
const String text = Disassemble(output);
|
||||
|
||||
// Zero-extensions: the ivec4 by 0xff, the int by 0xffff, and packed[1] (one element) by 0xff.
|
||||
const auto ands = LinesWith(text, "OpBitwiseAnd");
|
||||
ASSERT_EQ(ands.size(), 3u) << text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpBitwiseAnd %v4int"), 1u) << text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpBitwiseAnd %int"), 2u) << text;
|
||||
EXPECT_EQ(CountOccurrences(text, "%int_65535"), 2u) << text; // declaration + the one use
|
||||
EXPECT_NE(text.find("%int_255"), String::npos) << text;
|
||||
|
||||
// Sign-extensions: the uvec4 from 8 bits, the uint from 16, both at bit offset 0.
|
||||
const auto extracts = LinesWith(text, "OpBitFieldSExtract");
|
||||
ASSERT_EQ(extracts.size(), 2u) << text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpBitFieldSExtract %v4uint"), 1u) << text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpBitFieldSExtract %uint"), 1u) << text;
|
||||
for (const auto& line : extracts)
|
||||
EXPECT_NE(line.find(" %uint_0 %uint_"), String::npos) << line;
|
||||
EXPECT_EQ(CountOccurrences(text, " %uint_0 %uint_8"), 1u) << text;
|
||||
EXPECT_EQ(CountOccurrences(text, " %uint_0 %uint_16"), 1u) << text;
|
||||
|
||||
// The corrected inputs are now Private copies seeded once; the untouched ones stay Inputs.
|
||||
for (const char* name : {"%lightAndData", "%sbyte", "%shortAsInt", "%packed", "%sshort"}) {
|
||||
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;
|
||||
EXPECT_NE(text.find("%texc = OpVariable %_ptr_Input_v2uint Input"), String::npos) << text;
|
||||
|
||||
// The array is split into its three elements and rebuilt, one element corrected.
|
||||
EXPECT_EQ(CountOccurrences(text, "OpCompositeExtract %int") - CountOccurrences(original, "OpCompositeExtract %int"),
|
||||
3u)
|
||||
<< text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpCompositeConstruct %_arr_int_uint_3") -
|
||||
CountOccurrences(original, "OpCompositeConstruct %_arr_int_uint_3"),
|
||||
1u)
|
||||
<< text;
|
||||
|
||||
// Same interface: every location, width and type the program factory reflects is unchanged.
|
||||
EXPECT_EQ(ReflectInputs(output), ReflectInputs(input));
|
||||
}
|
||||
|
||||
// A list that names no integer input of the module - a float input, an empty location, a fragment
|
||||
// module - leaves the bytes alone, so a mis-keyed variant cannot change what the shader computes.
|
||||
TEST(ExtendNarrowIntegerVertexInputs, LeavesEverythingElseUnchanged) {
|
||||
MobileGL::Initialize();
|
||||
const auto modules = CompileProgram();
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
|
||||
NarrowIntegerVertexInputs floatOrEmpty;
|
||||
floatOrEmpty.zeroExtend8 = (1u << 0) | (1u << 12);
|
||||
floatOrEmpty.signExtend16 = 1u << 31;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(modules[0], output, floatOrEmpty, true));
|
||||
EXPECT_EQ(output, modules[0]);
|
||||
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(modules[1], output, SodiumLikeInputs(), true));
|
||||
EXPECT_EQ(output, modules[1]);
|
||||
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(modules[0], output, {}, true));
|
||||
EXPECT_EQ(output, modules[0]);
|
||||
}
|
||||
|
||||
// From SPIR-V 1.4 an entry point lists every global it uses, so the Private copies have to join
|
||||
// the new Inputs in the interface. Checked by re-labelling the module 1.4 and validating it for an
|
||||
// environment that accepts 1.4.
|
||||
TEST(ExtendNarrowIntegerVertexInputs, ListsPrivateCopiesInASpirv14Interface) {
|
||||
MobileGL::Initialize();
|
||||
const auto modules = CompileProgram();
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
Vector<Uint32> input = modules[0];
|
||||
ASSERT_GT(input.size(), 1u);
|
||||
input[1] = 0x00010400u;
|
||||
ASSERT_EQ(ValidationError(input, SPV_ENV_VULKAN_1_2), String{});
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(input, output, SodiumLikeInputs(), false));
|
||||
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 : {"%lightAndData", "%sbyte", "%shortAsInt", "%packed", "%sshort", "%texc"})
|
||||
EXPECT_NE(entryPoint[0].find(name), String::npos) << name << "\n" << entryPoint[0];
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/PositionFixupSitesTest.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
|
||||
//
|
||||
// Magma's clip-space fix-up rewrites gl_Position in front of every vertex the last
|
||||
// pre-rasterization stage produces. A geometry shader produces one at every emit it can execute,
|
||||
// and the front end leaves helper functions in place, so an EmitVertex() inside a helper needs the
|
||||
// fix-up too: without it those vertices reach the rasterizer un-remapped, and a GL depth below
|
||||
// zero is clipped away. A vertex shader's helper return, on the other hand, does not end the
|
||||
// invocation and must stay untouched.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "spirv.hpp"
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
|
||||
namespace {
|
||||
using Bit = MG_Backend::DirectVulkan::ProgramFactory::CompileOptionBit;
|
||||
using CompileOptionFlags = MG_Backend::DirectVulkan::ProgramFactory::CompileOptionFlags;
|
||||
|
||||
// One stage, linked and sanitized the way production hands it to the backend.
|
||||
Vector<Uint32> CompileStage(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
MobileGL::Initialize();
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
Vector<Uint32> module = binaryResult->front();
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
|
||||
return module;
|
||||
}
|
||||
|
||||
Vector<Uint32> FixUp(const Vector<Uint32>& spirv) {
|
||||
Vector<Uint32> out;
|
||||
EXPECT_TRUE(MG_Backend::DirectVulkan::TransformSpirvForVulkanPositionFix(
|
||||
spirv, out, CompileOptionFlags(Bit::PositionYFlip) | Bit::PositionZRemap));
|
||||
return out;
|
||||
}
|
||||
|
||||
String ValidationError(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
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);
|
||||
}
|
||||
|
||||
String Disassemble(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String text;
|
||||
EXPECT_TRUE(tools.Disassemble(spirv, &text));
|
||||
return text;
|
||||
}
|
||||
|
||||
// Whether `pointerId` is an access chain to gl_PerVertex's Position member.
|
||||
Bool IsPositionPointer(IRContext* context, Uint32 pointerId) {
|
||||
auto* defUse = context->get_def_use_mgr();
|
||||
const Instruction* chain = defUse->GetDef(pointerId);
|
||||
if (chain == nullptr || chain->opcode() != spv::Op::OpAccessChain || chain->NumInOperands() != 2) return false;
|
||||
const Instruction* base = defUse->GetDef(chain->GetSingleWordInOperand(0));
|
||||
const Instruction* index = defUse->GetDef(chain->GetSingleWordInOperand(1));
|
||||
if (base == nullptr || base->opcode() != spv::Op::OpVariable || index == nullptr ||
|
||||
index->opcode() != spv::Op::OpConstant)
|
||||
return false;
|
||||
const Uint32 structId = defUse->GetDef(base->type_id())->GetSingleWordInOperand(1);
|
||||
const Uint32 member = index->GetSingleWordInOperand(0);
|
||||
for (const Instruction& annotation : context->annotations()) {
|
||||
if (annotation.opcode() == spv::Op::OpMemberDecorate && annotation.GetSingleWordInOperand(0) == structId &&
|
||||
annotation.GetSingleWordInOperand(1) == member &&
|
||||
annotation.GetSingleWordInOperand(2) == Uint32(spv::Decoration::BuiltIn) &&
|
||||
annotation.GetSingleWordInOperand(3) == Uint32(spv::BuiltIn::Position))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether `inst` is the fix-up's closing store: Position stored back with its y negated, the
|
||||
// y taken from a load of Position itself. The shader's own stores never have that shape.
|
||||
Bool IsFixupStore(IRContext* context, const Instruction& inst) {
|
||||
if (inst.opcode() != spv::Op::OpStore || !IsPositionPointer(context, inst.GetSingleWordInOperand(0)))
|
||||
return false;
|
||||
auto* defUse = context->get_def_use_mgr();
|
||||
const Instruction* value = defUse->GetDef(inst.GetSingleWordInOperand(1));
|
||||
if (value == nullptr || value->opcode() != spv::Op::OpCompositeConstruct || value->NumInOperands() != 4)
|
||||
return false;
|
||||
const Instruction* negatedY = defUse->GetDef(value->GetSingleWordInOperand(1));
|
||||
if (negatedY == nullptr || negatedY->opcode() != spv::Op::OpFNegate) return false;
|
||||
const Instruction* y = defUse->GetDef(negatedY->GetSingleWordInOperand(0));
|
||||
if (y == nullptr || y->opcode() != spv::Op::OpCompositeExtract) return false;
|
||||
const Instruction* loaded = defUse->GetDef(y->GetSingleWordInOperand(0));
|
||||
return loaded != nullptr && loaded->opcode() == spv::Op::OpLoad &&
|
||||
IsPositionPointer(context, loaded->GetSingleWordInOperand(0));
|
||||
}
|
||||
|
||||
struct Sites {
|
||||
SizeT sites = 0;
|
||||
SizeT sitesInHelpers = 0;
|
||||
// Sites whose immediately preceding instruction is the fix-up's store.
|
||||
SizeT fixedSites = 0;
|
||||
SizeT fixupStores = 0;
|
||||
SizeT fixupStoresInHelpers = 0;
|
||||
};
|
||||
|
||||
// Emits for a geometry shader, returns for any other stage.
|
||||
Sites Inspect(const Vector<Uint32>& spirv) {
|
||||
Sites sites;
|
||||
std::unique_ptr<IRContext> context =
|
||||
spvtools::BuildModule(SPV_ENV_VULKAN_1_1, nullptr, spirv.data(), spirv.size());
|
||||
EXPECT_NE(context, nullptr);
|
||||
if (context == nullptr) return sites;
|
||||
const Instruction& entryPoint = *context->module()->entry_points().begin();
|
||||
const Bool geometry =
|
||||
static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) == spv::ExecutionModel::Geometry;
|
||||
const Uint32 entryFunctionId = entryPoint.GetSingleWordInOperand(1);
|
||||
for (auto& function : *context->module()) {
|
||||
const Bool helper = function.result_id() != entryFunctionId;
|
||||
for (auto& block : function) {
|
||||
const Instruction* previous = nullptr;
|
||||
for (const Instruction& inst : block) {
|
||||
const Bool isSite = geometry ? (inst.opcode() == spv::Op::OpEmitVertex ||
|
||||
inst.opcode() == spv::Op::OpEmitStreamVertex)
|
||||
: inst.opcode() == spv::Op::OpReturn;
|
||||
if (isSite) {
|
||||
++sites.sites;
|
||||
if (helper) ++sites.sitesInHelpers;
|
||||
if (previous != nullptr && IsFixupStore(context.get(), *previous)) ++sites.fixedSites;
|
||||
}
|
||||
if (IsFixupStore(context.get(), inst)) {
|
||||
++sites.fixupStores;
|
||||
if (helper) ++sites.fixupStoresInHelpers;
|
||||
}
|
||||
previous = &inst;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// gsprobe's helper case: each corner is written and emitted inside the helper.
|
||||
TEST(PositionFixupSitesTest, AnEmitInsideAHelperGetsTheFixup) {
|
||||
const auto spirv = CompileStage(GL_GEOMETRY_SHADER, R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
void corner(int i) { gl_Position = vec4(gl_in[i].gl_Position.xy, -0.5, 1.0); EmitVertex(); }
|
||||
void main() { corner(0); corner(1); corner(2); EndPrimitive(); }
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Sites before = Inspect(spirv);
|
||||
ASSERT_EQ(before.sites, 1u) << "the helper is expected to survive to the backend";
|
||||
ASSERT_EQ(before.sitesInHelpers, 1u);
|
||||
ASSERT_EQ(before.fixupStores, 0u);
|
||||
|
||||
const auto fixed = FixUp(spirv);
|
||||
EXPECT_EQ(ValidationError(fixed), String{}) << Disassemble(fixed);
|
||||
const Sites after = Inspect(fixed);
|
||||
EXPECT_EQ(after.sites, 1u);
|
||||
EXPECT_EQ(after.fixedSites, 1u) << Disassemble(fixed);
|
||||
EXPECT_EQ(after.fixupStores, 1u);
|
||||
}
|
||||
|
||||
// The value is written in main and only the emit sits in a helper.
|
||||
TEST(PositionFixupSitesTest, AHelperThatOnlyEmitsGetsTheFixup) {
|
||||
const auto spirv = CompileStage(GL_GEOMETRY_SHADER, R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
void emitIt() { EmitVertex(); }
|
||||
void main() {
|
||||
for (int i = 0; i < 3; i++) { gl_Position = vec4(gl_in[i].gl_Position.xy, -0.5, 1.0); emitIt(); }
|
||||
EndPrimitive();
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_EQ(Inspect(spirv).sitesInHelpers, 1u);
|
||||
|
||||
const auto fixed = FixUp(spirv);
|
||||
EXPECT_EQ(ValidationError(fixed), String{}) << Disassemble(fixed);
|
||||
const Sites after = Inspect(fixed);
|
||||
EXPECT_EQ(after.sites, 1u);
|
||||
EXPECT_EQ(after.fixedSites, 1u) << Disassemble(fixed);
|
||||
EXPECT_EQ(after.fixupStores, 1u);
|
||||
}
|
||||
|
||||
// Emits in main and in a helper side by side: each one is fixed exactly once.
|
||||
TEST(PositionFixupSitesTest, EmitsInMainAndInAHelperAreEachFixedOnce) {
|
||||
const auto spirv = CompileStage(GL_GEOMETRY_SHADER, R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
void corner(int i) { gl_Position = gl_in[i].gl_Position; EmitVertex(); }
|
||||
void main() {
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
corner(1);
|
||||
corner(2);
|
||||
EndPrimitive();
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Sites before = Inspect(spirv);
|
||||
ASSERT_EQ(before.sites, 2u);
|
||||
ASSERT_EQ(before.sitesInHelpers, 1u);
|
||||
|
||||
const auto fixed = FixUp(spirv);
|
||||
EXPECT_EQ(ValidationError(fixed), String{}) << Disassemble(fixed);
|
||||
const Sites after = Inspect(fixed);
|
||||
EXPECT_EQ(after.fixedSites, 2u) << Disassemble(fixed);
|
||||
EXPECT_EQ(after.fixupStores, 2u);
|
||||
EXPECT_EQ(after.fixupStoresInHelpers, 1u);
|
||||
}
|
||||
|
||||
// The shape that always worked: every emit in main, each one still fixed.
|
||||
TEST(PositionFixupSitesTest, EmitsInMainKeepTheirFixup) {
|
||||
const auto spirv = CompileStage(GL_GEOMETRY_SHADER, R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; EmitVertex(); }
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Sites before = Inspect(spirv);
|
||||
ASSERT_EQ(before.sites, 2u);
|
||||
ASSERT_EQ(before.sitesInHelpers, 0u);
|
||||
|
||||
const auto fixed = FixUp(spirv);
|
||||
EXPECT_EQ(ValidationError(fixed), String{}) << Disassemble(fixed);
|
||||
const Sites after = Inspect(fixed);
|
||||
EXPECT_EQ(after.fixedSites, 2u) << Disassemble(fixed);
|
||||
EXPECT_EQ(after.fixupStores, 2u);
|
||||
}
|
||||
|
||||
// A vertex shader that writes gl_Position in a helper: only main's return ends the invocation,
|
||||
// so that is the only place the fix-up goes.
|
||||
TEST(PositionFixupSitesTest, AVertexShaderHelperReturnIsNotAFixupSite) {
|
||||
const auto spirv = CompileStage(GL_VERTEX_SHADER, R"(#version 410 core
|
||||
layout(location = 0) in vec2 pos;
|
||||
void place(vec2 p) { gl_Position = vec4(p, -0.5, 1.0); }
|
||||
void main() { place(pos); }
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Sites before = Inspect(spirv);
|
||||
ASSERT_EQ(before.sitesInHelpers, 1u) << "the helper is expected to survive to the backend";
|
||||
|
||||
const auto fixed = FixUp(spirv);
|
||||
EXPECT_EQ(ValidationError(fixed), String{}) << Disassemble(fixed);
|
||||
const Sites after = Inspect(fixed);
|
||||
EXPECT_EQ(after.fixupStores, 1u) << Disassemble(fixed);
|
||||
EXPECT_EQ(after.fixupStoresInHelpers, 0u);
|
||||
EXPECT_EQ(after.fixedSites, 1u);
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/RetainGeometryOutputsTest.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
|
||||
//
|
||||
// RetainGeometryOutputsPass makes a geometry output keep its last written value across
|
||||
// EmitVertex(), as desktop drivers do. What has to hold: an output written before an earlier emit
|
||||
// is copied from its Private shadow into the real output right before every emit and is otherwise
|
||||
// never touched directly; outputs written before every emit, and stages that write everything
|
||||
// before every emit, stay as they were; unwritten builtin members stay inactive; the module stays
|
||||
// valid Vulkan SPIR-V whose ESSL translation compiles; and the sanitize chain both backends consume
|
||||
// runs it.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/RetainGeometryOutputsPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <glslang/Public/ResourceLimits.h>
|
||||
#include <glslang/Public/ShaderLang.h>
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
#include <spirv-tools/optimizer.hpp>
|
||||
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "spirv.hpp"
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::RetainGeometryOutputsPass;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
|
||||
namespace {
|
||||
Vector<Uint32> CompileStage(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileGeometry(const String& source) {
|
||||
return CompileStage(GL_GEOMETRY_SHADER, source);
|
||||
}
|
||||
|
||||
Vector<Uint32> Retain(const Vector<Uint32>& spirv) {
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(RetainGeometryOutputsPass::CreateRetainGeometryOutputsPass());
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
Vector<Uint32> out;
|
||||
EXPECT_TRUE(optimizer.Run(spirv.data(), spirv.size(), &out, options));
|
||||
return out;
|
||||
}
|
||||
|
||||
Bool Validates(const Vector<Uint32>& spirv, String& messages) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer([&messages](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) { messages += String(message) + "\n"; });
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
String Transpile(const Vector<Uint32>& spirv) {
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
||||
return essl ? essl.value() : String{};
|
||||
}
|
||||
|
||||
// Whether the ESSL SPIRV-Cross printed is accepted by an ES front end.
|
||||
Bool EsslCompiles(const String& essl, String& log) {
|
||||
glslang::TShader shader(EShLangGeometry);
|
||||
const char* strings[] = {essl.c_str()};
|
||||
shader.setStrings(strings, 1);
|
||||
const Bool ok = shader.parse(GetDefaultResources(), 320, EEsProfile, false, false, EShMsgDefault);
|
||||
log = shader.getInfoLog();
|
||||
return ok;
|
||||
}
|
||||
|
||||
std::unique_ptr<IRContext> Parse(const Vector<Uint32>& spirv) {
|
||||
return spvtools::BuildModule(SPV_ENV_VULKAN_1_1, nullptr, spirv.data(), spirv.size());
|
||||
}
|
||||
|
||||
// The OpVariable whose OpName is `name`, or 0.
|
||||
Uint32 VariableNamed(IRContext* context, const String& name) {
|
||||
for (const Instruction& debugInst : context->debugs2()) {
|
||||
if (debugInst.opcode() != spv::Op::OpName || debugInst.GetInOperand(1).AsString() != name) continue;
|
||||
const Instruction* def = context->get_def_use_mgr()->GetDef(debugInst.GetSingleWordInOperand(0));
|
||||
if (def != nullptr && def->opcode() == spv::Op::OpVariable) return def->result_id();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
spv::StorageClass StorageClassOf(IRContext* context, Uint32 variableId) {
|
||||
return static_cast<spv::StorageClass>(
|
||||
context->get_def_use_mgr()->GetDef(variableId)->GetSingleWordInOperand(0));
|
||||
}
|
||||
|
||||
// Whether `pointerId` addresses the whole of `variableId` (member < 0) or exactly its member.
|
||||
Bool Addresses(IRContext* context, Uint32 pointerId, Uint32 variableId, Int member) {
|
||||
if (member < 0) return pointerId == variableId;
|
||||
const Instruction* chain = context->get_def_use_mgr()->GetDef(pointerId);
|
||||
if (chain == nullptr || chain->opcode() != spv::Op::OpAccessChain || chain->NumInOperands() != 2 ||
|
||||
chain->GetSingleWordInOperand(0) != variableId)
|
||||
return false;
|
||||
const Instruction* index = context->get_def_use_mgr()->GetDef(chain->GetSingleWordInOperand(1));
|
||||
return index != nullptr && index->opcode() == spv::Op::OpConstant &&
|
||||
index->GetSingleWordInOperand(0) == Uint32(member);
|
||||
}
|
||||
|
||||
struct EmitSites {
|
||||
SizeT emits = 0;
|
||||
// Emits whose own straight-line run (since the previous emit or the block start) stores the output.
|
||||
SizeT emitsPrecededByStore = 0;
|
||||
SizeT storesToOutput = 0;
|
||||
SizeT loadsFromOutput = 0;
|
||||
};
|
||||
|
||||
EmitSites Inspect(IRContext* context, Uint32 variableId, Int member = -1) {
|
||||
EmitSites sites;
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
Bool storedSinceEmit = false;
|
||||
for (Instruction& inst : block) {
|
||||
if (inst.opcode() == spv::Op::OpEmitVertex || inst.opcode() == spv::Op::OpEmitStreamVertex) {
|
||||
++sites.emits;
|
||||
if (storedSinceEmit) ++sites.emitsPrecededByStore;
|
||||
storedSinceEmit = false;
|
||||
} else if (inst.opcode() == spv::Op::OpStore &&
|
||||
Addresses(context, inst.GetSingleWordInOperand(0), variableId, member)) {
|
||||
++sites.storesToOutput;
|
||||
storedSinceEmit = true;
|
||||
} else if (inst.opcode() == spv::Op::OpLoad &&
|
||||
Addresses(context, inst.GetSingleWordInOperand(0), variableId, member)) {
|
||||
++sites.loadsFromOutput;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
|
||||
Uint32 PerVertexBlock(IRContext* context) {
|
||||
for (const Instruction& global : context->types_values()) {
|
||||
if (global.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) != spv::StorageClass::Output)
|
||||
continue;
|
||||
const Instruction* pointer = context->get_def_use_mgr()->GetDef(global.type_id());
|
||||
const Instruction* pointee = context->get_def_use_mgr()->GetDef(pointer->GetSingleWordInOperand(1));
|
||||
if (pointee->opcode() == spv::Op::OpTypeStruct) return global.result_id();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ExpectValidAndCompilable(const Vector<Uint32>& spirv) {
|
||||
String messages;
|
||||
EXPECT_TRUE(Validates(spirv, messages)) << messages;
|
||||
const String essl = Transpile(spirv);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
String log;
|
||||
EXPECT_TRUE(EsslCompiles(essl, log)) << log << "\n" << essl;
|
||||
}
|
||||
|
||||
// gsprobe's case 3 and iterationT's gbuffers_terrain.gsh: a flat output written (and read
|
||||
// back) once, before the loop that emits; the provoking vertex is the third.
|
||||
const char* const kFlatBeforeLoop = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
in vec2 v_pos[];
|
||||
flat out float res;
|
||||
out vec2 p;
|
||||
void main() {
|
||||
res = abs(v_pos[0].x) + 1.0;
|
||||
res = exp2(round(log2(res)));
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; p = v_pos[i]; EmitVertex(); }
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// Everything written before every emit, one output through both arms of a branch.
|
||||
const char* const kWritesEverythingPerEmit = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
in vec2 v_pos[];
|
||||
flat out float res;
|
||||
out vec2 p;
|
||||
void main() {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (v_pos[i].x > 0.0) res = 1.0; else res = 2.0;
|
||||
gl_Position = gl_in[i].gl_Position;
|
||||
p = v_pos[i];
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// gl_PointSize written once in a gl_PerVertex that also declares gl_ClipDistance, plus the
|
||||
// layered-rendering idiom of a gl_Layer set before the emit loop.
|
||||
const char* const kBuiltinsBeforeLoop = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
void main() {
|
||||
gl_PointSize = 4.0;
|
||||
gl_Layer = 2;
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; EmitVertex(); }
|
||||
}
|
||||
)";
|
||||
|
||||
// The emit lives in a helper; main writes the flat output once and calls it per vertex.
|
||||
const char* const kEmitInHelper = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
flat out float res;
|
||||
void emitCorner(int i) { gl_Position = gl_in[i].gl_Position; EmitVertex(); }
|
||||
void main() {
|
||||
res = 42.0;
|
||||
emitCorner(0);
|
||||
emitCorner(1);
|
||||
emitCorner(2);
|
||||
}
|
||||
)";
|
||||
|
||||
// Fully written before every emit, but from its own value before the previous emit.
|
||||
const char* const kReadBackAfterEmit = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
flat out float res;
|
||||
void main() {
|
||||
res = 1.0;
|
||||
for (int i = 0; i < 3; i++) { res = res * 2.0; gl_Position = gl_in[i].gl_Position; EmitVertex(); }
|
||||
}
|
||||
)";
|
||||
|
||||
// A partially written output: .w once, .xyz per vertex.
|
||||
const char* const kPartialWrites = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
out vec4 c;
|
||||
void main() {
|
||||
c.w = 0.25;
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; c.xyz = vec3(float(i)); EmitVertex(); }
|
||||
}
|
||||
)";
|
||||
|
||||
// A user output block: only the member written before the loop is carried.
|
||||
const char* const kUserBlock = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
out Data { flat float res; vec2 p; } vOut;
|
||||
void main() {
|
||||
vOut.res = 42.0;
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; vOut.p = vec2(float(i)); EmitVertex(); }
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kStreams = R"(#version 410 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 4) out;
|
||||
layout(stream = 0) out float a;
|
||||
layout(stream = 1) out float b;
|
||||
void main() {
|
||||
a = 1.0;
|
||||
b = 2.0;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitStreamVertex(0);
|
||||
EmitStreamVertex(1);
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// The flat-before-loop shape plus a storage write after the emits, so DeferGeometryEmitsPass
|
||||
// has something to defer.
|
||||
const char* const kFlatBeforeLoopWithMemoryWrite = R"(#version 430 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
layout(std430, binding = 0) buffer Counter { uint n; };
|
||||
flat out float res;
|
||||
void main() {
|
||||
res = 42.0;
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; EmitVertex(); }
|
||||
atomicAdd(n, 1u);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kVertex = R"(#version 410 core
|
||||
out float res;
|
||||
void main() { res = 1.0; gl_Position = vec4(0.0); }
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(RetainGeometryOutputs, AFlatOutputWrittenBeforeTheLoopIsCopiedBeforeEveryEmit) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kFlatBeforeLoop);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
ExpectValidAndCompilable(out);
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
const Uint32 res = VariableNamed(context.get(), "res");
|
||||
const Uint32 shadow = VariableNamed(context.get(), "mg_retained_res");
|
||||
ASSERT_NE(res, 0u);
|
||||
ASSERT_NE(shadow, 0u);
|
||||
EXPECT_EQ(StorageClassOf(context.get(), res), spv::StorageClass::Output);
|
||||
EXPECT_EQ(StorageClassOf(context.get(), shadow), spv::StorageClass::Private);
|
||||
|
||||
// The real output is written only by the copy in front of the one emit, and never read.
|
||||
const EmitSites atOutput = Inspect(context.get(), res);
|
||||
EXPECT_EQ(atOutput.emits, 1u);
|
||||
EXPECT_EQ(atOutput.emitsPrecededByStore, 1u);
|
||||
EXPECT_EQ(atOutput.storesToOutput, 1u);
|
||||
EXPECT_EQ(atOutput.loadsFromOutput, 0u);
|
||||
// The shader's own two writes and its read-back moved to the shadow.
|
||||
const EmitSites atShadow = Inspect(context.get(), shadow);
|
||||
EXPECT_EQ(atShadow.storesToOutput, 2u);
|
||||
EXPECT_GE(atShadow.loadsFromOutput, 2u);
|
||||
|
||||
// `p` is written before every emit, so it is not carried.
|
||||
EXPECT_EQ(VariableNamed(context.get(), "mg_retained_p"), 0u);
|
||||
EXPECT_EQ(VariableNamed(context.get(), "mg_retained_Position"), 0u);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, AStageThatWritesEveryOutputBeforeEveryEmitIsLeftAlone) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kWritesEverythingPerEmit);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
EXPECT_EQ(Retain(spirv), spirv);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, ANonGeometryStageIsLeftAlone) {
|
||||
const Vector<Uint32> spirv = CompileStage(GL_VERTEX_SHADER, kVertex);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
EXPECT_EQ(Retain(spirv), spirv);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, OnlyTheWrittenBuiltinMembersAreCarried) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kBuiltinsBeforeLoop);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
String messages;
|
||||
EXPECT_TRUE(Validates(out, messages)) << messages;
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
EXPECT_NE(VariableNamed(context.get(), "mg_retained_PointSize"), 0u);
|
||||
EXPECT_NE(VariableNamed(context.get(), "mg_retained_Layer"), 0u);
|
||||
EXPECT_EQ(VariableNamed(context.get(), "mg_retained_Position"), 0u);
|
||||
EXPECT_EQ(VariableNamed(context.get(), "mg_retained_ClipDistance"), 0u);
|
||||
|
||||
const Uint32 perVertex = PerVertexBlock(context.get());
|
||||
ASSERT_NE(perVertex, 0u);
|
||||
const EmitSites pointSize = Inspect(context.get(), perVertex, 1);
|
||||
EXPECT_EQ(pointSize.emitsPrecededByStore, pointSize.emits);
|
||||
EXPECT_EQ(pointSize.storesToOutput, pointSize.emits);
|
||||
const EmitSites layer = Inspect(context.get(), VariableNamed(context.get(), "gl_Layer"));
|
||||
EXPECT_EQ(layer.emitsPrecededByStore, layer.emits);
|
||||
EXPECT_EQ(layer.storesToOutput, layer.emits);
|
||||
|
||||
const String essl = Transpile(out);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_EQ(essl.find("gl_ClipDistance"), String::npos) << "an unwritten builtin member became active\n" << essl;
|
||||
EXPECT_NE(essl.find("gl_Layer = "), String::npos) << essl;
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, OnlyTheWrittenUserBlockMemberIsCarried) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kUserBlock);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
ExpectValidAndCompilable(out);
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
EXPECT_NE(VariableNamed(context.get(), "mg_retained_res"), 0u);
|
||||
EXPECT_EQ(VariableNamed(context.get(), "mg_retained_p"), 0u);
|
||||
const Uint32 block = VariableNamed(context.get(), "vOut");
|
||||
ASSERT_NE(block, 0u);
|
||||
const EmitSites res = Inspect(context.get(), block, 0);
|
||||
EXPECT_EQ(res.emitsPrecededByStore, res.emits);
|
||||
EXPECT_EQ(res.storesToOutput, res.emits);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, AnEmitInsideAHelperGetsTheCopyToo) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kEmitInHelper);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
ExpectValidAndCompilable(out);
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), "res"));
|
||||
EXPECT_EQ(sites.emits, 1u);
|
||||
EXPECT_EQ(sites.emitsPrecededByStore, 1u);
|
||||
EXPECT_EQ(sites.storesToOutput, 1u);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, AnOutputReadBackAfterAnEmitIsCarried) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kReadBackAfterEmit);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
ExpectValidAndCompilable(out);
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), "res"));
|
||||
EXPECT_EQ(sites.storesToOutput, sites.emits);
|
||||
EXPECT_EQ(sites.loadsFromOutput, 0u) << "the read-back must see the shadow, not the emitted output";
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, APartiallyWrittenOutputIsCarriedWhole) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kPartialWrites);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
ExpectValidAndCompilable(out);
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), "c"));
|
||||
EXPECT_EQ(sites.emitsPrecededByStore, sites.emits);
|
||||
EXPECT_EQ(sites.storesToOutput, sites.emits);
|
||||
EXPECT_NE(VariableNamed(context.get(), "mg_retained_c"), 0u);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, StreamEmitsGetTheCopyToo) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kStreams);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
String messages;
|
||||
EXPECT_TRUE(Validates(out, messages)) << messages;
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
for (const char* name : {"a", "b"}) {
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), name));
|
||||
EXPECT_EQ(sites.emits, 2u) << name;
|
||||
EXPECT_EQ(sites.emitsPrecededByStore, 2u) << name;
|
||||
EXPECT_EQ(sites.storesToOutput, 2u) << name;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, TheDeferredEmitReplayCarriesTheRetainedValue) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kFlatBeforeLoopWithMemoryWrite);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> retained = Retain(spirv);
|
||||
ASSERT_NE(retained, spirv);
|
||||
Vector<uint32_t> deferredSpirv;
|
||||
bool deferred = false;
|
||||
ASSERT_TRUE(ShaderCompiler::DeferGeometryEmitsForEssl(retained, deferredSpirv, deferred, true));
|
||||
ASSERT_TRUE(deferred);
|
||||
ExpectValidAndCompilable(deferredSpirv);
|
||||
|
||||
// The snapshot taken at each recorded emit reads `res` after the copy, so the replay restores it.
|
||||
auto context = Parse(deferredSpirv);
|
||||
ASSERT_NE(context, nullptr);
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), "res"));
|
||||
EXPECT_EQ(sites.emits, 1u);
|
||||
EXPECT_EQ(sites.emitsPrecededByStore, 1u);
|
||||
EXPECT_GE(sites.loadsFromOutput, 1u);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, TheSanitizeChainBothBackendsConsumeRunsIt) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kFlatBeforeLoop);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
Vector<uint32_t> sanitized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(spirv, sanitized, true, true));
|
||||
ExpectValidAndCompilable(sanitized);
|
||||
|
||||
auto context = Parse(sanitized);
|
||||
ASSERT_NE(context, nullptr);
|
||||
ASSERT_NE(VariableNamed(context.get(), "mg_retained_res"), 0u);
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), "res"));
|
||||
EXPECT_EQ(sites.emitsPrecededByStore, sites.emits);
|
||||
EXPECT_EQ(sites.storesToOutput, sites.emits);
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/StripCompatPerVertexMembersTest.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
|
||||
|
||||
// A compatibility-profile stage that only uses core features must reach the backends as the
|
||||
// same Vulkan-valid SPIR-V as its core twin. glslang declares the legacy gl_PerVertex members
|
||||
// (gl_FrontColor, gl_TexCoord, ...) without a BuiltIn, which leaves a module that writes only
|
||||
// gl_Position invalid for Vulkan until the sanitize chain removes them.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/StripCompatPerVertexMembersPass.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
#include <spirv-tools/optimizer.hpp>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::StripCompatPerVertexMembersPass;
|
||||
|
||||
namespace {
|
||||
ShaderStage StageOf(GLenum type) {
|
||||
switch (type) {
|
||||
case GL_VERTEX_SHADER:
|
||||
return ShaderStage::Vertex;
|
||||
case GL_TESS_CONTROL_SHADER:
|
||||
return ShaderStage::TessControl;
|
||||
case GL_TESS_EVALUATION_SHADER:
|
||||
return ShaderStage::TessEval;
|
||||
case GL_GEOMETRY_SHADER:
|
||||
return ShaderStage::Geometry;
|
||||
default:
|
||||
return ShaderStage::Fragment;
|
||||
}
|
||||
}
|
||||
|
||||
// Preprocesses, compiles and links a whole program the way the link task does, and returns
|
||||
// one module per stage: straight out of GlslangToSpv, or after the shared sanitize chain.
|
||||
Vector<Vector<Uint32>> CompileProgram(const Vector<Pair<GLenum, String>>& stages, Bool sanitize = true) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const CompileEnv env{};
|
||||
Vector<String> sources;
|
||||
sources.reserve(stages.size());
|
||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||
Vector<GLenum> types;
|
||||
for (const auto& [type, text] : stages) {
|
||||
String& source = sources.emplace_back(text);
|
||||
PreprocessShaderSource(StageOf(type), source, env);
|
||||
ShaderAttrib attrib{.shaderType = type, .sourceStr = source};
|
||||
auto shader = ShaderCompiler::CompileShader(attrib);
|
||||
EXPECT_TRUE(shader) << (shader ? String{} : shader.error().log);
|
||||
if (!shader) return {};
|
||||
shaders.push_back(shader.value());
|
||||
types.push_back(type);
|
||||
}
|
||||
ProgramAttrib programAttrib{.shaders = shaders};
|
||||
auto program = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(program) << (program ? String{} : program.error().log);
|
||||
if (!program) return {};
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *program.value()};
|
||||
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binary) << (binary ? String{} : binary.error().log);
|
||||
if (!binary) return {};
|
||||
Vector<Vector<Uint32>> modules = Move(binary.value());
|
||||
if (sanitize) {
|
||||
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) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
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);
|
||||
}
|
||||
|
||||
String Transpile(const Vector<Uint32>& spirv) {
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
||||
return essl ? essl.value() : String{};
|
||||
}
|
||||
|
||||
// Runs the pass alone, as the sanitize chain would, on a hand-assembled module.
|
||||
Vector<Uint32> RunPass(const Vector<Uint32>& input) {
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(StripCompatPerVertexMembersPass::CreateStripCompatPerVertexMembersPass());
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
Vector<Uint32> output;
|
||||
EXPECT_TRUE(optimizer.Run(input.data(), input.size(), &output, options));
|
||||
return output;
|
||||
}
|
||||
|
||||
// Every compatibility source is compiled as `#version 460 compatibility`
|
||||
// (GetNormalizedVersionDirective), so its core twin is the 460 core source.
|
||||
String Compat(const char* body, const char* version = "410") {
|
||||
return String("#version ") + version + " compatibility\n" + body;
|
||||
}
|
||||
|
||||
String Core(const char* body) {
|
||||
return String("#version 460 core\n") + body;
|
||||
}
|
||||
|
||||
// Disassembly with ids renumbered in order of appearance, so two modules that differ only in
|
||||
// the ids glslang happened to allocate compare equal.
|
||||
String Canonical(const Vector<Uint32>& spirv) {
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(spvtools::CreateCompactIdsPass());
|
||||
Vector<Uint32> compacted;
|
||||
EXPECT_TRUE(optimizer.Run(spirv.data(), spirv.size(), &compacted));
|
||||
return Disassemble(compacted);
|
||||
}
|
||||
|
||||
// Compiles both programs and expects every compatibility module to validate and to equal the
|
||||
// core twin's.
|
||||
void ExpectSameAsCoreTwin(const Vector<Pair<GLenum, String>>& compatStages,
|
||||
const Vector<Pair<GLenum, String>>& coreStages) {
|
||||
const auto compat = CompileProgram(compatStages);
|
||||
const auto core = CompileProgram(coreStages);
|
||||
ASSERT_EQ(compat.size(), compatStages.size());
|
||||
ASSERT_EQ(core.size(), coreStages.size());
|
||||
for (SizeT i = 0; i < compat.size(); ++i) {
|
||||
EXPECT_EQ(ValidationError(compat[i]), "") << Disassemble(compat[i]);
|
||||
EXPECT_EQ(Canonical(compat[i]), Canonical(core[i])) << "stage " << i;
|
||||
}
|
||||
}
|
||||
|
||||
const char* kVertexBody = R"(in vec2 pos;
|
||||
out vec4 c;
|
||||
void main() {
|
||||
c = vec4(0.25, 0.5, 0.75, 1.0);
|
||||
gl_Position = vec4(pos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kFragmentBody = R"(in vec4 c;
|
||||
out vec4 o;
|
||||
void main() { o = c; }
|
||||
)";
|
||||
|
||||
// Writes the core members after the legacy ones, so their indices have to move.
|
||||
const char* kClipCullVertexBody = R"(in vec2 pos;
|
||||
void main() {
|
||||
gl_Position = vec4(pos, 0.0, 1.0);
|
||||
gl_PointSize = 2.0;
|
||||
gl_ClipDistance[0] = pos.x;
|
||||
gl_CullDistance[0] = pos.y;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kGeometryBody = R"(layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
in vec4 c[];
|
||||
out vec4 gc;
|
||||
void main() {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
gc = c[i];
|
||||
gl_Position = gl_in[i].gl_Position;
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kGeometryFragmentBody = R"(in vec4 gc;
|
||||
out vec4 o;
|
||||
void main() { o = gc; }
|
||||
)";
|
||||
|
||||
const char* kTessControlBody = R"(layout(vertices = 3) out;
|
||||
void main() {
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kTessEvalBody = R"(layout(triangles) in;
|
||||
void main() {
|
||||
gl_Position = gl_TessCoord.x * gl_in[0].gl_Position + gl_TessCoord.y * gl_in[1].gl_Position +
|
||||
gl_TessCoord.z * gl_in[2].gl_Position;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kPositionOnlyVertexBody = R"(in vec2 pos;
|
||||
void main() { gl_Position = vec4(pos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
const char* kWhiteFragmentBody = R"(out vec4 o;
|
||||
void main() { o = vec4(1.0); }
|
||||
)";
|
||||
|
||||
// A compatibility-only built-in in use: there is no core shape to give this stage.
|
||||
const char* kFrontColorVertexBody = R"(in vec2 pos;
|
||||
void main() {
|
||||
gl_FrontColor = vec4(1.0);
|
||||
gl_Position = vec4(pos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Reads a legacy member only into dead code, which the sanitize chain removes first.
|
||||
const char* kDeadLegacyReadGeometryBody = R"(layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main() {
|
||||
vec4 unused = gl_in[0].gl_FrontColor;
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
// A mixed built-in block reached through a whole-block load, which the pass cannot follow.
|
||||
const char* kWholeBlockLoadAsm = R"(
|
||||
OpCapability Geometry
|
||||
OpMemoryModel Logical GLSL450
|
||||
OpEntryPoint Geometry %main "main" %gl_in %out_block
|
||||
OpExecutionMode %main InputPoints
|
||||
OpExecutionMode %main Invocations 1
|
||||
OpExecutionMode %main OutputPoints
|
||||
OpExecutionMode %main OutputVertices 1
|
||||
OpMemberDecorate %gl_PerVertex 0 BuiltIn Position
|
||||
OpDecorate %gl_PerVertex Block
|
||||
%void = OpTypeVoid
|
||||
%fn_ty = OpTypeFunction %void
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%gl_PerVertex = OpTypeStruct %v4float %v4float
|
||||
%uint = OpTypeInt 32 0
|
||||
%uint_1 = OpConstant %uint 1
|
||||
%arr = OpTypeArray %gl_PerVertex %uint_1
|
||||
%ptr_in_arr = OpTypePointer Input %arr
|
||||
%gl_in = OpVariable %ptr_in_arr Input
|
||||
%ptr_out_s = OpTypePointer Output %gl_PerVertex
|
||||
%out_block = OpVariable %ptr_out_s Output
|
||||
%ptr_in_s = OpTypePointer Input %gl_PerVertex
|
||||
%int = OpTypeInt 32 1
|
||||
%int_0 = OpConstant %int 0
|
||||
%main = OpFunction %void None %fn_ty
|
||||
%entry = OpLabel
|
||||
%p = OpAccessChain %ptr_in_s %gl_in %int_0
|
||||
%v = OpLoad %gl_PerVertex %p
|
||||
OpStore %out_block %v
|
||||
OpEmitVertex
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
)";
|
||||
|
||||
// The OpTypeStruct line and the member decorations of every per-vertex block, in order.
|
||||
String PerVertexShape(const String& disassembly) {
|
||||
String shape;
|
||||
SizeT lineStart = 0;
|
||||
while (lineStart < disassembly.size()) {
|
||||
SizeT lineEnd = disassembly.find('\n', lineStart);
|
||||
if (lineEnd == String::npos) lineEnd = disassembly.size();
|
||||
const String line = disassembly.substr(lineStart, lineEnd - lineStart);
|
||||
if (line.find("%gl_PerVertex") != String::npos &&
|
||||
(line.find("OpTypeStruct") != String::npos || line.find("OpMemberDecorate") != String::npos)) {
|
||||
shape += line + "\n";
|
||||
}
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class StripCompatPerVertexMembersTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
};
|
||||
|
||||
TEST_F(StripCompatPerVertexMembersTest, VertexStageMatchesItsCoreTwin) {
|
||||
const auto raw =
|
||||
CompileProgram({{GL_VERTEX_SHADER, Compat(kVertexBody)}, {GL_FRAGMENT_SHADER, Compat(kFragmentBody)}}, false);
|
||||
ASSERT_EQ(raw.size(), 2u);
|
||||
// The defect, pinned: glslang's own module mixes legacy and built-in members.
|
||||
EXPECT_NE(ValidationError(raw[0]).find("BuiltIn"), String::npos) << Disassemble(raw[0]);
|
||||
|
||||
for (const char* version : {"410", "150"}) {
|
||||
SCOPED_TRACE(version);
|
||||
ExpectSameAsCoreTwin(
|
||||
{{GL_VERTEX_SHADER, Compat(kVertexBody, version)}, {GL_FRAGMENT_SHADER, Compat(kFragmentBody, version)}},
|
||||
{{GL_VERTEX_SHADER, Core(kVertexBody)}, {GL_FRAGMENT_SHADER, Core(kFragmentBody)}});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(StripCompatPerVertexMembersTest, CoreMembersAfterTheLegacyOnesAreRenumbered) {
|
||||
ExpectSameAsCoreTwin(
|
||||
{{GL_VERTEX_SHADER, Compat(kClipCullVertexBody)}, {GL_FRAGMENT_SHADER, Compat(kWhiteFragmentBody)}},
|
||||
{{GL_VERTEX_SHADER, Core(kClipCullVertexBody)}, {GL_FRAGMENT_SHADER, Core(kWhiteFragmentBody)}});
|
||||
}
|
||||
|
||||
TEST_F(StripCompatPerVertexMembersTest, GeometryStageInputAndOutputBlocks) {
|
||||
ExpectSameAsCoreTwin({{GL_VERTEX_SHADER, Compat(kVertexBody)},
|
||||
{GL_GEOMETRY_SHADER, Compat(kGeometryBody)},
|
||||
{GL_FRAGMENT_SHADER, Compat(kGeometryFragmentBody)}},
|
||||
{{GL_VERTEX_SHADER, Core(kVertexBody)},
|
||||
{GL_GEOMETRY_SHADER, Core(kGeometryBody)},
|
||||
{GL_FRAGMENT_SHADER, Core(kGeometryFragmentBody)}});
|
||||
}
|
||||
|
||||
TEST_F(StripCompatPerVertexMembersTest, TessellationStagesInputAndOutputBlocks) {
|
||||
ExpectSameAsCoreTwin({{GL_VERTEX_SHADER, Compat(kPositionOnlyVertexBody)},
|
||||
{GL_TESS_CONTROL_SHADER, Compat(kTessControlBody)},
|
||||
{GL_TESS_EVALUATION_SHADER, Compat(kTessEvalBody)},
|
||||
{GL_FRAGMENT_SHADER, Compat(kWhiteFragmentBody)}},
|
||||
{{GL_VERTEX_SHADER, Core(kPositionOnlyVertexBody)},
|
||||
{GL_TESS_CONTROL_SHADER, Core(kTessControlBody)},
|
||||
{GL_TESS_EVALUATION_SHADER, Core(kTessEvalBody)},
|
||||
{GL_FRAGMENT_SHADER, Core(kWhiteFragmentBody)}});
|
||||
}
|
||||
|
||||
TEST_F(StripCompatPerVertexMembersTest, ALegacyReadThatDiesInTheChainDoesNotPinTheBlock) {
|
||||
const auto compat = CompileProgram({{GL_VERTEX_SHADER, Compat(kPositionOnlyVertexBody)},
|
||||
{GL_GEOMETRY_SHADER, Compat(kDeadLegacyReadGeometryBody)},
|
||||
{GL_FRAGMENT_SHADER, Compat(kWhiteFragmentBody)}});
|
||||
ASSERT_EQ(compat.size(), 3u);
|
||||
EXPECT_EQ(ValidationError(compat[1]), "") << Disassemble(compat[1]);
|
||||
}
|
||||
|
||||
TEST_F(StripCompatPerVertexMembersTest, AnAccessedLegacyMemberKeepsTheBlock) {
|
||||
const auto raw = CompileProgram(
|
||||
{{GL_VERTEX_SHADER, Compat(kFrontColorVertexBody)}, {GL_FRAGMENT_SHADER, Compat(kWhiteFragmentBody)}}, false);
|
||||
const auto compat = CompileProgram(
|
||||
{{GL_VERTEX_SHADER, Compat(kFrontColorVertexBody)}, {GL_FRAGMENT_SHADER, Compat(kWhiteFragmentBody)}});
|
||||
ASSERT_EQ(raw.size(), 2u);
|
||||
ASSERT_EQ(compat.size(), 2u);
|
||||
const String shape = PerVertexShape(Disassemble(compat[0]));
|
||||
EXPECT_EQ(shape, PerVertexShape(Disassemble(raw[0])));
|
||||
EXPECT_NE(shape.find("OpTypeStruct %v4float %float %_arr_float_uint_1 %v4float %v4float"), String::npos) << shape;
|
||||
// The ES hop still takes the module as it did before.
|
||||
EXPECT_NE(Transpile(compat[0]).find("gl_Position"), String::npos);
|
||||
}
|
||||
|
||||
TEST_F(StripCompatPerVertexMembersTest, AWholeBlockLoadLeavesTheModuleUntouched) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
Vector<Uint32> module;
|
||||
ASSERT_TRUE(tools.Assemble(kWholeBlockLoadAsm, &module));
|
||||
EXPECT_EQ(RunPass(module), module);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
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. 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
|
||||
// low 8/16 bits sign-extended.
|
||||
// Plain data so the shader pass, the program cache key and the draw path share it.
|
||||
struct NarrowIntegerVertexInputs {
|
||||
Uint32 zeroExtend8 = 0;
|
||||
Uint32 zeroExtend16 = 0;
|
||||
Uint32 signExtend8 = 0;
|
||||
Uint32 signExtend16 = 0;
|
||||
|
||||
constexpr Bool Any() const { return (zeroExtend8 | zeroExtend16 | signExtend8 | signExtend16) != 0; }
|
||||
constexpr Bool operator==(const NarrowIntegerVertexInputs&) const = default;
|
||||
};
|
||||
// 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
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
|
||||
#include "SpirvPasses/FlattenInterfaceStructPass.h"
|
||||
#include "SpirvPasses/StripCompatPerVertexMembersPass.h"
|
||||
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
|
||||
#include "SpirvPasses/RenameBuiltinShadowingFunctionsPass.h"
|
||||
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
||||
@@ -33,6 +34,7 @@
|
||||
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||
#include "SpirvPasses/ExtendNarrowIntegerVertexInputsPass.h"
|
||||
#include "SpirvPasses/LowerPixelCenterIntegerPass.h"
|
||||
#include "SpirvPasses/ZeroLayerOutputPass.h"
|
||||
#include "SpirvPasses/FullCoverageSampleMaskPass.h"
|
||||
@@ -51,6 +53,7 @@
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
#include "SpirvPasses/StripIoBlockLocationsPass.h"
|
||||
#include "SpirvPasses/DeferGeometryEmitsPass.h"
|
||||
#include "SpirvPasses/RetainGeometryOutputsPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
@@ -1358,6 +1361,9 @@ namespace MobileGL {
|
||||
// Output (see above), so never-written outputs are trimmed from the
|
||||
// OpEntryPoint operand list here.
|
||||
optimizer.RegisterPass(CreateRemoveUnusedInterfaceVariablesPass());
|
||||
// Gives a compatibility-profile gl_PerVertex its core shape. After ADCE, so a
|
||||
// legacy member that is only read into dead code no longer counts as accessed.
|
||||
optimizer.RegisterPass(StripCompatPerVertexMembersPass::CreateStripCompatPerVertexMembersPass());
|
||||
// The two module-legality repairs, so the chain's output - the bytes every
|
||||
// consumer downstream sees - is valid Vulkan SPIR-V. Rect lowering used to
|
||||
// live only in the backends; a validating lane would flag every rectangle
|
||||
@@ -1371,6 +1377,12 @@ namespace MobileGL {
|
||||
RenameBuiltinShadowingFunctionsPass::CreateRenameBuiltinShadowingFunctionsPass());
|
||||
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
||||
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
||||
// Geometry outputs keep their last written value across EmitVertex(), as every
|
||||
// desktop driver keeps them; here rather than in a backend so both backends, and
|
||||
// every pass after this chain that works at emit time, see the retained values.
|
||||
// A non-geometry module, or one that writes every output before every emit, is
|
||||
// returned unchanged.
|
||||
optimizer.RegisterPass(RetainGeometryOutputsPass::CreateRetainGeometryOutputsPass());
|
||||
// The fp64 tail, and the ONE part of this chain that is not the same on every
|
||||
// backend. Both passes are skipped when the backend can consume Float64 itself
|
||||
// (`nativeFloat64`, i.e. VkPhysicalDeviceFeatures::shaderFloat64 on DirectVulkan):
|
||||
@@ -1973,6 +1985,32 @@ namespace MobileGL {
|
||||
enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const NarrowIntegerVertexInputs& inputs,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
ExtendNarrowIntegerVertexInputsPass::CreateExtendNarrowIntegerVertexInputsPass(inputs));
|
||||
|
||||
return RunOptimizerChecked("ExtendNarrowIntegerVertexInputsForVulkan", optimizer, inputBinary,
|
||||
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) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "SpvcSession.h"
|
||||
#include "glslang/TVarEntryInfo.h"
|
||||
#include "glslang/TMglGlslIoResolver.h"
|
||||
#include "NarrowIntegerVertexInputs.h"
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
@@ -342,6 +343,20 @@ namespace MobileGL {
|
||||
// See ZeroBaseVertexPass.
|
||||
static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Builds the vertex-shader variant that widens narrow integer attribute data the way
|
||||
// GL does: the inputs at the listed locations are zero- or sign-extended from 8/16
|
||||
// bits right after they are read. A module with no listed integer input comes back
|
||||
// unchanged. See ExtendNarrowIntegerVertexInputsPass.
|
||||
static bool ExtendNarrowIntegerVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
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.
|
||||
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.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 "ExtendNarrowIntegerVertexInputsPass.h"
|
||||
#include "NarrowIntegerRuntimeWidths.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
constexpr Uint32 kMaxLocations = 32;
|
||||
|
||||
// What one location's value needs: the width of the source data, and whether the
|
||||
// fetched value is sign-extended (true) or zero-extended (false) from it.
|
||||
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) {
|
||||
if (location >= kMaxLocations) return {};
|
||||
const Uint32 bit = 1u << location;
|
||||
if (inputs.zeroExtend8 & bit) return {8, false};
|
||||
if (inputs.zeroExtend16 & bit) return {16, false};
|
||||
if (inputs.signExtend8 & bit) return {8, true};
|
||||
if (inputs.signExtend16 & bit) return {16, true};
|
||||
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) {
|
||||
if (type == nullptr) return false;
|
||||
if (const auto* vector = type->AsVector()) type = vector->element_type();
|
||||
const auto* integer = type->AsInteger();
|
||||
return integer != nullptr && integer->width() == 32;
|
||||
}
|
||||
|
||||
// One input variable to correct, with every id the rewrite needs resolved up front.
|
||||
struct Target {
|
||||
Instruction* variable = nullptr;
|
||||
Uint32 pointeeTypeId = 0;
|
||||
Uint32 elementTypeId = 0;
|
||||
// 0 for a scalar or vector input; the element count of an array input.
|
||||
Uint32 arrayLength = 0;
|
||||
// One entry per element (one for a non-array input).
|
||||
std::vector<Extension> extensions;
|
||||
// Per element: the mask constant of a zero-extension, or the bit-count constant
|
||||
// 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;
|
||||
};
|
||||
|
||||
Bool IsDeclarationUse(const Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpDecorateId:
|
||||
case spv::Op::OpDecorateString:
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpEntryPoint:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status ExtendNarrowIntegerVertexInputsPass::Process() {
|
||||
const Bool runtime = m_mode == Mode::RuntimeWidths;
|
||||
if (!runtime && !m_inputs.Any()) return Status::SuccessWithoutChange;
|
||||
|
||||
auto* irContext = context();
|
||||
auto entryPoints = irContext->module()->entry_points();
|
||||
if (entryPoints.begin() == entryPoints.end()) return Status::SuccessWithoutChange;
|
||||
Instruction* entryPoint = &*entryPoints.begin();
|
||||
if (static_cast<spv::ExecutionModel>(entryPoint->GetSingleWordInOperand(0)) !=
|
||||
spv::ExecutionModel::Vertex) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
// The Private copy is seeded in ONE entry point; a second one reading the same input
|
||||
// would see it unwritten. glslang never emits two, so refuse rather than half-fix.
|
||||
auto secondEntryPoint = entryPoints.begin();
|
||||
if (++secondEntryPoint != entryPoints.end()) return Status::Failure;
|
||||
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
|
||||
std::unordered_map<Uint32, Uint32> locationById;
|
||||
for (const Instruction& annotation : irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) == spv::Decoration::Location)
|
||||
locationById[annotation.GetSingleWordInOperand(0)] = annotation.GetSingleWordInOperand(2);
|
||||
}
|
||||
|
||||
std::vector<Target> targets;
|
||||
for (Instruction& inst : irContext->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||
if (static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) != spv::StorageClass::Input)
|
||||
continue;
|
||||
const auto location = locationById.find(inst.result_id());
|
||||
if (location == locationById.end()) continue; // a built-in
|
||||
const Instruction* pointerType = defUseMgr->GetDef(inst.type_id());
|
||||
if (pointerType == nullptr) continue;
|
||||
|
||||
Target target;
|
||||
target.variable = &inst;
|
||||
target.pointeeTypeId = pointerType->GetSingleWordInOperand(1);
|
||||
target.elementTypeId = target.pointeeTypeId;
|
||||
Uint32 elementCount = 1;
|
||||
const Instruction* pointee = defUseMgr->GetDef(target.pointeeTypeId);
|
||||
if (pointee != nullptr && pointee->opcode() == spv::Op::OpTypeArray) {
|
||||
const auto* length = constantMgr->FindDeclaredConstant(pointee->GetSingleWordInOperand(1));
|
||||
if (length == nullptr || length->AsScalarConstant() == nullptr) continue;
|
||||
target.elementTypeId = pointee->GetSingleWordInOperand(0);
|
||||
target.arrayLength = static_cast<Uint32>(length->GetZeroExtendedValue());
|
||||
elementCount = target.arrayLength;
|
||||
}
|
||||
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(
|
||||
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));
|
||||
}
|
||||
if (targets.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||
spvtools::opt::Function* entryFunction = nullptr;
|
||||
for (auto& function : *irContext->module()) {
|
||||
if (function.result_id() == entryFunctionId) {
|
||||
entryFunction = &function;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entryFunction == nullptr || entryFunction->begin() == entryFunction->end()) {
|
||||
return Status::Failure;
|
||||
}
|
||||
|
||||
// Every type and constant the rewrite names is materialized BEFORE any variable is
|
||||
// added or moved: the types-and-values section is ordered, these lookups append to
|
||||
// it, and a variable may not forward-reference its type.
|
||||
const auto constantId = [&](Uint32 typeId, Uint32 scalarValue) -> Uint32 {
|
||||
const analysis::Type* type = typeMgr->GetType(typeId);
|
||||
if (type == nullptr) return 0;
|
||||
const analysis::Constant* constant = nullptr;
|
||||
if (const auto* vector = type->AsVector()) {
|
||||
const analysis::Constant* component =
|
||||
constantMgr->GetConstant(vector->element_type(), {scalarValue});
|
||||
const Instruction* componentInst =
|
||||
component ? constantMgr->GetDefiningInstruction(component) : nullptr;
|
||||
if (componentInst == nullptr) return 0;
|
||||
constant = constantMgr->GetConstant(
|
||||
type, std::vector<Uint32>(vector->element_count(), componentInst->result_id()));
|
||||
} else {
|
||||
constant = constantMgr->GetConstant(type, {scalarValue});
|
||||
}
|
||||
const Instruction* inst =
|
||||
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 (SizeT element = 0; element < target.extensions.size(); ++element) {
|
||||
const Extension& extension = target.extensions[element];
|
||||
Uint32 operandId = 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;
|
||||
if (extension.sign && zeroOffsetId == 0) {
|
||||
zeroOffsetId = constantMgr->GetUIntConstId(0);
|
||||
if (zeroOffsetId == 0) return Status::Failure;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
// An access chain names its result's storage class, so every chain into the
|
||||
// variable has to move to Private with it. Loads need nothing. Any other use -
|
||||
// a pointer passed on, copied or stored - is refused before anything changes,
|
||||
// rather than half-rewritten; glslang emits none of them for an input.
|
||||
std::vector<Instruction*> pending{target.variable};
|
||||
Bool unexpectedUse = false;
|
||||
while (!pending.empty() && !unexpectedUse) {
|
||||
Instruction* base = pending.back();
|
||||
pending.pop_back();
|
||||
// Collected first: the pointer-type lookups below may add instructions.
|
||||
std::vector<Instruction*> users;
|
||||
defUseMgr->ForEachUser(base, [&](Instruction* user) { users.push_back(user); });
|
||||
for (Instruction* user : users) {
|
||||
if (IsDeclarationUse(user) || user->opcode() == spv::Op::OpLoad) continue;
|
||||
const Bool isChain = user->opcode() == spv::Op::OpAccessChain ||
|
||||
user->opcode() == spv::Op::OpInBoundsAccessChain;
|
||||
const Instruction* chainType =
|
||||
isChain && user->GetSingleWordInOperand(0) == base->result_id()
|
||||
? defUseMgr->GetDef(user->type_id())
|
||||
: nullptr;
|
||||
const Uint32 privateChainTypeId =
|
||||
chainType ? typeMgr->FindPointerToType(chainType->GetSingleWordInOperand(1),
|
||||
spv::StorageClass::Private)
|
||||
: 0;
|
||||
if (privateChainTypeId == 0) {
|
||||
unexpectedUse = true;
|
||||
break;
|
||||
}
|
||||
target.accessChains.emplace_back(user, privateChainTypeId);
|
||||
pending.push_back(user);
|
||||
}
|
||||
}
|
||||
if (unexpectedUse) return Status::Failure;
|
||||
}
|
||||
|
||||
// From SPIR-V 1.4 the interface lists every global the entry point uses, Private
|
||||
// included; before it, Input and Output only.
|
||||
const Bool interfaceListsPrivate = irContext->module()->version() >= 0x00010400u;
|
||||
|
||||
auto& entryBlock = *entryFunction->begin();
|
||||
auto insertPoint = entryBlock.begin();
|
||||
while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) {
|
||||
++insertPoint;
|
||||
}
|
||||
if (insertPoint == entryBlock.end()) return Status::Failure;
|
||||
|
||||
// A TakeNextId failure (the id bound is exhausted) is reported once the rewrite is
|
||||
// over; the optimizer then discards the half-rewritten module.
|
||||
Bool outOfIds = false;
|
||||
const auto nextId = [&]() {
|
||||
const Uint32 id = TakeNextId();
|
||||
outOfIds = outOfIds || id == 0;
|
||||
return id;
|
||||
};
|
||||
const auto insert = [&](spv::Op opcode, Uint32 typeId, std::initializer_list<Uint32> operands) {
|
||||
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(
|
||||
MakeUnique<Instruction>(irContext, opcode, typeId, resultId, operandList));
|
||||
++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]});
|
||||
}
|
||||
return insert(spv::Op::OpBitFieldSExtract, target.elementTypeId,
|
||||
{valueId, zeroOffsetId, target.operandIds[element]});
|
||||
};
|
||||
|
||||
for (const auto& target : targets) {
|
||||
Instruction* variable = target.variable;
|
||||
const Uint32 privateId = variable->result_id();
|
||||
const Uint32 inputId = nextId();
|
||||
irContext->AddGlobalValue(MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpVariable, variable->type_id(), inputId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<Uint32>(spv::StorageClass::Input)}}}));
|
||||
|
||||
// Every decoration describes the interface, so all of them follow the Input.
|
||||
for (auto& annotation : irContext->annotations()) {
|
||||
const spv::Op op = annotation.opcode();
|
||||
if ((op == spv::Op::OpDecorate || op == spv::Op::OpDecorateId ||
|
||||
op == spv::Op::OpDecorateString) &&
|
||||
annotation.GetSingleWordInOperand(0) == privateId) {
|
||||
annotation.SetInOperand(0, {inputId});
|
||||
}
|
||||
}
|
||||
|
||||
// The original becomes the Private copy every existing access keeps reading. It
|
||||
// moves to the end of the section because its new pointer type may be newer.
|
||||
variable->SetResultType(target.privatePointerTypeId);
|
||||
variable->SetInOperand(0, {static_cast<Uint32>(spv::StorageClass::Private)});
|
||||
variable->RemoveFromList();
|
||||
irContext->AddGlobalValue(std::unique_ptr<Instruction>(variable));
|
||||
for (const auto& [chain, privateChainTypeId] : target.accessChains)
|
||||
chain->SetResultType(privateChainTypeId);
|
||||
|
||||
std::vector<Operand> interfaceOperands;
|
||||
for (Uint32 i = 0; i < entryPoint->NumInOperands(); ++i) {
|
||||
const Operand& operand = entryPoint->GetInOperand(i);
|
||||
if (i >= 3 && !interfaceListsPrivate && entryPoint->GetSingleWordInOperand(i) == privateId) {
|
||||
interfaceOperands.push_back({SPV_OPERAND_TYPE_ID, {inputId}});
|
||||
continue;
|
||||
}
|
||||
interfaceOperands.push_back(operand);
|
||||
}
|
||||
if (interfaceListsPrivate) interfaceOperands.push_back({SPV_OPERAND_TYPE_ID, {inputId}});
|
||||
entryPoint->SetInOperands(std::move(interfaceOperands));
|
||||
|
||||
const Uint32 loadedId = insert(spv::Op::OpLoad, target.pointeeTypeId, {inputId});
|
||||
Uint32 correctedId = 0;
|
||||
if (target.arrayLength == 0) {
|
||||
correctedId = extend(target, 0, loadedId);
|
||||
} else {
|
||||
std::vector<Uint32> elementIds;
|
||||
for (Uint32 element = 0; element < target.arrayLength; ++element) {
|
||||
const Uint32 elementId = nextId();
|
||||
insertPoint = insertPoint.InsertBefore(MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpCompositeExtract, target.elementTypeId, elementId,
|
||||
Instruction::OperandList{{SPV_OPERAND_TYPE_ID, {loadedId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {element}}}));
|
||||
++insertPoint;
|
||||
elementIds.push_back(extend(target, element, elementId));
|
||||
}
|
||||
correctedId = nextId();
|
||||
Instruction::OperandList constituents;
|
||||
for (const Uint32 id : elementIds)
|
||||
constituents.push_back({SPV_OPERAND_TYPE_ID, {id}});
|
||||
insertPoint = insertPoint.InsertBefore(MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpCompositeConstruct, target.pointeeTypeId, correctedId, constituents));
|
||||
++insertPoint;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken ExtendNarrowIntegerVertexInputsPass::
|
||||
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
|
||||
@@ -0,0 +1,59 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Gives the integer vertex inputs at the listed locations the value GL defines for
|
||||
// narrow integer attribute data (see NarrowIntegerVertexInputs). Each such input is
|
||||
// re-declared as a fresh Input variable carrying the original's interface decorations,
|
||||
// the original is demoted to a Private global, and the entry point seeds it once, at its
|
||||
// top, with the corrected value: the low 8/16 bits masked off for a zero-extension, or
|
||||
// sign-extended for a sign-extension. Every existing load and access chain keeps its id
|
||||
// and type, so no other instruction is rewritten, and an array input is corrected per
|
||||
// element (element i sits at location base + i). Inputs at other locations, and modules
|
||||
// that are not vertex shaders, are left untouched.
|
||||
//
|
||||
// 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
|
||||
} // namespace MobileGL
|
||||
@@ -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
|
||||
@@ -0,0 +1,513 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RetainGeometryOutputsPass.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 "RetainGeometryOutputsPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.h"
|
||||
#include "source/opt/cfg.h"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/decoration_manager.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/function.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/util/make_unique.h"
|
||||
#include "source/util/string_utils.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
using OperandList = std::vector<Operand>;
|
||||
// One bit per slot: whether the slot was fully written since the last emit.
|
||||
using WrittenSet = std::vector<Bool>;
|
||||
|
||||
Operand Id(uint32_t id) {
|
||||
return {SPV_OPERAND_TYPE_ID, {id}};
|
||||
}
|
||||
|
||||
std::unique_ptr<Instruction> Make(IRContext* context, spv::Op op, uint32_t typeId, uint32_t resultId,
|
||||
OperandList operands) {
|
||||
return spvtools::MakeUnique<Instruction>(context, op, typeId, resultId, operands);
|
||||
}
|
||||
|
||||
Bool IsEmit(spv::Op op) {
|
||||
return op == spv::Op::OpEmitVertex || op == spv::Op::OpEmitStreamVertex;
|
||||
}
|
||||
|
||||
Bool IsAccessChain(spv::Op op) {
|
||||
return op == spv::Op::OpAccessChain || op == spv::Op::OpInBoundsAccessChain;
|
||||
}
|
||||
|
||||
// Users that name a variable without touching its memory.
|
||||
Bool IsDeclarationUser(spv::Op op) {
|
||||
return op == spv::Op::OpName || op == spv::Op::OpDecorate || op == spv::Op::OpDecorateId ||
|
||||
op == spv::Op::OpDecorateString || op == spv::Op::OpEntryPoint ||
|
||||
op == spv::Op::OpGroupDecorate;
|
||||
}
|
||||
|
||||
// The value of an OpConstant integer, or nullopt for anything else.
|
||||
Optional<uint32_t> ConstantValue(IRContext* context, uint32_t id) {
|
||||
const Instruction* def = context->get_def_use_mgr()->GetDef(id);
|
||||
if (def == nullptr || def->opcode() != spv::Op::OpConstant || def->NumInOperands() < 1)
|
||||
return std::nullopt;
|
||||
return def->GetSingleWordInOperand(0);
|
||||
}
|
||||
|
||||
Bool IsBlockStruct(IRContext* context, uint32_t typeId) {
|
||||
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
|
||||
return type != nullptr && type->opcode() == spv::Op::OpTypeStruct &&
|
||||
context->get_decoration_mgr()->HasDecoration(typeId, uint32_t(spv::Decoration::Block));
|
||||
}
|
||||
|
||||
// Whether a Block-decorated struct sits anywhere inside this type: such a type
|
||||
// cannot be copied into a Private shadow.
|
||||
Bool ContainsBlock(IRContext* context, uint32_t typeId) {
|
||||
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
|
||||
if (type == nullptr) return false;
|
||||
switch (type->opcode()) {
|
||||
case spv::Op::OpTypeArray:
|
||||
case spv::Op::OpTypeRuntimeArray:
|
||||
return ContainsBlock(context, type->GetSingleWordInOperand(0));
|
||||
case spv::Op::OpTypeStruct:
|
||||
if (IsBlockStruct(context, typeId)) return true;
|
||||
for (uint32_t i = 0; i < type->NumInOperands(); ++i) {
|
||||
if (ContainsBlock(context, type->GetSingleWordInOperand(i))) return true;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Whether every use of this pointer, followed through access chains, is one the
|
||||
// redirection can retarget (loads, stores, copies, further access chains), whether
|
||||
// any of them writes, and every pointer derived from it. A pointer handed to a call
|
||||
// or used as a value is not redirectable: the callee's parameter type names the
|
||||
// Output storage class.
|
||||
struct PointerUse {
|
||||
Bool redirectable = true;
|
||||
Bool stored = false;
|
||||
std::vector<uint32_t> pointers;
|
||||
};
|
||||
void ScanPointerUses(IRContext* context, Instruction* pointer, PointerUse& use) {
|
||||
use.pointers.push_back(pointer->result_id());
|
||||
context->get_def_use_mgr()->ForEachUse(pointer, [&](Instruction* user, uint32_t operandIndex) {
|
||||
const spv::Op op = user->opcode();
|
||||
if (IsDeclarationUser(op)) return;
|
||||
switch (op) {
|
||||
case spv::Op::OpLoad:
|
||||
break;
|
||||
case spv::Op::OpStore:
|
||||
if (operandIndex == 0) {
|
||||
use.stored = true;
|
||||
} else {
|
||||
use.redirectable = false;
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpCopyMemory:
|
||||
case spv::Op::OpCopyMemorySized:
|
||||
if (operandIndex == 0) use.stored = true;
|
||||
break;
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
if (operandIndex == 2) {
|
||||
ScanPointerUses(context, user, use);
|
||||
} else {
|
||||
use.redirectable = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
use.redirectable = false;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// One output value that may be retained: a whole output variable, or one member
|
||||
// of an output block.
|
||||
struct Slot {
|
||||
uint32_t variableId = 0;
|
||||
Int64 member = -1;
|
||||
uint32_t valueTypeId = 0;
|
||||
uint32_t blockTypeId = 0;
|
||||
// Filled for the slots that are retained.
|
||||
uint32_t shadowId = 0;
|
||||
uint32_t privatePointerTypeId = 0;
|
||||
uint32_t outputPointerTypeId = 0;
|
||||
uint32_t memberIndexId = 0;
|
||||
};
|
||||
|
||||
String NameOf(IRContext* context, uint32_t id) {
|
||||
for (const Instruction& debugInst : context->debugs2()) {
|
||||
if (debugInst.opcode() == spv::Op::OpName && debugInst.GetSingleWordInOperand(0) == id)
|
||||
return debugInst.GetInOperand(1).AsString();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
String MemberNameOf(IRContext* context, uint32_t structTypeId, uint32_t member) {
|
||||
for (const Instruction& debugInst : context->debugs2()) {
|
||||
if (debugInst.opcode() == spv::Op::OpMemberName &&
|
||||
debugInst.GetSingleWordInOperand(0) == structTypeId &&
|
||||
debugInst.GetSingleWordInOperand(1) == member)
|
||||
return debugInst.GetInOperand(2).AsString();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// The shadow's debug name. A builtin's `gl_` prefix is dropped so the generated
|
||||
// source never spells a reserved name, nor one a text pass searches for.
|
||||
String ShadowName(String outputName) {
|
||||
if (outputName.empty()) return {};
|
||||
if (outputName.rfind("gl_", 0) == 0) outputName.erase(0, 3);
|
||||
while (!outputName.empty() && outputName.front() == '_')
|
||||
outputName.erase(0, 1);
|
||||
return outputName.empty() ? String{} : "mg_retained_" + outputName;
|
||||
}
|
||||
|
||||
// Which slot each pointer addresses: `whole` holds the pointers a store through
|
||||
// which rewrites the whole slot, `any` every pointer into a slot.
|
||||
struct SlotPointers {
|
||||
std::unordered_map<uint32_t, SizeT> whole;
|
||||
std::unordered_map<uint32_t, SizeT> any;
|
||||
};
|
||||
|
||||
// Runs a must-be-written dataflow over one function and marks every slot whose
|
||||
// value from before an earlier emit can be observed: at an emit (not fully written
|
||||
// since the previous one) or by a read (same condition). The entry function starts
|
||||
// with every slot counted as written, as nothing has been emitted yet; any other
|
||||
// function starts with none, since its caller may have emitted. A call that can
|
||||
// emit acts like an emit; writes inside callees are not followed.
|
||||
void MarkStaleSlots(IRContext* context, Function& function, Bool isEntryFunction,
|
||||
const SlotPointers& slotPointers,
|
||||
const std::unordered_set<uint32_t>& emittingFunctions, SizeT slotCount,
|
||||
std::vector<Bool>& stale) {
|
||||
const WrittenSet all(slotCount, true);
|
||||
const WrittenSet none(slotCount, false);
|
||||
auto transfer = [&](BasicBlock& block, WrittenSet& written, Bool check) {
|
||||
auto read = [&](uint32_t pointer) {
|
||||
const auto slot = slotPointers.any.find(pointer);
|
||||
if (check && slot != slotPointers.any.end() && !written[slot->second])
|
||||
stale[slot->second] = true;
|
||||
};
|
||||
for (Instruction& inst : block) {
|
||||
const spv::Op op = inst.opcode();
|
||||
if (IsEmit(op)) {
|
||||
if (check) {
|
||||
for (SizeT slot = 0; slot < slotCount; ++slot) {
|
||||
if (!written[slot]) stale[slot] = true;
|
||||
}
|
||||
}
|
||||
written = none;
|
||||
} else if (op == spv::Op::OpFunctionCall) {
|
||||
if (emittingFunctions.count(inst.GetSingleWordInOperand(0)) != 0) written = none;
|
||||
} else if (op == spv::Op::OpLoad) {
|
||||
read(inst.GetSingleWordInOperand(0));
|
||||
} else if (op == spv::Op::OpStore || op == spv::Op::OpCopyMemory ||
|
||||
op == spv::Op::OpCopyMemorySized) {
|
||||
if (op != spv::Op::OpStore) read(inst.GetSingleWordInOperand(1));
|
||||
const auto target = slotPointers.whole.find(inst.GetSingleWordInOperand(0));
|
||||
if (target != slotPointers.whole.end()) written[target->second] = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
spvtools::opt::CFG* cfg = context->cfg();
|
||||
const uint32_t entryLabel = function.begin()->id();
|
||||
std::unordered_map<uint32_t, WrittenSet> out;
|
||||
for (BasicBlock& block : function)
|
||||
out[block.id()] = all;
|
||||
auto entryState = [&](BasicBlock& block) {
|
||||
if (block.id() == entryLabel) return isEntryFunction ? all : none;
|
||||
WrittenSet written = all;
|
||||
for (uint32_t predecessor : cfg->preds(block.id())) {
|
||||
const auto found = out.find(predecessor);
|
||||
if (found == out.end()) continue;
|
||||
for (SizeT slot = 0; slot < slotCount; ++slot)
|
||||
written[slot] = written[slot] && found->second[slot];
|
||||
}
|
||||
return written;
|
||||
};
|
||||
for (Bool changed = true; changed;) {
|
||||
changed = false;
|
||||
for (BasicBlock& block : function) {
|
||||
WrittenSet written = entryState(block);
|
||||
transfer(block, written, false);
|
||||
if (written != out[block.id()]) {
|
||||
out[block.id()] = std::move(written);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (BasicBlock& block : function) {
|
||||
WrittenSet written = entryState(block);
|
||||
transfer(block, written, true);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status RetainGeometryOutputsPass::Process() {
|
||||
IRContext* irContext = context();
|
||||
auto* defUse = irContext->get_def_use_mgr();
|
||||
|
||||
// One geometry entry point.
|
||||
Instruction* entryPoint = nullptr;
|
||||
for (Instruction& candidate : irContext->module()->entry_points()) {
|
||||
if (entryPoint != nullptr) return Status::SuccessWithoutChange;
|
||||
entryPoint = &candidate;
|
||||
}
|
||||
if (entryPoint == nullptr || static_cast<spv::ExecutionModel>(entryPoint->GetSingleWordInOperand(0)) !=
|
||||
spv::ExecutionModel::Geometry) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||
|
||||
// Every emit, and every function a call to which can emit.
|
||||
std::vector<Instruction*> emits;
|
||||
std::unordered_set<uint32_t> emittingFunctions;
|
||||
for (Function& function : *irContext->module()) {
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
if (!IsEmit(inst->opcode())) return;
|
||||
emits.push_back(inst);
|
||||
emittingFunctions.insert(function.result_id());
|
||||
});
|
||||
}
|
||||
if (emits.empty()) return Status::SuccessWithoutChange;
|
||||
for (Bool grew = true; grew;) {
|
||||
grew = false;
|
||||
for (Function& function : *irContext->module()) {
|
||||
if (emittingFunctions.count(function.result_id()) != 0) continue;
|
||||
Bool callsEmitter = false;
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
if (inst->opcode() == spv::Op::OpFunctionCall &&
|
||||
emittingFunctions.count(inst->GetSingleWordInOperand(0)) != 0)
|
||||
callsEmitter = true;
|
||||
});
|
||||
if (callsEmitter) {
|
||||
emittingFunctions.insert(function.result_id());
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The candidate slots: every output, or output block member, the shader writes and
|
||||
// whose every use can be redirected.
|
||||
std::vector<Slot> slots;
|
||||
SlotPointers slotPointers;
|
||||
for (Instruction& global : irContext->types_values()) {
|
||||
if (global.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) != spv::StorageClass::Output) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t valueTypeId = defUse->GetDef(global.type_id())->GetSingleWordInOperand(1);
|
||||
if (!IsBlockStruct(irContext, valueTypeId)) {
|
||||
if (ContainsBlock(irContext, valueTypeId)) continue;
|
||||
PointerUse use;
|
||||
ScanPointerUses(irContext, &global, use);
|
||||
if (!use.stored || !use.redirectable) continue;
|
||||
slotPointers.whole[global.result_id()] = slots.size();
|
||||
for (uint32_t pointer : use.pointers)
|
||||
slotPointers.any[pointer] = slots.size();
|
||||
slots.push_back({global.result_id(), -1, valueTypeId});
|
||||
continue;
|
||||
}
|
||||
|
||||
// A block, member by member. A use of the block as a whole (never emitted by
|
||||
// the front end) leaves the whole block alone.
|
||||
const Instruction* blockType = defUse->GetDef(valueTypeId);
|
||||
const uint32_t memberCount = blockType->NumInOperands();
|
||||
std::vector<PointerUse> memberUses(memberCount);
|
||||
std::vector<std::vector<uint32_t>> memberWholePointers(memberCount);
|
||||
Bool wholeBlockUse = false;
|
||||
defUse->ForEachUse(&global, [&](Instruction* user, uint32_t operandIndex) {
|
||||
if (IsDeclarationUser(user->opcode())) return;
|
||||
if (!IsAccessChain(user->opcode()) || operandIndex != 2 || user->NumInOperands() < 2) {
|
||||
wholeBlockUse = true;
|
||||
return;
|
||||
}
|
||||
const auto member = ConstantValue(irContext, user->GetSingleWordInOperand(1));
|
||||
if (!member || *member >= memberCount) {
|
||||
wholeBlockUse = true;
|
||||
return;
|
||||
}
|
||||
ScanPointerUses(irContext, user, memberUses[*member]);
|
||||
if (user->NumInOperands() == 2) memberWholePointers[*member].push_back(user->result_id());
|
||||
});
|
||||
if (wholeBlockUse) continue;
|
||||
for (uint32_t member = 0; member < memberCount; ++member) {
|
||||
if (!memberUses[member].stored || !memberUses[member].redirectable) continue;
|
||||
for (uint32_t pointer : memberWholePointers[member])
|
||||
slotPointers.whole[pointer] = slots.size();
|
||||
for (uint32_t pointer : memberUses[member].pointers)
|
||||
slotPointers.any[pointer] = slots.size();
|
||||
slots.push_back({global.result_id(), Int64(member), blockType->GetSingleWordInOperand(member),
|
||||
valueTypeId});
|
||||
}
|
||||
}
|
||||
if (slots.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
// Which slots can be observed holding a value from before an earlier emit.
|
||||
std::vector<Bool> stale(slots.size(), false);
|
||||
for (Function& function : *irContext->module()) {
|
||||
MarkStaleSlots(irContext, function, function.result_id() == entryFunctionId, slotPointers,
|
||||
emittingFunctions, slots.size(), stale);
|
||||
}
|
||||
std::vector<Slot> retained;
|
||||
for (SizeT slot = 0; slot < slots.size(); ++slot) {
|
||||
if (stale[slot]) retained.push_back(slots[slot]);
|
||||
}
|
||||
if (retained.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
// ---- Types and constants, then the shadows, before any instruction changes ----
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constMgr = irContext->get_constant_mgr();
|
||||
for (Slot& slot : retained) {
|
||||
slot.privatePointerTypeId =
|
||||
typeMgr->FindPointerToType(slot.valueTypeId, spv::StorageClass::Private);
|
||||
if (slot.member >= 0) {
|
||||
slot.outputPointerTypeId =
|
||||
typeMgr->FindPointerToType(slot.valueTypeId, spv::StorageClass::Output);
|
||||
slot.memberIndexId = constMgr->GetUIntConstId(uint32_t(slot.member));
|
||||
}
|
||||
}
|
||||
|
||||
// Every access chain derived from a redirected pointer changes storage class with
|
||||
// it; collected (with the Private pointer type it needs) while def-use is intact.
|
||||
std::vector<std::pair<Instruction*, uint32_t>> retypes;
|
||||
std::unordered_set<Instruction*> retypeSeen;
|
||||
auto collectRetypes = [&](auto&& self, Instruction* pointer) -> void {
|
||||
defUse->ForEachUser(pointer, [&](Instruction* user) {
|
||||
if (!IsAccessChain(user->opcode()) || user->GetSingleWordInOperand(0) != pointer->result_id())
|
||||
return;
|
||||
if (!retypeSeen.insert(user).second) return;
|
||||
const uint32_t pointeeTypeId = defUse->GetDef(user->type_id())->GetSingleWordInOperand(1);
|
||||
retypes.emplace_back(user,
|
||||
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Private));
|
||||
self(self, user);
|
||||
});
|
||||
};
|
||||
|
||||
// Operand rewrites: (instruction, operand index counting result type and id, new id).
|
||||
struct Rewrite {
|
||||
Instruction* inst;
|
||||
uint32_t operand;
|
||||
uint32_t newId;
|
||||
};
|
||||
std::vector<Rewrite> rewrites;
|
||||
// Single-index chains onto a retained block member: the shadow replaces them.
|
||||
std::vector<Instruction*> aliasChains;
|
||||
// Deeper chains onto a retained block member: rebased onto the shadow.
|
||||
std::vector<std::pair<Instruction*, Slot*>> rebasedChains;
|
||||
|
||||
for (Slot& slot : retained) {
|
||||
slot.shadowId = irContext->TakeNextId();
|
||||
Instruction* variable = defUse->GetDef(slot.variableId);
|
||||
if (slot.member < 0) {
|
||||
defUse->ForEachUse(variable, [&](Instruction* user, uint32_t operandIndex) {
|
||||
if (!IsDeclarationUser(user->opcode()))
|
||||
rewrites.push_back({user, operandIndex, slot.shadowId});
|
||||
});
|
||||
collectRetypes(collectRetypes, variable);
|
||||
continue;
|
||||
}
|
||||
defUse->ForEachUser(variable, [&](Instruction* user) {
|
||||
if (!IsAccessChain(user->opcode()) || user->NumInOperands() < 2) return;
|
||||
const auto member = ConstantValue(irContext, user->GetSingleWordInOperand(1));
|
||||
if (!member || Int64(*member) != slot.member) return;
|
||||
if (user->NumInOperands() == 2) {
|
||||
aliasChains.push_back(user);
|
||||
defUse->ForEachUse(user, [&](Instruction* aliasUser, uint32_t operandIndex) {
|
||||
if (!IsDeclarationUser(aliasUser->opcode()))
|
||||
rewrites.push_back({aliasUser, operandIndex, slot.shadowId});
|
||||
});
|
||||
} else {
|
||||
rebasedChains.emplace_back(user, &slot);
|
||||
const uint32_t pointeeTypeId = defUse->GetDef(user->type_id())->GetSingleWordInOperand(1);
|
||||
retypes.emplace_back(user,
|
||||
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Private));
|
||||
retypeSeen.insert(user);
|
||||
}
|
||||
collectRetypes(collectRetypes, user);
|
||||
});
|
||||
}
|
||||
|
||||
for (Slot& slot : retained) {
|
||||
irContext->AddGlobalValue(
|
||||
Make(irContext, spv::Op::OpVariable, slot.privatePointerTypeId, slot.shadowId,
|
||||
{{SPV_OPERAND_TYPE_STORAGE_CLASS, {uint32_t(spv::StorageClass::Private)}}}));
|
||||
const String outputName = slot.member < 0
|
||||
? NameOf(irContext, slot.variableId)
|
||||
: MemberNameOf(irContext, slot.blockTypeId, uint32_t(slot.member));
|
||||
const String shadowName = ShadowName(outputName);
|
||||
if (!shadowName.empty()) {
|
||||
irContext->AddDebug2Inst(
|
||||
Make(irContext, spv::Op::OpName, 0, 0,
|
||||
{Id(slot.shadowId),
|
||||
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(shadowName.c_str())}}));
|
||||
}
|
||||
// From SPIR-V 1.4 the interface lists every global the entry point uses.
|
||||
if (irContext->module()->version() >= 0x00010400u) entryPoint->AddOperand(Id(slot.shadowId));
|
||||
}
|
||||
|
||||
// ---- Hand-built from here on; analyses are rebuilt once at the end ----------
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
|
||||
for (const Rewrite& rewrite : rewrites)
|
||||
rewrite.inst->SetOperand(rewrite.operand, {rewrite.newId});
|
||||
for (auto& [chain, slot] : rebasedChains) {
|
||||
OperandList operands;
|
||||
operands.push_back(Id(slot->shadowId));
|
||||
for (uint32_t i = 2; i < chain->NumInOperands(); ++i)
|
||||
operands.push_back(chain->GetInOperand(i));
|
||||
chain->SetInOperands(std::move(operands));
|
||||
}
|
||||
for (auto& [chain, typeId] : retypes)
|
||||
chain->SetResultType(typeId);
|
||||
for (Instruction* chain : aliasChains)
|
||||
irContext->KillInst(chain);
|
||||
|
||||
// The copy into the real outputs, immediately before every emit.
|
||||
for (Instruction* emit : emits) {
|
||||
for (const Slot& slot : retained) {
|
||||
const uint32_t value = irContext->TakeNextId();
|
||||
emit->InsertBefore(
|
||||
Make(irContext, spv::Op::OpLoad, slot.valueTypeId, value, {Id(slot.shadowId)}));
|
||||
uint32_t target = slot.variableId;
|
||||
if (slot.member >= 0) {
|
||||
target = irContext->TakeNextId();
|
||||
emit->InsertBefore(Make(irContext, spv::Op::OpAccessChain, slot.outputPointerTypeId, target,
|
||||
{Id(slot.variableId), Id(slot.memberIndexId)}));
|
||||
}
|
||||
emit->InsertBefore(Make(irContext, spv::Op::OpStore, 0, 0, {Id(target), Id(value)}));
|
||||
}
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken RetainGeometryOutputsPass::CreateRetainGeometryOutputsPass() {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<RetainGeometryOutputsPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,42 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RetainGeometryOutputsPass.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 "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Geometry outputs keep their last written value across EmitVertex(), as on every
|
||||
// desktop GL driver. GLSL leaves them undefined after an emit, but shaders rely on the
|
||||
// desktop behaviour: iterationT's terrain stage writes `flat out float
|
||||
// textureResolution` once and then emits three vertices, and the flat value is taken
|
||||
// from the LAST one (GL's default provoking vertex), which a driver that drops the
|
||||
// value after the first emit renders as random black triangles.
|
||||
//
|
||||
// Each output whose value from before an EARLIER emit can be observed - at a later emit
|
||||
// or by a read - gets a Private shadow; every load, store and access chain of it moves
|
||||
// to the shadow, which is copied into the real output immediately before every emit
|
||||
// (stream emits included). Block members are carried one by one, and only those the
|
||||
// shader writes, so no unwritten builtin member becomes active. A must-be-written
|
||||
// dataflow over each function's CFG finds them; a stage that writes every output
|
||||
// before every emit is left byte-identical. Outputs whose pointer escapes into a call,
|
||||
// and arrays of blocks, are left alone.
|
||||
class RetainGeometryOutputsPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "retain-geometry-outputs"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateRetainGeometryOutputsPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,261 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripCompatPerVertexMembersPass.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 "StripCompatPerVertexMembersPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// A struct that is a Block, has at least one BuiltIn member and at least one
|
||||
// member without: the shape glslang gives a compatibility-profile gl_PerVertex.
|
||||
struct MixedBlock {
|
||||
Instruction* type = nullptr;
|
||||
std::set<uint32_t> builtInMembers;
|
||||
// Every member index some access chain reaches, across all its variables.
|
||||
std::set<uint32_t> accessedMembers;
|
||||
// (access chain, in-operand) pairs that name a member, rewritten on a strip.
|
||||
std::vector<std::pair<Instruction*, uint32_t>> memberOperands;
|
||||
bool rejected = false;
|
||||
};
|
||||
|
||||
bool IsAccessChain(spv::Op op) {
|
||||
return op == spv::Op::OpAccessChain || op == spv::Op::OpInBoundsAccessChain;
|
||||
}
|
||||
|
||||
bool IsNameOrDecoration(const Instruction& inst) {
|
||||
return inst.IsDecoration() || inst.opcode() == spv::Op::OpName ||
|
||||
inst.opcode() == spv::Op::OpMemberName;
|
||||
}
|
||||
|
||||
std::map<uint32_t, MixedBlock> CollectMixedBlocks(IRContext* ctx) {
|
||||
std::set<uint32_t> blocks;
|
||||
std::map<uint32_t, std::set<uint32_t>> builtIns;
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpDecorate &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) == spv::Decoration::Block) {
|
||||
blocks.insert(ann.GetSingleWordInOperand(0));
|
||||
} else if (ann.opcode() == spv::Op::OpMemberDecorate &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::Decoration::BuiltIn) {
|
||||
builtIns[ann.GetSingleWordInOperand(0)].insert(ann.GetSingleWordInOperand(1));
|
||||
}
|
||||
}
|
||||
std::map<uint32_t, MixedBlock> mixed;
|
||||
for (auto& [structId, members] : builtIns) {
|
||||
if (blocks.count(structId) == 0) continue;
|
||||
Instruction* type = ctx->get_def_use_mgr()->GetDef(structId);
|
||||
if (type == nullptr || type->opcode() != spv::Op::OpTypeStruct) continue;
|
||||
if (members.size() >= type->NumInOperands()) continue;
|
||||
mixed[structId] = MixedBlock{type, members};
|
||||
}
|
||||
return mixed;
|
||||
}
|
||||
|
||||
// The struct's only uses may be names, decorations, Input/Output pointer types
|
||||
// and arrays of it, so no value of the whole block exists anywhere. A decoration
|
||||
// group could hide a BuiltIn from CollectMixedBlocks, so it rejects too.
|
||||
bool OnlyInterfaceTypesUse(IRContext* ctx, uint32_t typeId, bool allowArray) {
|
||||
return ctx->get_def_use_mgr()->WhileEachUser(typeId, [&](Instruction* user) {
|
||||
if (user->opcode() == spv::Op::OpGroupMemberDecorate) return false;
|
||||
if (IsNameOrDecoration(*user)) return true;
|
||||
if (user->opcode() == spv::Op::OpTypeArray && allowArray) {
|
||||
return OnlyInterfaceTypesUse(ctx, user->result_id(), false);
|
||||
}
|
||||
if (user->opcode() != spv::Op::OpTypePointer) return false;
|
||||
const auto storage = static_cast<spv::StorageClass>(user->GetSingleWordInOperand(0));
|
||||
return storage == spv::StorageClass::Input || storage == spv::StorageClass::Output;
|
||||
});
|
||||
}
|
||||
|
||||
// Follows every use of a pointer to the block (`arrayed`: to an array of it).
|
||||
// Only access chains are followed; anything else that takes the pointer rejects.
|
||||
void WalkPointerUses(IRContext* ctx, Instruction* pointer, bool arrayed, MixedBlock& block) {
|
||||
ctx->get_def_use_mgr()->ForEachUse(pointer, [&](Instruction* user, uint32_t operandIndex) {
|
||||
if (block.rejected) return;
|
||||
if (IsNameOrDecoration(*user) || user->opcode() == spv::Op::OpEntryPoint) return;
|
||||
// Operand 2 of an access chain is its base (result type and id come first).
|
||||
if (!IsAccessChain(user->opcode()) || operandIndex != 2) {
|
||||
block.rejected = true;
|
||||
return;
|
||||
}
|
||||
const uint32_t memberOperand = arrayed ? 2 : 1;
|
||||
if (user->NumInOperands() <= memberOperand) {
|
||||
// Stops at the block or the array: the result is another such pointer.
|
||||
const bool stillArrayed = arrayed && user->NumInOperands() == 1;
|
||||
WalkPointerUses(ctx, user, stillArrayed, block);
|
||||
return;
|
||||
}
|
||||
const uint32_t indexId = user->GetSingleWordInOperand(memberOperand);
|
||||
const auto* index = ctx->get_constant_mgr()->FindDeclaredConstant(indexId);
|
||||
if (index == nullptr || index->AsIntConstant() == nullptr) {
|
||||
block.rejected = true;
|
||||
return;
|
||||
}
|
||||
block.accessedMembers.insert(index->GetU32());
|
||||
block.memberOperands.emplace_back(user, memberOperand);
|
||||
});
|
||||
}
|
||||
|
||||
// The struct the Input/Output variable points at, and whether it is arrayed.
|
||||
uint32_t VariableBlockType(IRContext* ctx, const Instruction& var, bool& arrayed) {
|
||||
Instruction* pointer = ctx->get_def_use_mgr()->GetDef(var.type_id());
|
||||
if (pointer == nullptr || pointer->opcode() != spv::Op::OpTypePointer) return 0;
|
||||
Instruction* pointee = ctx->get_def_use_mgr()->GetDef(pointer->GetSingleWordInOperand(1));
|
||||
arrayed = pointee != nullptr && pointee->opcode() == spv::Op::OpTypeArray;
|
||||
if (arrayed) pointee = ctx->get_def_use_mgr()->GetDef(pointee->GetSingleWordInOperand(0));
|
||||
return pointee != nullptr && pointee->opcode() == spv::Op::OpTypeStruct ? pointee->result_id() : 0;
|
||||
}
|
||||
|
||||
// Kills an index constant or the array type of a dropped member, and the array
|
||||
// length behind it, once nothing but names and decorations use them.
|
||||
void KillIfUnused(IRContext* ctx, uint32_t id) {
|
||||
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
|
||||
if (def == nullptr) return;
|
||||
bool used = false;
|
||||
ctx->get_def_use_mgr()->ForEachUser(def, [&](Instruction* user) {
|
||||
if (!IsNameOrDecoration(*user)) used = true;
|
||||
});
|
||||
if (used) return;
|
||||
const bool isArray = def->opcode() == spv::Op::OpTypeArray;
|
||||
const uint32_t lengthId = isArray ? def->GetSingleWordInOperand(1) : 0;
|
||||
if (!isArray && def->opcode() != spv::Op::OpConstant) return;
|
||||
ctx->KillInst(def);
|
||||
if (lengthId != 0) KillIfUnused(ctx, lengthId);
|
||||
}
|
||||
|
||||
// Returns whether the module changed. The block stays as it was when a renumbered
|
||||
// index constant cannot be declared.
|
||||
bool StripBlock(IRContext* ctx, MixedBlock& block) {
|
||||
const uint32_t structId = block.type->result_id();
|
||||
std::map<uint32_t, uint32_t> newIndexOf;
|
||||
for (const uint32_t member : block.builtInMembers) {
|
||||
const uint32_t next = static_cast<uint32_t>(newIndexOf.size());
|
||||
newIndexOf[member] = next;
|
||||
}
|
||||
|
||||
// Constants first, while the type manager still sees the original struct.
|
||||
auto* constants = ctx->get_constant_mgr();
|
||||
std::vector<uint32_t> newIndexIds;
|
||||
for (auto& [chain, operand] : block.memberOperands) {
|
||||
const auto* oldIndex = constants->FindDeclaredConstant(chain->GetSingleWordInOperand(operand));
|
||||
const uint32_t newIndex = newIndexOf.at(oldIndex->GetU32());
|
||||
if (newIndex == oldIndex->GetU32()) {
|
||||
newIndexIds.push_back(0);
|
||||
continue;
|
||||
}
|
||||
Instruction* constant =
|
||||
constants->GetDefiningInstruction(constants->GetConstant(oldIndex->type(), {newIndex}));
|
||||
if (constant == nullptr) return !newIndexIds.empty();
|
||||
newIndexIds.push_back(constant->result_id());
|
||||
}
|
||||
std::set<uint32_t> oldIndexIds;
|
||||
for (SizeT i = 0; i < block.memberOperands.size(); ++i) {
|
||||
if (newIndexIds[i] == 0) continue;
|
||||
auto& [chain, operand] = block.memberOperands[i];
|
||||
oldIndexIds.insert(chain->GetSingleWordInOperand(operand));
|
||||
chain->SetInOperand(operand, {newIndexIds[i]});
|
||||
ctx->get_def_use_mgr()->AnalyzeInstUse(chain);
|
||||
}
|
||||
for (const uint32_t id : oldIndexIds) {
|
||||
KillIfUnused(ctx, id);
|
||||
}
|
||||
|
||||
std::vector<Instruction*> dropped;
|
||||
const auto renumber = [&](Instruction& inst, uint32_t memberOperand) {
|
||||
if (inst.GetSingleWordInOperand(0) != structId) return;
|
||||
const auto kept = newIndexOf.find(inst.GetSingleWordInOperand(memberOperand));
|
||||
if (kept == newIndexOf.end()) {
|
||||
dropped.push_back(&inst);
|
||||
} else {
|
||||
inst.SetInOperand(memberOperand, {kept->second});
|
||||
}
|
||||
};
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpMemberDecorate ||
|
||||
ann.opcode() == spv::Op::OpMemberDecorateString) {
|
||||
renumber(ann, 1);
|
||||
}
|
||||
}
|
||||
for (Instruction& debug : ctx->module()->debugs2()) {
|
||||
if (debug.opcode() == spv::Op::OpMemberName) renumber(debug, 1);
|
||||
}
|
||||
for (Instruction* inst : dropped) {
|
||||
ctx->KillInst(inst);
|
||||
}
|
||||
|
||||
std::vector<uint32_t> droppedTypes;
|
||||
Instruction::OperandList members;
|
||||
for (uint32_t i = 0; i < block.type->NumInOperands(); ++i) {
|
||||
const uint32_t memberType = block.type->GetSingleWordInOperand(i);
|
||||
if (newIndexOf.count(i) != 0) {
|
||||
members.push_back({SPV_OPERAND_TYPE_ID, {memberType}});
|
||||
} else {
|
||||
droppedTypes.push_back(memberType);
|
||||
}
|
||||
}
|
||||
block.type->SetInOperands(std::move(members));
|
||||
ctx->get_def_use_mgr()->AnalyzeInstUse(block.type);
|
||||
for (const uint32_t typeId : droppedTypes) {
|
||||
KillIfUnused(ctx, typeId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status StripCompatPerVertexMembersPass::Process() {
|
||||
IRContext* ctx = context();
|
||||
std::map<uint32_t, MixedBlock> blocks = CollectMixedBlocks(ctx);
|
||||
if (blocks.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
for (auto& [structId, block] : blocks) {
|
||||
block.rejected = !OnlyInterfaceTypesUse(ctx, structId, true);
|
||||
}
|
||||
for (Instruction& inst : ctx->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||
bool arrayed = false;
|
||||
const auto found = blocks.find(VariableBlockType(ctx, inst, arrayed));
|
||||
if (found == blocks.end() || found->second.rejected) continue;
|
||||
WalkPointerUses(ctx, &inst, arrayed, found->second);
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
for (auto& [structId, block] : blocks) {
|
||||
if (block.rejected) continue;
|
||||
bool legacyMemberUsed = false;
|
||||
for (const uint32_t member : block.accessedMembers) {
|
||||
legacyMemberUsed |= block.builtInMembers.count(member) == 0;
|
||||
}
|
||||
if (legacyMemberUsed) continue;
|
||||
changed |= StripBlock(ctx, block);
|
||||
}
|
||||
return changed ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken StripCompatPerVertexMembersPass::CreateStripCompatPerVertexMembersPass() {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<StripCompatPerVertexMembersPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,41 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripCompatPerVertexMembersPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Gives a compatibility-profile stage the per-vertex block its core twin has.
|
||||
//
|
||||
// Under a compatibility-profile #version glslang declares gl_PerVertex (and the gl_in /
|
||||
// gl_out arrays of it) with the legacy members gl_ClipVertex, gl_FrontColor,
|
||||
// gl_BackColor, gl_FrontSecondaryColor, gl_BackSecondaryColor, gl_TexCoord[] and
|
||||
// gl_FogFragCoord between the core ones, and emits the whole block whenever any
|
||||
// member is used. Those members have no SPIR-V BuiltIn, and Vulkan requires every
|
||||
// member of a built-in block to carry one, so such a module is invalid even when it
|
||||
// writes nothing but gl_Position, and DirectVulkan declines invalid modules.
|
||||
//
|
||||
// A Block struct mixing BuiltIn and plain members loses its plain members when no
|
||||
// access chain reaches them; the rest are renumbered. A block whose legacy member is
|
||||
// accessed, or that is reached in any way other than constant-indexed access chains
|
||||
// (a whole-block load, store or copy, a pointer escaping into a call), is left as is.
|
||||
class StripCompatPerVertexMembersPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-strip-compat-per-vertex-members"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateStripCompatPerVertexMembersPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -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
|
||||
|
||||
@@ -111,6 +111,16 @@ The bundled fixtures cover:
|
||||
- minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
|
||||
iterationT after entering a singleplayer world, with Iris' DSA path disabled.
|
||||

|
||||
- minecraft-1.20.1-fabric-iris-iterationt-in-world: captured from Minecraft 1.20.1 Fabric with Sodium 0.5.13, Iris
|
||||
1.7.6 and iterationT 3.2.0, in daylight with a block outlined. Iris 1.7 declares Sodium 0.5's lightmap input as
|
||||
`ivec4` while Sodium feeds it `GL_UNSIGNED_BYTE`, a pairing the 1.21.4 fixtures never reach: sign-extending that
|
||||
data turns all terrain black. The trace is a single-frame trim of a capture with a masked GL vendor; its ~64 MiB
|
||||
archive is mostly the pack's own textures (two 33 MiB RGBA32F atmosphere volumes and an 8 MiB noise volume).
|
||||
The golden is the Mesa picture and the NVIDIA capture is the alternate. The pack's outline shader compares the
|
||||
outline alpha with `== 0.4f` and scales its width by `sign(lineOffset.x)`. NVIDIA rounds UNORM8 102/255 exactly and
|
||||
fuses `ndc2.y - ndc1.y` into an FMA, so it draws a full gray box. Mesa is one ulp off and does not fuse, so the
|
||||
outline is black and its screen-horizontal edges have zero width. Both are within GL's precision rules.
|
||||

|
||||
- minecraft-1.21.4-fabric-iris-iterationrp-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
|
||||
iterationRP after entering a singleplayer world, framing the iterationRP name overlay over a lake with far-shore
|
||||
tree reflections. iterationRP's temporal auto-exposure makes a single-frame trim overexpose and drop the overlay,
|
||||
@@ -310,7 +320,7 @@ case pass" but "which cases diverge, and is the divergence reproducible":
|
||||
|
||||
| option | what it does |
|
||||
| --- | --- |
|
||||
| `--matrix` | run the **CI split subset** (the set `retrace-split` runs; 39 cases today) instead of the whole manifest. `--all` additionally includes the non-CI workloads such as rd12, which ID-P7-4 excludes from the exit-gate-3 denominator. Each case runs only the backends its own `ci_backends` names - and a case NAMED with `--case` that does not run the requested backend is an error, not a silent skip. |
|
||||
| `--matrix` | run the **CI split subset** (the set `retrace-split` runs; 40 cases today) instead of the whole manifest. `--all` additionally includes the non-CI workloads such as rd12, which ID-P7-4 excludes from the exit-gate-3 denominator. Each case runs only the backends its own `ci_backends` names - and a case NAMED with `--case` that does not run the requested backend is an error, not a silent skip. |
|
||||
| `--repeat N` | replay each case/backend N times. Repeats after the first pass `--reuse-fixture`, so they measure the replay and not the push. |
|
||||
| `--archive-dir DIR` | copy each repeat's `result.json`, actual PNG, both role logs, `transport-proof.json` and `logcat.txt` to `DIR/<case>-<backend>/repeat-NN/` **before the next run overwrites them**, plus `DIR/run.json` recording the APK SHA-256, the arm and the environment. |
|
||||
|
||||
@@ -485,7 +495,7 @@ python3 tools/trace_replay/run_tcp_matrix.py \
|
||||
`--runner` overrides the catalog runner; fixture paths default to the catalog's
|
||||
fixture directory and can be overridden with `--fixtures`. Repeat `--case` or
|
||||
`--backend` to select a subset. The default is the manifest's CI split subset
|
||||
(currently 39 cases), honoring each case's backend list. Explicit `--case
|
||||
(currently 40 cases), honoring each case's backend list. Explicit `--case
|
||||
minecraft-1.21.4-rd12-odinlite-in-world` permits that non-CI workload separately;
|
||||
an explicit `split:false` always remains excluded.
|
||||
|
||||
@@ -497,7 +507,7 @@ per backend and the rest is deliberately shared:
|
||||
|
||||
- **selection** - a case is planned for a backend only if its own `ci_backends`
|
||||
names it, so `minecraft-1.21.4-fabric-iris-iterationrp-in-world` appears in a
|
||||
DirectVulkan sweep (39 cases) and not in a DirectGLES one (38). A `--case` named
|
||||
DirectVulkan sweep (40 cases) and not in a DirectGLES one (39). A `--case` named
|
||||
explicitly that does not run a requested backend is a refusal that names the
|
||||
case, not one plan fewer and no message;
|
||||
- **golden and threshold** - from `backend_overrides.<backend>` in
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -256,6 +256,13 @@
|
||||
"golden": "minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world.0000115019.png",
|
||||
"target_call": 115019
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.20.1-fabric-iris-iterationt-in-world",
|
||||
"trace_archive": "minecraft-1.20.1-fabric-iris-iterationt-in-world.tgz",
|
||||
"golden": "minecraft-1.20.1-fabric-iris-iterationt-in-world.0000375557-linux-mesa.png",
|
||||
"alternate_golden": "minecraft-1.20.1-fabric-iris-iterationt-in-world.0000375557.png",
|
||||
"target_call": 375557
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.21.4-fabric-iris-photon-v1.1-in-world",
|
||||
"trace_archive": "minecraft-1.21.4-fabric-iris-photon-v1.1-in-world.tgz",
|
||||
|
||||
Reference in New Issue
Block a user