Author SHA1 Message Date
Emil a2eea0c378 Keep temporal diagnostic staging alive through resize
Native and manual checks / native (ubuntu-24.04) (push) Failing after 34s
Native and manual checks / manual (push) Successful in 31s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
2026-09-24 05:37:15 +03:00
Emil 9f97ca7a7d Expose opt-in temporal pixel diagnostics
Native and manual checks / native (ubuntu-24.04) (push) Failing after 35s
Native and manual checks / manual (push) Successful in 30s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
2026-09-24 05:25:53 +03:00
10 changed files with 361 additions and 33 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",
+10
View File
@@ -169,5 +169,15 @@ 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 SDL3::SDL3 Vulkan::Vulkan)
add_test(NAME render_temporal_diagnostics COMMAND faset_render_temporal_diagnostics_tests)
set_tests_properties(render_temporal_diagnostics PROPERTIES LABELS "gpu;p3")
add_test(NAME render_temporal_diagnostics_resize COMMAND
faset_render_temporal_diagnostics_tests --window-resize)
set_tests_properties(render_temporal_diagnostics_resize PROPERTIES
LABELS "gpu;window;p3" TIMEOUT 35 SKIP_RETURN_CODE 77)
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)
+101 -26
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;
@@ -2319,15 +2338,11 @@ struct Renderer::Impl {
statistics.lod_counts = {};
statistics.visibility_counters_valid = false;
statistics.requested_temporal_mode = config.temporal_mode;
statistics.effective_temporal_mode = select_effective_temporal_mode(
config.temporal_mode, temporal.capabilities);
statistics.temporal_fallback_reason = temporal_fallback_reason(
config.temporal_mode, temporal.capabilities);
statistics.temporal_history_valid = false;
statistics.temporal_valid_motion_instances = 0;
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();
@@ -2335,15 +2350,6 @@ struct Renderer::Impl {
it = it->second.owner.expired() ? bounds_cache.erase(it) : std::next(it);
for (auto it = opacity_cache.begin(); it != opacity_cache.end();)
it = it->second.owner.expired() ? opacity_cache.erase(it) : std::next(it);
statistics.requested_visibility_mode = config.visibility_mode;
statistics.effective_visibility_mode = select_effective_visibility_mode(
config.visibility_mode, scene.available, scene.hzb_supported && scene.hzb_mips);
const bool gpu_active = statistics.effective_visibility_mode != VisibilityMode::Direct;
const bool occlusion = statistics.effective_visibility_mode ==
VisibilityMode::GpuOcclusion;
const bool temporal_active = statistics.effective_temporal_mode != TemporalMode::Off;
statistics.gpu_visibility_active = gpu_active;
statistics.hzb_valid = false;
bool can_present = surface != VK_NULL_HANDLE;
if (surface) {
// A capture may render between normal event-loop iterations. Keep the window
@@ -2357,6 +2363,35 @@ struct Renderer::Impl {
if (can_present && (dirty_swapchain || !swapchain))
make_swapchain();
}
// A swapchain resize can recreate temporal targets and destroy their
// diagnostic staging buffer. Decide the actual paths and allocate the
// readback only after that resource lifetime boundary.
statistics.effective_temporal_mode = select_effective_temporal_mode(
config.temporal_mode, temporal.capabilities);
statistics.temporal_fallback_reason = temporal_fallback_reason(
config.temporal_mode, temporal.capabilities);
statistics.temporal_internal_width = temporal.internal_width;
statistics.temporal_internal_height = temporal.internal_height;
const bool temporal_active = statistics.effective_temporal_mode != TemporalMode::Off;
statistics.requested_visibility_mode = config.visibility_mode;
statistics.effective_visibility_mode = select_effective_visibility_mode(
config.visibility_mode, scene.available, scene.hzb_supported && scene.hzb_mips);
const bool gpu_active = statistics.effective_visibility_mode != VisibilityMode::Direct;
const bool occlusion = statistics.effective_visibility_mode ==
VisibilityMode::GpuOcclusion;
statistics.gpu_visibility_active = gpu_active;
statistics.hzb_valid = false;
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;
}
}
// Retire atlas/image resources no longer retained by a caller.
for (auto it = textures.begin(); it != textures.end();) {
if (it->first != white.get() && it->second.source.use_count() == 1) {
@@ -3627,6 +3662,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 +3700,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 +3822,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 +3971,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 +4024,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 +4206,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
+185
View File
@@ -0,0 +1,185 @@
#include "render_temporal_fixtures.hpp"
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include <vulkan/vulkan.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string_view>
#include <thread>
#include <vector>
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");
}
int resize_recreates_swapchain_with_diagnostics() {
#ifndef _WIN32
if (!std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY"))
return 77;
#endif
// A headless Vulkan ICD can render the other diagnostics tests but cannot
// create the SDL surface needed to exercise swapchain recreation.
if (!SDL_InitSubSystem(SDL_INIT_VIDEO))
return 77;
Uint32 required_count{};
const auto* required = SDL_Vulkan_GetInstanceExtensions(&required_count);
std::uint32_t available_count{};
const auto enumerated = vkEnumerateInstanceExtensionProperties(
nullptr, &available_count, nullptr) == VK_SUCCESS;
std::vector<VkExtensionProperties> available(available_count);
const bool queried = enumerated &&
vkEnumerateInstanceExtensionProperties(nullptr, &available_count,
available.data()) == VK_SUCCESS;
const bool surface_supported = required && queried &&
std::all_of(required, required + required_count, [&](const char* name) {
return std::any_of(available.begin(), available.end(), [&](const auto& extension) {
return std::strcmp(extension.extensionName, name) == 0;
});
});
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (!surface_supported)
return 77;
std::jthread watchdog([](std::stop_token stop) {
for (int i = 0; i < 150 && !stop.stop_requested(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(200));
if (!stop.stop_requested())
std::_Exit(70);
});
RendererConfig config;
config.width = 320;
config.height = 240;
config.headless = false;
config.validation = true;
config.temporal_mode = TemporalMode::TAA;
config.temporal_diagnostics = true;
Renderer renderer(config);
auto frame = lit_scene(config.width, config.height);
frame.draws.push_back(cube({0, 0, 0}, {.9f, .4f, .2f, 1}, "resized-cube"));
renderer.render(frame);
require(renderer.stats().temporal_counters_valid,
"Initial window frame must have valid temporal pixel counts");
const auto previous_width = renderer.width();
const auto previous_height = renderer.height();
renderer.resize(480, 270);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (std::chrono::steady_clock::now() < deadline) {
SDL_PumpEvents();
int pixel_width{}, pixel_height{};
int window_count{};
auto windows = SDL_GetWindows(&window_count);
if (windows && window_count == 1)
SDL_GetWindowSizeInPixels(windows[0], &pixel_width, &pixel_height);
SDL_free(windows);
if (pixel_width > 0 && pixel_height > 0 &&
(std::uint32_t(pixel_width) != previous_width ||
std::uint32_t(pixel_height) != previous_height))
break;
SDL_Delay(10);
}
if (std::chrono::steady_clock::now() >= deadline)
return 77;
renderer.render(frame);
require(renderer.width() != previous_width || renderer.height() != previous_height,
"Render must recreate swapchain and temporal targets at the new extent");
require(renderer.stats().temporal_counters_valid &&
renderer.stats().temporal_accepted_pixels == 0 &&
renderer.stats().temporal_rejected_pixels ==
renderer.width() * renderer.height() &&
renderer.stats().validation_errors == 0,
"Resize frame must reject old history and read back a fresh counter buffer");
renderer.render(frame);
require(renderer.stats().temporal_counters_valid &&
renderer.stats().temporal_accepted_pixels +
renderer.stats().temporal_rejected_pixels ==
renderer.width() * renderer.height() &&
renderer.stats().validation_errors == 0,
"Following frame must retain valid temporal counters after resize");
return 0;
}
} // namespace
int main(int argc, char** argv) {
try {
if (argc == 2 && std::string_view(argv[1]) == "--window-resize")
return resize_recreates_swapchain_with_diagnostics();
require(argc == 1, "Unknown temporal diagnostics test arguments");
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);