Merge commit '0c96ce5cdfde8503e1ffc3fd65fdc9d7f3793d35' into feat/p1-p3-integration

# Conflicts:
#	PLAN.md
#	docs/IMPLEMENTATION.md
#	docs/manual/editor/diagnostics.md
#	docs/manual/editor/lighting.md
#	docs/manual/editor/profiling.md
#	docs/validation/README.md
#	docs/validation/p3-lighting-2026-09-24/README.md
This commit is contained in:
Emil
2026-09-24 04:08:01 +03:00
43 changed files with 3963 additions and 139 deletions
+22 -2
View File
@@ -566,12 +566,22 @@ struct BuildService::Impl {
copy_required_file(player, staging / ("faset_player" + executable_suffix()));
copy_required_file(exporter, staging / ("faset_schema_exporter" + executable_suffix()));
for (const auto* file : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
"lightTileMain.spv", "lightTileMain.reflection.json",
"vertexMain.reflection.json", "fragmentMain.reflection.json",
"shadowMain.reflection.json", "gpuVertexMain.spv",
"gpuShadowMain.spv", "gpuCullMain.spv", "gpuHzbMain.spv",
"gpuPostCullMain.spv", "gpuVertexMain.reflection.json",
"gpuShadowMain.reflection.json", "gpuCullMain.reflection.json",
"gpuHzbMain.reflection.json", "gpuPostCullMain.reflection.json"})
"gpuHzbMain.reflection.json", "gpuPostCullMain.reflection.json",
"temporalResolveMain.spv", "temporalResolveMain.reflection.json",
"temporalCompositeVertexMain.spv",
"temporalCompositeVertexMain.reflection.json",
"temporalCompositeFragmentMain.spv",
"temporalCompositeFragmentMain.reflection.json",
"temporalVertexMain.spv", "temporalVertexMain.reflection.json",
"temporalFragmentMain.spv", "temporalFragmentMain.reflection.json",
"gpuTemporalVertexMain.spv",
"gpuTemporalVertexMain.reflection.json"})
copy_required_file(native_directory / "shaders" / file, staging / "shaders" / file);
copy_runtime_libraries(job, player, staging, native_directory, configuration);
Json files = Json::array();
@@ -821,12 +831,22 @@ struct BuildService::Impl {
copy_required_file(build_directory / ("faset_player" + executable_suffix()),
staging / ("faset_player" + executable_suffix()));
for (const auto* shader : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
"lightTileMain.spv", "lightTileMain.reflection.json",
"vertexMain.reflection.json", "fragmentMain.reflection.json",
"shadowMain.reflection.json", "gpuVertexMain.spv",
"gpuShadowMain.spv", "gpuCullMain.spv", "gpuHzbMain.spv",
"gpuPostCullMain.spv", "gpuVertexMain.reflection.json",
"gpuShadowMain.reflection.json", "gpuCullMain.reflection.json",
"gpuHzbMain.reflection.json", "gpuPostCullMain.reflection.json"})
"gpuHzbMain.reflection.json", "gpuPostCullMain.reflection.json",
"temporalResolveMain.spv", "temporalResolveMain.reflection.json",
"temporalCompositeVertexMain.spv",
"temporalCompositeVertexMain.reflection.json",
"temporalCompositeFragmentMain.spv",
"temporalCompositeFragmentMain.reflection.json",
"temporalVertexMain.spv", "temporalVertexMain.reflection.json",
"temporalFragmentMain.spv", "temporalFragmentMain.reflection.json",
"gpuTemporalVertexMain.spv",
"gpuTemporalVertexMain.reflection.json"})
copy_required_file(build_directory / "shaders" / shader,
staging / "shaders" / shader);
for (const auto& entry : fs::directory_iterator(build_directory)) {
+97
View File
@@ -28,6 +28,42 @@ const char* visibility_label(render::VisibilityMode mode) {
}
return "Unknown";
}
const char* temporal_label(render::TemporalMode mode) {
switch (mode) {
case render::TemporalMode::Off: return "Off";
case render::TemporalMode::TAA: return "TAA";
case render::TemporalMode::Upscale: return "Upscale";
}
return "Unknown";
}
const char* temporal_fallback_label(render::TemporalFallbackReason reason) {
using Reason = render::TemporalFallbackReason;
switch (reason) {
case Reason::None: return "none";
case Reason::ComputeUnavailable: return "compute unavailable";
case Reason::FormatUnavailable: return "format unavailable";
case Reason::ExtentUnsupported: return "extent unsupported";
}
return "unknown";
}
const char* temporal_reset_label(render::TemporalResetReason reason) {
using Reason = render::TemporalResetReason;
switch (reason) {
case Reason::None: return "none";
case Reason::FirstFrame: return "first frame";
case Reason::CameraCut: return "camera cut";
case Reason::CameraDiscontinuity: return "camera discontinuity";
case Reason::ViewChanged: return "view changed";
case Reason::ViewportChanged: return "viewport changed";
case Reason::ProjectionChanged: return "projection changed";
case Reason::Resize: return "resize";
case Reason::ModeChanged: return "mode changed";
case Reason::ScaleChanged: return "scale changed";
case Reason::ShaderReload: return "shader reload";
case Reason::Unsupported: return "unsupported";
}
return "unknown";
}
ImGuiKey key(std::string_view name) {
if (name.size() == 1 && name[0] >= 'A' && name[0] <= 'Z')
return static_cast<ImGuiKey>(ImGuiKey_A + name[0] - 'A');
@@ -58,6 +94,7 @@ struct DebugOverlay::Impl {
std::uint32_t overlay_buttons{}, editor_buttons{};
std::array<float, 2> pointer{-1, -1};
float scale{};
float last_upscale_scale{.67f};
std::array<float, 4> window_rect{};
render::FrameStats displayed;
std::shared_ptr<render::Texture> atlas;
@@ -266,6 +303,38 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
}
const auto selected_mode = renderer.visibility_mode();
ImGui::TextDisabled("Renderer mode; no scene or export changes");
ImGui::TextUnformatted("Temporal");
const auto current_temporal = renderer.temporal_mode();
if (current_temporal == render::TemporalMode::Upscale)
state.last_upscale_scale = renderer.render_scale();
const struct {
const char* label;
render::TemporalMode value;
} temporal_modes[] = {{"Off", render::TemporalMode::Off},
{"TAA", render::TemporalMode::TAA},
{"Upscale", render::TemporalMode::Upscale}};
for (int i = 0; i < 3; ++i) {
if (i)
ImGui::SameLine();
const bool selected = current_temporal == temporal_modes[i].value;
if (selected)
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{.35f, .33f, .43f, 1.f});
if (ImGui::Button(temporal_modes[i].label, {button_width, 0}))
renderer.set_temporal_mode(temporal_modes[i].value,
temporal_modes[i].value == render::TemporalMode::Upscale
? state.last_upscale_scale : 1.f);
if (selected)
ImGui::PopStyleColor();
}
if (renderer.temporal_mode() == render::TemporalMode::Upscale) {
int percent = static_cast<int>(std::lround(renderer.render_scale() * 100.f));
if (ImGui::SliderInt("Render scale", &percent, 50, 99, "%d%%")) {
state.last_upscale_scale = float(percent) / 100.f;
renderer.set_temporal_mode(render::TemporalMode::Upscale,
state.last_upscale_scale);
}
}
ImGui::TextDisabled("Viewport only; exported Player settings are separate");
ImGui::Separator();
ImGui::TextUnformatted("Previous completed frame");
ImGui::SameLine();
@@ -381,8 +450,36 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
ImGui::Text("Prepared LOD: %u / %u / %u / %u+", stats.lod_counts[0],
stats.lod_counts[1], stats.lod_counts[2], stats.lod_counts[3]);
ImGui::Separator();
ImGui::TextUnformatted("Temporal reconstruction");
ImGui::Text("Requested: %s Effective: %s",
temporal_label(stats.requested_temporal_mode),
temporal_label(stats.effective_temporal_mode));
if (stats.requested_temporal_mode != stats.effective_temporal_mode)
ImGui::TextDisabled("Fallback: %s",
temporal_fallback_label(stats.temporal_fallback_reason));
if (stats.effective_temporal_mode != render::TemporalMode::Off) {
ImGui::Text("Internal: %u x %u Output: %u x %u",
stats.temporal_internal_width, stats.temporal_internal_height,
renderer.width(), renderer.height());
ImGui::Text("History: %s Reset: %s",
stats.temporal_history_valid ? "valid" : "invalid",
temporal_reset_label(stats.temporal_reset_reason));
if (stats.gpu_ms > 0)
ImGui::Text("GPU: resolve %.2f composite %.2f UI %.2f ms",
stats.gpu_temporal_resolve_ms,
stats.gpu_temporal_composite_ms, stats.gpu_ui_ms);
}
ImGui::Separator();
ImGui::TextUnformatted("Lighting and shadows");
ImGui::Text("Lighting path: %s", stats.effective_lighting_path.c_str());
ImGui::Text("Light tiles: %u; GPU build %.2f ms",
stats.light_tile_count, stats.gpu_light_tiles_ms);
if (stats.light_tile_counts_valid)
ImGui::Text("Tile entries: %u; overflow tiles: %u",
stats.light_tile_candidate_count,
stats.light_tile_overflow_count);
else if (stats.light_tile_count)
ImGui::TextDisabled("Tile entry counts unavailable until diagnostics readback");
ImGui::Text("Local lights: %u submitted, %u omitted",
stats.submitted_local_lights, stats.omitted_local_lights);
ImGui::Text("Sun cascades: %u / %u effective",
+1071 -71
View File
File diff suppressed because it is too large Load Diff
+184 -23
View File
@@ -30,15 +30,56 @@ void locations(const Json& fields, std::initializer_list<const char*> types, con
++index;
}
}
void validate_tile_layout(const Json& layout) {
require(layout.at("stage") == "compute", "light tile shader stage changed");
const auto& descriptors = layout.at("descriptors");
require(descriptors.is_array() && descriptors.size() == 2,
"light tile descriptor count changed");
for (std::size_t i = 0; i < 2; ++i)
require(descriptors[i].at("set") == 0 && descriptors[i].at("binding") == i &&
descriptors[i].at("count") == 1 &&
descriptors[i].at("type") == "storage_buffer" &&
descriptors[i].at("element_stride") == (i == 0 ? 80 : 4),
"light tile descriptor ABI changed");
const auto& constants = layout.at("push_constants");
require(constants.is_array() && constants.size() == 1 &&
constants[0].at("offset") == 0 && constants[0].at("size") == 96,
"light tile push size changed");
const auto& members = constants[0].at("members");
require(members.is_array() && members.size() == 3,
"light tile push members changed");
const int offsets[] = {0, 64, 80};
const char* types[] = {"float32x4x4", "float32x4", "uint32x4"};
for (std::size_t i = 0; i < 3; ++i)
require(members[i].at("offset") == offsets[i] &&
members[i].at("size") == (i == 0 ? 64 : 16) &&
members[i].at("type") == types[i],
"light tile push field changed");
const auto& blocks = layout.at("spirv_push_constants");
require(blocks.is_array() && blocks.size() == 1 &&
blocks[0].at("members").size() == 3,
"light tile SPIR-V push block changed");
const auto& actual = blocks[0].at("members");
for (std::size_t i = 0; i < 3; ++i)
require(actual[i].at("member") == i && actual[i].at("offset") == offsets[i],
"light tile SPIR-V push offset changed");
require(actual[0].at("matrix_layout") == "row-major" &&
actual[0].at("matrix_stride") == 16,
"light tile SPIR-V matrix storage convention changed");
locations(layout.at("inputs"), {}, "light tile inputs");
locations(layout.at("outputs"), {}, "light tile outputs");
}
void validate_layout(const Json& layout, std::string_view entry) {
const bool fragment = entry == "fragmentMain";
const bool fragment = entry == "fragmentMain" || entry == "temporalFragmentMain";
const bool temporal = entry == "temporalVertexMain" ||
entry == "temporalFragmentMain";
require(layout.at("stage") == (fragment ? "fragment" : "vertex"), "shader stage changed");
const auto& descriptors = layout.at("descriptors");
require(descriptors.is_array() && descriptors.size() == 8, "descriptor count changed");
require(descriptors.is_array() && descriptors.size() == 9, "descriptor count changed");
for (std::size_t i = 0; i < descriptors.size(); ++i) {
const auto& binding = descriptors[i];
const auto set = i < 4 ? 0 : 1;
const auto slot = i % 4;
const auto slot = set == 0 ? i : i - 4;
require(binding.at("set") == set && binding.at("binding") == slot &&
binding.at("count") == 1,
"descriptor set, binding or array count changed");
@@ -46,8 +87,8 @@ void validate_layout(const Json& layout, std::string_view entry) {
: slot == 3 ? "sampled_image_2d" : "storage_buffer";
require(binding.at("type") == expected_type,
"descriptor type changed");
if (set == 1 && slot < 3)
require(binding.at("element_stride") == (slot == 2 ? 112 : 80),
if (set == 1 && slot != 3)
require(binding.at("element_stride") == (slot == 4 ? 4 : slot == 2 ? 112 : 80),
"lighting storage record stride changed");
require(fragment || !binding.at("used").get<bool>(),
"vertex texture bindings are unsupported");
@@ -77,24 +118,42 @@ void validate_layout(const Json& layout, std::string_view entry) {
"SPIR-V matrix storage convention changed");
}
if (fragment) {
locations(layout.at("inputs"),
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
"fragment inputs");
locations(layout.at("outputs"), {"float32x4"}, "fragment outputs");
if (temporal) {
locations(layout.at("inputs"),
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2",
"float32x4", "float32x4", "float32"}, "temporal fragment inputs");
locations(layout.at("outputs"), {"float32x4", "float32x4"},
"temporal fragment outputs");
} else {
locations(layout.at("inputs"),
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
"fragment inputs");
locations(layout.at("outputs"), {"float32x4"}, "fragment outputs");
}
} else {
locations(layout.at("inputs"),
{"float32x4", "float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
"vertex inputs");
if (temporal) {
locations(layout.at("inputs"),
{"float32x4", "float32x3", "float32x3", "float32x4", "float32x2",
"float32x2", "float32x4", "float32"}, "temporal vertex inputs");
locations(layout.at("outputs"),
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2",
"float32x4", "float32x4", "float32"}, "temporal vertex outputs");
} else {
locations(layout.at("inputs"),
{"float32x4", "float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
"vertex inputs");
if (entry == "vertexMain")
locations(layout.at("outputs"),
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
"vertex outputs");
else
locations(layout.at("outputs"), {}, "shadow outputs");
}
}
}
void validate_gpu_layout(const Json& layout, std::string_view entry) {
const bool graphics = entry == "gpuVertexMain" || entry == "gpuShadowMain";
const bool graphics = entry == "gpuVertexMain" || entry == "gpuShadowMain" ||
entry == "gpuTemporalVertexMain";
const bool hzb = entry == "gpuHzbMain";
const bool compute = !graphics;
require(layout.at("stage") == (compute ? "compute" : "vertex"), "GPU shader stage changed");
@@ -102,8 +161,8 @@ void validate_gpu_layout(const Json& layout, std::string_view entry) {
const std::size_t expected_count = graphics ? 3 : hzb ? 2 : 10;
require(descriptors.is_array() && descriptors.size() == expected_count,
"GPU descriptor count changed");
const std::array<int, 10> compute_strides{224, 16, 16, 4, 16, 4, 4, 0, 0, 208};
const std::array<int, 3> graphics_strides{224, 4, 208};
const std::array<int, 10> compute_strides{288, 16, 16, 4, 16, 4, 4, 0, 0, 208};
const std::array<int, 3> graphics_strides{288, 4, 208};
for (std::size_t i = 0; i < expected_count; ++i) {
const auto& binding = descriptors[i];
require(binding.at("set") == (graphics ? 2 : 0) && binding.at("binding") == i &&
@@ -147,7 +206,13 @@ void validate_gpu_layout(const Json& layout, std::string_view entry) {
locations(layout.at("inputs"),
{"float32x3", "float32x3", "float32x4", "float32x2"},
"GPU vertex inputs");
if (entry == "gpuVertexMain")
if (entry == "gpuVertexMain" || entry == "gpuTemporalVertexMain")
if (entry == "gpuTemporalVertexMain")
locations(layout.at("outputs"),
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2",
"float32x4", "float32x4", "float32"},
"GPU temporal vertex outputs");
else
locations(layout.at("outputs"),
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
"GPU vertex outputs");
@@ -158,6 +223,78 @@ void validate_gpu_layout(const Json& layout, std::string_view entry) {
locations(layout.at("outputs"), {}, "GPU compute outputs");
}
}
void validate_temporal_layout(const Json& layout, std::string_view entry) {
const bool resolve = entry == "temporalResolveMain";
const bool vertex = entry == "temporalCompositeVertexMain";
require(resolve || vertex || entry == "temporalCompositeFragmentMain",
"unknown temporal shader entry");
require(layout.at("stage") == (resolve ? "compute" : vertex ? "vertex" : "fragment"),
"temporal shader stage changed");
const auto& descriptors = layout.at("descriptors");
require(descriptors.is_array() && descriptors.size() == (resolve ? 7u : 1u),
"temporal descriptor count changed");
for (std::size_t i = 0; i < descriptors.size(); ++i) {
const auto& descriptor = descriptors[i];
require(descriptor.at("set") == 0 && descriptor.at("binding") == i &&
descriptor.at("count") == 1,
"temporal descriptor set, binding or count changed");
require(descriptor.at("type") ==
(resolve && i >= 5 ? "storage_image_2d" : "sampled_image_2d"),
"temporal image descriptor type changed");
require(descriptor.at("used") == (resolve || !vertex),
"temporal entry uses an unexpected image binding");
}
const auto& constants = layout.at("push_constants");
const auto& spirv_constants = layout.at("spirv_push_constants");
require(constants.is_array() && spirv_constants.is_array(),
"temporal push constants are malformed");
if (resolve) {
require(constants.size() == 1 && constants[0].at("offset") == 0 &&
constants[0].at("size") == 80 && spirv_constants.size() == 1,
"temporal resolve push block changed");
const auto& members = constants[0].at("members");
const auto& spirv_members = spirv_constants[0].at("members");
require(members.size() == 5 && spirv_members.size() == 5,
"temporal resolve push members changed");
const char* types[] = {"uint32x4", "float32x4", "float32x4", "uint32x4",
"float32x4"};
for (std::size_t i = 0; i < 5; ++i)
require(members[i].at("offset") == 16 * i &&
members[i].at("size") == 16 && members[i].at("type") == types[i] &&
spirv_members[i].at("member") == i &&
spirv_members[i].at("offset") == 16 * i,
"temporal resolve push member ABI changed");
} else
require(constants.empty() && spirv_constants.empty(),
"temporal composite unexpectedly uses push constants");
if (resolve) {
locations(layout.at("inputs"), {}, "temporal resolve inputs");
locations(layout.at("outputs"), {}, "temporal resolve outputs");
} else if (vertex) {
locations(layout.at("inputs"), {"float32x4"}, "temporal composite vertex inputs");
locations(layout.at("outputs"), {}, "temporal composite vertex outputs");
} else {
locations(layout.at("inputs"), {}, "temporal composite fragment inputs");
locations(layout.at("outputs"), {"float32x4"},
"temporal composite fragment outputs");
}
const auto& input_builtins = layout.at("input_builtins");
require(input_builtins.is_array() && input_builtins.size() == (vertex ? 0u : 1u),
"temporal entry input builtin count changed");
if (!vertex)
require(input_builtins[0].at("semantic") ==
(resolve ? "SV_DISPATCHTHREADID" : "SV_POSITION") &&
input_builtins[0].at("type") ==
(resolve ? "uint32x3" : "float32x4"),
"temporal entry input builtin changed");
const auto& output_builtins = layout.at("output_builtins");
require(output_builtins.is_array() && output_builtins.size() == (vertex ? 1u : 0u),
"temporal entry output builtin count changed");
if (vertex)
require(output_builtins[0].at("semantic") == "SV_POSITION" &&
output_builtins[0].at("type") == "float32x4",
"temporal composite vertex position changed");
}
void validate_spirv(const std::vector<std::uint32_t>& words, std::uint32_t execution_model) {
require(words.size() >= 5 && words[0] == 0x07230203 && words[1] >= 0x00010000 &&
words[1] <= 0x00010600 && words[3] > 0 && words[3] < (1u << 20) && words[4] == 0,
@@ -183,7 +320,7 @@ void validate_spirv(const std::vector<std::uint32_t>& words, std::uint32_t execu
require(entry_found, "SPIR-V main entry point missing");
}
detail::ShaderCode load(const std::filesystem::path& directory, const char* entry,
bool gpu = false) {
bool gpu = false, bool temporal = false) {
const auto bytes = read_bounded(directory / (std::string(entry) + ".spv"), 16 * 1024 * 1024);
require(bytes.size() >= 20 && bytes.size() % 4 == 0, "invalid SPIR-V byte length");
const auto metadata = Json::parse(
@@ -196,24 +333,34 @@ detail::ShaderCode load(const std::filesystem::path& directory, const char* entr
const auto& layout = metadata.at("layout");
const auto fingerprint = faset::sha256(layout.dump());
require(metadata.at("layout_fingerprint") == fingerprint, "layout fingerprint mismatch");
if (gpu)
if (temporal)
validate_temporal_layout(layout, entry);
else if (gpu)
validate_gpu_layout(layout, entry);
else if (std::string_view(entry) == "lightTileMain")
validate_tile_layout(layout);
else
validate_layout(layout, entry);
detail::ShaderCode result;
result.layout_fingerprint = fingerprint;
result.words.resize(bytes.size() / 4);
std::memcpy(result.words.data(), bytes.data(), bytes.size());
validate_spirv(result.words, gpu ? ((std::string_view(entry) == "gpuVertexMain" ||
std::string_view(entry) == "gpuShadowMain") ? 0u : 5u)
: (std::string_view(entry) == "fragmentMain" ? 4u : 0u));
const auto stage = std::string_view(entry) == "lightTileMain" ? 5u : temporal
? (std::string_view(entry) == "temporalResolveMain" ? 5u
: std::string_view(entry) == "temporalCompositeVertexMain" ? 0u : 4u)
: gpu ? ((std::string_view(entry) == "gpuVertexMain" ||
std::string_view(entry) == "gpuShadowMain" ||
std::string_view(entry) == "gpuTemporalVertexMain") ? 0u : 5u)
: ((std::string_view(entry) == "fragmentMain" ||
std::string_view(entry) == "temporalFragmentMain") ? 4u : 0u);
validate_spirv(result.words, stage);
return result;
}
} // namespace
std::array<detail::ShaderCode, 3>
std::array<detail::ShaderCode, 4>
detail::load_shader_bundle(const std::filesystem::path& directory) {
return {load(directory, "vertexMain"), load(directory, "fragmentMain"),
load(directory, "shadowMain")};
load(directory, "shadowMain"), load(directory, "lightTileMain")};
}
std::array<detail::ShaderCode, 5>
detail::load_gpu_shader_bundle(const std::filesystem::path& directory) {
@@ -221,8 +368,22 @@ detail::load_gpu_shader_bundle(const std::filesystem::path& directory) {
load(directory, "gpuCullMain", true), load(directory, "gpuHzbMain", true),
load(directory, "gpuPostCullMain", true)};
}
std::array<detail::ShaderCode, 3>
detail::load_temporal_shader_bundle(const std::filesystem::path& directory) {
return {load(directory, "temporalResolveMain", false, true),
load(directory, "temporalCompositeVertexMain", false, true),
load(directory, "temporalCompositeFragmentMain", false, true)};
}
std::array<detail::ShaderCode, 3>
detail::load_temporal_scene_shader_bundle(const std::filesystem::path& directory) {
return {load(directory, "temporalVertexMain"),
load(directory, "temporalFragmentMain"),
load(directory, "gpuTemporalVertexMain", true)};
}
void validate_shader_bundle(const std::filesystem::path& directory) {
(void)detail::load_shader_bundle(directory);
(void)detail::load_temporal_shader_bundle(directory);
(void)detail::load_temporal_scene_shader_bundle(directory);
}
void validate_gpu_shader_bundle(const std::filesystem::path& directory) {
(void)detail::load_gpu_shader_bundle(directory);
+7 -1
View File
@@ -10,7 +10,13 @@ struct ShaderCode {
std::vector<std::uint32_t> words;
std::string layout_fingerprint;
};
std::array<ShaderCode, 3> load_shader_bundle(const std::filesystem::path& directory);
// Direct graphics plus the independent 16x16 light-tile compute entry.
std::array<ShaderCode, 4> load_shader_bundle(const std::filesystem::path& directory);
// Order: opaque vertex, optional instanced shadow vertex, main cull, HZB, post cull.
std::array<ShaderCode, 5> load_gpu_shader_bundle(const std::filesystem::path& directory);
// Order: temporal resolve compute, full-screen composite vertex and fragment.
std::array<ShaderCode, 3> load_temporal_shader_bundle(const std::filesystem::path& directory);
// Direct temporal vertex/fragment and GPU temporal vertex; set 0 material,
// set 1 lighting and set 2 GPU scene stay at their established bindings.
std::array<ShaderCode, 3> load_temporal_scene_shader_bundle(const std::filesystem::path& directory);
} // namespace faset::render::detail
+198
View File
@@ -0,0 +1,198 @@
#include <faset/render/temporal.hpp>
#include <algorithm>
#include <cmath>
#include <stdexcept>
namespace faset::render {
TemporalFallbackReason temporal_fallback_reason(TemporalMode requested,
TemporalCapabilities available) noexcept {
if (requested == TemporalMode::Off)
return TemporalFallbackReason::None;
if (!available.compute)
return TemporalFallbackReason::ComputeUnavailable;
if (!available.formats)
return TemporalFallbackReason::FormatUnavailable;
if (!available.extent)
return TemporalFallbackReason::ExtentUnsupported;
return TemporalFallbackReason::None;
}
TemporalMode select_effective_temporal_mode(TemporalMode requested,
TemporalCapabilities available) noexcept {
return temporal_fallback_reason(requested, available) == TemporalFallbackReason::None
? requested : TemporalMode::Off;
}
std::array<std::uint32_t, 2> 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::uint32_t>(
std::max(1.0, std::ceil(static_cast<double>(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;
float place = 1.f / static_cast<float>(base);
while (index != 0) {
sample += static_cast<float>(index % base) * place;
index /= base;
place /= static_cast<float>(base);
}
return sample;
}
bool finite_camera(const TemporalHistoryKey& key) noexcept {
for (float value : key.camera_eye)
if (!std::isfinite(value))
return false;
for (float value : key.view_projection)
if (!std::isfinite(value))
return false;
return true;
}
std::array<float, 3> normalized_view_row(const TemporalHistoryKey& key, int row) noexcept {
std::array<float, 3> direction{key.view_projection[row], key.view_projection[row + 4],
key.view_projection[row + 8]};
const float length_squared = direction[0] * direction[0] +
direction[1] * direction[1] + direction[2] * direction[2];
if (!std::isfinite(length_squared) || length_squared < 1e-12f)
return {};
const float reciprocal = 1.f / std::sqrt(length_squared);
for (float& component : direction)
component *= reciprocal;
return direction;
}
bool large_axis_turn(const std::array<float, 3>& a,
const std::array<float, 3>& b) noexcept {
if (a == std::array<float, 3>{} || b == std::array<float, 3>{})
return true;
const float dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
return !std::isfinite(dot) || dot < 0.70710678f; // turn greater than 45 degrees
}
bool camera_discontinuity(const TemporalHistoryKey& previous,
const TemporalHistoryKey& current) noexcept {
if (!finite_camera(previous) || !finite_camera(current))
return true;
float translation_squared{};
for (int axis = 0; axis < 3; ++axis) {
const float delta = current.camera_eye[axis] - previous.camera_eye[axis];
translation_squared += delta * delta;
}
// A conservative world-space threshold catches unmarked teleports. Ordinary
// camera motion and smaller view changes are handled by motion vectors.
if (!std::isfinite(translation_squared) || translation_squared > 25.f)
return true;
// Compare horizontal and vertical camera axes too: comparing only forward
// cannot detect a sudden roll about the unchanged viewing direction.
for (int row = 0; row < 2; ++row)
if (large_axis_turn(normalized_view_row(previous, row),
normalized_view_row(current, row)))
return true;
// Perspective VP encodes forward in row four. Orthographic projection has
// a constant fourth row, so its third row carries the viewing direction.
auto a = normalized_view_row(previous, 3);
auto b = normalized_view_row(current, 3);
if (a == std::array<float, 3>{} && b == std::array<float, 3>{}) {
a = normalized_view_row(previous, 2);
b = normalized_view_row(current, 2);
}
return large_axis_turn(a, b);
}
} // namespace
TemporalHistoryDecision
evaluate_temporal_history(const std::optional<TemporalHistoryKey>& previous,
const TemporalHistoryKey& current) noexcept {
auto reset = [](TemporalResetReason reason) { return TemporalHistoryDecision{false, reason}; };
if (current.mode == TemporalMode::Off)
return reset(previous && previous->mode != TemporalMode::Off
? TemporalResetReason::ModeChanged : TemporalResetReason::None);
if (current.camera_cut)
return reset(TemporalResetReason::CameraCut);
if (!previous)
return reset(TemporalResetReason::FirstFrame);
const auto& before = *previous;
if (before.view_id != current.view_id)
return reset(TemporalResetReason::ViewChanged);
if (before.output_width != current.output_width ||
before.output_height != current.output_height)
return reset(TemporalResetReason::Resize);
if (before.internal_width != current.internal_width ||
before.internal_height != current.internal_height ||
before.render_scale != current.render_scale)
return reset(TemporalResetReason::ScaleChanged);
if (before.mode != current.mode)
return reset(TemporalResetReason::ModeChanged);
if (before.scene_rect != current.scene_rect)
return reset(TemporalResetReason::ViewportChanged);
if (before.projection != current.projection)
return reset(TemporalResetReason::ProjectionChanged);
if (before.shader_generation != current.shader_generation)
return reset(TemporalResetReason::ShaderReload);
if (camera_discontinuity(before, current))
return reset(TemporalResetReason::CameraDiscontinuity);
return {true, TemporalResetReason::None};
}
TemporalHistoryDecision
TemporalHistoryState::prepare(const TemporalHistoryKey& current) const noexcept {
return evaluate_temporal_history(completed_, current);
}
void TemporalHistoryState::complete(const TemporalHistoryKey& rendered) {
if (rendered.mode == TemporalMode::Off)
completed_.reset();
else
completed_ = rendered;
}
std::array<float, 2> temporal_jitter(std::uint64_t frame_index,
std::uint32_t viewport_width,
std::uint32_t viewport_height) {
if (viewport_width == 0 || viewport_height == 0)
throw std::invalid_argument("temporal jitter requires a nonzero viewport extent");
const auto phase = static_cast<std::uint32_t>(frame_index % 16) + 1;
return {(halton(phase, 2) - 0.5f) * (2.f / static_cast<float>(viewport_width)),
(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
+145
View File
@@ -0,0 +1,145 @@
#include <faset/render/temporal_reference.hpp>
#include <algorithm>
#include <cmath>
#include <limits>
#include <stdexcept>
namespace faset::render {
namespace {
constexpr std::uint64_t max_reference_pixels = 4'000'000;
std::size_t checked_pixel_count(std::uint32_t width, std::uint32_t height) {
const auto count = std::uint64_t(width) * height;
if (!width || !height || count > max_reference_pixels)
throw std::invalid_argument("Temporal reference image extent is unsupported");
return static_cast<std::size_t>(count);
}
bool valid_motion(const TemporalReferencePixel& pixel) noexcept {
return pixel.motion_valid && pixel.reactive < 1.f &&
std::isfinite(pixel.motion[0]) && std::isfinite(pixel.motion[1]) &&
std::isfinite(pixel.previous_depth) && pixel.previous_depth >= 0.f &&
pixel.previous_depth <= 1.f;
}
float bilinear_channel(const TemporalReferenceHistory& history, float u, float v,
std::size_t channel) noexcept {
const float x = u * history.width - .5f;
const float y = v * history.height - .5f;
const auto x0 = static_cast<int>(std::floor(x));
const auto y0 = static_cast<int>(std::floor(y));
const float tx = x - static_cast<float>(x0);
const float ty = y - static_cast<float>(y0);
const auto sample = [&](int sx, int sy) {
const auto px = std::clamp(sx, 0, static_cast<int>(history.width) - 1);
const auto py = std::clamp(sy, 0, static_cast<int>(history.height) - 1);
return history.color[std::size_t(py) * history.width + px][channel];
};
const float top = std::lerp(sample(x0, y0), sample(x0 + 1, y0), tx);
const float bottom = std::lerp(sample(x0, y0 + 1), sample(x0 + 1, y0 + 1), tx);
return std::lerp(top, bottom, ty);
}
} // namespace
TemporalReferenceResult temporal_reference_resolve(const TemporalReferenceFrame& current,
const TemporalReferenceHistory* previous) {
const auto count = checked_pixel_count(current.width, current.height);
if (current.pixels.size() != count)
throw std::invalid_argument("Temporal reference current pixel count differs from extent");
if (previous &&
(previous->width != current.width || previous->height != current.height ||
previous->color.size() != count || previous->depth.size() != count))
throw std::invalid_argument("Temporal reference history extent or record count differs");
for (const auto& pixel : current.pixels) {
if (!std::isfinite(pixel.depth) || pixel.depth < 0.f || pixel.depth > 1.f ||
!std::isfinite(pixel.reactive) || pixel.reactive < 0.f || pixel.reactive > 1.f)
throw std::invalid_argument("Temporal reference current depth/reactivity is invalid");
for (float channel : pixel.color)
if (!std::isfinite(channel))
throw std::invalid_argument("Temporal reference current color is nonfinite");
}
if (previous)
for (std::size_t i = 0; i < count; ++i) {
if (!std::isfinite(previous->depth[i]) || previous->depth[i] < 0.f ||
previous->depth[i] > 1.f)
throw std::invalid_argument("Temporal reference history depth is invalid");
for (float channel : previous->color[i])
if (!std::isfinite(channel))
throw std::invalid_argument("Temporal reference history color is nonfinite");
}
TemporalReferenceResult result;
result.history.width = current.width;
result.history.height = current.height;
result.history.color.resize(count);
result.history.depth.resize(count);
result.accepted.resize(count);
for (std::uint32_t y = 0; y < current.height; ++y)
for (std::uint32_t x = 0; x < current.width; ++x) {
const auto i = std::size_t(y) * current.width + x;
const auto& center = current.pixels[i];
auto& output = result.history.color[i];
output = center.color;
result.history.depth[i] = center.depth;
if (!previous || !valid_motion(center))
continue;
// Dilate from the nearest compatible depth at a silhouette, but
// never borrow foreground motion for newly exposed background or
// resurrect a center pixel with no previous transform.
const TemporalReferencePixel* selected = &center;
const float current_depth_tolerance = .002f + .01f * center.depth;
const auto min_y = y ? y - 1 : y;
const auto min_x = x ? x - 1 : x;
const auto max_y = std::min(y + 1, current.height - 1);
const auto max_x = std::min(x + 1, current.width - 1);
for (auto sy = min_y; sy <= max_y; ++sy)
for (auto sx = min_x; sx <= max_x; ++sx) {
const auto& candidate = current.pixels[std::size_t(sy) * current.width + sx];
if (valid_motion(candidate) &&
std::abs(candidate.depth - center.depth) <=
current_depth_tolerance && candidate.depth < selected->depth)
selected = &candidate;
}
const float u = (static_cast<float>(x) + .5f) / current.width - selected->motion[0];
const float v = (static_cast<float>(y) + .5f) / current.height - selected->motion[1];
if (!std::isfinite(u) || !std::isfinite(v) || u < 0.f || v < 0.f ||
u >= 1.f || v >= 1.f)
continue;
const auto px = std::min(static_cast<std::uint32_t>(u * previous->width),
previous->width - 1);
const auto py = std::min(static_cast<std::uint32_t>(v * previous->height),
previous->height - 1);
const float sampled_depth = previous->depth[std::size_t(py) * previous->width + px];
const float depth_tolerance = .002f + .01f * selected->previous_depth;
if (std::abs(sampled_depth - selected->previous_depth) > depth_tolerance)
continue;
const float motion_pixels = std::hypot(selected->motion[0] * current.width,
selected->motion[1] * current.height);
const float weight = .9f * (1.f - center.reactive) / (1.f + .5f * motion_pixels);
if (!std::isfinite(weight) || weight <= 0.f)
continue;
for (std::size_t channel = 0; channel < 3; ++channel) {
float neighborhood_min = std::numeric_limits<float>::infinity();
float neighborhood_max = -neighborhood_min;
for (auto sy = min_y; sy <= max_y; ++sy)
for (auto sx = min_x; sx <= max_x; ++sx) {
const auto value = current.pixels[std::size_t(sy) * current.width + sx]
.color[channel];
neighborhood_min = std::min(neighborhood_min, value);
neighborhood_max = std::max(neighborhood_max, value);
}
const float prior = std::clamp(bilinear_channel(*previous, u, v, channel),
neighborhood_min, neighborhood_max);
output[channel] = std::lerp(center.color[channel], prior, weight);
}
result.accepted[i] = 1;
++result.accepted_count;
}
return result;
}
} // namespace faset::render