Define independent temporal motion metadata and UV oracle

This commit is contained in:
Emil
2026-09-24 02:56:51 +03:00
parent 068f2e9af9
commit 17177357fb
6 changed files with 116 additions and 5 deletions
+3
View File
@@ -57,6 +57,9 @@ if(BUILD_TESTING)
target_link_libraries(faset_render_lighting_policy_tests PRIVATE faset_render)
add_test(NAME render_lighting_policy COMMAND faset_render_lighting_policy_tests)
set_tests_properties(render_lighting_policy PROPERTIES LABELS "p3")
add_executable(faset_render_temporal_motion_tests "${PROJECT_SOURCE_DIR}/tests/render_temporal_motion_tests.cpp")
target_link_libraries(faset_render_temporal_motion_tests PRIVATE faset_render)
add_test(NAME render_temporal_motion COMMAND faset_render_temporal_motion_tests)
add_executable(faset_render_temporal_policy_tests "${PROJECT_SOURCE_DIR}/tests/render_temporal_policy_tests.cpp")
target_link_libraries(faset_render_temporal_policy_tests PRIVATE faset_render)
add_test(NAME render_temporal_policy COMMAND faset_render_temporal_policy_tests)
+7
View File
@@ -74,4 +74,11 @@ std::array<float, 2> temporal_jitter(std::uint64_t frame_index,
std::uint32_t viewport_width,
std::uint32_t viewport_height);
// Scene-local normalized UV motion, current minus previous. Both clips use
// their own jittered scene VP and the same local vertex. Invalid/behind-eye
// clips have no usable history; the shader writes velocity validity zero.
std::optional<std::array<float, 2>>
project_motion(const std::array<float, 4>& current_clip,
const std::array<float, 4>& previous_clip) noexcept;
} // namespace faset::render
+11 -3
View File
@@ -37,11 +37,19 @@ struct InstanceUpdate {
bool previous_valid{};
};
// GPU instance metadata: history valid, stable slot, then generation low/high.
// GPU instance metadata: x bits 0/1 separately mark previous HZB bounds and
// previous temporal transform; the other lanes carry stable slot/generation.
// A zero generation identifies an anonymous, untracked draw.
inline constexpr std::uint32_t gpu_hzb_history_bit = 1U;
inline constexpr std::uint32_t gpu_temporal_history_bit = 2U;
constexpr std::array<std::uint32_t, 4>
gpu_instance_metadata(const InstanceUpdate& update, bool history_compatible) noexcept {
return {update.previous_valid && history_compatible ? 1U : 0U, update.slot,
gpu_instance_metadata(const InstanceUpdate& update, bool hzb_compatible,
bool temporal_compatible = false) noexcept {
return {update.previous_valid
? (hzb_compatible ? gpu_hzb_history_bit : 0U) |
(temporal_compatible ? gpu_temporal_history_bit : 0U)
: 0U,
update.slot,
static_cast<std::uint32_t>(update.generation),
static_cast<std::uint32_t>(update.generation >> 32)};
}
+2 -2
View File
@@ -26,7 +26,7 @@ struct InstanceRecord {
float4 currentExtent; // 160..175: world AABB half extents
float4 previousCenter; // 176..191
float4 previousExtent; // 192..207
uint4 metadata; // 208..223: x=previousValid, y=stableSlot,
uint4 metadata; // 208..223: x bits 0=HZB, 1=temporal prior valid; y=stableSlot,
// z=generation low 32, w=generation high 32 (zero = untracked)
};
struct ViewRecord {
@@ -200,7 +200,7 @@ void gpuCullMain(uint3 dispatchId : SV_DispatchThreadID) {
ViewRecord view = cullViews[0];
if (!inFrustum(instance.currentCenter, instance.currentExtent,
view.currentViewProjection)) return;
bool guessedHidden = view.flags.x != 0 && instance.metadata.x != 0 &&
bool guessedHidden = view.flags.x != 0 && (instance.metadata.x & 1u) != 0 &&
occluded(instance.previousCenter, instance.previousExtent,
view.previousViewProjection, view.previousViewport,
view.previousHzbSize, previousHzb);
+22
View File
@@ -141,4 +141,26 @@ std::array<float, 2> temporal_jitter(std::uint64_t frame_index,
(halton(phase, 3) - 0.5f) * (2.f / static_cast<float>(viewport_height))};
}
std::optional<std::array<float, 2>>
project_motion(const std::array<float, 4>& current_clip,
const std::array<float, 4>& previous_clip) noexcept {
for (float coordinate : current_clip)
if (!std::isfinite(coordinate))
return std::nullopt;
for (float coordinate : previous_clip)
if (!std::isfinite(coordinate))
return std::nullopt;
if (current_clip[3] <= 0.f || previous_clip[3] <= 0.f)
return std::nullopt;
std::array<float, 2> motion{};
for (std::size_t axis = 0; axis < 2; ++axis) {
const float current_uv = current_clip[axis] / current_clip[3] * 0.5f + 0.5f;
const float previous_uv = previous_clip[axis] / previous_clip[3] * 0.5f + 0.5f;
motion[axis] = current_uv - previous_uv;
if (!std::isfinite(motion[axis]))
return std::nullopt;
}
return motion;
}
} // namespace faset::render
+71
View File
@@ -0,0 +1,71 @@
#include <faset/render/renderer.hpp>
#include <faset/render/temporal.hpp>
#include <faset/render/visibility.hpp>
#include <array>
#include <cmath>
#include <memory>
#include <stdexcept>
using namespace faset::render;
namespace {
void require(bool value, const char* message) {
if (!value)
throw std::runtime_error(message);
}
void motion_uses_current_minus_previous_scene_uv() {
const auto motion = project_motion(std::array<float, 4>{0.2f, -0.2f, 0.6f, 1.f},
std::array<float, 4>{0.f, 0.f, 0.4f, 1.f});
require(motion && std::abs((*motion)[0] - 0.1f) < 1e-6f &&
std::abs((*motion)[1] + 0.1f) < 1e-6f,
"Motion sign and units must be current minus previous normalized scene UV");
const auto perspective = project_motion(std::array<float, 4>{0.6f, 0.f, 0.8f, 2.f},
std::array<float, 4>{0.2f, 0.f, 0.5f, 1.f});
require(perspective && std::abs((*perspective)[0] - 0.05f) < 1e-6f,
"Motion must divide each frame's clip coordinates by its own W");
require(!project_motion({0, 0, 0, 1}, {0, 0, 0, 0}),
"A previous vertex on the eye plane cannot carry valid motion");
require(!project_motion({0, 0, 0, 1}, {0, 0, 0, -1}),
"A previous vertex behind the camera cannot carry valid motion");
require(!project_motion({INFINITY, 0, 0, 1}, {0, 0, 0, 1}),
"Nonfinite clip coordinates cannot enter temporal history");
}
void hzb_and_color_history_have_independent_bits() {
auto mesh = cube_mesh();
auto replacement = std::make_shared<Mesh>(*mesh);
const Bounds bounds{{-1, -1, -1}, {1, 1, 1}};
InstanceTracker tracker;
const auto model_a = transform({0, 0, 0});
const auto model_b = transform({1, 0, 0});
const auto first = tracker.update("cube", mesh, model_a, bounds, "main");
require(gpu_instance_metadata(first, true, true)[0] == 0,
"A first-frame tracked instance has neither prior HZB nor color history");
tracker.finish_frame();
const auto moved = tracker.update("cube", mesh, model_b, bounds, "main");
require(moved.previous_valid && moved.previous_model == model_a,
"A stable instance must retain its prior model in Direct or GPU mode");
require(gpu_instance_metadata(moved, true, false)[0] == 1,
"Only prior HZB eligibility sets bit zero");
require(gpu_instance_metadata(moved, false, true)[0] == 2,
"Temporal transform eligibility must not require HZB history");
require(gpu_instance_metadata(moved, true, true)[0] == 3,
"Both independent history bits may be valid in GPU occlusion mode");
require(gpu_instance_metadata(moved, false, false)[0] == 0,
"Neither history bit may leak after an incompatible frame");
tracker.finish_frame();
const auto replaced = tracker.update("cube", replacement, model_b, bounds, "main");
require(!replaced.previous_valid && gpu_instance_metadata(replaced, true, true)[0] == 0,
"A mesh identity change must reject both previous bounds and motion");
require(gpu_instance_metadata({}, true, true)[0] == 0,
"An anonymous draw cannot inherit another draw's transform");
}
} // namespace
int main() {
motion_uses_current_minus_previous_scene_uv();
hzb_and_color_history_have_independent_bits();
}