223 lines
12 KiB
C++
223 lines
12 KiB
C++
#include "shader_contract.hpp"
|
|
#include <cstring>
|
|
#include <faset/core/hash.hpp>
|
|
#include <faset/core/io.hpp>
|
|
#include <faset/render/renderer.hpp>
|
|
#include <stdexcept>
|
|
#include <string_view>
|
|
|
|
namespace faset::render {
|
|
namespace {
|
|
using faset::Json;
|
|
void require(bool value, const std::string& message) {
|
|
if (!value)
|
|
throw std::runtime_error("Shader contract: " + message);
|
|
}
|
|
std::string read_bounded(const std::filesystem::path& path, std::uintmax_t maximum) {
|
|
const auto native = faset::native_io_path(path);
|
|
require(std::filesystem::is_regular_file(native), "missing " + faset::path_to_utf8(path));
|
|
require(std::filesystem::file_size(native) <= maximum,
|
|
"oversized " + faset::path_to_utf8(path));
|
|
return faset::read_text(path);
|
|
}
|
|
void locations(const Json& fields, std::initializer_list<const char*> types, const char* label) {
|
|
require(fields.is_array() && fields.size() == types.size(),
|
|
std::string(label) + " count changed");
|
|
std::size_t index{};
|
|
for (const auto* type : types) {
|
|
require(fields[index].at("location") == index && fields[index].at("type") == type,
|
|
std::string(label) + " location/type changed");
|
|
++index;
|
|
}
|
|
}
|
|
void validate_layout(const Json& layout, std::string_view entry) {
|
|
const bool fragment = entry == "fragmentMain";
|
|
require(layout.at("stage") == (fragment ? "fragment" : "vertex"), "shader stage changed");
|
|
const auto& descriptors = layout.at("descriptors");
|
|
require(descriptors.is_array() && descriptors.size() == 4, "descriptor count changed");
|
|
for (std::size_t i = 0; i < descriptors.size(); ++i) {
|
|
const auto& binding = descriptors[i];
|
|
require(binding.at("set") == 0 && binding.at("binding") == i && binding.at("count") == 1,
|
|
"descriptor set, binding or array count changed");
|
|
require(binding.at("type") == (i % 2 ? "sampler" : "sampled_image_2d"),
|
|
"descriptor type changed");
|
|
require(fragment || !binding.at("used").get<bool>(),
|
|
"vertex texture bindings are unsupported");
|
|
}
|
|
const auto& constants = layout.at("push_constants");
|
|
require(constants.is_array() && constants.size() == 1 && constants[0].at("offset") == 0 &&
|
|
constants[0].at("size") == 96,
|
|
"push-constant block changed");
|
|
const auto& members = constants[0].at("members");
|
|
require(members.is_array() && members.size() == 3, "push-constant member count changed");
|
|
const int offsets[] = {0, 64, 80}, sizes[] = {64, 16, 16};
|
|
const char* types[] = {"float32x4x4", "float32x4", "float32x4"};
|
|
for (std::size_t i = 0; i < 3; ++i)
|
|
require(members[i].at("offset") == offsets[i] && members[i].at("size") == sizes[i] &&
|
|
members[i].at("type") == types[i],
|
|
"push-constant member layout changed");
|
|
const auto& blocks = layout.at("spirv_push_constants");
|
|
require(blocks.is_array() && blocks.size() <= 1, "SPIR-V push-constant block count changed");
|
|
for (const auto& block : blocks) {
|
|
const auto& actual = block.at("members");
|
|
require(actual.is_array() && actual.size() == 3, "SPIR-V push-constant members changed");
|
|
for (std::size_t i = 0; i < 3; ++i)
|
|
require(actual[i].at("member") == i && actual[i].at("offset") == offsets[i],
|
|
"SPIR-V push-constant offsets changed");
|
|
// Slang lowers column-major host matrices to a transposed SPIR-V matrix type.
|
|
require(actual[0].at("matrix_layout") == "row-major" && actual[0].at("matrix_stride") == 16,
|
|
"SPIR-V matrix storage convention changed");
|
|
}
|
|
if (fragment) {
|
|
locations(layout.at("inputs"),
|
|
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
|
|
"fragment inputs");
|
|
locations(layout.at("outputs"), {"float32x4"}, "fragment outputs");
|
|
} else {
|
|
locations(layout.at("inputs"),
|
|
{"float32x4", "float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
|
|
"vertex inputs");
|
|
if (entry == "vertexMain")
|
|
locations(layout.at("outputs"),
|
|
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
|
|
"vertex outputs");
|
|
else
|
|
locations(layout.at("outputs"), {}, "shadow outputs");
|
|
}
|
|
}
|
|
void validate_gpu_layout(const Json& layout, std::string_view entry) {
|
|
const bool graphics = entry == "gpuVertexMain" || entry == "gpuShadowMain";
|
|
const bool hzb = entry == "gpuHzbMain";
|
|
const bool compute = !graphics;
|
|
require(layout.at("stage") == (compute ? "compute" : "vertex"), "GPU shader stage changed");
|
|
const auto& descriptors = layout.at("descriptors");
|
|
const std::size_t expected_count = graphics ? 3 : hzb ? 2 : 10;
|
|
require(descriptors.is_array() && descriptors.size() == expected_count,
|
|
"GPU descriptor count changed");
|
|
const std::array<int, 10> compute_strides{224, 16, 16, 4, 16, 4, 4, 0, 0, 208};
|
|
const std::array<int, 3> graphics_strides{224, 4, 208};
|
|
for (std::size_t i = 0; i < expected_count; ++i) {
|
|
const auto& binding = descriptors[i];
|
|
require(binding.at("set") == (graphics ? 1 : 0) && binding.at("binding") == i &&
|
|
binding.at("count") == 1,
|
|
"GPU descriptor set, binding or count changed");
|
|
const int stride = graphics ? graphics_strides[i] : hzb ? 0 : compute_strides[i];
|
|
const char* type = hzb ? (i == 0 ? "sampled_image_2d" : "storage_image_2d")
|
|
: stride > 0 ? "storage_buffer" : "sampled_image_2d";
|
|
require(binding.at("type") == type, "GPU descriptor type changed");
|
|
if (stride > 0)
|
|
require(binding.at("element_stride") == stride, "GPU storage record stride changed");
|
|
}
|
|
const auto& constants = layout.at("push_constants");
|
|
require(constants.is_array() && constants.size() == 1 &&
|
|
constants[0].at("offset") == 0 &&
|
|
constants[0].at("size") == (graphics ? 112 : 16),
|
|
"GPU push-constant block changed");
|
|
const auto& members = constants[0].at("members");
|
|
require(members.is_array() && members.size() == 4, "GPU push-constant fields changed");
|
|
const int graphics_offsets[] = {0, 64, 80, 96};
|
|
const int graphics_sizes[] = {64, 16, 16, 16};
|
|
const char* graphics_types[] = {"float32x4x4", "float32x4", "float32x4", "uint32x4"};
|
|
for (std::size_t i = 0; i < 4; ++i) {
|
|
require(members[i].at("offset") == (graphics ? graphics_offsets[i] : int(i) * 4) &&
|
|
members[i].at("size") == (graphics ? graphics_sizes[i] : 4) &&
|
|
members[i].at("type") == (graphics ? graphics_types[i] : "uint32"),
|
|
"GPU push-constant layout changed");
|
|
}
|
|
const auto& blocks = layout.at("spirv_push_constants");
|
|
require(blocks.is_array() && blocks.size() == 1, "GPU SPIR-V push block changed");
|
|
const auto& actual = blocks[0].at("members");
|
|
require(actual.is_array() && actual.size() == 4, "GPU SPIR-V push members changed");
|
|
for (std::size_t i = 0; i < 4; ++i)
|
|
require(actual[i].at("member") == i &&
|
|
actual[i].at("offset") == (graphics ? graphics_offsets[i] : int(i) * 4),
|
|
"GPU SPIR-V push offsets changed");
|
|
if (graphics) {
|
|
require(actual[0].at("matrix_layout") == "row-major" &&
|
|
actual[0].at("matrix_stride") == 16,
|
|
"GPU SPIR-V matrix storage convention changed");
|
|
locations(layout.at("inputs"),
|
|
{"float32x3", "float32x3", "float32x4", "float32x2"},
|
|
"GPU vertex inputs");
|
|
if (entry == "gpuVertexMain")
|
|
locations(layout.at("outputs"),
|
|
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
|
|
"GPU vertex outputs");
|
|
else
|
|
locations(layout.at("outputs"), {}, "GPU shadow outputs");
|
|
} else {
|
|
locations(layout.at("inputs"), {}, "GPU compute inputs");
|
|
locations(layout.at("outputs"), {}, "GPU compute outputs");
|
|
}
|
|
}
|
|
void validate_spirv(const std::vector<std::uint32_t>& words, std::uint32_t execution_model) {
|
|
require(words.size() >= 5 && words[0] == 0x07230203 && words[1] >= 0x00010000 &&
|
|
words[1] <= 0x00010600 && words[3] > 0 && words[3] < (1u << 20) && words[4] == 0,
|
|
"invalid SPIR-V header");
|
|
bool entry_found{};
|
|
for (std::size_t offset = 5; offset < words.size();) {
|
|
const auto count = words[offset] >> 16;
|
|
const auto opcode = words[offset] & 0xffff;
|
|
require(count > 0 && count <= words.size() - offset, "malformed SPIR-V instruction");
|
|
if (opcode == 15) { // OpEntryPoint
|
|
require(count >= 4, "malformed SPIR-V entry point");
|
|
const char* name = reinterpret_cast<const char*>(&words[offset + 3]);
|
|
const auto available = (count - 3) * sizeof(std::uint32_t);
|
|
const auto* terminator = static_cast<const char*>(std::memchr(name, 0, available));
|
|
require(terminator != nullptr, "unterminated SPIR-V entry name");
|
|
if (std::string_view(name, terminator - name) == "main") {
|
|
require(words[offset + 1] == execution_model, "SPIR-V entry stage changed");
|
|
entry_found = true;
|
|
}
|
|
}
|
|
offset += count;
|
|
}
|
|
require(entry_found, "SPIR-V main entry point missing");
|
|
}
|
|
detail::ShaderCode load(const std::filesystem::path& directory, const char* entry,
|
|
bool gpu = false) {
|
|
const auto bytes = read_bounded(directory / (std::string(entry) + ".spv"), 16 * 1024 * 1024);
|
|
require(bytes.size() >= 20 && bytes.size() % 4 == 0, "invalid SPIR-V byte length");
|
|
const auto metadata = Json::parse(
|
|
read_bounded(directory / (std::string(entry) + ".reflection.json"), 1024 * 1024));
|
|
require(metadata.at("format") == "faset.shader-reflection" && metadata.at("version") == 1,
|
|
"unsupported reflection version");
|
|
require(metadata.at("source_entry") == entry && metadata.at("entry_point") == "main",
|
|
"reflection entry point mismatch");
|
|
require(metadata.at("spirv_sha256") == faset::sha256(bytes), "SPIR-V/reflection hash mismatch");
|
|
const auto& layout = metadata.at("layout");
|
|
const auto fingerprint = faset::sha256(layout.dump());
|
|
require(metadata.at("layout_fingerprint") == fingerprint, "layout fingerprint mismatch");
|
|
if (gpu)
|
|
validate_gpu_layout(layout, entry);
|
|
else
|
|
validate_layout(layout, entry);
|
|
detail::ShaderCode result;
|
|
result.layout_fingerprint = fingerprint;
|
|
result.words.resize(bytes.size() / 4);
|
|
std::memcpy(result.words.data(), bytes.data(), bytes.size());
|
|
validate_spirv(result.words, gpu ? ((std::string_view(entry) == "gpuVertexMain" ||
|
|
std::string_view(entry) == "gpuShadowMain") ? 0u : 5u)
|
|
: (std::string_view(entry) == "fragmentMain" ? 4u : 0u));
|
|
return result;
|
|
}
|
|
} // namespace
|
|
std::array<detail::ShaderCode, 3>
|
|
detail::load_shader_bundle(const std::filesystem::path& directory) {
|
|
return {load(directory, "vertexMain"), load(directory, "fragmentMain"),
|
|
load(directory, "shadowMain")};
|
|
}
|
|
std::array<detail::ShaderCode, 5>
|
|
detail::load_gpu_shader_bundle(const std::filesystem::path& directory) {
|
|
return {load(directory, "gpuVertexMain", true), load(directory, "gpuShadowMain", true),
|
|
load(directory, "gpuCullMain", true), load(directory, "gpuHzbMain", true),
|
|
load(directory, "gpuPostCullMain", true)};
|
|
}
|
|
void validate_shader_bundle(const std::filesystem::path& directory) {
|
|
(void)detail::load_shader_bundle(directory);
|
|
}
|
|
void validate_gpu_shader_bundle(const std::filesystem::path& directory) {
|
|
(void)detail::load_gpu_shader_bundle(directory);
|
|
}
|
|
} // namespace faset::render
|