/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ // Regression test for issue #47: the fused UI uber-kernel // (shaders/ui-fused.comp.glsl) and its C++ push-constant mirror // (Crafter::UIFusedHeader). The fused kernel is large and hand-written, and // its push-constant block must stay byte-for-byte identical to UIFusedHeader // or DispatchFused silently corrupts every fused frame. This test: // // 1. compiles the real ui-fused.comp.glsl with glslang (same target-env the // engine builds it with), proving the uber-kernel still compiles and its // GL_GOOGLE_include of ui-shared.glsl resolves; // 2. runs spirv-val over the result under the engine's block-layout flags; // 3. asserts exactly one push-constant variable survives (a well-formed // single block, like every other UI shader); // 4. parses the push-constant struct's member Offset decorations and asserts // they are exactly {0,16,32,48,64,80,96,112} — the layout of // UIFusedHeader (four uvec4 headers at 0/16/32/48 then four vec4 clip // rects at 64/80/96/112, 128 bytes total). Any add/remove/reorder of a // member shifts these offsets and fails here, catching a GLSL/C++ drift // that the C++-side static_assert(sizeof==128) alone cannot. // // glslang and spirv-val are invoked at runtime, so the test declares them as // required tools (see project.cpp). It needs no GPU device. #include import Crafter.Graphics; // for the static_assert(sizeof(UIFusedHeader)==128) import std; using namespace Crafter; namespace { namespace fs = std::filesystem; int RunCommand(const std::string& cmd) { int status = std::system(cmd.c_str()); if (status == -1) return -1; return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f); } std::vector ReadSpirv(const fs::path& p) { std::ifstream f(p, std::ios::binary | std::ios::ate); if (!f) return {}; std::streamsize size = f.tellg(); f.seekg(0); std::vector words(static_cast(size) / sizeof(std::uint32_t)); f.read(reinterpret_cast(words.data()), size); return words; } // Count OpVariable instructions in the PushConstant storage class (SC == 9). int CountPushConstantVariables(const std::vector& words) { constexpr std::uint32_t OpVariable = 59; constexpr std::uint32_t StorageClassPushConstant = 9; int count = 0; for (std::size_t i = 5; i < words.size();) { std::uint32_t len = words[i] >> 16; if (len == 0 || i + len > words.size()) break; if ((words[i] & 0xFFFFu) == OpVariable && len >= 4 && words[i + 3] == StorageClassPushConstant) ++count; i += len; } return count; } // The single PushConstant struct's member byte offsets, sorted ascending. // Finds the OpTypePointer in the PushConstant storage class (SC == 9), takes // the struct type it points at, and collects every Offset (decoration 35) // declared on that struct via OpMemberDecorate. std::vector PushConstantMemberOffsets(const std::vector& words) { constexpr std::uint32_t OpTypePointer = 32; constexpr std::uint32_t OpMemberDecorate = 72; constexpr std::uint32_t StorageClassPushConstant = 9; constexpr std::uint32_t DecorationOffset = 35; std::uint32_t structId = 0; for (std::size_t i = 5; i < words.size();) { std::uint32_t len = words[i] >> 16; if (len == 0 || i + len > words.size()) break; // OpTypePointer: [op][result id][storage class][pointee type id] if ((words[i] & 0xFFFFu) == OpTypePointer && len >= 4 && words[i + 2] == StorageClassPushConstant) { structId = words[i + 3]; break; } i += len; } if (structId == 0) return {}; std::vector offsets; for (std::size_t i = 5; i < words.size();) { std::uint32_t len = words[i] >> 16; if (len == 0 || i + len > words.size()) break; // OpMemberDecorate: [op][struct id][member][decoration][operands...] if ((words[i] & 0xFFFFu) == OpMemberDecorate && len >= 5 && words[i + 1] == structId && words[i + 3] == DecorationOffset) { offsets.push_back(words[i + 4]); } i += len; } std::ranges::sort(offsets); offsets.erase(std::ranges::unique(offsets).begin(), offsets.end()); return offsets; } // Locate shaders/ui-fused.comp.glsl whether the test runs from the build // output dir or the project root (mirrors ShapeTextCache's font probing). fs::path LocateShader() { const std::array candidates = { "shaders/ui-fused.comp.glsl", "../shaders/ui-fused.comp.glsl", "../../shaders/ui-fused.comp.glsl", "../../../shaders/ui-fused.comp.glsl", }; for (const auto& c : candidates) { std::error_code ec; if (fs::exists(c, ec)) return c; } return {}; } } // namespace int main() { // Sanity: the C++ mirror is the size the shader layout assumes. static_assert(sizeof(UIFusedHeader) == 128); const fs::path src = LocateShader(); if (src.empty()) { std::println(std::cerr, "could not locate shaders/ui-fused.comp.glsl from {}", fs::current_path().string()); return 1; } const fs::path dir = fs::temp_directory_path() / "crafter-uifused-test"; std::error_code ec; fs::create_directories(dir, ec); const fs::path spv = dir / "ui-fused.comp.spv"; // 1. Compile the real uber-kernel. GL_GOOGLE_include resolves // ui-shared.glsl relative to the source directory. std::string compile = "glslang --target-env vulkan1.4 -V -S comp \"" + src.string() + "\" -o \"" + spv.string() + "\" > /dev/null"; if (RunCommand(compile) != 0) { std::println(std::cerr, "glslang failed to compile ui-fused.comp.glsl"); return 1; } std::vector words = ReadSpirv(spv); if (words.size() < 5) { std::println(std::cerr, "could not read compiled SPIR-V"); return 1; } // 2. spirv-val under the engine's relaxed block-layout flags. std::string validate = "spirv-val \"" + spv.string() + "\" --relax-block-layout --scalar-block-layout --target-env vulkan1.4"; if (RunCommand(validate) != 0) { std::println(std::cerr, "spirv-val rejected ui-fused.comp.spv"); return 1; } // 3. Exactly one push-constant block. int pcVars = CountPushConstantVariables(words); if (pcVars != 1) { std::println(std::cerr, "expected exactly 1 push-constant variable, found {}", pcVars); return 1; } // 4. Member offsets must match UIFusedHeader's layout exactly. const std::vector expected = {0, 16, 32, 48, 64, 80, 96, 112}; std::vector offsets = PushConstantMemberOffsets(words); if (offsets != expected) { std::print(std::cerr, "push-constant member offsets mismatch — expected {{"); for (auto o : expected) std::print(std::cerr, "{} ", o); std::print(std::cerr, "}}, got {{"); for (auto o : offsets) std::print(std::cerr, "{} ", o); std::println(std::cerr, "}}"); return 1; } std::println(std::cout, "ui-fused shader ok (push-constant vars: {}, member offsets pinned to UIFusedHeader, 128 bytes)", pcVars); return 0; }