From da249f30e2bc0ef159ef6ada06f6065022b86289 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 5 Sep 2026 22:47:07 -0400 Subject: [PATCH] [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 and , 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. --- .../GLState/ProgramState/ProgramArtifacts.h | 400 ++++++++++++++++++ .../GLState/ProgramState/ProgramObject.h | 385 +---------------- MobileGL/MG_Test/Program/CMakeLists.txt | 21 + .../MG_Test/Program/ProgramArtifactsTest.cpp | 47 ++ 4 files changed, 482 insertions(+), 371 deletions(-) create mode 100644 MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h create mode 100755 MobileGL/MG_Test/Program/ProgramArtifactsTest.cpp diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h b/MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h new file mode 100644 index 00000000..4afd48a2 --- /dev/null +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h @@ -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 // 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 // std::set (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 ".". `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 program; + + // The owned reflection snapshot. Indexed by TProgram index; see the structs above. + Vector uniformReflection; + Vector blockReflection; + Vector pipeInputReflection; + Vector 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 computeLocalSize{}; + // Replaces program->getUniformIndex(name). Maps the reflected name to its + // TProgram uniform index. + UnorderedMap uniformIndexByName; + + // Attributes (Vertex in) + Vector attribs; + Vector attribTypes; + + // FragData (Frag out): the per-link snapshot of the explicit request maps. + UnorderedMap linkedFragDataLocation; + UnorderedMap 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 glUniformIndexToTProgram; + Vector tProgramUniformIndexToGl; + Vector glBlockIndexToTProgram; + Vector 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 glUniformBlockIndexToBlock; // GL uniform-block index -> block index + Vector 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 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 uniformInitialValues; + UnorderedMap 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 writtenUniformLocationBits; + Vector writtenUniformIndexBits; + Vector writtenUniformIndices; + // Ordered by location, + // aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`" + Vector uniformIndexInTProgram; + // ditto. Will be set at glUniform1i + Vector 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 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 uniformBlockIndexByName; + Vector 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 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 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 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 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 xfbInterfaceNames; + Vector xfbStrides; + Vector 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> generatedSpirv; + Bool enableSpirvValidation = false; + // Byte offset of each uniform location inside globalUboScratch, or + // kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass. + Vector uniformOffsets; + Vector 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 diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index 506f0fac..de2d6069 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -9,6 +9,7 @@ #pragma once #include #include "ShaderObject.h" +#include "ProgramArtifacts.h" #include #include @@ -38,70 +39,19 @@ namespace MobileGL::MG_State::GLState { // 1024 GL 4.3 requires. static constexpr Int MAX_UNIFORM_LOCATIONS = static_cast(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 ".". `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 program; - - // The owned reflection snapshot. Indexed by TProgram index; see the structs above. - Vector uniformReflection; - Vector blockReflection; - Vector pipeInputReflection; - Vector 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 computeLocalSize{}; - // Replaces program->getUniformIndex(name). Maps the reflected name to its - // TProgram uniform index. - UnorderedMap uniformIndexByName; - - // Attributes (Vertex in) - Vector attribs; - Vector attribTypes; - - // FragData (Frag out): the per-link snapshot of the explicit request maps. - UnorderedMap linkedFragDataLocation; - UnorderedMap 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 glUniformIndexToTProgram; - Vector tProgramUniformIndexToGl; - Vector glBlockIndexToTProgram; - Vector 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 glUniformBlockIndexToBlock; // GL uniform-block index -> block index - Vector 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 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 uniformInitialValues; - UnorderedMap 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 writtenUniformLocationBits; - Vector writtenUniformIndexBits; - Vector writtenUniformIndices; - // Ordered by location, - // aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`" - Vector uniformIndexInTProgram; - // ditto. Will be set at glUniform1i - Vector 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 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 uniformBlockIndexByName; - Vector 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 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 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 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 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 xfbInterfaceNames; - Vector xfbStrides; - Vector 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> generatedSpirv; - Bool enableSpirvValidation = false; - // Byte offset of each uniform location inside globalUboScratch, or - // kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass. - Vector uniformOffsets; - Vector 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 diff --git a/MobileGL/MG_Test/Program/CMakeLists.txt b/MobileGL/MG_Test/Program/CMakeLists.txt index 95104cc4..9fb4f9a4 100644 --- a/MobileGL/MG_Test/Program/CMakeLists.txt +++ b/MobileGL/MG_Test/Program/CMakeLists.txt @@ -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) diff --git a/MobileGL/MG_Test/Program/ProgramArtifactsTest.cpp b/MobileGL/MG_Test/Program/ProgramArtifactsTest.cpp new file mode 100755 index 00000000..78436c45 --- /dev/null +++ b/MobileGL/MG_Test/Program/ProgramArtifactsTest.cpp @@ -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 +#include +// For the alias checks only. +#include + +#include + +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); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + 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); + static_assert(std::is_standard_layout_v); + EXPECT_EQ(sizeof(TypeFacts), 44u); + EXPECT_EQ(alignof(TypeFacts), 4u); + } +} // namespace