[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:
2026-09-05 23:11:00 -04:00
parent 8566a288f8
commit da249f30e2
4 changed files with 482 additions and 371 deletions
@@ -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
+21
View File
@@ -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
View File
@@ -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