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
+25
View File
@@ -6,6 +6,7 @@
#include <optional>
#include <string>
#include <vector>
#include <faset/render/temporal.hpp>
namespace faset::render {
using Vec2 = std::array<float, 2>;
@@ -136,6 +137,7 @@ struct Snapshot {
std::optional<CameraFrustum> camera_frustum{};
};
enum class VisibilityMode { Direct, GpuFrustum, GpuOcclusion };
enum class LightingMode { Auto, Forward, Tiled };
// CPU-only validation used before publishing a game or creating Vulkan pipelines.
void validate_shader_bundle(const std::filesystem::path& directory);
void validate_gpu_shader_bundle(const std::filesystem::path& directory);
@@ -146,6 +148,11 @@ struct RendererConfig {
bool headless{false};
bool validation{true};
VisibilityMode visibility_mode{VisibilityMode::Direct};
TemporalMode temporal_mode{TemporalMode::Off};
float render_scale{1.f};
// Auto uses measured Forward on the reference workload. Tiled can be forced
// for comparison and falls back to Forward if its device path is unavailable.
LightingMode lighting_mode{LightingMode::Auto};
// GPU counter readback is diagnostic-only; normal visibility uses no CPU feedback.
bool visibility_diagnostics{false};
// Optional isolated shader bundle, useful for editor preview and shader reload tests.
@@ -204,6 +211,21 @@ struct FrameStats {
double gpu_main_cull_ms{}, gpu_main_raster_ms{}, gpu_hzb_ms{};
double gpu_post_cull_ms{}, gpu_post_raster_ms{};
double gpu_sun_shadow_ms{}, gpu_local_shadow_ms{};
TemporalMode requested_temporal_mode{TemporalMode::Off};
TemporalMode effective_temporal_mode{TemporalMode::Off};
TemporalFallbackReason temporal_fallback_reason{TemporalFallbackReason::None};
TemporalResetReason temporal_reset_reason{TemporalResetReason::FirstFrame};
bool temporal_history_valid{};
std::uint32_t temporal_valid_motion_instances{};
std::uint32_t temporal_internal_width{}, temporal_internal_height{};
std::array<float, 2> temporal_jitter{};
double gpu_temporal_resolve_ms{}, gpu_temporal_composite_ms{}, gpu_ui_ms{};
std::vector<std::string> graph_passes;
double gpu_light_tiles_ms{};
std::uint32_t light_tile_count{};
// Optional tile-list readback, valid only when visibility diagnostics are on.
bool light_tile_counts_valid{};
std::uint32_t light_tile_candidate_count{}, light_tile_overflow_count{};
std::string effective_lighting_path{"forward"};
std::string device;
};
@@ -224,6 +246,9 @@ class Renderer {
void resize(std::uint32_t width, std::uint32_t height);
void set_visibility_mode(VisibilityMode);
VisibilityMode visibility_mode() const;
void set_temporal_mode(TemporalMode mode, float render_scale = 1.f);
TemporalMode temporal_mode() const;
float render_scale() const;
void set_visibility_diagnostics(bool enabled);
// Reads the most recently completed HZB mip for editor diagnostics only.
// Normal visibility decisions remain entirely on the GPU.
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#include <array>
#include <cstdint>
#include <optional>
#include <string>
namespace faset::render {
enum class TemporalMode { Off, TAA, Upscale };
struct TemporalCapabilities {
bool compute{};
bool formats{};
bool extent{};
};
enum class TemporalFallbackReason {
None,
ComputeUnavailable,
FormatUnavailable,
ExtentUnsupported
};
TemporalFallbackReason temporal_fallback_reason(TemporalMode requested,
TemporalCapabilities available) noexcept;
TemporalMode select_effective_temporal_mode(TemporalMode requested,
TemporalCapabilities available) noexcept;
// Validate the requested mode and scale before allocating scene targets.
// Off/TAA are 1:1; Upscale accepts [0.5, 1). Ceil rounding is deterministic
// for odd output extents and never produces a zero-sized internal target.
std::array<std::uint32_t, 2> temporal_internal_extent(std::uint32_t output_width,
std::uint32_t output_height,
TemporalMode mode, float render_scale);
enum class TemporalResetReason {
None,
FirstFrame,
CameraCut,
CameraDiscontinuity,
ViewChanged,
ViewportChanged,
ProjectionChanged,
Resize,
ModeChanged,
ScaleChanged,
ShaderReload,
Unsupported
};
// Snapshot matrices/rect stay unjittered. This is independent of HZB history:
// Direct rendering and GPU frustum mode can still accumulate temporal color.
// A missing previous key means no completed frame is available to reuse.
struct TemporalHistoryKey {
std::string view_id;
std::uint32_t output_width{}, output_height{};
std::uint32_t internal_width{}, internal_height{};
std::array<float, 4> scene_rect{};
std::array<float, 16> projection{}, view_projection{};
std::array<float, 3> camera_eye{};
TemporalMode mode{TemporalMode::Off};
float render_scale{1};
std::uint64_t shader_generation{};
bool camera_cut{};
};
struct TemporalHistoryDecision {
bool valid{};
TemporalResetReason reason{TemporalResetReason::FirstFrame};
};
TemporalHistoryDecision
evaluate_temporal_history(const std::optional<TemporalHistoryKey>& previous,
const TemporalHistoryKey& current) noexcept;
// The renderer calls prepare before recording and complete only after a
// successful GPU submission. An exception or failed submission leaves the
// previously completed key intact. This state is independent of P2 HZB.
class TemporalHistoryState {
public:
TemporalHistoryDecision prepare(const TemporalHistoryKey& current) const noexcept;
void complete(const TemporalHistoryKey& rendered);
private:
std::optional<TemporalHistoryKey> completed_;
};
// Sixteen-phase Halton(2,3) offset in clip-space units for scene rasterization.
// UI, picking and history keys use unjittered space. Culling must account for
// this jitter with a conservative edge; HZB depth must match jittered geometry.
// A zero viewport extent throws std::invalid_argument.
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
@@ -0,0 +1,45 @@
#pragma once
#include <array>
#include <cstdint>
#include <vector>
namespace faset::render {
// CPU-only 1:1 oracle for the temporal resolve shader. Color is already
// display-referred; motion is current-minus-previous normalized scene UV.
// The renderer does not call this per-pixel path in production.
struct TemporalReferencePixel {
std::array<float, 4> color{0, 0, 0, 1};
float depth{1};
std::array<float, 2> motion{};
float previous_depth{1};
bool motion_valid{};
float reactive{};
};
struct TemporalReferenceFrame {
std::uint32_t width{}, height{};
std::vector<TemporalReferencePixel> pixels;
};
struct TemporalReferenceHistory {
std::uint32_t width{}, height{};
std::vector<std::array<float, 4>> color;
std::vector<float> depth;
};
struct TemporalReferenceResult {
TemporalReferenceHistory history;
std::vector<std::uint8_t> accepted;
std::uint32_t accepted_count{};
};
// A null previous history is the first-frame/cut fallback. Extents must agree
// when history is supplied; reset it instead of reprojecting stale dimensions.
// Input is bounded to four million pixels so synthetic tests cannot consume
// unbounded memory. Invalid motion rejects history for that pixel.
TemporalReferenceResult temporal_reference_resolve(
const TemporalReferenceFrame& current,
const TemporalReferenceHistory* previous = nullptr);
} // 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)};
}