From 4b8f9b8132bdb893189575fd0b71f6ec5eef998a Mon Sep 17 00:00:00 2001 From: Emil <65846814+emil28092005@users.noreply.github.com> Date: Thu, 24 Sep 2026 02:01:36 +0300 Subject: [PATCH] Validate temporal scale and deterministic internal extent --- include/faset/render/temporal.hpp | 7 +++++ src/render/temporal.cpp | 20 ++++++++++++++ tests/render_temporal_policy_tests.cpp | 37 ++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/include/faset/render/temporal.hpp b/include/faset/render/temporal.hpp index 6ed7187..25c111c 100644 --- a/include/faset/render/temporal.hpp +++ b/include/faset/render/temporal.hpp @@ -26,6 +26,13 @@ TemporalFallbackReason temporal_fallback_reason(TemporalMode requested, TemporalMode select_effective_temporal_mode(TemporalMode requested, TemporalCapabilities available) noexcept; +// Validate the requested mode and scale before allocating scene targets. +// Off/TAA are 1:1; Upscale accepts [0.5, 1). Ceil rounding is deterministic +// for odd output extents and never produces a zero-sized internal target. +std::array temporal_internal_extent(std::uint32_t output_width, + std::uint32_t output_height, + TemporalMode mode, float render_scale); + enum class TemporalResetReason { None, FirstFrame, diff --git a/src/render/temporal.cpp b/src/render/temporal.cpp index ecba009..5a9e8af 100644 --- a/src/render/temporal.cpp +++ b/src/render/temporal.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -23,6 +24,25 @@ TemporalMode select_effective_temporal_mode(TemporalMode requested, ? requested : TemporalMode::Off; } +std::array temporal_internal_extent(std::uint32_t output_width, + std::uint32_t output_height, + TemporalMode mode, float render_scale) { + if ((mode != TemporalMode::Off && mode != TemporalMode::TAA && + mode != TemporalMode::Upscale) || + !output_width || !output_height || !std::isfinite(render_scale) || + (mode == TemporalMode::Upscale + ? render_scale < 0.5f || render_scale >= 1.f + : render_scale != 1.f)) + throw std::invalid_argument("Invalid temporal mode, scale or output extent"); + if (mode != TemporalMode::Upscale) + return {output_width, output_height}; + const auto scaled = [&](std::uint32_t extent) { + return static_cast( + std::max(1.0, std::ceil(static_cast(extent) * render_scale))); + }; + return {scaled(output_width), scaled(output_height)}; +} + namespace { float halton(std::uint32_t index, std::uint32_t base) noexcept { float sample = 0.f; diff --git a/tests/render_temporal_policy_tests.cpp b/tests/render_temporal_policy_tests.cpp index 32adcb9..0dc764b 100644 --- a/tests/render_temporal_policy_tests.cpp +++ b/tests/render_temporal_policy_tests.cpp @@ -171,6 +171,42 @@ void deterministic_jitter() { } require(rejected_zero_extent, "Zero viewport extent cannot produce finite clip jitter"); } + +void render_scale_policy() { + const auto full = temporal_internal_extent(320, 240, TemporalMode::Off, 1.f); + require(full == std::array{320, 240}, + "Off keeps scene and output at the same extent"); + require(temporal_internal_extent(319, 241, TemporalMode::TAA, 1.f) == + std::array{319, 241}, + "TAA is a one-to-one reconstruction mode"); + require(temporal_internal_extent(320, 240, TemporalMode::Upscale, .67f) == + std::array{215, 161}, + "Upscale uses deterministic ceil dimensions for odd pixel products"); + require(temporal_internal_extent(1, 1, TemporalMode::Upscale, .5f) == + std::array{1, 1}, + "A supported output always has at least one internal pixel"); + auto rejected = [](TemporalMode mode, float scale) { + try { + (void)temporal_internal_extent(320, 240, mode, scale); + return false; + } catch (const std::invalid_argument&) { + return true; + } + }; + require(rejected(TemporalMode::TAA, .75f) && rejected(TemporalMode::Upscale, 1.f) && + rejected(TemporalMode::Upscale, .49f) && + rejected(TemporalMode::Upscale, std::numeric_limits::quiet_NaN()) && + rejected(TemporalMode::Off, .67f) && + rejected(static_cast(42), 1.f), + "Invalid mode/scale combinations must fail before target allocation"); + bool zero_rejected = false; + try { + (void)temporal_internal_extent(0, 240, TemporalMode::TAA, 1.f); + } catch (const std::invalid_argument&) { + zero_rejected = true; + } + require(zero_rejected, "Zero output width is not a valid temporal target"); +} } // namespace int main() { @@ -179,4 +215,5 @@ int main() { rendered_history_and_camera_motion(); incompatible_view_state(); deterministic_jitter(); + render_scale_policy(); }