Keep temporal diagnostic staging alive through resize
This commit is contained in:
@@ -171,8 +171,13 @@ if(BUILD_TESTING)
|
|||||||
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
|
add_executable(faset_render_temporal_diagnostics_tests
|
||||||
"${PROJECT_SOURCE_DIR}/tests/render_temporal_diagnostics_tests.cpp")
|
"${PROJECT_SOURCE_DIR}/tests/render_temporal_diagnostics_tests.cpp")
|
||||||
target_link_libraries(faset_render_temporal_diagnostics_tests PRIVATE faset_render)
|
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)
|
add_test(NAME render_temporal_diagnostics COMMAND faset_render_temporal_diagnostics_tests)
|
||||||
set_tests_properties(render_temporal_diagnostics PROPERTIES LABELS "gpu;p3")
|
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()
|
endif()
|
||||||
install(FILES ${FASET_SHADER_OUTPUTS} DESTINATION shaders)
|
install(FILES ${FASET_SHADER_OUTPUTS} DESTINATION shaders)
|
||||||
|
|||||||
+29
-26
@@ -2338,14 +2338,8 @@ struct Renderer::Impl {
|
|||||||
statistics.lod_counts = {};
|
statistics.lod_counts = {};
|
||||||
statistics.visibility_counters_valid = false;
|
statistics.visibility_counters_valid = false;
|
||||||
statistics.requested_temporal_mode = config.temporal_mode;
|
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_history_valid = false;
|
||||||
statistics.temporal_valid_motion_instances = 0;
|
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_jitter = {};
|
||||||
statistics.temporal_counters_valid = false;
|
statistics.temporal_counters_valid = false;
|
||||||
statistics.temporal_accepted_pixels = statistics.temporal_rejected_pixels = 0;
|
statistics.temporal_accepted_pixels = statistics.temporal_rejected_pixels = 0;
|
||||||
@@ -2356,26 +2350,6 @@ struct Renderer::Impl {
|
|||||||
it = it->second.owner.expired() ? bounds_cache.erase(it) : std::next(it);
|
it = it->second.owner.expired() ? bounds_cache.erase(it) : std::next(it);
|
||||||
for (auto it = opacity_cache.begin(); it != opacity_cache.end();)
|
for (auto it = opacity_cache.begin(); it != opacity_cache.end();)
|
||||||
it = it->second.owner.expired() ? opacity_cache.erase(it) : std::next(it);
|
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;
|
|
||||||
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;
|
bool can_present = surface != VK_NULL_HANDLE;
|
||||||
if (surface) {
|
if (surface) {
|
||||||
// A capture may render between normal event-loop iterations. Keep the window
|
// A capture may render between normal event-loop iterations. Keep the window
|
||||||
@@ -2389,6 +2363,35 @@ struct Renderer::Impl {
|
|||||||
if (can_present && (dirty_swapchain || !swapchain))
|
if (can_present && (dirty_swapchain || !swapchain))
|
||||||
make_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.
|
// Retire atlas/image resources no longer retained by a caller.
|
||||||
for (auto it = textures.begin(); it != textures.end();) {
|
for (auto it = textures.begin(); it != textures.end();) {
|
||||||
if (it->first != white.get() && it->second.source.use_count() == 1) {
|
if (it->first != white.get() && it->second.source.use_count() == 1) {
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
#include "render_temporal_fixtures.hpp"
|
#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 <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <string_view>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
using namespace faset::render;
|
using namespace faset::render;
|
||||||
using namespace faset::render::temporal_test;
|
using namespace faset::render::temporal_test;
|
||||||
@@ -72,10 +82,98 @@ void pixel_decisions_are_diagnostic_only(VisibilityMode visibility) {
|
|||||||
require(renderer.pixels() == reference.pixels(),
|
require(renderer.pixels() == reference.pixels(),
|
||||||
"Diagnostic toggles preserve the Off image");
|
"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
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main(int argc, char** argv) {
|
||||||
try {
|
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::Direct);
|
||||||
pixel_decisions_are_diagnostic_only(VisibilityMode::GpuFrustum);
|
pixel_decisions_are_diagnostic_only(VisibilityMode::GpuFrustum);
|
||||||
std::cout << "Temporal pixel diagnostics are opt-in and exact\n";
|
std::cout << "Temporal pixel diagnostics are opt-in and exact\n";
|
||||||
|
|||||||
Reference in New Issue
Block a user