203 lines
7.9 KiB
C++
203 lines
7.9 KiB
C++
|
|
/*
|
||
|
|
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 <cstdlib>
|
||
|
|
|
||
|
|
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<std::uint32_t> 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<std::uint32_t> words(static_cast<std::size_t>(size) / sizeof(std::uint32_t));
|
||
|
|
f.read(reinterpret_cast<char*>(words.data()), size);
|
||
|
|
return words;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Count OpVariable instructions in the PushConstant storage class (SC == 9).
|
||
|
|
int CountPushConstantVariables(const std::vector<std::uint32_t>& 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<std::uint32_t> PushConstantMemberOffsets(const std::vector<std::uint32_t>& 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<std::uint32_t> 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<fs::path, 4> 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<std::uint32_t> 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<std::uint32_t> expected = {0, 16, 32, 48, 64, 80, 96, 112};
|
||
|
|
std::vector<std::uint32_t> 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;
|
||
|
|
}
|