From 068f2e9af9768cd9e4688ed8c93836bdb18b0584 Mon Sep 17 00:00:00 2001 From: Emil <65846814+emil28092005@users.noreply.github.com> Date: Thu, 24 Sep 2026 01:39:56 +0300 Subject: [PATCH] Handle camera roll and explicit temporal Off --- include/faset/render/temporal.hpp | 5 +-- src/render/temporal.cpp | 50 ++++++++++++++++---------- tests/render_temporal_policy_tests.cpp | 25 +++++++++++++ 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/include/faset/render/temporal.hpp b/include/faset/render/temporal.hpp index 7fa371e..82c63cc 100644 --- a/include/faset/render/temporal.hpp +++ b/include/faset/render/temporal.hpp @@ -66,8 +66,9 @@ TemporalHistoryDecision evaluate_temporal_history(const std::optional& previous, const TemporalHistoryKey& current) noexcept; -// Sixteen-phase Halton(2,3) offset in clip-space units, for the scene raster -// projection only. UI, picking, culling, and history keys use unjittered space. +// Sixteen-phase Halton(2,3) offset in clip-space units for scene rasterization. +// UI, picking and history keys use unjittered space. Culling must account for +// this jitter with a conservative edge; HZB depth must match jittered geometry. // A zero viewport extent throws std::invalid_argument. std::array temporal_jitter(std::uint64_t frame_index, std::uint32_t viewport_width, diff --git a/src/render/temporal.cpp b/src/render/temporal.cpp index c6dba9c..f8c3b46 100644 --- a/src/render/temporal.cpp +++ b/src/render/temporal.cpp @@ -45,18 +45,11 @@ bool finite_camera(const TemporalHistoryKey& key) noexcept { return true; } -std::array view_direction(const TemporalHistoryKey& key) noexcept { - // A perspective VP encodes view forward in its fourth row. An orthographic - // projection has a constant fourth row; its third row carries direction. - std::array direction{key.view_projection[3], key.view_projection[7], - key.view_projection[11]}; - float length_squared = direction[0] * direction[0] + direction[1] * direction[1] + - direction[2] * direction[2]; - if (length_squared < 1e-12f) { - direction = {key.view_projection[2], key.view_projection[6], key.view_projection[10]}; - length_squared = direction[0] * direction[0] + direction[1] * direction[1] + - direction[2] * direction[2]; - } +std::array normalized_view_row(const TemporalHistoryKey& key, int row) noexcept { + std::array direction{key.view_projection[row], key.view_projection[row + 4], + key.view_projection[row + 8]}; + const float length_squared = direction[0] * direction[0] + + direction[1] * direction[1] + direction[2] * direction[2]; if (!std::isfinite(length_squared) || length_squared < 1e-12f) return {}; const float reciprocal = 1.f / std::sqrt(length_squared); @@ -65,6 +58,14 @@ std::array view_direction(const TemporalHistoryKey& key) noexcept { return direction; } +bool large_axis_turn(const std::array& a, + const std::array& b) noexcept { + if (a == std::array{} || b == std::array{}) + return true; + const float dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + return !std::isfinite(dot) || dot < 0.70710678f; // turn greater than 45 degrees +} + bool camera_discontinuity(const TemporalHistoryKey& previous, const TemporalHistoryKey& current) noexcept { if (!finite_camera(previous) || !finite_camera(current)) @@ -78,11 +79,21 @@ bool camera_discontinuity(const TemporalHistoryKey& previous, // camera motion and smaller view changes are handled by motion vectors. if (!std::isfinite(translation_squared) || translation_squared > 25.f) return true; - const auto a = view_direction(previous), b = view_direction(current); - if (a == std::array{} || b == std::array{}) - return true; - const float dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; - return !std::isfinite(dot) || dot < 0.70710678f; // turn greater than 45 degrees + // Compare horizontal and vertical camera axes too: comparing only forward + // cannot detect a sudden roll about the unchanged viewing direction. + for (int row = 0; row < 2; ++row) + if (large_axis_turn(normalized_view_row(previous, row), + normalized_view_row(current, row))) + return true; + // Perspective VP encodes forward in row four. Orthographic projection has + // a constant fourth row, so its third row carries the viewing direction. + auto a = normalized_view_row(previous, 3); + auto b = normalized_view_row(current, 3); + if (a == std::array{} && b == std::array{}) { + a = normalized_view_row(previous, 2); + b = normalized_view_row(current, 2); + } + return large_axis_turn(a, b); } } // namespace @@ -90,6 +101,9 @@ TemporalHistoryDecision evaluate_temporal_history(const std::optional& previous, const TemporalHistoryKey& current) noexcept { auto reset = [](TemporalResetReason reason) { return TemporalHistoryDecision{false, reason}; }; + if (current.mode == TemporalMode::Off) + return reset(previous && previous->mode != TemporalMode::Off + ? TemporalResetReason::ModeChanged : TemporalResetReason::None); if (current.camera_cut) return reset(TemporalResetReason::CameraCut); if (!previous) @@ -106,8 +120,6 @@ evaluate_temporal_history(const std::optional& previous, return reset(TemporalResetReason::ScaleChanged); if (before.mode != current.mode) return reset(TemporalResetReason::ModeChanged); - if (current.mode == TemporalMode::Off) - return reset(TemporalResetReason::Unsupported); if (before.scene_rect != current.scene_rect) return reset(TemporalResetReason::ViewportChanged); if (before.projection != current.projection) diff --git a/tests/render_temporal_policy_tests.cpp b/tests/render_temporal_policy_tests.cpp index 4091a2a..32adcb9 100644 --- a/tests/render_temporal_policy_tests.cpp +++ b/tests/render_temporal_policy_tests.cpp @@ -75,6 +75,19 @@ void rendered_history_and_camera_motion() { TemporalResetReason::CameraDiscontinuity, "A large camera turn must invalidate history"); current = previous; + current.view_projection[0] = -1; + current.view_projection[5] = -1; + require(evaluate_temporal_history(previous, current).reason == + TemporalResetReason::CameraDiscontinuity, + "A 180-degree roll must invalidate history even when forward is unchanged"); + current = previous; + current.view_projection[0] = 0.98480775f; + current.view_projection[1] = 0.17364818f; + current.view_projection[4] = -0.17364818f; + current.view_projection[5] = 0.98480775f; + require(evaluate_temporal_history(previous, current).valid, + "An ordinary small camera roll must retain compatible history"); + current = previous; current.view_projection[0] = std::numeric_limits::quiet_NaN(); require(evaluate_temporal_history(previous, current).reason == TemporalResetReason::CameraDiscontinuity, @@ -121,6 +134,17 @@ void incompatible_view_state() { "A changed shading generation invalidates history"); } +void intentionally_disabled_temporal_mode() { + auto off = steady_view(); + off.mode = TemporalMode::Off; + const auto first = evaluate_temporal_history(std::nullopt, off); + require(!first.valid && first.reason == TemporalResetReason::None, + "Explicit Off has no temporal history to reset or unsupported fallback to report"); + const auto later = evaluate_temporal_history(off, off); + require(!later.valid && later.reason == TemporalResetReason::None, + "Continuing in Off must remain a deliberate non-temporal mode"); +} + void deterministic_jitter() { const auto first = temporal_jitter(0, 320, 240); const auto second = temporal_jitter(1, 320, 240); @@ -151,6 +175,7 @@ void deterministic_jitter() { int main() { capability_fallback(); + intentionally_disabled_temporal_mode(); rendered_history_and_camera_motion(); incompatible_view_state(); deterministic_jitter();