mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Refactor] (Program): extract ProgramArtifacts.h - move TypeFacts, ResourceReflection, XfbVarying, LinkArtifacts and SpirvArtifacts to namespace scope with in-class aliases so every existing spelling compiles unchanged; pure move
- P0.5 of the MGPipe disaggregation (ROADMAP P0.5, ARCHITECTURE.md:260): the five reflection types a link produces now live in a header that includes only <Includes.h> and <set>, so a future server-side consumer can name them without dragging ShaderObject.h / SpvcSession.h / the transpiler behind it. - Struct bodies move verbatim, comments included, re-indented one level; no field is added, removed, reordered or re-typed. The two glslang-typed members (LinkArtifacts::program, uniformInitialValues) stay as they are (B.0 D5); the comment mentions of glslang types are respelled without the scope token so the include-closure gate's "glslang:: exactly twice" limit holds. - kInvalidUniformOffset moves to namespace scope (SpirvArtifacts defaults to it); ProgramObject::kInvalidUniformOffset is defined from it, so the two cannot drift. - ProgramObject keeps in-class aliases (fully qualified on the right-hand side) for all nine names, so none of the 8 includers nor any ProgramObject::X spelling changes. - ProgramArtifactsTest pins the aliases as the same types (is_same_v) and TypeFacts as a 44-byte POD; its first include is the new header, so it is also the proof that the header is self-contained.
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.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> // String/Vector/Array/UnorderedMap/SharedPtr + GL enums. DEBT NOTE (MGPipeTypes.h:24-31 style):
|
||||
// Includes.h:80-84 still pulls glslang and :59 spirv_cross_c.h; this header is glslang-free BY
|
||||
// SYMBOL (what P7's `nm -D | grep glslang` measures), not by preprocessed text. A textually
|
||||
// glslang-free closure needs MG_Util/Types.h split off Includes.h (Types.h:11 includes it
|
||||
// back) - out of scope for P0.5.
|
||||
#include <set> // std::set<String> (LinkArtifacts); NOT provided by Includes.h on its own terms
|
||||
// PURITY: no ShaderObject.h, no SpvcSession.h, nothing under MG_Util/ShaderTranspiler/, no Config.h,
|
||||
// no MG_Backend/, no BufferState/. scripts/check_include_closure.py probe "artifacts-header" (ROADMAP P0.5;
|
||||
// ARCHITECTURE.md:260) asserts that closure. the glslang scope token appears exactly twice below (B.0 D5: the two
|
||||
// glslang-typed LinkArtifacts members moved verbatim) and the gate pins that count.
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// Sentinel for a uniform location without global-UBO backing storage (should not
|
||||
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
||||
// Namespace scope so SpirvArtifacts::reservedNumSamplesOffset can default to it;
|
||||
// ProgramObject::kInvalidUniformOffset is defined from this one.
|
||||
inline constexpr Uint kInvalidUniformOffset = ~0u;
|
||||
|
||||
// Everything the query surface ever asked a glslang TType, flattened. Twenty
|
||||
// predicates, no recursion: nothing post-link ever walks a struct, a type name or the
|
||||
// AST, so a POD covers the whole surface exactly.
|
||||
struct TypeFacts {
|
||||
Bool isArray = false;
|
||||
// A runtime-sized array (a storage block's unsized trailing member) is an array
|
||||
// that is NOT sized; GL_ARRAY_SIZE reports 0 for it.
|
||||
Bool isSizedArray = false;
|
||||
Bool isMatrix = false;
|
||||
Bool isVector = false;
|
||||
Bool isOpaque = false;
|
||||
Bool isTexture = false;
|
||||
Bool isImage = false;
|
||||
Bool isDouble = false; // getBasicType() == EbtDouble
|
||||
Bool isVoid = false; // getBasicType() == EbtVoid (hidden block members)
|
||||
Bool isBuffer = false; // getQualifier().storage == EvqBuffer
|
||||
Bool isPatch = false; // getQualifier().patch
|
||||
Bool hasIndex = false; // getQualifier().hasIndex()
|
||||
Bool hasFormat = false; // getQualifier().hasFormat()
|
||||
Int vectorSize = 0;
|
||||
Int matrixCols = 0;
|
||||
Int matrixRows = 0;
|
||||
Int layoutIndex = 0; // getQualifier().layoutIndex
|
||||
Uint layoutFormat = 0; // getQualifier().getFormat()
|
||||
// glslang TLayoutMatrix, widened. For a uniform this is already RESOLVED against
|
||||
// the owning block's qualifier, so the getUniformBlock() fallback the old
|
||||
// accessors carried is gone.
|
||||
Int layoutMatrix = 0;
|
||||
// glslang TBasicType, widened - ApplyUniformInitialValues and the typed
|
||||
// glGetUniform* paths compare against a handful of enumerators.
|
||||
Int basicType = 0;
|
||||
};
|
||||
|
||||
// One glslang TObjectReflection, flattened. Used for uniforms, blocks, pipe inputs
|
||||
// and pipe outputs alike, because glslang reflects all four as TObjectReflection.
|
||||
struct ResourceReflection {
|
||||
String name;
|
||||
GLenum glDefineType = 0;
|
||||
Int offset = -1;
|
||||
// TObjectReflection::size, RAW. For a uniform prefer `arraySize` below, which is
|
||||
// the resolved GL_UNIFORM_SIZE answer.
|
||||
Int size = 0;
|
||||
// TObjectReflection::index - for a uniform, the TPROGRAM block index owning it
|
||||
// (-1 for a default-block one; translate with GlBlockIndexFromTProgram).
|
||||
Int index = -1;
|
||||
Int counterIndex = -1;
|
||||
Int arrayStride = 0;
|
||||
Int topLevelArraySize = 0;
|
||||
Int topLevelArrayStride = 0;
|
||||
Int binding = -1;
|
||||
Int location = -1; // layoutLocation()
|
||||
// EShLanguageMask of the stages that reference it; 0 means "declared but read by
|
||||
// nobody", which is what the dead-default-block-uniform filter tests.
|
||||
Uint32 stages = 0;
|
||||
// GL_UNIFORM_SIZE / GL_ARRAY_SIZE, already resolved through the
|
||||
// isSizedArray()/getOuterArraySize()/size fallback.
|
||||
GLint arraySize = 1;
|
||||
TypeFacts type;
|
||||
};
|
||||
|
||||
using UniformReflection = ResourceReflection;
|
||||
using BlockReflection = ResourceReflection;
|
||||
using PipeInputReflection = ResourceReflection;
|
||||
using PipeOutputReflection = ResourceReflection;
|
||||
|
||||
// Transform feedback (GL 3.0 core: glTransformFeedbackVaryings applies on
|
||||
// the NEXT link; the linked snapshot below is what draws and queries see).
|
||||
struct XfbVarying {
|
||||
String name;
|
||||
GLenum type = GL_FLOAT;
|
||||
GLint size = 1; // array element count
|
||||
Uint32 bufferIndex = 0; // capture buffer slot
|
||||
Uint32 offsetBytes = 0; // offset within the capture buffer
|
||||
Uint32 byteSize = 0; // bytes captured per vertex for this varying
|
||||
// Offset within the gap-free record a backend that cannot express the GL
|
||||
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
||||
Uint32 packedOffsetBytes = 0;
|
||||
|
||||
// GL 4.6 core 11.1.2.1 / 7.3.1.1: a member of an output interface block is
|
||||
// captured under "<block name>.<member>". `name` keeps that GL spelling (it is
|
||||
// what the interface queries and the ESSL backend's driver-side capture list
|
||||
// need, since SPIRV-Cross re-emits the block under its own type name), while
|
||||
// the three fields below carry what a SPIR-V backend needs instead: the
|
||||
// decoration target is the block's *instance* variable and the member index
|
||||
// inside it. blockMemberIndex < 0 means "not a block member".
|
||||
String blockInstanceName;
|
||||
String blockName;
|
||||
Int blockMemberIndex = -1;
|
||||
// Which element of an arrayed block member this capture names, -1 for "the
|
||||
// member as a whole". SPIR-V cannot decorate a single array element, so a
|
||||
// backend needs the element index to tell a full run from a partial one.
|
||||
Int blockMemberElement = -1;
|
||||
};
|
||||
|
||||
// ---- P1: everything a link PRODUCES, in one movable block ----
|
||||
//
|
||||
// The membership rule is mechanical, not editorial: this is exactly the field list
|
||||
// ResetLinkArtifacts() clears (plus the four it forgot to - infoLog,
|
||||
// linkedFragDataLocation/Index and the geometry strip-capture pair - which are just
|
||||
// as much link output). Nothing else belongs here.
|
||||
//
|
||||
// Why a struct: once glLinkProgram runs on a worker (P1 stage 4) the worker writes
|
||||
// its OWN LinkArtifacts and the GL thread publishes it with a single move, instead
|
||||
// of thirty cross-thread field assignments. Until then this is a pure refactor.
|
||||
//
|
||||
// Access rule (invariant I5): the member below is private and reachable ONLY
|
||||
// through ProgramObject::Artifacts(), which calls EnsureLinkJoined() first. That is
|
||||
// what makes "every read of link output joins the pending link" a property the
|
||||
// compiler checks rather than a review item - a new reader cannot spell the field
|
||||
// without going through the gate. m_artifacts lives in ProgramObject and is private
|
||||
// there; the type being namespace-scope changes nothing about that gate.
|
||||
// ---- the owned mirror of glslang's reflection ----
|
||||
//
|
||||
// WHY THIS EXISTS. Every GL query about a linked program used to be answered by
|
||||
// asking the live glslang TProgram - program->getUniform(i).getType()->isMatrix()
|
||||
// and friends. That made the TProgram part of the program's PERMANENT state, which
|
||||
// in turn made the whole front end (parse + link) unskippable: the L1 shader
|
||||
// translation memo could hand back the SPIR-V but the reflection still had to be
|
||||
// rebuilt from a freshly parsed AST.
|
||||
//
|
||||
// These three tables are a snapshot of everything the query surface ever reads off
|
||||
// the TProgram, in PLAIN OWNED VALUES - no TType*, no TString, nothing pointing into
|
||||
// a glslang pool. Taken once at the tail of DoReflection (SnapshotGlslangReflection),
|
||||
// they are copyable, immutable after the link, and safe to memoize and share between
|
||||
// ProgramObjects and threads. Once they are filled, `program` is dead weight to
|
||||
// everything except DoReflection itself.
|
||||
//
|
||||
// INDEXED BY TPROGRAM INDEX, deliberately: that is the space uniformIndexInTProgram,
|
||||
// glUniformIndexToTProgram and tProgramUniformIndexToGl already speak, so every
|
||||
// accessor that used to call program->getUniform(i) indexes uniformReflection[i]
|
||||
// instead, unchanged in every other respect.
|
||||
|
||||
struct LinkArtifacts {
|
||||
// Live only between LinkProgram() and the end of DoReflection. Everything after
|
||||
// that reads the owned mirror below; a link served from the L1 memo never
|
||||
// constructs one at all, so this is null for such a program and MUST NOT be
|
||||
// dereferenced outside DoReflection.
|
||||
SharedPtr<glslang::TProgram> program;
|
||||
|
||||
// The owned reflection snapshot. Indexed by TProgram index; see the structs above.
|
||||
Vector<UniformReflection> uniformReflection;
|
||||
Vector<BlockReflection> blockReflection;
|
||||
Vector<PipeInputReflection> pipeInputReflection;
|
||||
Vector<PipeOutputReflection> pipeOutputReflection;
|
||||
// Program-level scalars glslang answers off the linked intermediates.
|
||||
// Whether the program's LAST stage is the fragment stage. A color number - and so a
|
||||
// color index - exists only there; a separable tess/geometry/vertex program's
|
||||
// outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
|
||||
// separate-programs-tess-control).
|
||||
Bool lastStageIsFragment = false;
|
||||
Array<GLuint, 3> computeLocalSize{};
|
||||
// Replaces program->getUniformIndex(name). Maps the reflected name to its
|
||||
// TProgram uniform index.
|
||||
UnorderedMap<String, Int> uniformIndexByName;
|
||||
|
||||
// Attributes (Vertex in)
|
||||
Vector<String> attribs;
|
||||
Vector<GLenum> attribTypes;
|
||||
|
||||
// FragData (Frag out): the per-link snapshot of the explicit request maps.
|
||||
UnorderedMap<String, Uint> linkedFragDataLocation;
|
||||
UnorderedMap<String, Uint> linkedFragDataIndex;
|
||||
|
||||
// GL-facing index spaces (see the translation helpers above): GL active-uniform
|
||||
// index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram
|
||||
// block index. -1 marks a TProgram entry GL does not expose (dead default-block
|
||||
// uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself).
|
||||
Vector<Int> glUniformIndexToTProgram;
|
||||
Vector<Int> tProgramUniformIndexToGl;
|
||||
Vector<Int> glBlockIndexToTProgram;
|
||||
Vector<Int> tProgramBlockIndexToGl;
|
||||
// GL_UNIFORM_BLOCK index space: ACTUAL uniform blocks only, a strict subsequence of
|
||||
// glBlockIndexToTProgram above.
|
||||
//
|
||||
// That list is the BLOCK space - everything the relaxed parse produced except
|
||||
// MGL_GLOBAL_UBO - and it is what the backends walk and what every block-keyed table
|
||||
// here (uniformBlockBinding, uniformBlockIndexByName, blockReflection ordering) is
|
||||
// indexed by. It is NOT the GL uniform-block list: MobileGL does not pass
|
||||
// EShReflectionSeparateBuffers to buildReflection, so glslang routes BUFFER blocks
|
||||
// through indexToUniformBlock too, and the list therefore also carries every shader
|
||||
// storage block and every synthesized gl_AtomicCounterBlock_N. GL 4.6 core 7.6 gives
|
||||
// those their own enumerations (GL_SHADER_STORAGE_BLOCK and
|
||||
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS respectively), and GL_ACTIVE_UNIFORM_BLOCKS /
|
||||
// glGetActiveUniformBlock*/glGetUniformBlockIndex must not see either.
|
||||
//
|
||||
// Kept as a SECOND space rather than filtering the first in place: DirectGLES assigns
|
||||
// one ESSL uniform-buffer binding point per entry of the block list as it walks it
|
||||
// (Managers.cpp CacheResourceLocations and the matching per-draw loop in
|
||||
// DirectGLES.cpp), so compacting that list would renumber every backend binding
|
||||
// point, and tProgramBlockIndexToGl[i] < 0 is what DoReflection and
|
||||
// BuildGlobalUboRouting read as "member of the synthesized global UBO".
|
||||
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
|
||||
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
|
||||
// Per-link merged snapshot of the layout(location = N) qualifiers the attached
|
||||
// shaders' default-block uniforms declared, as glslang recorded them at the point
|
||||
// its relaxed remap dropped them (the relaxed parse drops them from reflection; the
|
||||
// DoReflection assigner restores them from here).
|
||||
UnorderedMap<String, Int> linkedExplicitUniformLocations;
|
||||
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
|
||||
// declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform
|
||||
// reads until the application overwrites it, and relinking restores it - but the
|
||||
// relaxed parse turns those uniforms into members of MGL_GLOBAL_UBO, where SPIR-V
|
||||
// cannot carry an initializer, so the value only survives as this side-channel.
|
||||
// Applied into the uniform shadow at the phase-B publish (ApplyUniformInitialValues).
|
||||
Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;
|
||||
UnorderedMap<String, Uint> uniformLocations;
|
||||
// ---- "written since link" (see MarkUniformWrittenAtLocation) ----
|
||||
// In LinkArtifacts deliberately: a link is exactly the event that retracts every
|
||||
// write (GL resets uniforms to their initial values), so living here means the set
|
||||
// is cleared by the same three paths that clear the rest of a link's output -
|
||||
// Link()'s whole-struct reset, ResetLinkArtifacts, and the publish's move - and no
|
||||
// fourth reset site can be forgotten. Empty (and never allocated) for a program
|
||||
// that never asked to be separable.
|
||||
Vector<Uint64> writtenUniformLocationBits;
|
||||
Vector<Uint64> writtenUniformIndexBits;
|
||||
Vector<Uint> writtenUniformIndices;
|
||||
// Ordered by location,
|
||||
// aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
|
||||
Vector<Int> uniformIndexInTProgram;
|
||||
// ditto. Will be set at glUniform1i
|
||||
Vector<Int> uniformSamplerOrImageUnitIndex;
|
||||
// Sampler/image layout(binding = N) initial texture/image units, captured by
|
||||
// TMglGlslIoResolver at mapIO's collect callback - the last point at which the
|
||||
// qualifier still says what the shader declared. An OUTPUT of the link, not an
|
||||
// input to it: nothing supplies this map, the resolver fills it.
|
||||
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
|
||||
|
||||
// Ordered by uniform block index
|
||||
// index is DIFFERENT from binding!!!
|
||||
//
|
||||
// Let's define UniformBlockIndex == the order at glslang getUniformBlock()
|
||||
// aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies:
|
||||
// `prog->getUniformBlock(i) == "BlockName"`
|
||||
// These stuff are present for GL semantics, not for backend inspection
|
||||
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
|
||||
UnorderedMap<String, Uint> uniformBlockIndexByName;
|
||||
Vector<Int> uniformBlockBinding;
|
||||
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
|
||||
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||
//
|
||||
// ALSO SEEDED AT LINK, by ProgramLinkTask::SeedDefaultStorageBlockBindings, with the
|
||||
// GL-mandated binding 0 for every storage block whose shader declared no
|
||||
// layout(binding = N). Those blocks have no other way to be told apart from a block
|
||||
// that declared one: glslang's IO mapper invents a binding and writes it into the
|
||||
// qualifier, so the reflection reports the invention. A seed is therefore "GL's
|
||||
// default binding for this block", and a later glShaderStorageBlockBinding simply
|
||||
// overwrites it - default and rebind travel one path.
|
||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||
// Block type names of the storage blocks the program's shaders declared with NO
|
||||
// layout(binding = N). Input to the seeding above; filled during mapIO by
|
||||
// TMglGlslIoResolver, which is the last observer that can still tell a declared
|
||||
// binding from an invented one - and, unlike the per-shader lexer this replaced,
|
||||
// sees the declaration with its macros expanded.
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
// The same list for UNIFORM blocks, and it is needed for the same reason: glslang's
|
||||
// auto-mapper assigns every uniform block a binding whether or not the shader asked
|
||||
// for one, so uniformBlockBinding below cannot tell "declared 1" from "invented 1".
|
||||
// GL 4.6 core 7.6.2 requires an unqualified block to report ZERO.
|
||||
std::set<String> uniformBlocksWithoutBinding;
|
||||
|
||||
Uint activeUniformCount = 0;
|
||||
// This program's fragment stage read gl_NumSamples, so the source pipeline lowered it
|
||||
// onto the reserved default-block uniform (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)
|
||||
// and the draw path owes it the draw framebuffer's sample count before every draw.
|
||||
//
|
||||
// PHASE A on purpose, even though the byte offset it needs is phase-B output: the
|
||||
// gate has to be answerable without joining the SPIR-V job, or every draw of every
|
||||
// program would pay a join to discover it has nothing to write.
|
||||
Bool usesReservedNumSamples = false;
|
||||
Uint maxUniformLocation = 0;
|
||||
Int uniformNameMaxLength = 0;
|
||||
Int attribInNameMaxLength = 0;
|
||||
Int uniformBlockNameMaxLength = 0;
|
||||
|
||||
String infoLog;
|
||||
Bool linkStatus = false;
|
||||
|
||||
// Transform feedback: the linked snapshot (the request lives outside, on the
|
||||
// GL-thread-owned side).
|
||||
Vector<XfbVarying> xfbVaryings;
|
||||
// The glTransformFeedbackVaryings request list exactly as this link consumed it,
|
||||
// INCLUDING the gl_NextBuffer / gl_SkipComponentsN pseudo-varyings that
|
||||
// xfbVaryings deliberately drops (they steer the capture layout and must never
|
||||
// reach a backend's varying list). GL_TRANSFORM_FEEDBACK_VARYING enumerates the
|
||||
// full request, pseudo-varyings and all, so the interface query needs its own copy.
|
||||
Vector<String> xfbInterfaceNames;
|
||||
Vector<Uint32> xfbStrides;
|
||||
Vector<Uint32> gsStripTriangles;
|
||||
Bool gsStripCaptureFixup = false;
|
||||
GLenum gsInputPrimitive = GL_NONE;
|
||||
// GL_TESS_CONTROL_OUTPUT_VERTICES: the `layout(vertices = N) out` of the linked
|
||||
// tessellation control stage, or 0 when the program has none. Checked against
|
||||
// GL_MAX_PATCH_VERTICES at link (GL 4.6 core 11.2.1.1).
|
||||
Int tcsOutputVertices = 0;
|
||||
// The rest of the geometry stage's link properties, and the tessellation evaluation
|
||||
// stage's. Every one of these is a glGetProgramiv answer that had no source at all:
|
||||
// the query surface listed the geometry pnames only to fall through to
|
||||
// GL_INVALID_ENUM, and the GL_TESS_GEN_* pnames were not mentioned anywhere. They
|
||||
// come from the linked intermediates for the same reason gsInputPrimitive and
|
||||
// tcsOutputVertices do - glslang has already merged the compilation units' layout
|
||||
// qualifiers and diagnosed contradictions, so the linked program is the thing that
|
||||
// knows.
|
||||
GLenum gsOutputPrimitive = GL_NONE;
|
||||
Int gsMaxVertices = 0;
|
||||
Int gsInvocations = 0;
|
||||
// The tessellation evaluation stage's layout: GL_QUADS / GL_TRIANGLES / GL_ISOLINES,
|
||||
// GL_EQUAL / GL_FRACTIONAL_EVEN / GL_FRACTIONAL_ODD, GL_CW / GL_CCW, and point mode.
|
||||
GLenum tessGenMode = GL_NONE;
|
||||
GLenum tessGenSpacing = GL_NONE;
|
||||
GLenum tessGenVertexOrder = GL_NONE;
|
||||
Bool tessGenPointMode = false;
|
||||
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
Int xfbVaryingNameMaxLength = 0;
|
||||
Bool xfbNeedsScatteredCapture = false;
|
||||
Uint32 xfbPackedStride = 0;
|
||||
};
|
||||
|
||||
// ---- everything phase B of a link produces, in one movable block ----
|
||||
//
|
||||
// The membership rule is the same mechanical one LinkArtifacts uses: this is exactly
|
||||
// what ProgramSpirvTask writes, which is what makes moving it THE publish. It is
|
||||
// deliberately NOT part of LinkArtifacts, and that separation is what routes the five
|
||||
// readers of SPIR-V-derived data through their own join gate by compiler rather than
|
||||
// by review - m_spirv is private and Spirv() is the only spelling that reaches it.
|
||||
//
|
||||
// Why these three and nothing else: `generatedSpirv` has no GL-thread reader at all
|
||||
// (every consumer is a backend draw/prepare path), and `uniformOffsets` +
|
||||
// `globalUboScratch` are the ONLY things glUniform*/glGetUniform* need that are
|
||||
// derived from the OPTIMIZED SPIR-V rather than from glslang reflection - spirv-opt
|
||||
// runs in place and can delete a uniform, or the whole global UBO, so the offsets
|
||||
// cannot be lifted out of glslang's reflection instead.
|
||||
struct SpirvArtifacts {
|
||||
Vector<Vector<unsigned>> generatedSpirv;
|
||||
Bool enableSpirvValidation = false;
|
||||
// Byte offset of each uniform location inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
||||
Vector<Uint> uniformOffsets;
|
||||
Vector<Uint8> globalUboScratch;
|
||||
// Byte offset of the reserved gl_NumSamples stand-in inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Taken by NAME from the SPIR-V metadata rather than through
|
||||
// uniformOffsets, because the member has no GL location at all: the link task keeps
|
||||
// it out of the GL-visible uniform index space so no application can see or write it.
|
||||
Uint reservedNumSamplesOffset = kInvalidUniformOffset;
|
||||
// False for a program whose SPIR-V was never produced (phase B cancelled at
|
||||
// teardown or by a relink) or whose optimizer run failed. GL has no way to
|
||||
// retract a LINK_STATUS it already reported true, so such a program stays
|
||||
// "linked" and every reflection answer it has given stays correct - it is simply
|
||||
// not drawable, which the backends already express through their link-status
|
||||
// gates.
|
||||
Bool spirvStatus = false;
|
||||
// Whether these modules KEPT their 64-bit floats instead of being narrowed to 32
|
||||
// (ShaderTranspiler::DemoteFloat64Pass). Decided per PROGRAM, never per module - the
|
||||
// global UBO is one buffer all stages read, so two stages disagreeing about whether a
|
||||
// `uniform double` occupies 4 or 8 bytes would put every uniform after it at a
|
||||
// different offset in each. Recorded here rather than re-derived from the backend
|
||||
// because it is the layout THESE modules were built with: it is what the routing
|
||||
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
|
||||
// width the shader actually declares.
|
||||
Bool nativeFloat64 = false;
|
||||
// Whether gl_PointSize was demoted out of THESE modules' tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) because the backend cannot host
|
||||
// the built-in there. Per PROGRAM by construction - a consumer whose producer
|
||||
// kept the built-in would read garbage - and recorded here rather than
|
||||
// re-derived because it cannot be: the rewrite's whole point is that the final
|
||||
// bytes no longer declare the capability that armed it. The backends read it to
|
||||
// respell a "gl_PointSize" transform-feedback capture as the carrier
|
||||
// (ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME). The GL reflection surface
|
||||
// deliberately keeps answering "gl_PointSize": demotion happens after phase A,
|
||||
// so every query keeps the truthful GL spelling.
|
||||
Bool pointSizeDemoted = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "ShaderObject.h"
|
||||
#include "ProgramArtifacts.h"
|
||||
|
||||
#include <MG_Util/Metrics/BufferMetrics.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
@@ -38,70 +39,19 @@ namespace MobileGL::MG_State::GLState {
|
||||
// 1024 GL 4.3 requires.
|
||||
static constexpr Int MAX_UNIFORM_LOCATIONS = static_cast<Int>(glslang::TQualifier::layoutLocationEnd);
|
||||
|
||||
// Everything the query surface ever asked a glslang::TType, flattened. Twenty
|
||||
// predicates, no recursion: nothing post-link ever walks a struct, a type name or the
|
||||
// AST, so a POD covers the whole surface exactly.
|
||||
struct TypeFacts {
|
||||
Bool isArray = false;
|
||||
// A runtime-sized array (a storage block's unsized trailing member) is an array
|
||||
// that is NOT sized; GL_ARRAY_SIZE reports 0 for it.
|
||||
Bool isSizedArray = false;
|
||||
Bool isMatrix = false;
|
||||
Bool isVector = false;
|
||||
Bool isOpaque = false;
|
||||
Bool isTexture = false;
|
||||
Bool isImage = false;
|
||||
Bool isDouble = false; // getBasicType() == EbtDouble
|
||||
Bool isVoid = false; // getBasicType() == EbtVoid (hidden block members)
|
||||
Bool isBuffer = false; // getQualifier().storage == EvqBuffer
|
||||
Bool isPatch = false; // getQualifier().patch
|
||||
Bool hasIndex = false; // getQualifier().hasIndex()
|
||||
Bool hasFormat = false; // getQualifier().hasFormat()
|
||||
Int vectorSize = 0;
|
||||
Int matrixCols = 0;
|
||||
Int matrixRows = 0;
|
||||
Int layoutIndex = 0; // getQualifier().layoutIndex
|
||||
Uint layoutFormat = 0; // getQualifier().getFormat()
|
||||
// glslang::TLayoutMatrix, widened. For a uniform this is already RESOLVED against
|
||||
// the owning block's qualifier, so the getUniformBlock() fallback the old
|
||||
// accessors carried is gone.
|
||||
Int layoutMatrix = 0;
|
||||
// glslang::TBasicType, widened - ApplyUniformInitialValues and the typed
|
||||
// glGetUniform* paths compare against a handful of enumerators.
|
||||
Int basicType = 0;
|
||||
};
|
||||
|
||||
// One glslang::TObjectReflection, flattened. Used for uniforms, blocks, pipe inputs
|
||||
// and pipe outputs alike, because glslang reflects all four as TObjectReflection.
|
||||
struct ResourceReflection {
|
||||
String name;
|
||||
GLenum glDefineType = 0;
|
||||
Int offset = -1;
|
||||
// TObjectReflection::size, RAW. For a uniform prefer `arraySize` below, which is
|
||||
// the resolved GL_UNIFORM_SIZE answer.
|
||||
Int size = 0;
|
||||
// TObjectReflection::index - for a uniform, the TPROGRAM block index owning it
|
||||
// (-1 for a default-block one; translate with GlBlockIndexFromTProgram).
|
||||
Int index = -1;
|
||||
Int counterIndex = -1;
|
||||
Int arrayStride = 0;
|
||||
Int topLevelArraySize = 0;
|
||||
Int topLevelArrayStride = 0;
|
||||
Int binding = -1;
|
||||
Int location = -1; // layoutLocation()
|
||||
// EShLanguageMask of the stages that reference it; 0 means "declared but read by
|
||||
// nobody", which is what the dead-default-block-uniform filter tests.
|
||||
Uint32 stages = 0;
|
||||
// GL_UNIFORM_SIZE / GL_ARRAY_SIZE, already resolved through the
|
||||
// isSizedArray()/getOuterArraySize()/size fallback.
|
||||
GLint arraySize = 1;
|
||||
TypeFacts type;
|
||||
};
|
||||
|
||||
using UniformReflection = ResourceReflection;
|
||||
using BlockReflection = ResourceReflection;
|
||||
using PipeInputReflection = ResourceReflection;
|
||||
// The five reflection/artifact types live at namespace scope in ProgramArtifacts.h
|
||||
// (P0.5). Re-exported here so every existing spelling (ProgramObject::LinkArtifacts,
|
||||
// ProgramObject::TypeFacts, ...) compiles unchanged. Fully qualified on the right-hand
|
||||
// side on purpose: an unqualified `TypeFacts` would name the alias being declared.
|
||||
using TypeFacts = MobileGL::MG_State::GLState::TypeFacts;
|
||||
using ResourceReflection = MobileGL::MG_State::GLState::ResourceReflection;
|
||||
using UniformReflection = ResourceReflection;
|
||||
using BlockReflection = ResourceReflection;
|
||||
using PipeInputReflection = ResourceReflection;
|
||||
using PipeOutputReflection = ResourceReflection;
|
||||
using XfbVarying = MobileGL::MG_State::GLState::XfbVarying;
|
||||
using LinkArtifacts = MobileGL::MG_State::GLState::LinkArtifacts;
|
||||
using SpirvArtifacts = MobileGL::MG_State::GLState::SpirvArtifacts;
|
||||
|
||||
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
|
||||
// Cancel-not-join, exactly like ~ShaderObject: the link job owns its inputs, so an
|
||||
@@ -544,7 +494,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
// Sentinel for a uniform location without global-UBO backing storage (should not
|
||||
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
||||
static constexpr Uint kInvalidUniformOffset = ~0u;
|
||||
static constexpr Uint kInvalidUniformOffset = MobileGL::MG_State::GLState::kInvalidUniformOffset;
|
||||
// PHASE B (joins the SPIR-V job; see EnsureSpirvJoined).
|
||||
//
|
||||
// BOUNDS-CHECKED, and that is not defensive padding - it is the load-bearing half of
|
||||
@@ -1141,313 +1091,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
return it == m_shaders.end() ? -1 : (Int)std::distance(m_shaders.begin(), it);
|
||||
}
|
||||
|
||||
// Transform feedback (GL 3.0 core: glTransformFeedbackVaryings applies on
|
||||
// the NEXT link; the linked snapshot below is what draws and queries see).
|
||||
struct XfbVarying {
|
||||
String name;
|
||||
GLenum type = GL_FLOAT;
|
||||
GLint size = 1; // array element count
|
||||
Uint32 bufferIndex = 0; // capture buffer slot
|
||||
Uint32 offsetBytes = 0; // offset within the capture buffer
|
||||
Uint32 byteSize = 0; // bytes captured per vertex for this varying
|
||||
// Offset within the gap-free record a backend that cannot express the GL
|
||||
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
||||
Uint32 packedOffsetBytes = 0;
|
||||
|
||||
// GL 4.6 core 11.1.2.1 / 7.3.1.1: a member of an output interface block is
|
||||
// captured under "<block name>.<member>". `name` keeps that GL spelling (it is
|
||||
// what the interface queries and the ESSL backend's driver-side capture list
|
||||
// need, since SPIRV-Cross re-emits the block under its own type name), while
|
||||
// the three fields below carry what a SPIR-V backend needs instead: the
|
||||
// decoration target is the block's *instance* variable and the member index
|
||||
// inside it. blockMemberIndex < 0 means "not a block member".
|
||||
String blockInstanceName;
|
||||
String blockName;
|
||||
Int blockMemberIndex = -1;
|
||||
// Which element of an arrayed block member this capture names, -1 for "the
|
||||
// member as a whole". SPIR-V cannot decorate a single array element, so a
|
||||
// backend needs the element index to tell a full run from a partial one.
|
||||
Int blockMemberElement = -1;
|
||||
};
|
||||
|
||||
// ---- P1: everything a link PRODUCES, in one movable block ----
|
||||
//
|
||||
// The membership rule is mechanical, not editorial: this is exactly the field list
|
||||
// ResetLinkArtifacts() clears (plus the four it forgot to - infoLog,
|
||||
// linkedFragDataLocation/Index and the geometry strip-capture pair - which are just
|
||||
// as much link output). Nothing else belongs here.
|
||||
//
|
||||
// Why a struct: once glLinkProgram runs on a worker (P1 stage 4) the worker writes
|
||||
// its OWN LinkArtifacts and the GL thread publishes it with a single move, instead
|
||||
// of thirty cross-thread field assignments. Until then this is a pure refactor.
|
||||
//
|
||||
// Access rule (invariant I5): the member below is private and reachable ONLY
|
||||
// through ProgramObject::Artifacts(), which calls EnsureLinkJoined() first. That is
|
||||
// what makes "every read of link output joins the pending link" a property the
|
||||
// compiler checks rather than a review item - a new reader cannot spell the field
|
||||
// without going through the gate.
|
||||
// ---- the owned mirror of glslang's reflection ----
|
||||
//
|
||||
// WHY THIS EXISTS. Every GL query about a linked program used to be answered by
|
||||
// asking the live glslang::TProgram - program->getUniform(i).getType()->isMatrix()
|
||||
// and friends. That made the TProgram part of the program's PERMANENT state, which
|
||||
// in turn made the whole front end (parse + link) unskippable: the L1 shader
|
||||
// translation memo could hand back the SPIR-V but the reflection still had to be
|
||||
// rebuilt from a freshly parsed AST.
|
||||
//
|
||||
// These three tables are a snapshot of everything the query surface ever reads off
|
||||
// the TProgram, in PLAIN OWNED VALUES - no TType*, no TString, nothing pointing into
|
||||
// a glslang pool. Taken once at the tail of DoReflection (SnapshotGlslangReflection),
|
||||
// they are copyable, immutable after the link, and safe to memoize and share between
|
||||
// ProgramObjects and threads. Once they are filled, `program` is dead weight to
|
||||
// everything except DoReflection itself.
|
||||
//
|
||||
// INDEXED BY TPROGRAM INDEX, deliberately: that is the space uniformIndexInTProgram,
|
||||
// glUniformIndexToTProgram and tProgramUniformIndexToGl already speak, so every
|
||||
// accessor that used to call program->getUniform(i) indexes uniformReflection[i]
|
||||
// instead, unchanged in every other respect.
|
||||
|
||||
struct LinkArtifacts {
|
||||
// Live only between LinkProgram() and the end of DoReflection. Everything after
|
||||
// that reads the owned mirror below; a link served from the L1 memo never
|
||||
// constructs one at all, so this is null for such a program and MUST NOT be
|
||||
// dereferenced outside DoReflection.
|
||||
SharedPtr<glslang::TProgram> program;
|
||||
|
||||
// The owned reflection snapshot. Indexed by TProgram index; see the structs above.
|
||||
Vector<UniformReflection> uniformReflection;
|
||||
Vector<BlockReflection> blockReflection;
|
||||
Vector<PipeInputReflection> pipeInputReflection;
|
||||
Vector<PipeOutputReflection> pipeOutputReflection;
|
||||
// Program-level scalars glslang answers off the linked intermediates.
|
||||
// Whether the program's LAST stage is the fragment stage. A color number - and so a
|
||||
// color index - exists only there; a separable tess/geometry/vertex program's
|
||||
// outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
|
||||
// separate-programs-tess-control).
|
||||
Bool lastStageIsFragment = false;
|
||||
Array<GLuint, 3> computeLocalSize{};
|
||||
// Replaces program->getUniformIndex(name). Maps the reflected name to its
|
||||
// TProgram uniform index.
|
||||
UnorderedMap<String, Int> uniformIndexByName;
|
||||
|
||||
// Attributes (Vertex in)
|
||||
Vector<String> attribs;
|
||||
Vector<GLenum> attribTypes;
|
||||
|
||||
// FragData (Frag out): the per-link snapshot of the explicit request maps.
|
||||
UnorderedMap<String, Uint> linkedFragDataLocation;
|
||||
UnorderedMap<String, Uint> linkedFragDataIndex;
|
||||
|
||||
// GL-facing index spaces (see the translation helpers above): GL active-uniform
|
||||
// index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram
|
||||
// block index. -1 marks a TProgram entry GL does not expose (dead default-block
|
||||
// uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself).
|
||||
Vector<Int> glUniformIndexToTProgram;
|
||||
Vector<Int> tProgramUniformIndexToGl;
|
||||
Vector<Int> glBlockIndexToTProgram;
|
||||
Vector<Int> tProgramBlockIndexToGl;
|
||||
// GL_UNIFORM_BLOCK index space: ACTUAL uniform blocks only, a strict subsequence of
|
||||
// glBlockIndexToTProgram above.
|
||||
//
|
||||
// That list is the BLOCK space - everything the relaxed parse produced except
|
||||
// MGL_GLOBAL_UBO - and it is what the backends walk and what every block-keyed table
|
||||
// here (uniformBlockBinding, uniformBlockIndexByName, blockReflection ordering) is
|
||||
// indexed by. It is NOT the GL uniform-block list: MobileGL does not pass
|
||||
// EShReflectionSeparateBuffers to buildReflection, so glslang routes BUFFER blocks
|
||||
// through indexToUniformBlock too, and the list therefore also carries every shader
|
||||
// storage block and every synthesized gl_AtomicCounterBlock_N. GL 4.6 core 7.6 gives
|
||||
// those their own enumerations (GL_SHADER_STORAGE_BLOCK and
|
||||
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS respectively), and GL_ACTIVE_UNIFORM_BLOCKS /
|
||||
// glGetActiveUniformBlock*/glGetUniformBlockIndex must not see either.
|
||||
//
|
||||
// Kept as a SECOND space rather than filtering the first in place: DirectGLES assigns
|
||||
// one ESSL uniform-buffer binding point per entry of the block list as it walks it
|
||||
// (Managers.cpp CacheResourceLocations and the matching per-draw loop in
|
||||
// DirectGLES.cpp), so compacting that list would renumber every backend binding
|
||||
// point, and tProgramBlockIndexToGl[i] < 0 is what DoReflection and
|
||||
// BuildGlobalUboRouting read as "member of the synthesized global UBO".
|
||||
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
|
||||
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
|
||||
// Per-link merged snapshot of the layout(location = N) qualifiers the attached
|
||||
// shaders' default-block uniforms declared, as glslang recorded them at the point
|
||||
// its relaxed remap dropped them (the relaxed parse drops them from reflection; the
|
||||
// DoReflection assigner restores them from here).
|
||||
UnorderedMap<String, Int> linkedExplicitUniformLocations;
|
||||
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
|
||||
// declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform
|
||||
// reads until the application overwrites it, and relinking restores it - but the
|
||||
// relaxed parse turns those uniforms into members of MGL_GLOBAL_UBO, where SPIR-V
|
||||
// cannot carry an initializer, so the value only survives as this side-channel.
|
||||
// Applied into the uniform shadow at the phase-B publish (ApplyUniformInitialValues).
|
||||
Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;
|
||||
UnorderedMap<String, Uint> uniformLocations;
|
||||
// ---- "written since link" (see MarkUniformWrittenAtLocation) ----
|
||||
// In LinkArtifacts deliberately: a link is exactly the event that retracts every
|
||||
// write (GL resets uniforms to their initial values), so living here means the set
|
||||
// is cleared by the same three paths that clear the rest of a link's output -
|
||||
// Link()'s whole-struct reset, ResetLinkArtifacts, and the publish's move - and no
|
||||
// fourth reset site can be forgotten. Empty (and never allocated) for a program
|
||||
// that never asked to be separable.
|
||||
Vector<Uint64> writtenUniformLocationBits;
|
||||
Vector<Uint64> writtenUniformIndexBits;
|
||||
Vector<Uint> writtenUniformIndices;
|
||||
// Ordered by location,
|
||||
// aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
|
||||
Vector<Int> uniformIndexInTProgram;
|
||||
// ditto. Will be set at glUniform1i
|
||||
Vector<Int> uniformSamplerOrImageUnitIndex;
|
||||
// Sampler/image layout(binding = N) initial texture/image units, captured by
|
||||
// TMglGlslIoResolver at mapIO's collect callback - the last point at which the
|
||||
// qualifier still says what the shader declared. An OUTPUT of the link, not an
|
||||
// input to it: nothing supplies this map, the resolver fills it.
|
||||
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
|
||||
|
||||
// Ordered by uniform block index
|
||||
// index is DIFFERENT from binding!!!
|
||||
//
|
||||
// Let's define UniformBlockIndex == the order at glslang getUniformBlock()
|
||||
// aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies:
|
||||
// `prog->getUniformBlock(i) == "BlockName"`
|
||||
// These stuff are present for GL semantics, not for backend inspection
|
||||
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
|
||||
UnorderedMap<String, Uint> uniformBlockIndexByName;
|
||||
Vector<Int> uniformBlockBinding;
|
||||
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
|
||||
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||
//
|
||||
// ALSO SEEDED AT LINK, by ProgramLinkTask::SeedDefaultStorageBlockBindings, with the
|
||||
// GL-mandated binding 0 for every storage block whose shader declared no
|
||||
// layout(binding = N). Those blocks have no other way to be told apart from a block
|
||||
// that declared one: glslang's IO mapper invents a binding and writes it into the
|
||||
// qualifier, so the reflection reports the invention. A seed is therefore "GL's
|
||||
// default binding for this block", and a later glShaderStorageBlockBinding simply
|
||||
// overwrites it - default and rebind travel one path.
|
||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||
// Block type names of the storage blocks the program's shaders declared with NO
|
||||
// layout(binding = N). Input to the seeding above; filled during mapIO by
|
||||
// TMglGlslIoResolver, which is the last observer that can still tell a declared
|
||||
// binding from an invented one - and, unlike the per-shader lexer this replaced,
|
||||
// sees the declaration with its macros expanded.
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
// The same list for UNIFORM blocks, and it is needed for the same reason: glslang's
|
||||
// auto-mapper assigns every uniform block a binding whether or not the shader asked
|
||||
// for one, so uniformBlockBinding below cannot tell "declared 1" from "invented 1".
|
||||
// GL 4.6 core 7.6.2 requires an unqualified block to report ZERO.
|
||||
std::set<String> uniformBlocksWithoutBinding;
|
||||
|
||||
Uint activeUniformCount = 0;
|
||||
// This program's fragment stage read gl_NumSamples, so the source pipeline lowered it
|
||||
// onto the reserved default-block uniform (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)
|
||||
// and the draw path owes it the draw framebuffer's sample count before every draw.
|
||||
//
|
||||
// PHASE A on purpose, even though the byte offset it needs is phase-B output: the
|
||||
// gate has to be answerable without joining the SPIR-V job, or every draw of every
|
||||
// program would pay a join to discover it has nothing to write.
|
||||
Bool usesReservedNumSamples = false;
|
||||
Uint maxUniformLocation = 0;
|
||||
Int uniformNameMaxLength = 0;
|
||||
Int attribInNameMaxLength = 0;
|
||||
Int uniformBlockNameMaxLength = 0;
|
||||
|
||||
String infoLog;
|
||||
Bool linkStatus = false;
|
||||
|
||||
// Transform feedback: the linked snapshot (the request lives outside, on the
|
||||
// GL-thread-owned side).
|
||||
Vector<XfbVarying> xfbVaryings;
|
||||
// The glTransformFeedbackVaryings request list exactly as this link consumed it,
|
||||
// INCLUDING the gl_NextBuffer / gl_SkipComponentsN pseudo-varyings that
|
||||
// xfbVaryings deliberately drops (they steer the capture layout and must never
|
||||
// reach a backend's varying list). GL_TRANSFORM_FEEDBACK_VARYING enumerates the
|
||||
// full request, pseudo-varyings and all, so the interface query needs its own copy.
|
||||
Vector<String> xfbInterfaceNames;
|
||||
Vector<Uint32> xfbStrides;
|
||||
Vector<Uint32> gsStripTriangles;
|
||||
Bool gsStripCaptureFixup = false;
|
||||
GLenum gsInputPrimitive = GL_NONE;
|
||||
// GL_TESS_CONTROL_OUTPUT_VERTICES: the `layout(vertices = N) out` of the linked
|
||||
// tessellation control stage, or 0 when the program has none. Checked against
|
||||
// GL_MAX_PATCH_VERTICES at link (GL 4.6 core 11.2.1.1).
|
||||
Int tcsOutputVertices = 0;
|
||||
// The rest of the geometry stage's link properties, and the tessellation evaluation
|
||||
// stage's. Every one of these is a glGetProgramiv answer that had no source at all:
|
||||
// the query surface listed the geometry pnames only to fall through to
|
||||
// GL_INVALID_ENUM, and the GL_TESS_GEN_* pnames were not mentioned anywhere. They
|
||||
// come from the linked intermediates for the same reason gsInputPrimitive and
|
||||
// tcsOutputVertices do - glslang has already merged the compilation units' layout
|
||||
// qualifiers and diagnosed contradictions, so the linked program is the thing that
|
||||
// knows.
|
||||
GLenum gsOutputPrimitive = GL_NONE;
|
||||
Int gsMaxVertices = 0;
|
||||
Int gsInvocations = 0;
|
||||
// The tessellation evaluation stage's layout: GL_QUADS / GL_TRIANGLES / GL_ISOLINES,
|
||||
// GL_EQUAL / GL_FRACTIONAL_EVEN / GL_FRACTIONAL_ODD, GL_CW / GL_CCW, and point mode.
|
||||
GLenum tessGenMode = GL_NONE;
|
||||
GLenum tessGenSpacing = GL_NONE;
|
||||
GLenum tessGenVertexOrder = GL_NONE;
|
||||
Bool tessGenPointMode = false;
|
||||
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
Int xfbVaryingNameMaxLength = 0;
|
||||
Bool xfbNeedsScatteredCapture = false;
|
||||
Uint32 xfbPackedStride = 0;
|
||||
};
|
||||
|
||||
// ---- everything phase B of a link produces, in one movable block ----
|
||||
//
|
||||
// The membership rule is the same mechanical one LinkArtifacts uses: this is exactly
|
||||
// what ProgramSpirvTask writes, which is what makes moving it THE publish. It is
|
||||
// deliberately NOT part of LinkArtifacts, and that separation is what routes the five
|
||||
// readers of SPIR-V-derived data through their own join gate by compiler rather than
|
||||
// by review - m_spirv is private and Spirv() is the only spelling that reaches it.
|
||||
//
|
||||
// Why these three and nothing else: `generatedSpirv` has no GL-thread reader at all
|
||||
// (every consumer is a backend draw/prepare path), and `uniformOffsets` +
|
||||
// `globalUboScratch` are the ONLY things glUniform*/glGetUniform* need that are
|
||||
// derived from the OPTIMIZED SPIR-V rather than from glslang reflection - spirv-opt
|
||||
// runs in place and can delete a uniform, or the whole global UBO, so the offsets
|
||||
// cannot be lifted out of glslang's reflection instead.
|
||||
struct SpirvArtifacts {
|
||||
Vector<Vector<unsigned>> generatedSpirv;
|
||||
Bool enableSpirvValidation = false;
|
||||
// Byte offset of each uniform location inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
||||
Vector<Uint> uniformOffsets;
|
||||
Vector<Uint8> globalUboScratch;
|
||||
// Byte offset of the reserved gl_NumSamples stand-in inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Taken by NAME from the SPIR-V metadata rather than through
|
||||
// uniformOffsets, because the member has no GL location at all: the link task keeps
|
||||
// it out of the GL-visible uniform index space so no application can see or write it.
|
||||
Uint reservedNumSamplesOffset = kInvalidUniformOffset;
|
||||
// False for a program whose SPIR-V was never produced (phase B cancelled at
|
||||
// teardown or by a relink) or whose optimizer run failed. GL has no way to
|
||||
// retract a LINK_STATUS it already reported true, so such a program stays
|
||||
// "linked" and every reflection answer it has given stays correct - it is simply
|
||||
// not drawable, which the backends already express through their link-status
|
||||
// gates.
|
||||
Bool spirvStatus = false;
|
||||
// Whether these modules KEPT their 64-bit floats instead of being narrowed to 32
|
||||
// (ShaderTranspiler::DemoteFloat64Pass). Decided per PROGRAM, never per module - the
|
||||
// global UBO is one buffer all stages read, so two stages disagreeing about whether a
|
||||
// `uniform double` occupies 4 or 8 bytes would put every uniform after it at a
|
||||
// different offset in each. Recorded here rather than re-derived from the backend
|
||||
// because it is the layout THESE modules were built with: it is what the routing
|
||||
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
|
||||
// width the shader actually declares.
|
||||
Bool nativeFloat64 = false;
|
||||
// Whether gl_PointSize was demoted out of THESE modules' tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) because the backend cannot host
|
||||
// the built-in there. Per PROGRAM by construction - a consumer whose producer
|
||||
// kept the built-in would read garbage - and recorded here rather than
|
||||
// re-derived because it cannot be: the rewrite's whole point is that the final
|
||||
// bytes no longer declare the capability that armed it. The backends read it to
|
||||
// respell a "gl_PointSize" transform-feedback capture as the carrier
|
||||
// (ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME). The GL reflection surface
|
||||
// deliberately keeps answering "gl_PointSize": demotion happens after phase A,
|
||||
// so every query keeps the truthful GL spelling.
|
||||
Bool pointSizeDemoted = false;
|
||||
};
|
||||
|
||||
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
||||
// Static and taking the block explicitly, because from stage 4 the link BODY needs
|
||||
// them while its artifacts still live on the job node, not on any ProgramObject. The
|
||||
|
||||
@@ -275,3 +275,24 @@ gtest_discover_tests(OptimisticStatusTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS
|
||||
gtest_discover_tests(AsyncTeardownTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
# Same reason again: several cases leave A links outstanding while B compiles and links.
|
||||
gtest_discover_tests(XfbFrontendOrderInvarianceTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
|
||||
# P0.5: the ProgramArtifacts.h header on its own - the alias identity proofs, the POD trip
|
||||
# wire and the archive field tables. Its first include is the artifacts header, so this
|
||||
# binary is also the compile-time proof that the header is self-contained.
|
||||
add_executable(
|
||||
ProgramArtifactsTest
|
||||
ProgramArtifactsTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(ProgramArtifactsTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
ProgramArtifactsTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
gtest_discover_tests(ProgramArtifactsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// MobileGL - MobileGL/MG_Test/Program/ProgramArtifactsTest.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
|
||||
|
||||
// First include on purpose: the artifacts header must be self-contained (P0.5 gate A).
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
|
||||
#include <gtest/gtest.h>
|
||||
// For the alias checks only.
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
namespace {
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_State::GLState;
|
||||
|
||||
// P0.5 moved the five types to namespace scope and left in-class aliases behind so the
|
||||
// existing spellings compile unchanged. The classic failure of that move is a COPY: two
|
||||
// same-named definitions that both compile and silently split the type. is_same_v is the
|
||||
// compile-time proof that ProgramObject::X and GLState::X are one type.
|
||||
TEST(ProgramArtifacts, AliasesAreTheSameTypes) {
|
||||
static_assert(std::is_same_v<ProgramObject::TypeFacts, TypeFacts>);
|
||||
static_assert(std::is_same_v<ProgramObject::ResourceReflection, ResourceReflection>);
|
||||
static_assert(std::is_same_v<ProgramObject::XfbVarying, XfbVarying>);
|
||||
static_assert(std::is_same_v<ProgramObject::LinkArtifacts, LinkArtifacts>);
|
||||
static_assert(std::is_same_v<ProgramObject::SpirvArtifacts, SpirvArtifacts>);
|
||||
static_assert(std::is_same_v<ProgramObject::UniformReflection, UniformReflection>);
|
||||
static_assert(std::is_same_v<ProgramObject::BlockReflection, BlockReflection>);
|
||||
static_assert(std::is_same_v<ProgramObject::PipeInputReflection, PipeInputReflection>);
|
||||
static_assert(std::is_same_v<ProgramObject::PipeOutputReflection, PipeOutputReflection>);
|
||||
static_assert(ProgramObject::kInvalidUniformOffset == kInvalidUniformOffset);
|
||||
EXPECT_EQ(ProgramObject::kInvalidUniformOffset, kInvalidUniformOffset);
|
||||
EXPECT_EQ(kInvalidUniformOffset, ~0u);
|
||||
}
|
||||
|
||||
// 13 Bool + 3 bytes of padding + 7 x 4-byte scalars on every ABI.
|
||||
TEST(ProgramArtifacts, TypeFactsIsPodOf44Bytes) {
|
||||
static_assert(std::is_trivially_copyable_v<TypeFacts>);
|
||||
static_assert(std::is_standard_layout_v<TypeFacts>);
|
||||
EXPECT_EQ(sizeof(TypeFacts), 44u);
|
||||
EXPECT_EQ(alignof(TypeFacts), 4u);
|
||||
}
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user