From 56a5bc62f6729e1f3ee99fee57b73c9b49cd0d8d Mon Sep 17 00:00:00 2001 From: Emil <65846814+emil28092005@users.noreply.github.com> Date: Thu, 24 Sep 2026 02:05:39 +0300 Subject: [PATCH] Commit temporal history only after completed frames --- include/faset/render/temporal.hpp | 12 ++++++++++ src/render/temporal.cpp | 12 ++++++++++ tests/render_temporal_policy_tests.cpp | 31 ++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/include/faset/render/temporal.hpp b/include/faset/render/temporal.hpp index 25c111c..e52d3bc 100644 --- a/include/faset/render/temporal.hpp +++ b/include/faset/render/temporal.hpp @@ -73,6 +73,18 @@ TemporalHistoryDecision evaluate_temporal_history(const std::optional& previous, const TemporalHistoryKey& current) noexcept; +// The renderer calls prepare before recording and complete only after a +// successful GPU submission. An exception or failed submission leaves the +// previously completed key intact. This state is independent of P2 HZB. +class TemporalHistoryState { + public: + TemporalHistoryDecision prepare(const TemporalHistoryKey& current) const noexcept; + void complete(const TemporalHistoryKey& rendered); + + private: + std::optional completed_; +}; + // 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. diff --git a/src/render/temporal.cpp b/src/render/temporal.cpp index 5a9e8af..f2be1c5 100644 --- a/src/render/temporal.cpp +++ b/src/render/temporal.cpp @@ -151,6 +151,18 @@ evaluate_temporal_history(const std::optional& previous, return {true, TemporalResetReason::None}; } +TemporalHistoryDecision +TemporalHistoryState::prepare(const TemporalHistoryKey& current) const noexcept { + return evaluate_temporal_history(completed_, current); +} + +void TemporalHistoryState::complete(const TemporalHistoryKey& rendered) { + if (rendered.mode == TemporalMode::Off) + completed_.reset(); + else + completed_ = rendered; +} + std::array temporal_jitter(std::uint64_t frame_index, std::uint32_t viewport_width, std::uint32_t viewport_height) { diff --git a/tests/render_temporal_policy_tests.cpp b/tests/render_temporal_policy_tests.cpp index 0dc764b..520d492 100644 --- a/tests/render_temporal_policy_tests.cpp +++ b/tests/render_temporal_policy_tests.cpp @@ -207,6 +207,36 @@ void render_scale_policy() { } require(zero_rejected, "Zero output width is not a valid temporal target"); } + +void completed_frames_only_become_history() { + TemporalHistoryState history; + auto frame = steady_view(); + require(history.prepare(frame).reason == TemporalResetReason::FirstFrame, + "A prepared first frame has no committed history"); + history.complete(frame); + require(history.prepare(frame).valid, + "A successfully completed frame becomes reusable history"); + + auto failed_frame = frame; + failed_frame.view_id = "failed-submit-view"; + require(history.prepare(failed_frame).reason == TemporalResetReason::ViewChanged, + "A candidate view switch is detected before submission"); + try { + throw std::runtime_error("synthetic queue submit failure"); + } catch (const std::runtime_error&) { + // The caller never invokes complete() on a failed submission. + } + require(history.prepare(frame).valid, + "A failed submission must not replace the last completed history key"); + require(history.prepare(failed_frame).reason == TemporalResetReason::ViewChanged, + "An uncommitted frame must not become the next frame's predecessor"); + + frame.mode = TemporalMode::Off; + history.complete(frame); + frame.mode = TemporalMode::TAA; + require(history.prepare(frame).reason == TemporalResetReason::FirstFrame, + "Completing an Off frame discards temporal history"); +} } // namespace int main() { @@ -216,4 +246,5 @@ int main() { incompatible_view_state(); deterministic_jitter(); render_scale_policy(); + completed_frames_only_become_history(); }