Add checked Slang shaders for GPU visibility and HZB
This commit is contained in:
@@ -148,6 +148,12 @@ int test_main(int argc, char** argv) {
|
||||
const auto first = builds.wait(builds.start_build());
|
||||
check(first.state == "succeeded", "Valid custom schema v2 publishes: " + first.error);
|
||||
const auto directory = path_from_utf8(first.result.at("directory").get<std::string>());
|
||||
for (const auto* entry : {"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||
"gpuHzbMain", "gpuPostCullMain"})
|
||||
for (const auto* extension : {".spv", ".reflection.json"})
|
||||
check(fs::is_regular_file(directory / "shaders" /
|
||||
(std::string(entry) + extension)),
|
||||
"Published Player contains every checked P2 shader artifact");
|
||||
const auto player = path_from_utf8(first.result.at("player").get<std::string>());
|
||||
const auto schema = path_from_utf8(first.result.at("schema").get<std::string>());
|
||||
const auto last_build = builds.config().cache_root / "last_build.json";
|
||||
|
||||
@@ -60,7 +60,9 @@ int tool_main(int argc, char** argv) {
|
||||
for (const auto* target : {"faset_player", "faset_schema_exporter"})
|
||||
fs::copy_file(self, build / (std::string(target) + suffix),
|
||||
fs::copy_options::overwrite_existing);
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain",
|
||||
"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||
"gpuHzbMain", "gpuPostCullMain"})
|
||||
for (const auto* extension : {".spv", ".reflection.json"})
|
||||
atomic_write(build / "shaders" / (std::string(entry) + extension), "fixture\n");
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "shader_contract.hpp"
|
||||
#include <faset/core/hash.hpp>
|
||||
#include <faset/core/io.hpp>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace {
|
||||
void require(bool valid, const char* message) {
|
||||
if (!valid)
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
template <class Function> void must_reject(Function&& function, const char* message) {
|
||||
try {
|
||||
function();
|
||||
} catch (const std::exception&) {
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
} // namespace
|
||||
int main() {
|
||||
const auto original = fs::path(FASET_TEST_SHADER_DIRECTORY);
|
||||
const auto temporary = fs::temp_directory_path() / "faset-gpu-shader-contract-test";
|
||||
struct Cleanup {
|
||||
fs::path path;
|
||||
~Cleanup() { std::error_code ignored; fs::remove_all(path, ignored); }
|
||||
} cleanup{temporary};
|
||||
try {
|
||||
fs::create_directories(temporary);
|
||||
constexpr const char* entries[] = {"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||
"gpuHzbMain", "gpuPostCullMain"};
|
||||
for (const auto* entry : entries)
|
||||
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||
const auto file = std::string(entry) + extension;
|
||||
fs::copy_file(original / file, temporary / file, fs::copy_options::overwrite_existing);
|
||||
}
|
||||
auto shaders = faset::render::detail::load_gpu_shader_bundle(temporary);
|
||||
for (const auto& shader : shaders)
|
||||
require(!shader.words.empty() && !shader.layout_fingerprint.empty(),
|
||||
"Every P2 entry is valid SPIR-V with checked metadata");
|
||||
auto shader_file = temporary / "gpuCullMain.spv";
|
||||
const auto shader_bytes = faset::read_text(shader_file);
|
||||
faset::atomic_write(shader_file, "corrupt");
|
||||
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||
"Corrupt P2 SPIR-V must be rejected");
|
||||
faset::atomic_write(shader_file, shader_bytes);
|
||||
auto reflection_file = temporary / "gpuPostCullMain.reflection.json";
|
||||
auto metadata = faset::read_json(reflection_file);
|
||||
metadata["layout_fingerprint"] = "tampered";
|
||||
faset::atomic_write_json(reflection_file, metadata);
|
||||
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||
"Tampered P2 layout fingerprint must be rejected");
|
||||
metadata = faset::read_json(original / "gpuPostCullMain.reflection.json");
|
||||
metadata["layout"]["descriptors"][0]["element_stride"] = 208;
|
||||
metadata["layout_fingerprint"] = faset::sha256(metadata["layout"].dump());
|
||||
faset::atomic_write_json(reflection_file, metadata);
|
||||
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||
"A consistently rehashed but incompatible GPU record stride must be rejected");
|
||||
fs::remove(temporary / "gpuHzbMain.spv");
|
||||
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||
"Missing P2 entry must be rejected");
|
||||
std::cout << "GPU shader bundle validates every entry and rejects corrupt or missing artifacts\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,9 @@ int main() {
|
||||
try {
|
||||
const auto bundle = temporary / "shaders";
|
||||
fs::create_directories(bundle);
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain",
|
||||
"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||
"gpuHzbMain", "gpuPostCullMain"})
|
||||
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||
const auto name = std::string(entry) + extension;
|
||||
fs::copy_file(path_from_utf8(FASET_TEST_SHADER_DIRECTORY) / name, bundle / name);
|
||||
@@ -76,7 +78,9 @@ int main() {
|
||||
require(deep_bundle.native().size() > 300,
|
||||
"Shader file fixture must exceed the legacy Windows path limit");
|
||||
fs::create_directories(native_io_path(deep_bundle));
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain",
|
||||
"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||
"gpuHzbMain", "gpuPostCullMain"})
|
||||
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||
const auto name = std::string(entry) + extension;
|
||||
atomic_write(deep_bundle / name, read_text(bundle / name));
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""The cooked shader interface must preserve GPU resource kinds and element strides."""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / "tools" / "compile_shader.py"
|
||||
SPEC = importlib.util.spec_from_file_location("faset_compile_shader", SCRIPT)
|
||||
assert SPEC and SPEC.loader
|
||||
shader = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(shader)
|
||||
|
||||
|
||||
def parameter(name: str, index: int, shape: str, access: str, stride: int | None = None):
|
||||
result = {"kind": "scalar", "scalarType": "uint32", "sizes": []}
|
||||
if stride is not None:
|
||||
result["sizes"] = [{"kind": "uniform", "value": stride, "alignment": 16}]
|
||||
return {
|
||||
"name": name,
|
||||
"binding": {"kind": "descriptorTableSlot", "index": index, "space": 0},
|
||||
"type": {
|
||||
"kind": "resource",
|
||||
"baseShape": shape,
|
||||
"access": access,
|
||||
"resultType": result,
|
||||
"sizes": [{"kind": "descriptorTableSlot", "value": 1}],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ReflectionTests(unittest.TestCase):
|
||||
def test_gpu_storage_resources_keep_kind_and_stride(self):
|
||||
parameters = [
|
||||
parameter("instances", 0, "structuredBuffer", "read", 224),
|
||||
parameter("visibleIds", 1, "structuredBuffer", "readWrite", 4),
|
||||
parameter("depthOutput", 2, "texture2D", "readWrite"),
|
||||
parameter("depthInput", 3, "texture2D", "read"),
|
||||
]
|
||||
raw = {
|
||||
"parameters": parameters,
|
||||
"entryPoints": [{
|
||||
"name": "gpuCullMain", "stage": "compute",
|
||||
"bindings": [{"name": p["name"], "binding": {"used": 1}} for p in parameters],
|
||||
}],
|
||||
}
|
||||
spirv = struct.pack("<5I", 0x07230203, 0x00010600, 0, 1, 0)
|
||||
layout = shader.normalize(raw, spirv, "gpuCullMain")["layout"]
|
||||
descriptors = layout["descriptors"]
|
||||
self.assertEqual(
|
||||
[(d["type"], d.get("element_stride")) for d in descriptors],
|
||||
[("storage_buffer", 224), ("storage_buffer", 4),
|
||||
("storage_image_2d", None), ("sampled_image_2d", None)],
|
||||
)
|
||||
|
||||
def test_real_slang_compute_interface(self):
|
||||
compiler = os.environ["FASET_TEST_SLANGC"]
|
||||
with tempfile.TemporaryDirectory(prefix="faset-shader-reflection-") as directory:
|
||||
process = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--compiler", compiler, "--source",
|
||||
str(SCRIPT.parents[1] / "shaders" / "gpu_scene.slang"), "--entry",
|
||||
"gpuCullMain", "--define", "FASET_GPU_CULL=1", "--output", directory],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
self.assertEqual(process.returncode, 0, process.stderr)
|
||||
metadata = json.loads((Path(directory) / "gpuCullMain.reflection.json").read_text())
|
||||
bindings = {item["binding"]: item for item in metadata["layout"]["descriptors"]}
|
||||
self.assertEqual([bindings[n]["element_stride"] for n in (0, 1, 2, 3, 4, 5, 6, 9)],
|
||||
[224, 16, 16, 4, 16, 4, 4, 208])
|
||||
self.assertEqual([bindings[n]["type"] for n in (7, 8)],
|
||||
["sampled_image_2d", "sampled_image_2d"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user