Specify temporal GPU graph and image acceptance in red tests
This commit is contained in:
@@ -72,6 +72,14 @@ 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_graph_tests "${PROJECT_SOURCE_DIR}/tests/render_temporal_graph_tests.cpp")
|
||||
target_link_libraries(faset_render_temporal_graph_tests PRIVATE faset_render)
|
||||
add_test(NAME render_temporal_graph COMMAND faset_render_temporal_graph_tests)
|
||||
set_tests_properties(render_temporal_graph PROPERTIES LABELS "gpu")
|
||||
add_executable(faset_render_temporal_acceptance_tests "${PROJECT_SOURCE_DIR}/tests/render_temporal_acceptance_tests.cpp")
|
||||
target_link_libraries(faset_render_temporal_acceptance_tests PRIVATE faset_render)
|
||||
add_test(NAME render_temporal_acceptance COMMAND faset_render_temporal_acceptance_tests)
|
||||
set_tests_properties(render_temporal_acceptance PROPERTIES LABELS "gpu")
|
||||
add_executable(faset_render_temporal_shader_contract_tests "${PROJECT_SOURCE_DIR}/tests/render_temporal_shader_contract_tests.cpp")
|
||||
target_include_directories(faset_render_temporal_shader_contract_tests PRIVATE "${PROJECT_SOURCE_DIR}/src/render")
|
||||
target_link_libraries(faset_render_temporal_shader_contract_tests PRIVATE faset_render faset_core)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "render_temporal_fixtures.hpp"
|
||||
#include <faset/render/temporal.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
using namespace faset::render;
|
||||
using namespace faset::render::temporal_test;
|
||||
|
||||
namespace {
|
||||
void moving_reveal_and_camera_resets(VisibilityMode visibility) {
|
||||
constexpr std::uint32_t width = 160, height = 120;
|
||||
auto config = headless_config(width, height, visibility);
|
||||
config.temporal_mode = TemporalMode::TAA;
|
||||
Renderer taa(config);
|
||||
config.temporal_mode = TemporalMode::Off;
|
||||
Renderer off(config);
|
||||
|
||||
auto frame = lit_scene(width, height);
|
||||
frame.draws.push_back(cube({0, 0, -2}, {.1f, .95f, .2f, 1}, "background"));
|
||||
frame.draws.push_back(cube({0, 0, 1}, {.95f, .1f, .1f, 1}, "door"));
|
||||
frame.ui_quads.push_back({2, 2, 25, 12, {.8f, .7f, .25f, 1}});
|
||||
taa.render(frame);
|
||||
require(!taa.stats().temporal_history_valid &&
|
||||
taa.stats().temporal_reset_reason == TemporalResetReason::FirstFrame,
|
||||
"The first TAA frame must use only current color");
|
||||
taa.render(frame);
|
||||
require(taa.stats().temporal_history_valid,
|
||||
"An unchanged second frame must accept eligible temporal history");
|
||||
|
||||
frame.draws[1].model = transform({3, 0, 1});
|
||||
taa.render(frame);
|
||||
off.render(frame);
|
||||
require(mean_rgb_error(taa.pixels(), off.pixels(), width, height, {75, 55, 10, 10}) <= 8.0,
|
||||
"Opening a foreground door reveals current background without old-color trail");
|
||||
require(taa.stats().validation_errors == 0,
|
||||
"Moving-disocclusion resolve must pass Vulkan validation");
|
||||
require(pixel(taa.pixels(), width, 4, 4) == pixel(off.pixels(), width, 4, 4),
|
||||
"Moving scene and TAA must leave UI pixel-exact");
|
||||
|
||||
frame.camera_cut = true;
|
||||
frame.eye = {1, 0, 6};
|
||||
frame.view_projection = multiply(frame.projection, look_at(frame.eye, {0, 0, 0}));
|
||||
taa.render(frame);
|
||||
off.render(frame);
|
||||
require(!taa.stats().temporal_history_valid &&
|
||||
taa.stats().temporal_reset_reason == TemporalResetReason::CameraCut &&
|
||||
mean_rgb_error(taa.pixels(), off.pixels(), width, height,
|
||||
{75, 55, 10, 10}) <= 8.0,
|
||||
"Explicit camera cut must discard stale color immediately");
|
||||
frame.camera_cut = false;
|
||||
frame.eye = {7, 0, 6};
|
||||
frame.view_projection = multiply(frame.projection, look_at(frame.eye, {0, 0, 0}));
|
||||
taa.render(frame);
|
||||
require(!taa.stats().temporal_history_valid &&
|
||||
taa.stats().temporal_reset_reason ==
|
||||
TemporalResetReason::CameraDiscontinuity,
|
||||
"An unmarked large camera teleport also discards history");
|
||||
|
||||
frame.eye = {0, 0, 6};
|
||||
frame.view_projection = multiply(frame.projection, look_at(frame.eye, {0, 0, 0}));
|
||||
frame.draws[0].mesh = std::make_shared<Mesh>(*frame.draws[0].mesh);
|
||||
taa.render(frame);
|
||||
require(taa.stats().validation_errors == 0,
|
||||
"Mesh identity change and camera return must keep temporal output valid");
|
||||
}
|
||||
|
||||
void lower_resolution_scene_and_output_ui() {
|
||||
constexpr std::uint32_t width = 320, height = 240;
|
||||
auto config = headless_config(width, height, VisibilityMode::Direct);
|
||||
config.temporal_mode = TemporalMode::Upscale;
|
||||
config.render_scale = .67f;
|
||||
Renderer upscale(config);
|
||||
auto frame = lit_scene(width, height);
|
||||
frame.draws.push_back(cube({0, 0, 0}, {.8f, .6f, .25f, 1}, "thin-scene"));
|
||||
frame.ui_quads.push_back({2, 2, 25, 12, {.8f, .7f, .25f, 1}});
|
||||
upscale.render(frame);
|
||||
require(upscale.stats().effective_temporal_mode == TemporalMode::Upscale &&
|
||||
upscale.stats().temporal_internal_width == 215 &&
|
||||
upscale.stats().temporal_internal_height == 161 &&
|
||||
upscale.pixels().size() == std::size_t(width) * height * 4,
|
||||
"0.67 upscale rasterizes 215x161 while capture remains 320x240");
|
||||
require(upscale.stats().validation_errors == 0,
|
||||
"Upscale image resize and composite pass Vulkan validation");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
moving_reveal_and_camera_resets(VisibilityMode::Direct);
|
||||
moving_reveal_and_camera_resets(VisibilityMode::GpuFrustum);
|
||||
moving_reveal_and_camera_resets(VisibilityMode::GpuOcclusion);
|
||||
lower_resolution_scene_and_output_ui();
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
#include <faset/render/renderer.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace faset::render::temporal_test {
|
||||
inline void require(bool condition, const char* message) {
|
||||
if (!condition)
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
struct Region {
|
||||
std::uint32_t x{}, y{}, width{}, height{};
|
||||
};
|
||||
|
||||
inline double mean_rgb_error(const std::vector<std::uint8_t>& actual,
|
||||
const std::vector<std::uint8_t>& reference,
|
||||
std::uint32_t image_width, std::uint32_t image_height,
|
||||
Region region) {
|
||||
require(actual.size() == reference.size() &&
|
||||
actual.size() == std::size_t(image_width) * image_height * 4 &&
|
||||
region.width && region.height && region.x <= image_width &&
|
||||
region.y <= image_height && region.width <= image_width - region.x &&
|
||||
region.height <= image_height - region.y,
|
||||
"Temporal image metric requires equal images and an in-bounds ROI");
|
||||
std::uint64_t difference{};
|
||||
for (auto y = region.y; y < region.y + region.height; ++y)
|
||||
for (auto x = region.x; x < region.x + region.width; ++x) {
|
||||
const auto base = (std::size_t(y) * image_width + x) * 4;
|
||||
for (std::size_t channel = 0; channel < 3; ++channel)
|
||||
difference += static_cast<std::uint64_t>(
|
||||
std::abs(int(actual[base + channel]) - int(reference[base + channel])));
|
||||
}
|
||||
return double(difference) / (double(region.width) * region.height * 3);
|
||||
}
|
||||
|
||||
inline std::array<std::uint8_t, 4> pixel(const std::vector<std::uint8_t>& rgba,
|
||||
std::uint32_t width, std::uint32_t x,
|
||||
std::uint32_t y) {
|
||||
const auto base = (std::size_t(y) * width + x) * 4;
|
||||
require(base + 3 < rgba.size(), "Requested temporal test pixel is outside the image");
|
||||
return {rgba[base], rgba[base + 1], rgba[base + 2], rgba[base + 3]};
|
||||
}
|
||||
|
||||
inline DrawItem cube(Vec3 position, Color color, std::string key) {
|
||||
DrawItem draw;
|
||||
draw.mesh = cube_mesh();
|
||||
draw.model = transform(position);
|
||||
draw.color = color;
|
||||
draw.instance_key = std::move(key);
|
||||
draw.cast_shadow = false;
|
||||
return draw;
|
||||
}
|
||||
|
||||
inline Snapshot lit_scene(std::uint32_t width, std::uint32_t height) {
|
||||
Snapshot frame;
|
||||
frame.view_id = "temporal-acceptance-main";
|
||||
frame.eye = {0, 0, 6};
|
||||
frame.projection = perspective(.9f, float(width) / float(height), .1f, 50.f);
|
||||
frame.view_projection = multiply(frame.projection, look_at(frame.eye, {0, 0, 0}));
|
||||
frame.authored_lights_present = true;
|
||||
frame.sun = SunLight{"test-sun", {-.5f, -1.f, -.3f}, {1, 1, 1, 1}, 3.f, false};
|
||||
return frame;
|
||||
}
|
||||
|
||||
inline RendererConfig headless_config(std::uint32_t width, std::uint32_t height,
|
||||
VisibilityMode visibility) {
|
||||
RendererConfig config;
|
||||
config.width = width;
|
||||
config.height = height;
|
||||
config.headless = true;
|
||||
config.validation = true;
|
||||
config.visibility_mode = visibility;
|
||||
return config;
|
||||
}
|
||||
} // namespace faset::render::temporal_test
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "render_temporal_fixtures.hpp"
|
||||
#include <faset/render/temporal.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace faset::render;
|
||||
using namespace faset::render::temporal_test;
|
||||
|
||||
namespace {
|
||||
std::size_t position(const std::vector<std::string>& passes, const char* name) {
|
||||
const auto found = std::find(passes.begin(), passes.end(), name);
|
||||
require(found != passes.end(), "Required temporal graph pass is absent");
|
||||
return static_cast<std::size_t>(found - passes.begin());
|
||||
}
|
||||
|
||||
void offset_scene_and_sharp_ui() {
|
||||
constexpr std::uint32_t width = 191, height = 127;
|
||||
auto config = headless_config(width, height, VisibilityMode::GpuOcclusion);
|
||||
config.temporal_mode = TemporalMode::TAA;
|
||||
Renderer taa(config);
|
||||
config.temporal_mode = TemporalMode::Off;
|
||||
Renderer off(config);
|
||||
|
||||
auto frame = lit_scene(141, 103);
|
||||
frame.scene_rect = {13, 7, 141, 103};
|
||||
frame.draws.push_back(cube({0, 0, 0}, {.9f, .55f, .25f, 1}, "opaque"));
|
||||
frame.draws.push_back(cube({.4f, 0, 1}, {.2f, .6f, .9f, .45f}, "translucent"));
|
||||
Sprite sprite;
|
||||
sprite.position = {-.7f, -.4f, .7f};
|
||||
sprite.size = {.8f, .8f};
|
||||
sprite.color = {.3f, .8f, .3f, .8f};
|
||||
frame.sprites.push_back(sprite);
|
||||
frame.ui_quads.push_back({2, 2, 24, 14, {.95f, .8f, .2f, 1}});
|
||||
frame.ui_text.push_back({2, 22, "Temporal UI", {1, 1, 1, 1}, 12});
|
||||
|
||||
taa.render(frame);
|
||||
off.render(frame);
|
||||
const auto& stats = taa.stats();
|
||||
require(stats.effective_temporal_mode == TemporalMode::TAA &&
|
||||
stats.effective_visibility_mode == VisibilityMode::GpuOcclusion,
|
||||
"Graph acceptance must exercise temporal resolve after P2 occlusion");
|
||||
const auto& passes = stats.graph_passes;
|
||||
require(position(passes, "PostRasterScene") < position(passes, "TemporalResolve") &&
|
||||
position(passes, "TemporalResolve") <
|
||||
position(passes, "TemporalComposite") &&
|
||||
position(passes, "TemporalComposite") < position(passes, "UI"),
|
||||
"Post-cull scene color/velocity must resolve before final-resolution UI");
|
||||
require(pixel(taa.pixels(), width, 4, 4) == pixel(off.pixels(), width, 4, 4),
|
||||
"Opaque UI remains pixel-exact and unjittered on an offset scene viewport");
|
||||
require(pixel(taa.pixels(), width, 180, 120) == pixel(off.pixels(), width, 180, 120),
|
||||
"Pixels outside the offset scene rectangle retain the Off clear result");
|
||||
require(stats.validation_errors == 0,
|
||||
"Temporal graph attachment store/load and transitions pass Vulkan validation");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
offset_scene_and_sharp_ui();
|
||||
}
|
||||
Reference in New Issue
Block a user