diff --git a/cmake/Renderer.cmake b/cmake/Renderer.cmake index 4b9561f..f042f82 100644 --- a/cmake/Renderer.cmake +++ b/cmake/Renderer.cmake @@ -171,8 +171,13 @@ if(BUILD_TESTING) 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) + 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) diff --git a/src/render/renderer.cpp b/src/render/renderer.cpp index 25edd5a..968caa8 100644 --- a/src/render/renderer.cpp +++ b/src/render/renderer.cpp @@ -2338,14 +2338,8 @@ 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; @@ -2356,26 +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; - 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; if (surface) { // 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)) 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) { diff --git a/tests/render_temporal_diagnostics_tests.cpp b/tests/render_temporal_diagnostics_tests.cpp index 4ac8fbd..cb3911c 100644 --- a/tests/render_temporal_diagnostics_tests.cpp +++ b/tests/render_temporal_diagnostics_tests.cpp @@ -1,8 +1,18 @@ #include "render_temporal_fixtures.hpp" +#include +#include +#include +#include +#include +#include #include +#include #include #include +#include +#include +#include using namespace faset::render; using namespace faset::render::temporal_test; @@ -72,10 +82,98 @@ void pixel_decisions_are_diagnostic_only(VisibilityMode visibility) { 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 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 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";