Add temporal history and jitter policy
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
#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] = 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 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();
|
||||
rendered_history_and_camera_motion();
|
||||
incompatible_view_state();
|
||||
deterministic_jitter();
|
||||
}
|
||||
Reference in New Issue
Block a user