Files
Faset_Engine/tests/render_temporal_policy_tests.cpp
T

183 lines
8.2 KiB
C++

#include <faset/render/temporal.hpp>
#include <cmath>
#include <limits>
#include <optional>
#include <stdexcept>
namespace {
using namespace faset::render;
void require(bool condition, const char* message) {
if (!condition)
throw std::runtime_error(message);
}
void capability_fallback() {
constexpr TemporalCapabilities full{true, true, true};
require(select_effective_temporal_mode(TemporalMode::Off, {}) == TemporalMode::Off,
"Off must not require temporal GPU capabilities");
require(temporal_fallback_reason(TemporalMode::Off, {}) == TemporalFallbackReason::None,
"Explicit Off is not a capability fallback");
require(select_effective_temporal_mode(TemporalMode::TAA, full) == TemporalMode::TAA,
"Available TAA must remain active");
require(select_effective_temporal_mode(TemporalMode::Upscale, full) == TemporalMode::Upscale,
"Available upscaling must remain active");
require(select_effective_temporal_mode(TemporalMode::TAA, {false, true, true}) ==
TemporalMode::Off &&
temporal_fallback_reason(TemporalMode::TAA, {false, true, true}) ==
TemporalFallbackReason::ComputeUnavailable,
"Missing compute must produce a named Off fallback");
require(temporal_fallback_reason(TemporalMode::TAA, {true, false, true}) ==
TemporalFallbackReason::FormatUnavailable,
"Missing sampled/storage formats must identify the format fallback");
require(temporal_fallback_reason(TemporalMode::Upscale, {true, true, false}) ==
TemporalFallbackReason::ExtentUnsupported,
"An unsupported target extent must identify the extent fallback");
}
TemporalHistoryKey steady_view() {
TemporalHistoryKey key;
key.view_id = "main-camera";
key.output_width = key.internal_width = 320;
key.output_height = key.internal_height = 240;
key.scene_rect = {7, 11, 301, 219};
key.projection = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
key.view_projection = key.projection;
key.mode = TemporalMode::TAA;
key.render_scale = 1;
key.shader_generation = 4;
return key;
}
void rendered_history_and_camera_motion() {
const auto previous = steady_view();
auto current = previous;
require(evaluate_temporal_history(std::nullopt, current).reason ==
TemporalResetReason::FirstFrame,
"A first temporal frame must not claim history");
current.camera_eye = {0.5f, 0, 0};
current.view_projection[12] = -0.5f;
const auto ordinary_motion = evaluate_temporal_history(previous, current);
require(ordinary_motion.valid && ordinary_motion.reason == TemporalResetReason::None,
"Ordinary camera motion must retain compatible history");
current.camera_cut = true;
require(evaluate_temporal_history(previous, current).reason == TemporalResetReason::CameraCut,
"An explicit cut must override otherwise compatible history");
current.camera_cut = false;
current.camera_eye = {6, 0, 0};
require(evaluate_temporal_history(previous, current).reason ==
TemporalResetReason::CameraDiscontinuity,
"A large unmarked teleport must invalidate history");
current = previous;
current.view_projection[10] = -1;
require(evaluate_temporal_history(previous, current).reason ==
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<float>::quiet_NaN();
require(evaluate_temporal_history(previous, current).reason ==
TemporalResetReason::CameraDiscontinuity,
"Nonfinite camera matrices cannot admit history");
}
void incompatible_view_state() {
const auto previous = steady_view();
auto current = previous;
current.view_id = "other-camera";
require(evaluate_temporal_history(previous, current).reason == TemporalResetReason::ViewChanged,
"A second view cannot inherit another view's color history");
current = previous;
++current.output_width;
require(evaluate_temporal_history(previous, current).reason == TemporalResetReason::Resize,
"Output resize invalidates history");
current = previous;
current.scene_rect[0] += 1;
require(evaluate_temporal_history(previous, current).reason ==
TemporalResetReason::ViewportChanged,
"Moving the editor viewport invalidates history");
current = previous;
current.projection[0] += 0.1f;
require(evaluate_temporal_history(previous, current).reason ==
TemporalResetReason::ProjectionChanged,
"FOV or aspect change invalidates history");
current = previous;
current.mode = TemporalMode::Upscale;
current.render_scale = 0.67f;
current.internal_width = 215;
current.internal_height = 161;
require(evaluate_temporal_history(previous, current).reason ==
TemporalResetReason::ScaleChanged,
"New internal render scale invalidates full-resolution history");
current = previous;
current.mode = TemporalMode::Off;
require(evaluate_temporal_history(previous, current).reason ==
TemporalResetReason::ModeChanged,
"Turning temporal processing off invalidates history");
current = previous;
++current.shader_generation;
require(evaluate_temporal_history(previous, current).reason ==
TemporalResetReason::ShaderReload,
"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);
require(std::abs(first[0]) < 1e-7f && std::abs(first[1] + 1.f / 720.f) < 1e-7f,
"First Halton(2,3) sample must be a clip-space offset");
require(std::abs(second[0] + 1.f / 640.f) < 1e-7f &&
std::abs(second[1] - 1.f / 720.f) < 1e-7f,
"Second Halton sample must visit a different subpixel position");
require(first == temporal_jitter(16, 320, 240),
"The finite sequence must repeat on frame sixteen");
require(std::abs(temporal_jitter(0, 640, 480)[1] * 2.f - first[1]) < 1e-7f,
"Clip jitter must scale inversely with viewport extent");
for (std::uint64_t frame = 0; frame < 16; ++frame) {
const auto sample = temporal_jitter(frame, 319, 241);
require(std::abs(sample[0]) <= 1.f / 319.f &&
std::abs(sample[1]) <= 1.f / 241.f,
"Every jitter sample must stay within half an output pixel");
}
bool rejected_zero_extent = false;
try {
(void)temporal_jitter(0, 0, 240);
} catch (const std::invalid_argument&) {
rejected_zero_extent = true;
}
require(rejected_zero_extent, "Zero viewport extent cannot produce finite clip jitter");
}
} // namespace
int main() {
capability_fallback();
intentionally_disabled_temporal_mode();
rendered_history_and_camera_motion();
incompatible_view_state();
deterministic_jitter();
}