Expose opt-in temporal pixel diagnostics
This commit is contained in:
@@ -188,6 +188,14 @@ Json profileFrames(const std::vector<ProfileSample>& samples) {
|
|||||||
{"temporal_internal_width", sample.lighting.temporal_internal_width},
|
{"temporal_internal_width", sample.lighting.temporal_internal_width},
|
||||||
{"temporal_internal_height", sample.lighting.temporal_internal_height},
|
{"temporal_internal_height", sample.lighting.temporal_internal_height},
|
||||||
{"temporal_jitter", sample.lighting.temporal_jitter},
|
{"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",
|
{"gpu_temporal_resolve_ms",
|
||||||
gpuMeasured ? Json(sample.lighting.gpu_temporal_resolve_ms) : Json(nullptr)},
|
gpuMeasured ? Json(sample.lighting.gpu_temporal_resolve_ms) : Json(nullptr)},
|
||||||
{"gpu_temporal_composite_ms",
|
{"gpu_temporal_composite_ms",
|
||||||
|
|||||||
@@ -169,5 +169,10 @@ if(BUILD_TESTING)
|
|||||||
"${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/tests/test_p3_lighting_benchmark.py"
|
"${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/tests/test_p3_lighting_benchmark.py"
|
||||||
--real-executable "$<TARGET_FILE:faset_p3_lighting_benchmark>")
|
--real-executable "$<TARGET_FILE:faset_p3_lighting_benchmark>")
|
||||||
set_tests_properties(render_lighting_benchmark_smoke PROPERTIES LABELS "gpu;p3" TIMEOUT 90)
|
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()
|
endif()
|
||||||
install(FILES ${FASET_SHADER_OUTPUTS} DESTINATION shaders)
|
install(FILES ${FASET_SHADER_OUTPUTS} DESTINATION shaders)
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ struct RendererConfig {
|
|||||||
LightingMode lighting_mode{LightingMode::Auto};
|
LightingMode lighting_mode{LightingMode::Auto};
|
||||||
// GPU counter readback is diagnostic-only; normal visibility uses no CPU feedback.
|
// GPU counter readback is diagnostic-only; normal visibility uses no CPU feedback.
|
||||||
bool visibility_diagnostics{false};
|
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.
|
// Optional isolated shader bundle, useful for editor preview and shader reload tests.
|
||||||
std::filesystem::path shader_directory{};
|
std::filesystem::path shader_directory{};
|
||||||
};
|
};
|
||||||
@@ -219,6 +221,8 @@ struct FrameStats {
|
|||||||
std::uint32_t temporal_valid_motion_instances{};
|
std::uint32_t temporal_valid_motion_instances{};
|
||||||
std::uint32_t temporal_internal_width{}, temporal_internal_height{};
|
std::uint32_t temporal_internal_width{}, temporal_internal_height{};
|
||||||
std::array<float, 2> temporal_jitter{};
|
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{};
|
double gpu_temporal_resolve_ms{}, gpu_temporal_composite_ms{}, gpu_ui_ms{};
|
||||||
std::vector<std::string> graph_passes;
|
std::vector<std::string> graph_passes;
|
||||||
double gpu_light_tiles_ms{};
|
double gpu_light_tiles_ms{};
|
||||||
@@ -250,6 +254,7 @@ class Renderer {
|
|||||||
TemporalMode temporal_mode() const;
|
TemporalMode temporal_mode() const;
|
||||||
float render_scale() const;
|
float render_scale() const;
|
||||||
void set_visibility_diagnostics(bool enabled);
|
void set_visibility_diagnostics(bool enabled);
|
||||||
|
void set_temporal_diagnostics(bool enabled);
|
||||||
// Reads the most recently completed HZB mip for editor diagnostics only.
|
// Reads the most recently completed HZB mip for editor diagnostics only.
|
||||||
// Normal visibility decisions remain entirely on the GPU.
|
// Normal visibility decisions remain entirely on the GPU.
|
||||||
std::optional<HzbDebugImage> hzb_debug_image(std::uint32_t mip = 0);
|
std::optional<HzbDebugImage> hzb_debug_image(std::uint32_t mip = 0);
|
||||||
|
|||||||
+10
-1
@@ -7,7 +7,7 @@ struct TemporalResolveParameters {
|
|||||||
uint4 dimensions; // output width/height, internal width/height
|
uint4 dimensions; // output width/height, internal width/height
|
||||||
float4 outputSceneRect; // output-pixel x/y/width/height
|
float4 outputSceneRect; // output-pixel x/y/width/height
|
||||||
float4 internalSceneRect; // internal-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
|
float4 jitterMotion; // xy = current-minus-prior local UV; z = static camera
|
||||||
};
|
};
|
||||||
[[vk::push_constant]] ConstantBuffer<TemporalResolveParameters> temporalParameters;
|
[[vk::push_constant]] ConstantBuffer<TemporalResolveParameters> temporalParameters;
|
||||||
@@ -20,6 +20,9 @@ struct TemporalResolveParameters {
|
|||||||
RWTexture2D<float4> nextHistoryColor;
|
RWTexture2D<float4> nextHistoryColor;
|
||||||
[[vk::binding(6,0)]] [vk::image_format("r32f")]
|
[[vk::binding(6,0)]] [vk::image_format("r32f")]
|
||||||
RWTexture2D<float> nextHistoryDepth;
|
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) {
|
int2 clampScenePixel(int2 pixel) {
|
||||||
return clamp(pixel, int2(0), int2(temporalParameters.dimensions.zw) - 1);
|
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 float4 currentColor = sceneColorAt(currentPixel);
|
||||||
const float currentDepth = sceneDepthAt(currentPixel);
|
const float currentDepth = sceneDepthAt(currentPixel);
|
||||||
float4 resolved = currentColor;
|
float4 resolved = currentColor;
|
||||||
|
bool acceptedHistory = false;
|
||||||
|
|
||||||
if (insideScene && temporalParameters.flags.x != 0) {
|
if (insideScene && temporalParameters.flags.x != 0) {
|
||||||
const float4 centerMotion = sceneVelocityAt(currentPixel);
|
const float4 centerMotion = sceneVelocityAt(currentPixel);
|
||||||
@@ -190,6 +194,7 @@ void temporalResolveMain(uint3 dispatchId : SV_DispatchThreadID) {
|
|||||||
(!matchingFar || !nearerOccluder)) {
|
(!matchingFar || !nearerOccluder)) {
|
||||||
const float3 priorColor = clamp(sampled, low, high);
|
const float3 priorColor = clamp(sampled, low, high);
|
||||||
resolved.rgb = lerp(currentColor.rgb, priorColor, weight);
|
resolved.rgb = lerp(currentColor.rgb, priorColor, weight);
|
||||||
|
acceptedHistory = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,6 +203,10 @@ void temporalResolveMain(uint3 dispatchId : SV_DispatchThreadID) {
|
|||||||
}
|
}
|
||||||
nextHistoryColor[outputPixel] = resolved;
|
nextHistoryColor[outputPixel] = resolved;
|
||||||
nextHistoryDepth[outputPixel] = currentDepth;
|
nextHistoryDepth[outputPixel] = currentDepth;
|
||||||
|
if (insideScene && temporalParameters.flags.y != 0) {
|
||||||
|
uint oldCount;
|
||||||
|
InterlockedAdd(historyDecisionCounts[acceptedHistory ? 0 : 1], 1, oldCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#elif defined(FASET_TEMPORAL_COMPOSITE)
|
#elif defined(FASET_TEMPORAL_COMPOSITE)
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ ImGuiKey key(std::string_view name) {
|
|||||||
} // namespace
|
} // namespace
|
||||||
struct DebugOverlay::Impl {
|
struct DebugOverlay::Impl {
|
||||||
ImGuiContext* context{};
|
ImGuiContext* context{};
|
||||||
bool visible{}, freeze{}, show_hzb{};
|
bool visible{}, freeze{}, show_hzb{}, count_temporal_pixels{};
|
||||||
bool hzb_sampled{}, hzb_available{};
|
bool hzb_sampled{}, hzb_available{};
|
||||||
int hzb_mip{3}, hzb_last_mip{-1};
|
int hzb_mip{3}, hzb_last_mip{-1};
|
||||||
std::uint64_t hzb_frame{};
|
std::uint64_t hzb_frame{};
|
||||||
@@ -464,6 +464,21 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
|
|||||||
ImGui::Text("History: %s Reset: %s",
|
ImGui::Text("History: %s Reset: %s",
|
||||||
stats.temporal_history_valid ? "valid" : "invalid",
|
stats.temporal_history_valid ? "valid" : "invalid",
|
||||||
temporal_reset_label(stats.temporal_reset_reason));
|
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)
|
if (stats.gpu_ms > 0)
|
||||||
ImGui::Text("GPU: resolve %.2f composite %.2f UI %.2f ms",
|
ImGui::Text("GPU: resolve %.2f composite %.2f UI %.2f ms",
|
||||||
stats.gpu_temporal_resolve_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.
|
// GPU counters are a diagnostics readback, never a normal renderer dependency.
|
||||||
renderer.set_visibility_diagnostics(state.visible);
|
renderer.set_visibility_diagnostics(state.visible);
|
||||||
|
renderer.set_temporal_diagnostics(state.visible && state.count_temporal_pixels &&
|
||||||
|
renderer.temporal_mode() != render::TemporalMode::Off);
|
||||||
ImGui::Render();
|
ImGui::Render();
|
||||||
const auto* data = ImGui::GetDrawData();
|
const auto* data = ImGui::GetDrawData();
|
||||||
if (!data || !data->Valid)
|
if (!data || !data->Valid)
|
||||||
|
|||||||
+83
-11
@@ -213,6 +213,7 @@ struct SceneResources {
|
|||||||
};
|
};
|
||||||
struct TemporalResources {
|
struct TemporalResources {
|
||||||
Image scene_color, velocity, history_color[2], history_depth[2];
|
Image scene_color, velocity, history_color[2], history_depth[2];
|
||||||
|
Buffer pixel_counts, pixel_counts_stage;
|
||||||
VkDescriptorSetLayout resolve_layout{}, composite_layout{};
|
VkDescriptorSetLayout resolve_layout{}, composite_layout{};
|
||||||
VkDescriptorPool descriptor_pool{};
|
VkDescriptorPool descriptor_pool{};
|
||||||
VkDescriptorSet resolve_sets[2]{}, composite_sets[2]{};
|
VkDescriptorSet resolve_sets[2]{}, composite_sets[2]{};
|
||||||
@@ -429,6 +430,8 @@ struct Renderer::Impl {
|
|||||||
destroy(image);
|
destroy(image);
|
||||||
for (auto& image : temporal.history_depth)
|
for (auto& image : temporal.history_depth)
|
||||||
destroy(image);
|
destroy(image);
|
||||||
|
destroy(temporal.pixel_counts);
|
||||||
|
destroy(temporal.pixel_counts_stage);
|
||||||
destroy(vertices);
|
destroy(vertices);
|
||||||
destroy(readback);
|
destroy(readback);
|
||||||
destroy(lighting_header);
|
destroy(lighting_header);
|
||||||
@@ -963,6 +966,8 @@ struct Renderer::Impl {
|
|||||||
destroy(image);
|
destroy(image);
|
||||||
for (auto& image : temporal.history_depth)
|
for (auto& image : temporal.history_depth)
|
||||||
destroy(image);
|
destroy(image);
|
||||||
|
destroy(temporal.pixel_counts);
|
||||||
|
destroy(temporal.pixel_counts_stage);
|
||||||
temporal.has_completed_image = false;
|
temporal.has_completed_image = false;
|
||||||
scene.hzb_history_valid = false;
|
scene.hzb_history_valid = false;
|
||||||
temporal.capabilities.extent = width <= max_image_dimension &&
|
temporal.capabilities.extent = width <= max_image_dimension &&
|
||||||
@@ -998,6 +1003,10 @@ struct Renderer::Impl {
|
|||||||
image = make_image(width, height, VK_FORMAT_R32_SFLOAT,
|
image = make_image(width, height, VK_FORMAT_R32_SFLOAT,
|
||||||
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT,
|
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT,
|
||||||
VK_IMAGE_ASPECT_COLOR_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;
|
temporal.completed_index = 0;
|
||||||
}
|
}
|
||||||
const auto padded_width = std::bit_ceil(internal[0]);
|
const auto padded_width = std::bit_ceil(internal[0]);
|
||||||
@@ -1036,9 +1045,10 @@ struct Renderer::Impl {
|
|||||||
if (temporal.descriptor_pool)
|
if (temporal.descriptor_pool)
|
||||||
vkDestroyDescriptorPool(device, temporal.descriptor_pool, nullptr);
|
vkDestroyDescriptorPool(device, temporal.descriptor_pool, nullptr);
|
||||||
temporal.descriptor_pool = {};
|
temporal.descriptor_pool = {};
|
||||||
const std::array<VkDescriptorPoolSize, 2> sizes{{
|
const std::array<VkDescriptorPoolSize, 3> sizes{{
|
||||||
{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 12},
|
{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{};
|
VkDescriptorPoolCreateInfo pool_info{};
|
||||||
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||||
pool_info.maxSets = 4;
|
pool_info.maxSets = 4;
|
||||||
@@ -1072,7 +1082,7 @@ struct Renderer::Impl {
|
|||||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL},
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL},
|
||||||
{VK_NULL_HANDLE, temporal.history_color[next].view, VK_IMAGE_LAYOUT_GENERAL},
|
{VK_NULL_HANDLE, temporal.history_color[next].view, VK_IMAGE_LAYOUT_GENERAL},
|
||||||
{VK_NULL_HANDLE, temporal.history_depth[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) {
|
for (std::uint32_t binding = 0; binding < 7; ++binding) {
|
||||||
writes[binding].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
writes[binding].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
writes[binding].dstSet = temporal.resolve_sets[next];
|
writes[binding].dstSet = temporal.resolve_sets[next];
|
||||||
@@ -1082,15 +1092,23 @@ struct Renderer::Impl {
|
|||||||
? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
||||||
writes[binding].pImageInfo = &images[binding];
|
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].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
writes[7].dstSet = temporal.composite_sets[next];
|
writes[7].dstSet = temporal.resolve_sets[next];
|
||||||
writes[7].dstBinding = 0;
|
writes[7].dstBinding = 7;
|
||||||
writes[7].descriptorCount = 1;
|
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{
|
const VkDescriptorImageInfo composite_image{
|
||||||
VK_NULL_HANDLE, temporal.history_color[next].view,
|
VK_NULL_HANDLE, temporal.history_color[next].view,
|
||||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
|
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()),
|
vkUpdateDescriptorSets(device, static_cast<std::uint32_t>(writes.size()),
|
||||||
writes.data(), 0, nullptr);
|
writes.data(), 0, nullptr);
|
||||||
}
|
}
|
||||||
@@ -1802,10 +1820,11 @@ struct Renderer::Impl {
|
|||||||
if (!temporal.capabilities.compute || !temporal.capabilities.formats)
|
if (!temporal.capabilities.compute || !temporal.capabilities.formats)
|
||||||
return;
|
return;
|
||||||
if (!temporal.resolve_layout) {
|
if (!temporal.resolve_layout) {
|
||||||
std::array<VkDescriptorSetLayoutBinding, 7> bindings{};
|
std::array<VkDescriptorSetLayoutBinding, 8> bindings{};
|
||||||
for (std::uint32_t i = 0; i < bindings.size(); ++i)
|
for (std::uint32_t i = 0; i < bindings.size(); ++i)
|
||||||
bindings[i] = {i, i < 5 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE
|
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};
|
1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
|
||||||
VkDescriptorSetLayoutCreateInfo descriptor_info{};
|
VkDescriptorSetLayoutCreateInfo descriptor_info{};
|
||||||
descriptor_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_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_width = temporal.internal_width;
|
||||||
statistics.temporal_internal_height = temporal.internal_height;
|
statistics.temporal_internal_height = temporal.internal_height;
|
||||||
statistics.temporal_jitter = {};
|
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_temporal_resolve_ms = statistics.gpu_temporal_composite_ms =
|
||||||
statistics.gpu_ui_ms = 0;
|
statistics.gpu_ui_ms = 0;
|
||||||
statistics.graph_passes.clear();
|
statistics.graph_passes.clear();
|
||||||
@@ -2342,6 +2363,17 @@ struct Renderer::Impl {
|
|||||||
const bool occlusion = statistics.effective_visibility_mode ==
|
const bool occlusion = statistics.effective_visibility_mode ==
|
||||||
VisibilityMode::GpuOcclusion;
|
VisibilityMode::GpuOcclusion;
|
||||||
const bool temporal_active = statistics.effective_temporal_mode != TemporalMode::Off;
|
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.gpu_visibility_active = gpu_active;
|
||||||
statistics.hzb_valid = false;
|
statistics.hzb_valid = false;
|
||||||
bool can_present = surface != VK_NULL_HANDLE;
|
bool can_present = surface != VK_NULL_HANDLE;
|
||||||
@@ -3627,6 +3659,15 @@ struct Renderer::Impl {
|
|||||||
add_pass("TemporalResolve",
|
add_pass("TemporalResolve",
|
||||||
{"scene_color", "depth", "scene_velocity", "history_previous"},
|
{"scene_color", "depth", "scene_velocity", "history_previous"},
|
||||||
{"resolved_color", "resolved_depth"}, [&] {
|
{"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,
|
transition(command, temporal.scene_color,
|
||||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||||
VK_IMAGE_ASPECT_COLOR_BIT);
|
VK_IMAGE_ASPECT_COLOR_BIT);
|
||||||
@@ -3656,7 +3697,8 @@ struct Renderer::Impl {
|
|||||||
temporal.previous_unjittered_vp == snapshot.view_projection;
|
temporal.previous_unjittered_vp == snapshot.view_projection;
|
||||||
ResolvePush parameters{{width, height, raster_width, raster_height},
|
ResolvePush parameters{{width, height, raster_width, raster_height},
|
||||||
output_scene_viewport, scene_viewport,
|
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] -
|
{(statistics.temporal_jitter[0] -
|
||||||
temporal.previous_jitter[0]) * .5f,
|
temporal.previous_jitter[0]) * .5f,
|
||||||
(statistics.temporal_jitter[1] -
|
(statistics.temporal_jitter[1] -
|
||||||
@@ -3777,6 +3819,19 @@ struct Renderer::Impl {
|
|||||||
VK_PIPELINE_STAGE_2_HOST_BIT,
|
VK_PIPELINE_STAGE_2_HOST_BIT,
|
||||||
VK_ACCESS_2_HOST_READ_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)
|
if (swap_index)
|
||||||
add_pass("Presentation", {"color"}, {"swapchain"}, [&] {
|
add_pass("Presentation", {"color"}, {"swapchain"}, [&] {
|
||||||
@@ -3913,6 +3968,17 @@ struct Renderer::Impl {
|
|||||||
vkUnmapMemory(device, light_tile_readback.memory);
|
vkUnmapMemory(device, light_tile_readback.memory);
|
||||||
statistics.light_tile_counts_valid = true;
|
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();
|
instance_tracker.finish_frame();
|
||||||
scene.previous_vp = raster_vp;
|
scene.previous_vp = raster_vp;
|
||||||
scene.previous_projection = snapshot.projection;
|
scene.previous_projection = snapshot.projection;
|
||||||
@@ -3955,7 +4021,8 @@ struct Renderer::Impl {
|
|||||||
statistics.gpu_allocated_bytes += image.allocation_size;
|
statistics.gpu_allocated_bytes += image.allocation_size;
|
||||||
}
|
}
|
||||||
statistics.gpu_allocated_bytes += temporal.scene_color.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)
|
for (const auto& image : temporal.history_color)
|
||||||
statistics.gpu_allocated_bytes += image.allocation_size;
|
statistics.gpu_allocated_bytes += image.allocation_size;
|
||||||
for (const auto& image : temporal.history_depth)
|
for (const auto& image : temporal.history_depth)
|
||||||
@@ -4136,6 +4203,11 @@ float Renderer::render_scale() const {
|
|||||||
void Renderer::set_visibility_diagnostics(bool enabled) {
|
void Renderer::set_visibility_diagnostics(bool enabled) {
|
||||||
impl_->config.visibility_diagnostics = 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) {
|
std::optional<HzbDebugImage> Renderer::hzb_debug_image(std::uint32_t mip) {
|
||||||
auto& renderer = *impl_;
|
auto& renderer = *impl_;
|
||||||
if (!renderer.scene.hzb_history_valid || !renderer.scene.hzb_supported)
|
if (!renderer.scene.hzb_history_valid || !renderer.scene.hzb_supported)
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ void validate_temporal_layout(const Json& layout, std::string_view entry) {
|
|||||||
require(layout.at("stage") == (resolve ? "compute" : vertex ? "vertex" : "fragment"),
|
require(layout.at("stage") == (resolve ? "compute" : vertex ? "vertex" : "fragment"),
|
||||||
"temporal shader stage changed");
|
"temporal shader stage changed");
|
||||||
const auto& descriptors = layout.at("descriptors");
|
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");
|
"temporal descriptor count changed");
|
||||||
for (std::size_t i = 0; i < descriptors.size(); ++i) {
|
for (std::size_t i = 0; i < descriptors.size(); ++i) {
|
||||||
const auto& descriptor = descriptors[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,
|
descriptor.at("count") == 1,
|
||||||
"temporal descriptor set, binding or count changed");
|
"temporal descriptor set, binding or count changed");
|
||||||
require(descriptor.at("type") ==
|
require(descriptor.at("type") ==
|
||||||
(resolve && i >= 5 ? "storage_image_2d" : "sampled_image_2d"),
|
(resolve && i == 7 ? "storage_buffer"
|
||||||
"temporal image descriptor type changed");
|
: 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),
|
require(descriptor.at("used") == (resolve || !vertex),
|
||||||
"temporal entry uses an unexpected image binding");
|
"temporal entry uses an unexpected image binding");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,9 +66,14 @@ with tempfile.TemporaryDirectory(prefix="faset-player-diagnostics-") as temporar
|
|||||||
"temporal_fallback_reason", "temporal_reset_reason",
|
"temporal_fallback_reason", "temporal_reset_reason",
|
||||||
"temporal_history_valid", "temporal_internal_width",
|
"temporal_history_valid", "temporal_internal_width",
|
||||||
"temporal_internal_height", "temporal_jitter",
|
"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"]:
|
"gpu_temporal_resolve_ms", "gpu_temporal_composite_ms", "gpu_ui_ms"]:
|
||||||
assert key in sample, (key, sample)
|
assert key in sample, (key, sample)
|
||||||
assert sample["effective_temporal_mode"] == temporal_mode, 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":
|
if temporal_mode != "off":
|
||||||
assert sample["temporal_internal_width"] > 0, sample
|
assert sample["temporal_internal_width"] > 0, sample
|
||||||
assert sample["temporal_internal_height"] > 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 reflection = temporary / "temporalResolveMain.reflection.json";
|
||||||
auto metadata = faset::read_json(reflection);
|
auto metadata = faset::read_json(reflection);
|
||||||
require(metadata["layout"]["stage"] == "compute" &&
|
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,
|
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"]["descriptors"][5]["binding"] = 8;
|
||||||
metadata["layout_fingerprint"] = faset::sha256(metadata["layout"].dump());
|
metadata["layout_fingerprint"] = faset::sha256(metadata["layout"].dump());
|
||||||
faset::atomic_write_json(reflection, metadata);
|
faset::atomic_write_json(reflection, metadata);
|
||||||
@@ -59,6 +61,14 @@ int main() {
|
|||||||
"A rehashed temporal image binding change must be rejected");
|
"A rehashed temporal image binding change must be rejected");
|
||||||
faset::atomic_write_json(
|
faset::atomic_write_json(
|
||||||
reflection, faset::read_json(original / "temporalResolveMain.reflection.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";
|
auto fragment = temporary / "temporalCompositeFragmentMain.spv";
|
||||||
const auto bytes = faset::read_text(fragment);
|
const auto bytes = faset::read_text(fragment);
|
||||||
|
|||||||
Reference in New Issue
Block a user