Compile and validate temporal resolve shader bundle
This commit is contained in:
@@ -34,6 +34,21 @@ foreach(FASET_ENTRY gpuVertexMain gpuShadowMain gpuCullMain gpuHzbMain gpuPostCu
|
||||
DEPENDS "${PROJECT_SOURCE_DIR}/shaders/gpu_scene.slang" "${PROJECT_SOURCE_DIR}/tools/compile_shader.py" VERBATIM)
|
||||
list(APPEND FASET_SHADER_OUTPUTS "${FASET_SHADER_OUTPUT}" "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.reflection.json")
|
||||
endforeach()
|
||||
foreach(FASET_ENTRY temporalResolveMain temporalCompositeVertexMain temporalCompositeFragmentMain)
|
||||
if(FASET_ENTRY STREQUAL "temporalResolveMain")
|
||||
set(FASET_TEMPORAL_DEFINE FASET_TEMPORAL_RESOLVE=1)
|
||||
else()
|
||||
set(FASET_TEMPORAL_DEFINE FASET_TEMPORAL_COMPOSITE=1)
|
||||
endif()
|
||||
set(FASET_SHADER_OUTPUT "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.spv")
|
||||
add_custom_command(OUTPUT "${FASET_SHADER_OUTPUT}" "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.reflection.json"
|
||||
COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/tools/compile_shader.py"
|
||||
--compiler "${SLANGC_EXECUTABLE}" --source "${PROJECT_SOURCE_DIR}/shaders/temporal.slang"
|
||||
--entry "${FASET_ENTRY}" --define "${FASET_TEMPORAL_DEFINE}" --output "${FASET_SHADER_DIRECTORY}"
|
||||
BYPRODUCTS "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.slang-reflection.json"
|
||||
DEPENDS "${PROJECT_SOURCE_DIR}/shaders/temporal.slang" "${PROJECT_SOURCE_DIR}/tools/compile_shader.py" VERBATIM)
|
||||
list(APPEND FASET_SHADER_OUTPUTS "${FASET_SHADER_OUTPUT}" "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.reflection.json")
|
||||
endforeach()
|
||||
add_custom_command(OUTPUT "${FASET_SHADER_DIRECTORY}/compatibility.spv"
|
||||
COMMAND "${SLANGC_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/shaders/compatibility.hlsl"
|
||||
-entry compatibilityMain -stage compute -target spirv -profile spirv_1_6
|
||||
@@ -57,6 +72,11 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(faset_render_lighting_policy_tests PRIVATE faset_render)
|
||||
add_test(NAME render_lighting_policy COMMAND faset_render_lighting_policy_tests)
|
||||
set_tests_properties(render_lighting_policy PROPERTIES LABELS "p3")
|
||||
add_executable(faset_render_temporal_shader_contract_tests "${PROJECT_SOURCE_DIR}/tests/render_temporal_shader_contract_tests.cpp")
|
||||
target_include_directories(faset_render_temporal_shader_contract_tests PRIVATE "${PROJECT_SOURCE_DIR}/src/render")
|
||||
target_link_libraries(faset_render_temporal_shader_contract_tests PRIVATE faset_render faset_core)
|
||||
target_compile_definitions(faset_render_temporal_shader_contract_tests PRIVATE FASET_TEST_SHADER_DIRECTORY="${FASET_SHADER_DIRECTORY}")
|
||||
add_test(NAME render_temporal_shader_contract COMMAND faset_render_temporal_shader_contract_tests)
|
||||
add_executable(faset_render_temporal_reference_tests "${PROJECT_SOURCE_DIR}/tests/render_temporal_reference_tests.cpp")
|
||||
target_link_libraries(faset_render_temporal_reference_tests PRIVATE faset_render)
|
||||
add_test(NAME render_temporal_reference COMMAND faset_render_temporal_reference_tests)
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Temporal scene resolve and full-resolution composite. No material/lighting
|
||||
// descriptors are consumed here; the scene was shaded before this pass.
|
||||
// Velocity target: xy = current-minus-prior scene-local UV, z = expected prior
|
||||
// clip depth, w = opaque motion validity (zero for reactive/invalid pixels).
|
||||
#if defined(FASET_TEMPORAL_RESOLVE)
|
||||
struct TemporalResolveParameters {
|
||||
uint4 dimensions; // output width/height, internal width/height
|
||||
float4 outputSceneRect; // output-pixel x/y/width/height
|
||||
float4 internalSceneRect; // internal-pixel x/y/width/height
|
||||
uint4 flags; // x = prior history valid
|
||||
};
|
||||
[[vk::push_constant]] ConstantBuffer<TemporalResolveParameters> temporalParameters;
|
||||
[[vk::binding(0,0)]] Texture2D<float4> currentSceneColor;
|
||||
[[vk::binding(1,0)]] Texture2D<float> currentSceneDepth;
|
||||
[[vk::binding(2,0)]] Texture2D<float4> currentSceneVelocity;
|
||||
[[vk::binding(3,0)]] Texture2D<float4> previousHistoryColor;
|
||||
[[vk::binding(4,0)]] Texture2D<float> previousHistoryDepth;
|
||||
[[vk::binding(5,0)]] [vk::image_format("rgba16f")]
|
||||
RWTexture2D<float4> nextHistoryColor;
|
||||
[[vk::binding(6,0)]] [vk::image_format("r32f")]
|
||||
RWTexture2D<float> nextHistoryDepth;
|
||||
|
||||
int2 clampScenePixel(int2 pixel) {
|
||||
return clamp(pixel, int2(0), int2(temporalParameters.dimensions.zw) - 1);
|
||||
}
|
||||
float4 sceneColorAt(int2 pixel) {
|
||||
return currentSceneColor.Load(int3(clampScenePixel(pixel), 0));
|
||||
}
|
||||
float sceneDepthAt(int2 pixel) {
|
||||
return currentSceneDepth.Load(int3(clampScenePixel(pixel), 0));
|
||||
}
|
||||
float4 sceneVelocityAt(int2 pixel) {
|
||||
return currentSceneVelocity.Load(int3(clampScenePixel(pixel), 0));
|
||||
}
|
||||
float4 historyBilinear(float2 uv) {
|
||||
float2 position = uv * float2(temporalParameters.dimensions.xy) - .5;
|
||||
int2 base = int2(floor(position));
|
||||
float2 fraction = position - float2(base);
|
||||
int2 limit = int2(temporalParameters.dimensions.xy) - 1;
|
||||
int2 p00 = clamp(base, int2(0), limit);
|
||||
int2 p10 = clamp(base + int2(1, 0), int2(0), limit);
|
||||
int2 p01 = clamp(base + int2(0, 1), int2(0), limit);
|
||||
int2 p11 = clamp(base + int2(1, 1), int2(0), limit);
|
||||
float4 top = lerp(previousHistoryColor.Load(int3(p00, 0)),
|
||||
previousHistoryColor.Load(int3(p10, 0)), fraction.x);
|
||||
float4 bottom = lerp(previousHistoryColor.Load(int3(p01, 0)),
|
||||
previousHistoryColor.Load(int3(p11, 0)), fraction.x);
|
||||
return lerp(top, bottom, fraction.y);
|
||||
}
|
||||
|
||||
[shader("compute")]
|
||||
[numthreads(8, 8, 1)]
|
||||
void temporalResolveMain(uint3 dispatchId : SV_DispatchThreadID) {
|
||||
const uint2 outputPixel = dispatchId.xy;
|
||||
const uint2 outputExtent = temporalParameters.dimensions.xy;
|
||||
const uint2 internalExtent = temporalParameters.dimensions.zw;
|
||||
if (outputPixel.x >= outputExtent.x || outputPixel.y >= outputExtent.y) return;
|
||||
|
||||
const float2 center = float2(outputPixel) + .5;
|
||||
const float4 outputRect = temporalParameters.outputSceneRect;
|
||||
const float4 internalRect = temporalParameters.internalSceneRect;
|
||||
const bool insideScene = all(center >= outputRect.xy) &&
|
||||
all(center < outputRect.xy + outputRect.zw) &&
|
||||
all(outputRect.zw > 0);
|
||||
const float2 sceneLocalUV = insideScene
|
||||
? (center - outputRect.xy) / outputRect.zw : float2(0);
|
||||
const float2 internalPosition = insideScene
|
||||
? internalRect.xy + sceneLocalUV * internalRect.zw
|
||||
: center / float2(outputExtent) * float2(internalExtent);
|
||||
const int2 currentPixel = clampScenePixel(int2(floor(internalPosition)));
|
||||
const float4 currentColor = sceneColorAt(currentPixel);
|
||||
const float currentDepth = sceneDepthAt(currentPixel);
|
||||
float4 resolved = currentColor;
|
||||
|
||||
if (insideScene && temporalParameters.flags.x != 0) {
|
||||
const float4 centerMotion = sceneVelocityAt(currentPixel);
|
||||
if (all(isfinite(centerMotion)) && centerMotion.w > 0 &&
|
||||
centerMotion.z >= 0 && centerMotion.z <= 1) {
|
||||
float4 selectedMotion = centerMotion;
|
||||
float selectedDepth = currentDepth;
|
||||
const float currentTolerance = .002 + .01 * currentDepth;
|
||||
[unroll] for (int dy = -1; dy <= 1; ++dy)
|
||||
[unroll] for (int dx = -1; dx <= 1; ++dx) {
|
||||
const int2 neighbor = clampScenePixel(currentPixel + int2(dx, dy));
|
||||
const float depth = sceneDepthAt(neighbor);
|
||||
const float4 motion = sceneVelocityAt(neighbor);
|
||||
if (all(isfinite(motion)) && motion.w > 0 && motion.z >= 0 &&
|
||||
motion.z <= 1 && abs(depth - currentDepth) <= currentTolerance &&
|
||||
depth < selectedDepth) {
|
||||
selectedDepth = depth;
|
||||
selectedMotion = motion;
|
||||
}
|
||||
}
|
||||
const float2 previousLocalUV = sceneLocalUV - selectedMotion.xy;
|
||||
if (all(isfinite(previousLocalUV)) && all(previousLocalUV >= 0) &&
|
||||
all(previousLocalUV < 1)) {
|
||||
const float2 previousOutputUV =
|
||||
(outputRect.xy + previousLocalUV * outputRect.zw) / float2(outputExtent);
|
||||
if (all(previousOutputUV >= 0) && all(previousOutputUV < 1)) {
|
||||
const int2 priorPixel = clamp(
|
||||
int2(floor(previousOutputUV * float2(outputExtent))), int2(0),
|
||||
int2(outputExtent) - 1);
|
||||
const float priorDepth = previousHistoryDepth.Load(int3(priorPixel, 0));
|
||||
const float depthTolerance = .002 + .01 * selectedMotion.z;
|
||||
if (isfinite(priorDepth) &&
|
||||
abs(priorDepth - selectedMotion.z) <= depthTolerance) {
|
||||
float3 low = float3(1e30), high = float3(-1e30);
|
||||
[unroll] for (int dy = -1; dy <= 1; ++dy)
|
||||
[unroll] for (int dx = -1; dx <= 1; ++dx) {
|
||||
const float3 color = sceneColorAt(currentPixel + int2(dx, dy)).rgb;
|
||||
low = min(low, color);
|
||||
high = max(high, color);
|
||||
}
|
||||
const float2 motionPixels = selectedMotion.xy * outputRect.zw;
|
||||
const float weight = .9 * saturate(centerMotion.w) /
|
||||
(1 + .5 * length(motionPixels));
|
||||
const float3 priorColor = clamp(historyBilinear(previousOutputUV).rgb,
|
||||
low, high);
|
||||
resolved.rgb = lerp(currentColor.rgb, priorColor, weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nextHistoryColor[outputPixel] = resolved;
|
||||
nextHistoryDepth[outputPixel] = currentDepth;
|
||||
}
|
||||
|
||||
#elif defined(FASET_TEMPORAL_COMPOSITE)
|
||||
[[vk::binding(0,0)]] Texture2D<float4> resolvedHistoryColor;
|
||||
|
||||
[shader("vertex")]
|
||||
float4 temporalCompositeVertexMain(uint vertexId : SV_VertexID) : SV_Position {
|
||||
const float2 position = vertexId == 0 ? float2(-1, -1)
|
||||
: vertexId == 1 ? float2(3, -1) : float2(-1, 3);
|
||||
return float4(position, 0, 1);
|
||||
}
|
||||
|
||||
[shader("fragment")]
|
||||
float4 temporalCompositeFragmentMain(float4 position : SV_Position) : SV_Target {
|
||||
// Scene shading is already display-referred. No second tone or gamma pass.
|
||||
return resolvedHistoryColor.Load(int3(int2(position.xy), 0));
|
||||
}
|
||||
#else
|
||||
#error Select FASET_TEMPORAL_RESOLVE or FASET_TEMPORAL_COMPOSITE.
|
||||
#endif
|
||||
@@ -158,6 +158,75 @@ void validate_gpu_layout(const Json& layout, std::string_view entry) {
|
||||
locations(layout.at("outputs"), {}, "GPU compute outputs");
|
||||
}
|
||||
}
|
||||
void validate_temporal_layout(const Json& layout, std::string_view entry) {
|
||||
const bool resolve = entry == "temporalResolveMain";
|
||||
const bool vertex = entry == "temporalCompositeVertexMain";
|
||||
require(resolve || vertex || entry == "temporalCompositeFragmentMain",
|
||||
"unknown temporal shader entry");
|
||||
require(layout.at("stage") == (resolve ? "compute" : vertex ? "vertex" : "fragment"),
|
||||
"temporal shader stage changed");
|
||||
const auto& descriptors = layout.at("descriptors");
|
||||
require(descriptors.is_array() && descriptors.size() == (resolve ? 7u : 1u),
|
||||
"temporal descriptor count changed");
|
||||
for (std::size_t i = 0; i < descriptors.size(); ++i) {
|
||||
const auto& descriptor = descriptors[i];
|
||||
require(descriptor.at("set") == 0 && descriptor.at("binding") == i &&
|
||||
descriptor.at("count") == 1,
|
||||
"temporal descriptor set, binding or count changed");
|
||||
require(descriptor.at("type") ==
|
||||
(resolve && i >= 5 ? "storage_image_2d" : "sampled_image_2d"),
|
||||
"temporal image descriptor type changed");
|
||||
require(descriptor.at("used") == (resolve || !vertex),
|
||||
"temporal entry uses an unexpected image binding");
|
||||
}
|
||||
const auto& constants = layout.at("push_constants");
|
||||
const auto& spirv_constants = layout.at("spirv_push_constants");
|
||||
require(constants.is_array() && spirv_constants.is_array(),
|
||||
"temporal push constants are malformed");
|
||||
if (resolve) {
|
||||
require(constants.size() == 1 && constants[0].at("offset") == 0 &&
|
||||
constants[0].at("size") == 64 && spirv_constants.size() == 1,
|
||||
"temporal resolve push block changed");
|
||||
const auto& members = constants[0].at("members");
|
||||
const auto& spirv_members = spirv_constants[0].at("members");
|
||||
require(members.size() == 4 && spirv_members.size() == 4,
|
||||
"temporal resolve push members changed");
|
||||
const char* types[] = {"uint32x4", "float32x4", "float32x4", "uint32x4"};
|
||||
for (std::size_t i = 0; i < 4; ++i)
|
||||
require(members[i].at("offset") == 16 * i &&
|
||||
members[i].at("size") == 16 && members[i].at("type") == types[i] &&
|
||||
spirv_members[i].at("member") == i &&
|
||||
spirv_members[i].at("offset") == 16 * i,
|
||||
"temporal resolve push member ABI changed");
|
||||
} else
|
||||
require(constants.empty() && spirv_constants.empty(),
|
||||
"temporal composite unexpectedly uses push constants");
|
||||
if (resolve) {
|
||||
locations(layout.at("inputs"), {}, "temporal resolve inputs");
|
||||
locations(layout.at("outputs"), {}, "temporal resolve outputs");
|
||||
} else if (vertex) {
|
||||
locations(layout.at("inputs"), {}, "temporal composite vertex inputs");
|
||||
locations(layout.at("outputs"), {}, "temporal composite vertex outputs");
|
||||
} else {
|
||||
locations(layout.at("inputs"), {}, "temporal composite fragment inputs");
|
||||
locations(layout.at("outputs"), {"float32x4"},
|
||||
"temporal composite fragment outputs");
|
||||
}
|
||||
const auto& input_builtins = layout.at("input_builtins");
|
||||
require(input_builtins.is_array() && input_builtins.size() == 1 &&
|
||||
input_builtins[0].at("semantic") ==
|
||||
(resolve ? "SV_DISPATCHTHREADID" : vertex ? "SV_VERTEXID" : "SV_POSITION") &&
|
||||
input_builtins[0].at("type") == (vertex ? "uint32" : resolve ? "uint32x3"
|
||||
: "float32x4"),
|
||||
"temporal entry input builtin changed");
|
||||
const auto& output_builtins = layout.at("output_builtins");
|
||||
require(output_builtins.is_array() && output_builtins.size() == (vertex ? 1u : 0u),
|
||||
"temporal entry output builtin count changed");
|
||||
if (vertex)
|
||||
require(output_builtins[0].at("semantic") == "SV_POSITION" &&
|
||||
output_builtins[0].at("type") == "float32x4",
|
||||
"temporal composite vertex position changed");
|
||||
}
|
||||
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,
|
||||
@@ -183,7 +252,7 @@ void validate_spirv(const std::vector<std::uint32_t>& words, std::uint32_t execu
|
||||
require(entry_found, "SPIR-V main entry point missing");
|
||||
}
|
||||
detail::ShaderCode load(const std::filesystem::path& directory, const char* entry,
|
||||
bool gpu = false) {
|
||||
bool gpu = false, bool temporal = 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(
|
||||
@@ -196,7 +265,9 @@ detail::ShaderCode load(const std::filesystem::path& directory, const char* entr
|
||||
const auto& layout = metadata.at("layout");
|
||||
const auto fingerprint = faset::sha256(layout.dump());
|
||||
require(metadata.at("layout_fingerprint") == fingerprint, "layout fingerprint mismatch");
|
||||
if (gpu)
|
||||
if (temporal)
|
||||
validate_temporal_layout(layout, entry);
|
||||
else if (gpu)
|
||||
validate_gpu_layout(layout, entry);
|
||||
else
|
||||
validate_layout(layout, entry);
|
||||
@@ -204,9 +275,13 @@ detail::ShaderCode load(const std::filesystem::path& directory, const char* entr
|
||||
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));
|
||||
const auto stage = temporal
|
||||
? (std::string_view(entry) == "temporalResolveMain" ? 5u
|
||||
: std::string_view(entry) == "temporalCompositeVertexMain" ? 0u : 4u)
|
||||
: gpu ? ((std::string_view(entry) == "gpuVertexMain" ||
|
||||
std::string_view(entry) == "gpuShadowMain") ? 0u : 5u)
|
||||
: (std::string_view(entry) == "fragmentMain" ? 4u : 0u);
|
||||
validate_spirv(result.words, stage);
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
@@ -221,6 +296,12 @@ detail::load_gpu_shader_bundle(const std::filesystem::path& directory) {
|
||||
load(directory, "gpuCullMain", true), load(directory, "gpuHzbMain", true),
|
||||
load(directory, "gpuPostCullMain", true)};
|
||||
}
|
||||
std::array<detail::ShaderCode, 3>
|
||||
detail::load_temporal_shader_bundle(const std::filesystem::path& directory) {
|
||||
return {load(directory, "temporalResolveMain", false, true),
|
||||
load(directory, "temporalCompositeVertexMain", false, true),
|
||||
load(directory, "temporalCompositeFragmentMain", false, true)};
|
||||
}
|
||||
void validate_shader_bundle(const std::filesystem::path& directory) {
|
||||
(void)detail::load_shader_bundle(directory);
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ struct ShaderCode {
|
||||
std::array<ShaderCode, 3> load_shader_bundle(const std::filesystem::path& directory);
|
||||
// Order: opaque vertex, optional instanced shadow vertex, main cull, HZB, post cull.
|
||||
std::array<ShaderCode, 5> load_gpu_shader_bundle(const std::filesystem::path& directory);
|
||||
// Order: temporal resolve compute, full-screen composite vertex and fragment.
|
||||
std::array<ShaderCode, 3> load_temporal_shader_bundle(const std::filesystem::path& directory);
|
||||
} // namespace faset::render::detail
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "shader_contract.hpp"
|
||||
#include <faset/core/hash.hpp>
|
||||
#include <faset/core/io.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace {
|
||||
void require(bool value, const char* message) {
|
||||
if (!value)
|
||||
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::path_from_utf8("Faset temporal shaders " + faset::new_id());
|
||||
struct Cleanup {
|
||||
fs::path path;
|
||||
~Cleanup() { std::error_code error; fs::remove_all(faset::native_io_path(path), error); }
|
||||
} cleanup{temporary};
|
||||
fs::create_directories(faset::native_io_path(temporary));
|
||||
constexpr const char* entries[] = {"temporalResolveMain", "temporalCompositeVertexMain",
|
||||
"temporalCompositeFragmentMain"};
|
||||
for (const auto* entry : entries)
|
||||
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||
const auto name = std::string(entry) + extension;
|
||||
fs::copy_file(faset::native_io_path(original / name),
|
||||
faset::native_io_path(temporary / name));
|
||||
}
|
||||
|
||||
const auto bundle = faset::render::detail::load_temporal_shader_bundle(temporary);
|
||||
for (const auto& shader : bundle)
|
||||
require(!shader.words.empty() && !shader.layout_fingerprint.empty(),
|
||||
"Every temporal shader entry has valid checked SPIR-V and reflection");
|
||||
|
||||
auto reflection = temporary / "temporalResolveMain.reflection.json";
|
||||
auto metadata = faset::read_json(reflection);
|
||||
require(metadata["layout"]["stage"] == "compute" &&
|
||||
metadata["layout"]["descriptors"].size() == 7 &&
|
||||
metadata["layout"]["push_constants"][0]["size"] == 64,
|
||||
"Temporal resolve ABI contains seven images and a 64-byte push block");
|
||||
metadata["layout"]["descriptors"][5]["binding"] = 8;
|
||||
metadata["layout_fingerprint"] = faset::sha256(metadata["layout"].dump());
|
||||
faset::atomic_write_json(reflection, metadata);
|
||||
must_reject([&] { (void)faset::render::detail::load_temporal_shader_bundle(temporary); },
|
||||
"A rehashed temporal image binding change must be rejected");
|
||||
faset::atomic_write_json(
|
||||
reflection, faset::read_json(original / "temporalResolveMain.reflection.json"));
|
||||
|
||||
auto fragment = temporary / "temporalCompositeFragmentMain.spv";
|
||||
const auto bytes = faset::read_text(fragment);
|
||||
faset::atomic_write(fragment, "corrupt");
|
||||
must_reject([&] { (void)faset::render::detail::load_temporal_shader_bundle(temporary); },
|
||||
"A broken composite shader cannot enter the temporal bundle");
|
||||
faset::atomic_write(fragment, bytes);
|
||||
fs::remove(faset::native_io_path(temporary / "temporalCompositeVertexMain.spv"));
|
||||
must_reject([&] { (void)faset::render::detail::load_temporal_shader_bundle(temporary); },
|
||||
"A missing temporal entry cannot enter a complete shader bundle");
|
||||
}
|
||||
Reference in New Issue
Block a user