Expose opt-in temporal pixel diagnostics

This commit is contained in:
Emil
2026-09-24 05:27:44 +03:00
parent 03e4727109
commit 219e796378
10 changed files with 240 additions and 18 deletions
+8
View File
@@ -188,6 +188,14 @@ Json profileFrames(const std::vector<ProfileSample>& samples) {
{"temporal_internal_width", sample.lighting.temporal_internal_width},
{"temporal_internal_height", sample.lighting.temporal_internal_height},
{"temporal_jitter", sample.lighting.temporal_jitter},
{"temporal_counters_valid",
sample.lighting.temporal_counters_valid},
{"temporal_accepted_pixels",
sample.lighting.temporal_counters_valid
? Json(sample.lighting.temporal_accepted_pixels) : Json(nullptr)},
{"temporal_rejected_pixels",
sample.lighting.temporal_counters_valid
? Json(sample.lighting.temporal_rejected_pixels) : Json(nullptr)},
{"gpu_temporal_resolve_ms",
gpuMeasured ? Json(sample.lighting.gpu_temporal_resolve_ms) : Json(nullptr)},
{"gpu_temporal_composite_ms",
+5
View File
@@ -169,5 +169,10 @@ if(BUILD_TESTING)
"${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/tests/test_p3_lighting_benchmark.py"
--real-executable "$<TARGET_FILE:faset_p3_lighting_benchmark>")
set_tests_properties(render_lighting_benchmark_smoke PROPERTIES LABELS "gpu;p3" TIMEOUT 90)
add_executable(faset_render_temporal_diagnostics_tests
"${PROJECT_SOURCE_DIR}/tests/render_temporal_diagnostics_tests.cpp")
target_link_libraries(faset_render_temporal_diagnostics_tests PRIVATE faset_render)
add_test(NAME render_temporal_diagnostics COMMAND faset_render_temporal_diagnostics_tests)
set_tests_properties(render_temporal_diagnostics PROPERTIES LABELS "gpu;p3")
endif()
install(FILES ${FASET_SHADER_OUTPUTS} DESTINATION shaders)
+5
View File
@@ -155,6 +155,8 @@ struct RendererConfig {
LightingMode lighting_mode{LightingMode::Auto};
// GPU counter readback is diagnostic-only; normal visibility uses no CPU feedback.
bool visibility_diagnostics{false};
// Exact temporal pixel counters are opt-in; normal TAA avoids GPU readback.
bool temporal_diagnostics{false};
// Optional isolated shader bundle, useful for editor preview and shader reload tests.
std::filesystem::path shader_directory{};
};
@@ -219,6 +221,8 @@ struct FrameStats {
std::uint32_t temporal_valid_motion_instances{};
std::uint32_t temporal_internal_width{}, temporal_internal_height{};
std::array<float, 2> temporal_jitter{};
bool temporal_counters_valid{};
std::uint32_t temporal_accepted_pixels{}, temporal_rejected_pixels{};
double gpu_temporal_resolve_ms{}, gpu_temporal_composite_ms{}, gpu_ui_ms{};
std::vector<std::string> graph_passes;
double gpu_light_tiles_ms{};
@@ -250,6 +254,7 @@ class Renderer {
TemporalMode temporal_mode() const;
float render_scale() const;
void set_visibility_diagnostics(bool enabled);
void set_temporal_diagnostics(bool enabled);
// Reads the most recently completed HZB mip for editor diagnostics only.
// Normal visibility decisions remain entirely on the GPU.
std::optional<HzbDebugImage> hzb_debug_image(std::uint32_t mip = 0);
+10 -1
View File
@@ -7,7 +7,7 @@ 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
uint4 flags; // x = prior history valid; y = count pixel decisions
float4 jitterMotion; // xy = current-minus-prior local UV; z = static camera
};
[[vk::push_constant]] ConstantBuffer<TemporalResolveParameters> temporalParameters;
@@ -20,6 +20,9 @@ struct TemporalResolveParameters {
RWTexture2D<float4> nextHistoryColor;
[[vk::binding(6,0)]] [vk::image_format("r32f")]
RWTexture2D<float> nextHistoryDepth;
// A tiny GPU counter buffer is bound for the normal pipeline too, but touched
// only when explicit Editor diagnostics requests exact per-pixel statistics.
[[vk::binding(7,0)]] RWStructuredBuffer<uint> historyDecisionCounts;
int2 clampScenePixel(int2 pixel) {
return clamp(pixel, int2(0), int2(temporalParameters.dimensions.zw) - 1);
@@ -90,6 +93,7 @@ void temporalResolveMain(uint3 dispatchId : SV_DispatchThreadID) {
const float4 currentColor = sceneColorAt(currentPixel);
const float currentDepth = sceneDepthAt(currentPixel);
float4 resolved = currentColor;
bool acceptedHistory = false;
if (insideScene && temporalParameters.flags.x != 0) {
const float4 centerMotion = sceneVelocityAt(currentPixel);
@@ -190,6 +194,7 @@ void temporalResolveMain(uint3 dispatchId : SV_DispatchThreadID) {
(!matchingFar || !nearerOccluder)) {
const float3 priorColor = clamp(sampled, low, high);
resolved.rgb = lerp(currentColor.rgb, priorColor, weight);
acceptedHistory = true;
}
}
}
@@ -198,6 +203,10 @@ void temporalResolveMain(uint3 dispatchId : SV_DispatchThreadID) {
}
nextHistoryColor[outputPixel] = resolved;
nextHistoryDepth[outputPixel] = currentDepth;
if (insideScene && temporalParameters.flags.y != 0) {
uint oldCount;
InterlockedAdd(historyDecisionCounts[acceptedHistory ? 0 : 1], 1, oldCount);
}
}
#elif defined(FASET_TEMPORAL_COMPOSITE)
+18 -1
View File
@@ -86,7 +86,7 @@ ImGuiKey key(std::string_view name) {
} // namespace
struct DebugOverlay::Impl {
ImGuiContext* context{};
bool visible{}, freeze{}, show_hzb{};
bool visible{}, freeze{}, show_hzb{}, count_temporal_pixels{};
bool hzb_sampled{}, hzb_available{};
int hzb_mip{3}, hzb_last_mip{-1};
std::uint64_t hzb_frame{};
@@ -464,6 +464,21 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
ImGui::Text("History: %s Reset: %s",
stats.temporal_history_valid ? "valid" : "invalid",
temporal_reset_label(stats.temporal_reset_reason));
ImGui::Text("Jitter (clip): %+.5f, %+.5f",
stats.temporal_jitter[0], stats.temporal_jitter[1]);
ImGui::TextDisabled("Internal pixel offset: %+.3f, %+.3f",
stats.temporal_jitter[0] * stats.temporal_internal_width * .5f,
stats.temporal_jitter[1] * stats.temporal_internal_height * .5f);
ImGui::Checkbox("Count temporal pixels (GPU readback)",
&state.count_temporal_pixels);
if (stats.temporal_counters_valid)
ImGui::Text("History pixels: %u accepted, %u rejected",
stats.temporal_accepted_pixels,
stats.temporal_rejected_pixels);
else if (state.count_temporal_pixels)
ImGui::TextDisabled("History pixel counts unavailable for this frame");
else
ImGui::TextDisabled("History pixel counts off");
if (stats.gpu_ms > 0)
ImGui::Text("GPU: resolve %.2f composite %.2f UI %.2f ms",
stats.gpu_temporal_resolve_ms,
@@ -520,6 +535,8 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
}
// GPU counters are a diagnostics readback, never a normal renderer dependency.
renderer.set_visibility_diagnostics(state.visible);
renderer.set_temporal_diagnostics(state.visible && state.count_temporal_pixels &&
renderer.temporal_mode() != render::TemporalMode::Off);
ImGui::Render();
const auto* data = ImGui::GetDrawData();
if (!data || !data->Valid)
+83 -11
View File
@@ -213,6 +213,7 @@ struct SceneResources {
};
struct TemporalResources {
Image scene_color, velocity, history_color[2], history_depth[2];
Buffer pixel_counts, pixel_counts_stage;
VkDescriptorSetLayout resolve_layout{}, composite_layout{};
VkDescriptorPool descriptor_pool{};
VkDescriptorSet resolve_sets[2]{}, composite_sets[2]{};
@@ -429,6 +430,8 @@ struct Renderer::Impl {
destroy(image);
for (auto& image : temporal.history_depth)
destroy(image);
destroy(temporal.pixel_counts);
destroy(temporal.pixel_counts_stage);
destroy(vertices);
destroy(readback);
destroy(lighting_header);
@@ -963,6 +966,8 @@ struct Renderer::Impl {
destroy(image);
for (auto& image : temporal.history_depth)
destroy(image);
destroy(temporal.pixel_counts);
destroy(temporal.pixel_counts_stage);
temporal.has_completed_image = false;
scene.hzb_history_valid = false;
temporal.capabilities.extent = width <= max_image_dimension &&
@@ -998,6 +1003,10 @@ struct Renderer::Impl {
image = make_image(width, height, VK_FORMAT_R32_SFLOAT,
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT,
VK_IMAGE_ASPECT_COLOR_BIT);
temporal.pixel_counts = make_buffer(2 * sizeof(std::uint32_t),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
temporal.completed_index = 0;
}
const auto padded_width = std::bit_ceil(internal[0]);
@@ -1036,9 +1045,10 @@ struct Renderer::Impl {
if (temporal.descriptor_pool)
vkDestroyDescriptorPool(device, temporal.descriptor_pool, nullptr);
temporal.descriptor_pool = {};
const std::array<VkDescriptorPoolSize, 2> sizes{{
const std::array<VkDescriptorPoolSize, 3> sizes{{
{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 12},
{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 4}}};
{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 4},
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2}}};
VkDescriptorPoolCreateInfo pool_info{};
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
pool_info.maxSets = 4;
@@ -1072,7 +1082,7 @@ struct Renderer::Impl {
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL},
{VK_NULL_HANDLE, temporal.history_color[next].view, VK_IMAGE_LAYOUT_GENERAL},
{VK_NULL_HANDLE, temporal.history_depth[next].view, VK_IMAGE_LAYOUT_GENERAL}}};
std::array<VkWriteDescriptorSet, 8> writes{};
std::array<VkWriteDescriptorSet, 9> writes{};
for (std::uint32_t binding = 0; binding < 7; ++binding) {
writes[binding].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[binding].dstSet = temporal.resolve_sets[next];
@@ -1082,15 +1092,23 @@ struct Renderer::Impl {
? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
writes[binding].pImageInfo = &images[binding];
}
const VkDescriptorBufferInfo count_buffer{temporal.pixel_counts.handle, 0,
2 * sizeof(std::uint32_t)};
writes[7].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[7].dstSet = temporal.composite_sets[next];
writes[7].dstBinding = 0;
writes[7].dstSet = temporal.resolve_sets[next];
writes[7].dstBinding = 7;
writes[7].descriptorCount = 1;
writes[7].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
writes[7].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
writes[7].pBufferInfo = &count_buffer;
writes[8].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[8].dstSet = temporal.composite_sets[next];
writes[8].dstBinding = 0;
writes[8].descriptorCount = 1;
writes[8].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
const VkDescriptorImageInfo composite_image{
VK_NULL_HANDLE, temporal.history_color[next].view,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
writes[7].pImageInfo = &composite_image;
writes[8].pImageInfo = &composite_image;
vkUpdateDescriptorSets(device, static_cast<std::uint32_t>(writes.size()),
writes.data(), 0, nullptr);
}
@@ -1802,10 +1820,11 @@ struct Renderer::Impl {
if (!temporal.capabilities.compute || !temporal.capabilities.formats)
return;
if (!temporal.resolve_layout) {
std::array<VkDescriptorSetLayoutBinding, 7> bindings{};
std::array<VkDescriptorSetLayoutBinding, 8> bindings{};
for (std::uint32_t i = 0; i < bindings.size(); ++i)
bindings[i] = {i, i < 5 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE
: VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
: i < 7 ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE
: VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
VkDescriptorSetLayoutCreateInfo descriptor_info{};
descriptor_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
@@ -2328,6 +2347,8 @@ struct Renderer::Impl {
statistics.temporal_internal_width = temporal.internal_width;
statistics.temporal_internal_height = temporal.internal_height;
statistics.temporal_jitter = {};
statistics.temporal_counters_valid = false;
statistics.temporal_accepted_pixels = statistics.temporal_rejected_pixels = 0;
statistics.gpu_temporal_resolve_ms = statistics.gpu_temporal_composite_ms =
statistics.gpu_ui_ms = 0;
statistics.graph_passes.clear();
@@ -2342,6 +2363,17 @@ struct Renderer::Impl {
const bool occlusion = statistics.effective_visibility_mode ==
VisibilityMode::GpuOcclusion;
const bool temporal_active = statistics.effective_temporal_mode != TemporalMode::Off;
bool collect_temporal_counts = temporal_active && config.temporal_diagnostics;
if (collect_temporal_counts && !temporal.pixel_counts_stage.handle) {
try {
temporal.pixel_counts_stage = make_buffer(2 * sizeof(std::uint32_t),
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
VK_MEMORY_PROPERTY_HOST_CACHED_BIT);
} catch (const std::exception&) {
collect_temporal_counts = false;
}
}
statistics.gpu_visibility_active = gpu_active;
statistics.hzb_valid = false;
bool can_present = surface != VK_NULL_HANDLE;
@@ -3627,6 +3659,15 @@ struct Renderer::Impl {
add_pass("TemporalResolve",
{"scene_color", "depth", "scene_velocity", "history_previous"},
{"resolved_color", "resolved_depth"}, [&] {
if (collect_temporal_counts) {
vkCmdFillBuffer(command, temporal.pixel_counts.handle, 0,
2 * sizeof(std::uint32_t), 0);
scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
VK_ACCESS_2_SHADER_STORAGE_READ_BIT |
VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT);
}
transition(command, temporal.scene_color,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_IMAGE_ASPECT_COLOR_BIT);
@@ -3656,7 +3697,8 @@ struct Renderer::Impl {
temporal.previous_unjittered_vp == snapshot.view_projection;
ResolvePush parameters{{width, height, raster_width, raster_height},
output_scene_viewport, scene_viewport,
{statistics.temporal_history_valid ? 1u : 0u, 0, 0, 0},
{statistics.temporal_history_valid ? 1u : 0u,
collect_temporal_counts ? 1u : 0u, 0, 0},
{(statistics.temporal_jitter[0] -
temporal.previous_jitter[0]) * .5f,
(statistics.temporal_jitter[1] -
@@ -3777,6 +3819,19 @@ struct Renderer::Impl {
VK_PIPELINE_STAGE_2_HOST_BIT,
VK_ACCESS_2_HOST_READ_BIT);
}
if (collect_temporal_counts) {
scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
VK_PIPELINE_STAGE_2_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT);
const VkBufferCopy count_copy{0, 0, 2 * sizeof(std::uint32_t)};
vkCmdCopyBuffer(command, temporal.pixel_counts.handle,
temporal.pixel_counts_stage.handle, 1, &count_copy);
scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_2_HOST_BIT,
VK_ACCESS_2_HOST_READ_BIT);
}
});
if (swap_index)
add_pass("Presentation", {"color"}, {"swapchain"}, [&] {
@@ -3913,6 +3968,17 @@ struct Renderer::Impl {
vkUnmapMemory(device, light_tile_readback.memory);
statistics.light_tile_counts_valid = true;
}
if (collect_temporal_counts) {
void* mapped_counts{};
check(vkMapMemory(device, temporal.pixel_counts_stage.memory, 0,
temporal.pixel_counts_stage.size, 0, &mapped_counts),
"Read temporal pixel diagnostics");
const auto* words = static_cast<const std::uint32_t*>(mapped_counts);
statistics.temporal_accepted_pixels = words[0];
statistics.temporal_rejected_pixels = words[1];
vkUnmapMemory(device, temporal.pixel_counts_stage.memory);
statistics.temporal_counters_valid = true;
}
instance_tracker.finish_frame();
scene.previous_vp = raster_vp;
scene.previous_projection = snapshot.projection;
@@ -3955,7 +4021,8 @@ struct Renderer::Impl {
statistics.gpu_allocated_bytes += image.allocation_size;
}
statistics.gpu_allocated_bytes += temporal.scene_color.allocation_size +
temporal.velocity.allocation_size;
temporal.velocity.allocation_size + temporal.pixel_counts.allocation_size +
temporal.pixel_counts_stage.allocation_size;
for (const auto& image : temporal.history_color)
statistics.gpu_allocated_bytes += image.allocation_size;
for (const auto& image : temporal.history_depth)
@@ -4136,6 +4203,11 @@ float Renderer::render_scale() const {
void Renderer::set_visibility_diagnostics(bool enabled) {
impl_->config.visibility_diagnostics = enabled;
}
void Renderer::set_temporal_diagnostics(bool enabled) {
impl_->config.temporal_diagnostics = enabled;
if (!enabled)
impl_->destroy(impl_->temporal.pixel_counts_stage);
}
std::optional<HzbDebugImage> Renderer::hzb_debug_image(std::uint32_t mip) {
auto& renderer = *impl_;
if (!renderer.scene.hzb_history_valid || !renderer.scene.hzb_supported)
+7 -3
View File
@@ -231,7 +231,7 @@ void validate_temporal_layout(const Json& layout, std::string_view 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),
require(descriptors.is_array() && descriptors.size() == (resolve ? 8u : 1u),
"temporal descriptor count changed");
for (std::size_t i = 0; i < descriptors.size(); ++i) {
const auto& descriptor = descriptors[i];
@@ -239,8 +239,12 @@ void validate_temporal_layout(const Json& layout, std::string_view entry) {
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");
(resolve && i == 7 ? "storage_buffer"
: resolve && i >= 5 ? "storage_image_2d" : "sampled_image_2d"),
"temporal descriptor type changed");
if (resolve && i == 7)
require(descriptor.at("element_stride") == 4,
"temporal counter element stride changed");
require(descriptor.at("used") == (resolve || !vertex),
"temporal entry uses an unexpected image binding");
}
+5
View File
@@ -66,9 +66,14 @@ with tempfile.TemporaryDirectory(prefix="faset-player-diagnostics-") as temporar
"temporal_fallback_reason", "temporal_reset_reason",
"temporal_history_valid", "temporal_internal_width",
"temporal_internal_height", "temporal_jitter",
"temporal_counters_valid", "temporal_accepted_pixels",
"temporal_rejected_pixels",
"gpu_temporal_resolve_ms", "gpu_temporal_composite_ms", "gpu_ui_ms"]:
assert key in sample, (key, sample)
assert sample["effective_temporal_mode"] == temporal_mode, sample
assert sample["temporal_counters_valid"] is False, sample
assert sample["temporal_accepted_pixels"] is None and \
sample["temporal_rejected_pixels"] is None, sample
if temporal_mode != "off":
assert sample["temporal_internal_width"] > 0, sample
assert sample["temporal_internal_height"] > 0, sample
@@ -0,0 +1,87 @@
#include "render_temporal_fixtures.hpp"
#include <cstdint>
#include <iostream>
#include <stdexcept>
using namespace faset::render;
using namespace faset::render::temporal_test;
namespace {
void pixel_decisions_are_diagnostic_only(VisibilityMode visibility) {
constexpr std::uint32_t width = 96, height = 72;
auto config = headless_config(width, height, visibility);
config.temporal_mode = TemporalMode::TAA;
Renderer renderer(config);
Renderer reference(config);
auto frame = lit_scene(width, height);
frame.draws.push_back(cube({0, 0, 0}, {.9f, .4f, .2f, 1}, "diagnostic-cube"));
renderer.render(frame);
reference.render(frame);
require(!renderer.stats().temporal_counters_valid &&
renderer.stats().temporal_accepted_pixels == 0 &&
renderer.stats().temporal_rejected_pixels == 0,
"Normal TAA must not read back per-pixel history diagnostics");
renderer.set_temporal_diagnostics(true);
renderer.render(frame);
reference.render(frame);
const auto accepted = renderer.stats().temporal_accepted_pixels;
const auto rejected = renderer.stats().temporal_rejected_pixels;
require(renderer.stats().temporal_counters_valid && accepted > 0 &&
accepted + rejected == width * height,
"Requested TAA diagnostics count each scene pixel and reuse eligible history");
require(renderer.pixels() == reference.pixels(),
"Counting pixels must not alter the TAA image");
frame.camera_cut = true;
renderer.render(frame);
reference.render(frame);
require(renderer.stats().temporal_counters_valid &&
renderer.stats().temporal_accepted_pixels == 0 &&
renderer.stats().temporal_rejected_pixels == width * height,
"A camera cut rejects every per-pixel history sample");
frame.camera_cut = false;
frame.scene_rect = {7, 9, 80, 50};
renderer.render(frame);
reference.render(frame);
require(renderer.stats().temporal_counters_valid &&
renderer.stats().temporal_accepted_pixels == 0 &&
renderer.stats().temporal_rejected_pixels == 80 * 50,
"The counter covers the scene rectangle, excluding output chrome pixels");
renderer.set_temporal_diagnostics(false);
renderer.render(frame);
reference.render(frame);
require(!renderer.stats().temporal_counters_valid &&
renderer.stats().temporal_accepted_pixels == 0 &&
renderer.stats().temporal_rejected_pixels == 0,
"Disabled diagnostics expose no stale values and perform no readback");
renderer.set_temporal_mode(TemporalMode::Off);
reference.set_temporal_mode(TemporalMode::Off);
renderer.set_temporal_diagnostics(true);
renderer.render(frame);
reference.render(frame);
require(!renderer.stats().temporal_counters_valid,
"Off mode does not claim temporal pixel diagnostics");
require(renderer.stats().validation_errors == 0,
"Diagnostic counter transitions pass Vulkan validation");
require(renderer.pixels() == reference.pixels(),
"Diagnostic toggles preserve the Off image");
}
} // namespace
int main() {
try {
pixel_decisions_are_diagnostic_only(VisibilityMode::Direct);
pixel_decisions_are_diagnostic_only(VisibilityMode::GpuFrustum);
std::cout << "Temporal pixel diagnostics are opt-in and exact\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -49,9 +49,11 @@ int main() {
auto reflection = temporary / "temporalResolveMain.reflection.json";
auto metadata = faset::read_json(reflection);
require(metadata["layout"]["stage"] == "compute" &&
metadata["layout"]["descriptors"].size() == 7 &&
metadata["layout"]["descriptors"].size() == 8 &&
metadata["layout"]["descriptors"][7]["type"] == "storage_buffer" &&
metadata["layout"]["descriptors"][7]["element_stride"] == 4 &&
metadata["layout"]["push_constants"][0]["size"] == 80,
"Temporal resolve ABI contains seven images and an 80-byte push block");
"Temporal resolve ABI contains seven images, counters and an 80-byte push block");
metadata["layout"]["descriptors"][5]["binding"] = 8;
metadata["layout_fingerprint"] = faset::sha256(metadata["layout"].dump());
faset::atomic_write_json(reflection, metadata);
@@ -59,6 +61,14 @@ int main() {
"A rehashed temporal image binding change must be rejected");
faset::atomic_write_json(
reflection, faset::read_json(original / "temporalResolveMain.reflection.json"));
metadata = faset::read_json(reflection);
metadata["layout"]["descriptors"][7]["element_stride"] = 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 counter stride 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);