Files
Faset_Engine/tests/render_reload_tests.cpp
T

154 lines
7.5 KiB
C++

#include <chrono>
#include <faset/core/hash.hpp>
#include <faset/core/io.hpp>
#include <faset/core/process.hpp>
#include <faset/render/renderer.hpp>
#include <filesystem>
#include <iostream>
#include <thread>
using namespace faset;
namespace fs = std::filesystem;
namespace {
void require(bool value, const char* message) {
if (!value)
throw std::runtime_error(message);
}
int compile(const fs::path& source, const fs::path& output) {
Process process(
{{FASET_PYTHON_EXECUTABLE, FASET_SHADER_COMPILE_TOOL, "--compiler", FASET_TEST_SLANGC,
"--source", source.string(), "--entry", "fragmentMain", "--output", output.string()},
{},
{}});
std::string diagnostics;
while (true) {
auto result = process.poll();
diagnostics += result.output;
if (!result.running) {
if (result.exit_code != 0)
require(diagnostics.find("intentional_shader_compile_failure") != std::string::npos,
"Slang failure must preserve its useful compiler diagnostic");
return result.exit_code.value_or(1);
}
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
} // namespace
int main() {
const auto temporary = fs::temp_directory_path() / ("faset-shader-reload-" + new_id());
struct Cleanup {
fs::path path;
~Cleanup() {
std::error_code ignored;
fs::remove_all(path, ignored);
}
} cleanup{temporary};
try {
const auto bundle = temporary / "shaders";
fs::create_directories(bundle);
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
for (const auto* extension : {".spv", ".reflection.json"}) {
const auto name = std::string(entry) + extension;
fs::copy_file(fs::path(FASET_TEST_SHADER_DIRECTORY) / name, bundle / name);
}
const auto source = temporary / "reload.slang";
const auto original_source = read_text(FASET_TEST_SHADER_SOURCE);
const auto original_spirv = read_text(bundle / "fragmentMain.spv");
const auto original_reflection = read_text(bundle / "fragmentMain.reflection.json");
const auto original_fingerprint = Json::parse(original_reflection).at("layout_fingerprint");
render::validate_shader_bundle(bundle);
render::RendererConfig configuration;
configuration.width = configuration.height = 64;
configuration.headless = true;
configuration.validation = true;
configuration.shader_directory = bundle;
render::Renderer renderer(configuration);
render::Snapshot scene;
scene.ui_quads.push_back({0, 0, 64, 64, {1, .8f, .4f, 1}});
renderer.render(scene);
auto expected = renderer.pixels();
require(renderer.stats().gpu_allocated_bytes > 1024 * 1024 &&
renderer.stats().texture_count >= 1,
"Frame profile reports live Vulkan allocations and textures");
auto restore = [&] {
atomic_write(bundle / "fragmentMain.spv", original_spirv);
atomic_write(bundle / "fragmentMain.reflection.json", original_reflection);
};
auto retained = [&] {
std::string error;
require(!renderer.reload_shaders(error) && !error.empty(),
"Unsafe shader reload must fail with a diagnostic");
renderer.render(scene);
require(renderer.pixels() == expected,
"Rejected shader reload must retain working pixels");
require(renderer.stats().validation_errors == 0,
"Rejected bytecode must not reach Vulkan validation");
};
atomic_write(bundle / "fragmentMain.spv", "damaged bytecode");
retained();
restore();
auto malformed = original_spirv;
for (int i = 0; i < 4; ++i)
malformed[20 + i] = 0; // zero-word SPIR-V instruction
auto metadata = Json::parse(original_reflection);
metadata["spirv_sha256"] = sha256(malformed);
atomic_write(bundle / "fragmentMain.spv", malformed);
atomic_write_json(bundle / "fragmentMain.reflection.json", metadata);
retained();
restore();
auto incompatible = original_source;
auto at = incompatible.find("[[vk::binding(2,0)]]");
require(at != std::string::npos, "Shader descriptor fixture exists");
incompatible.replace(at, std::string("[[vk::binding(2,0)]]").size(),
"[[vk::binding(7,0)]]");
atomic_write(source, incompatible);
require(compile(source, bundle) == 0, "Compile real incompatible descriptor layout");
require(read_json(bundle / "fragmentMain.reflection.json").at("layout_fingerprint") !=
original_fingerprint,
"Descriptor edit changes normalized layout fingerprint");
retained();
restore();
incompatible = original_source;
at = incompatible.find("column_major float4x4");
require(at != std::string::npos, "Shader matrix fixture exists");
incompatible.replace(at, std::string("column_major float4x4").size(), "row_major float4x4");
atomic_write(source, incompatible);
require(compile(source, bundle) == 0, "Compile real incompatible matrix storage");
retained();
restore();
auto compatible = original_source;
at = compatible.find(" float4 sampled =");
require(at != std::string::npos, "Shader fragment fixture exists");
compatible.insert(at, " v.color.rgb *= 0.5;\n");
atomic_write(source, compatible);
require(compile(source, bundle) == 0, "Compile real compatible shader edit");
require(read_json(bundle / "fragmentMain.reflection.json").at("layout_fingerprint") ==
original_fingerprint,
"Source-only behavior edit preserves normalized layout fingerprint");
std::string error;
require(renderer.reload_shaders(error), "Compatible shader edit reloads successfully");
renderer.render(scene);
const auto changed = renderer.pixels();
require(changed[0] + 50 < expected[0], "Compatible reload changes actual rendered pixels");
expected = changed;
const auto last_spirv = read_text(bundle / "fragmentMain.spv");
const auto last_reflection = read_text(bundle / "fragmentMain.reflection.json");
atomic_write(source, compatible + "\n#error intentional_shader_compile_failure\n");
require(compile(source, bundle) != 0, "Real Slang compile failure is reported");
require(read_text(bundle / "fragmentMain.spv") == last_spirv &&
read_text(bundle / "fragmentMain.reflection.json") == last_reflection,
"Compile failure preserves both published shader artifacts");
require(renderer.reload_shaders(error),
"Last successfully compiled artifacts remain reloadable");
renderer.render(scene);
require(renderer.pixels() == expected, "Compile failure preserves last good rendering");
require(renderer.stats().validation_errors == 0,
"Shader reload regression has no Vulkan validation errors");
std::cout << "Normalized reflection, malformed SPIR-V, incompatible descriptors/matrices, "
"real compile failure and compatible pixel reload passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}