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
+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");
}