Report effective visibility and stable GPU instance identity
Native and manual checks / native (windows-2025) (push) Waiting to run
Native and manual checks / native (ubuntu-24.04) (push) Failing after 36s
Native and manual checks / manual (push) Successful in 28s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (windows-2025) (push) Waiting to run
Native and manual checks / native (ubuntu-24.04) (push) Failing after 36s
Native and manual checks / manual (push) Successful in 28s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
This commit is contained in:
+26
-6
@@ -28,6 +28,17 @@ using Json = nlohmann::json;
|
||||
double milliseconds(Clock::time_point begin, Clock::time_point end) {
|
||||
return std::chrono::duration<double, std::milli>(end - begin).count();
|
||||
}
|
||||
const char* visibility_mode_name(faset::render::VisibilityMode mode) {
|
||||
switch (mode) {
|
||||
case faset::render::VisibilityMode::Direct:
|
||||
return "direct";
|
||||
case faset::render::VisibilityMode::GpuFrustum:
|
||||
return "gpu-frustum";
|
||||
case faset::render::VisibilityMode::GpuOcclusion:
|
||||
return "gpu-occlusion";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
struct ProfileSample {
|
||||
double wall{}, simulation{}, snapshot{}, render{}, rendererCpu{}, gpu{}, readbackCpu{};
|
||||
faset::runtime::FrameStats runtime;
|
||||
@@ -36,6 +47,7 @@ struct ProfileSample {
|
||||
std::uint32_t textureCount{};
|
||||
bool physicsDebug{};
|
||||
bool gpuVisibilityActive{};
|
||||
faset::render::VisibilityMode effectiveVisibilityMode{faset::render::VisibilityMode::Direct};
|
||||
};
|
||||
Json distribution(std::vector<double> values) {
|
||||
if (values.empty())
|
||||
@@ -82,7 +94,9 @@ Json profileFrames(const std::vector<ProfileSample>& samples) {
|
||||
{"gpu_allocated_bytes", sample.gpuAllocatedBytes},
|
||||
{"texture_count", sample.textureCount},
|
||||
{"physics_debug", sample.physicsDebug},
|
||||
{"gpu_visibility_active", sample.gpuVisibilityActive}});
|
||||
{"gpu_visibility_active", sample.gpuVisibilityActive},
|
||||
{"effective_visibility_mode",
|
||||
visibility_mode_name(sample.effectiveVisibilityMode)}});
|
||||
}
|
||||
return {{"samples", std::move(frames)},
|
||||
{"summary_ms",
|
||||
@@ -555,10 +569,12 @@ int player_main(int argc, char** argv) {
|
||||
printGameplayLogs();
|
||||
const auto renderStarted = Clock::now();
|
||||
renderer.render(snapshot);
|
||||
if (frames == 0 && visibilityMode != faset::render::VisibilityMode::Direct &&
|
||||
!renderer.stats().gpu_visibility_active)
|
||||
std::cerr << "Requested GPU visibility is unavailable on this device; "
|
||||
"using Direct rendering.\n";
|
||||
if (frames == 0 &&
|
||||
visibilityMode != renderer.stats().effective_visibility_mode)
|
||||
std::cerr << "Requested " << visibilityName << " visibility is unavailable; "
|
||||
<< "using "
|
||||
<< visibility_mode_name(renderer.stats().effective_visibility_mode)
|
||||
<< " rendering.\n";
|
||||
const auto frameFinished = Clock::now();
|
||||
if (frames == 0)
|
||||
firstFrameMs = milliseconds(started, frameFinished);
|
||||
@@ -571,7 +587,7 @@ int player_main(int argc, char** argv) {
|
||||
milliseconds(renderStarted, frameFinished), measured.cpu_ms, measured.gpu_ms,
|
||||
measured.readback_cpu_ms, runtimeStats, measured.draw_calls, measured.vertices,
|
||||
measured.gpu_allocated_bytes, measured.texture_count, debugPhysics,
|
||||
measured.gpu_visibility_active});
|
||||
measured.gpu_visibility_active, measured.effective_visibility_mode});
|
||||
}
|
||||
++frames;
|
||||
}
|
||||
@@ -601,6 +617,8 @@ int player_main(int argc, char** argv) {
|
||||
{"validation_errors", stats.validation_errors},
|
||||
{"presentation_mode", headless ? "offscreen" : "windowed"},
|
||||
{"visibility_mode", visibilityName},
|
||||
{"effective_visibility_mode",
|
||||
visibility_mode_name(stats.effective_visibility_mode)},
|
||||
{"simulation_mode", "synthetic_fixed_timestep"},
|
||||
{"fixed_delta_seconds", config.fixedDelta},
|
||||
{"percentile_method", "nearest_rank_all_completed_frames_no_warmup_exclusion"},
|
||||
@@ -627,6 +645,8 @@ int player_main(int argc, char** argv) {
|
||||
{"dimension", document.value("dimension", 3)},
|
||||
{"device", stats.device},
|
||||
{"visibility_mode", visibilityName},
|
||||
{"effective_visibility_mode",
|
||||
visibility_mode_name(stats.effective_visibility_mode)},
|
||||
{"gpu_visibility_active", stats.gpu_visibility_active},
|
||||
{"validation_errors", stats.validation_errors}}
|
||||
.dump()
|
||||
|
||||
@@ -10,7 +10,7 @@ build/linux-debug/faset_editor --project examples/projects/collect-3d --gui
|
||||
|
||||
On Windows, use `windows-debug` for both presets and `build/windows-debug/faset_editor.exe`. Press **F12** to show or hide the panel. Drag its title bar to move it; **Freeze counters** holds a completed-frame sample for inspection. Closing the panel does not stop rendering. Pointer gestures inside the overlay are kept out of the authoring UI.
|
||||
|
||||
Use the **Visibility** selector to compare **Direct**, **GPU frustum**, and **GPU occlusion** on the same open scene. This is a live renderer setting for the Editor viewport; it does not change the scene or exported game. The selected mode is independent of **Freeze counters**. The counters describe the previous completed frame, so render one more frame after changing modes before reading them. **Path: active** under GPU visibility confirms that the GPU path actually ran; a selected GPU mode by itself is not evidence that it ran.
|
||||
Use the **Visibility** selector to compare **Direct**, **GPU frustum**, and **GPU occlusion** on the same open scene. This is a live renderer setting for the Editor viewport; it does not change the scene or exported game. The selected mode is independent of **Freeze counters**. The counters describe the previous completed frame, so render one more frame after changing modes before reading them. **Effective path** names the algorithm that actually ran. A **Fallback from** line appears when device or target capabilities prevent the selected mode; for example, GPU occlusion may use GPU frustum if HZB is unavailable.
|
||||
|
||||
The panel reports the previous completed frame: renderer wall time, GPU timestamp time where available, synchronous readback time, draw calls, packed vertices, culled meshes, textures, explicit Vulkan allocation sizes, actual validation availability/errors, and GPU pass-label count. It also shows whether GPU visibility ran, submitted indirect bins, visible instances, frustum rejects, deferred and post-pass visible instances, HZB history validity, counts per prepared LOD level, and GPU pass timings where available. GPU counts are explicitly marked unavailable until the first frame rendered with diagnostics open; only a displayed zero is a measured zero. **Previous HZB history: invalid** is expected after a camera cut or resize until compatible depth history is available. A current HZB preview can still exist after that first frame because it was built from the current depth. Renderer wall time includes waiting for GPU work; it is not thread CPU usage. Memory excludes driver-internal allocations. The overlay itself adds drawing work, so hide it for a baseline performance measurement.
|
||||
|
||||
|
||||
@@ -80,10 +80,11 @@ bounded run:
|
||||
```
|
||||
|
||||
Accepted values are `direct`, `gpu-frustum`, and `gpu-occlusion`; Direct is the
|
||||
default. The profile records the requested `visibility_mode` and each frame's
|
||||
`gpu_visibility_active` state. Check that state when interpreting a GPU run: a
|
||||
requested mode can fall back if the required device profile is unavailable. The
|
||||
Editor reports the same distinction as **Path: active**. See
|
||||
default. The profile records the requested `visibility_mode`, the run's and each
|
||||
frame's `effective_visibility_mode`, and each frame's `gpu_visibility_active` state.
|
||||
Compare requested and effective modes before interpreting a GPU run: a missing GPU
|
||||
profile falls back to Direct, while missing HZB can reduce GPU occlusion to GPU
|
||||
frustum. The Editor shows the **Effective path** and any **Fallback from** line. See
|
||||
[Diagnostics](diagnostics.md) for the counters and HZB preview.
|
||||
|
||||
For a repeatable offscreen comparison, build and run the P2 benchmark harness:
|
||||
|
||||
@@ -150,6 +150,9 @@ struct FrameStats {
|
||||
std::uint32_t texture_count{};
|
||||
std::uint32_t vertices{}, draw_calls{}, culled_meshes{}, validation_errors{};
|
||||
bool gpu_visibility_active{}, hzb_valid{};
|
||||
// Requested and actual paths for the last frame; actual may be less capable.
|
||||
VisibilityMode requested_visibility_mode{VisibilityMode::Direct};
|
||||
VisibilityMode effective_visibility_mode{VisibilityMode::Direct};
|
||||
bool visibility_counters_valid{};
|
||||
std::uint32_t gpu_bins{}, gpu_visible_instances{}, gpu_frustum_rejected{};
|
||||
std::uint32_t gpu_occlusion_deferred{}, gpu_post_visible{};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <faset/render/renderer.hpp>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
@@ -22,6 +23,12 @@ Bounds local_bounds(const Mesh& mesh);
|
||||
Bounds transformed_bounds(const Bounds& local, const Mat4& model);
|
||||
Bounds transformed_bounds(const Mesh& mesh, const Mat4& model);
|
||||
|
||||
// Resolve a requested mode against the current device and target capabilities.
|
||||
// Missing HZB retains GPU frustum culling, but callers must report that fallback.
|
||||
VisibilityMode select_effective_visibility_mode(VisibilityMode requested,
|
||||
bool gpu_available,
|
||||
bool hzb_available) noexcept;
|
||||
|
||||
struct InstanceUpdate {
|
||||
std::uint32_t slot{};
|
||||
std::uint64_t generation{};
|
||||
@@ -30,6 +37,15 @@ struct InstanceUpdate {
|
||||
bool previous_valid{};
|
||||
};
|
||||
|
||||
// GPU instance metadata: history valid, stable slot, then generation low/high.
|
||||
// A zero generation identifies an anonymous, untracked draw.
|
||||
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,
|
||||
static_cast<std::uint32_t>(update.generation),
|
||||
static_cast<std::uint32_t>(update.generation >> 32)};
|
||||
}
|
||||
|
||||
// One update per logical instance per rendered frame. finish_frame() promotes the
|
||||
// current state to *rendered* history and retires keys absent from that frame.
|
||||
// Reordering the input never renumbers live instances. Mesh changes retain the
|
||||
|
||||
@@ -26,7 +26,8 @@ struct InstanceRecord {
|
||||
float4 currentExtent; // 160..175: world AABB half extents
|
||||
float4 previousCenter; // 176..191
|
||||
float4 previousExtent; // 192..207
|
||||
uint4 metadata; // 208..223: x=previousValid, others reserved
|
||||
uint4 metadata; // 208..223: x=previousValid, y=stableSlot,
|
||||
// z=generation low 32, w=generation high 32 (zero = untracked)
|
||||
};
|
||||
struct ViewRecord {
|
||||
column_major float4x4 currentViewProjection; // 0..63
|
||||
|
||||
@@ -17,6 +17,17 @@ struct CurrentContext {
|
||||
ImGui::SetCurrentContext(previous);
|
||||
}
|
||||
};
|
||||
const char* visibility_label(render::VisibilityMode mode) {
|
||||
switch (mode) {
|
||||
case render::VisibilityMode::Direct:
|
||||
return "Direct";
|
||||
case render::VisibilityMode::GpuFrustum:
|
||||
return "GPU frustum";
|
||||
case render::VisibilityMode::GpuOcclusion:
|
||||
return "GPU occlusion";
|
||||
}
|
||||
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');
|
||||
@@ -272,7 +283,10 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
|
||||
stats.texture_count);
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("GPU visibility");
|
||||
ImGui::Text("Path: %s", stats.gpu_visibility_active ? "active" : "inactive");
|
||||
ImGui::Text("Effective path: %s", visibility_label(stats.effective_visibility_mode));
|
||||
if (stats.requested_visibility_mode != stats.effective_visibility_mode)
|
||||
ImGui::TextDisabled("Fallback from %s",
|
||||
visibility_label(stats.requested_visibility_mode));
|
||||
ImGui::Text("Indirect bins: %u", stats.gpu_bins);
|
||||
if (stats.gpu_visibility_active && stats.visibility_counters_valid) {
|
||||
ImGui::Text("Visible: %u Frustum rejected: %u",
|
||||
|
||||
@@ -1634,10 +1634,12 @@ struct Renderer::Impl {
|
||||
it = it->second.owner.expired() ? bounds_cache.erase(it) : std::next(it);
|
||||
for (auto it = opacity_cache.begin(); it != opacity_cache.end();)
|
||||
it = it->second.owner.expired() ? opacity_cache.erase(it) : std::next(it);
|
||||
const bool gpu_active = scene.available &&
|
||||
config.visibility_mode != VisibilityMode::Direct;
|
||||
const bool occlusion = gpu_active && scene.hzb_supported && scene.hzb_mips &&
|
||||
config.visibility_mode == VisibilityMode::GpuOcclusion;
|
||||
statistics.requested_visibility_mode = config.visibility_mode;
|
||||
statistics.effective_visibility_mode = select_effective_visibility_mode(
|
||||
config.visibility_mode, scene.available, scene.hzb_supported && scene.hzb_mips);
|
||||
const bool gpu_active = statistics.effective_visibility_mode != VisibilityMode::Direct;
|
||||
const bool occlusion = statistics.effective_visibility_mode ==
|
||||
VisibilityMode::GpuOcclusion;
|
||||
statistics.gpu_visibility_active = gpu_active;
|
||||
statistics.hzb_valid = false;
|
||||
bool can_present = surface != VK_NULL_HANDLE;
|
||||
@@ -1831,7 +1833,7 @@ struct Renderer::Impl {
|
||||
(previous.previous_bounds.max[axis] -
|
||||
previous.previous_bounds.min[axis]) * .5f;
|
||||
}
|
||||
instance.metadata[0] = previous.previous_valid && history_compatible ? 1 : 0;
|
||||
instance.metadata = gpu_instance_metadata(previous, history_compatible);
|
||||
if (gpu_frame.instances.size() >= UINT32_MAX)
|
||||
throw std::overflow_error("GPU scene instance capacity exceeded");
|
||||
bin_it->instances.push_back(static_cast<std::uint32_t>(gpu_frame.instances.size()));
|
||||
|
||||
@@ -14,6 +14,16 @@ void validate(const Bounds& bounds) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
VisibilityMode select_effective_visibility_mode(VisibilityMode requested,
|
||||
bool gpu_available,
|
||||
bool hzb_available) noexcept {
|
||||
if (!gpu_available || requested == VisibilityMode::Direct)
|
||||
return VisibilityMode::Direct;
|
||||
if (requested == VisibilityMode::GpuOcclusion && hzb_available)
|
||||
return VisibilityMode::GpuOcclusion;
|
||||
return VisibilityMode::GpuFrustum;
|
||||
}
|
||||
|
||||
Bounds local_bounds(const Mesh& mesh) {
|
||||
if (mesh.vertices.empty())
|
||||
throw std::invalid_argument("Empty mesh has no bounds");
|
||||
|
||||
@@ -47,6 +47,9 @@ with tempfile.TemporaryDirectory(prefix="faset-player-diagnostics-") as temporar
|
||||
assert selected.returncode == 0, (mode, selected.stdout, selected.stderr)
|
||||
mode_report = json.loads(mode_profile.read_text(encoding="utf-8"))
|
||||
assert mode_report["visibility_mode"] == mode, mode_report
|
||||
assert mode_report["effective_visibility_mode"] == mode, mode_report
|
||||
assert all(sample["effective_visibility_mode"] == mode
|
||||
for sample in mode_report["samples"]), mode_report["samples"]
|
||||
assert all(sample["gpu_visibility_active"] is active
|
||||
for sample in mode_report["samples"]), mode_report["samples"]
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <faset/render/renderer.hpp>
|
||||
#include <faset/render/visibility.hpp>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -15,6 +18,49 @@ void require(bool condition, const std::string& message) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
void stable_gpu_identity_metadata() {
|
||||
auto mesh = cube_mesh();
|
||||
auto replacement = std::make_shared<Mesh>(*mesh);
|
||||
const Bounds bounds{{-1, -1, -1}, {1, 1, 1}};
|
||||
InstanceTracker tracker;
|
||||
const auto first = tracker.update("first", mesh, identity, bounds, "game");
|
||||
const auto second = tracker.update("second", mesh, identity, bounds, "game");
|
||||
const auto first_metadata = gpu_instance_metadata(first, true);
|
||||
const auto second_metadata = gpu_instance_metadata(second, true);
|
||||
require(first_metadata[0] == 0 && first_metadata[1] == first.slot &&
|
||||
first_metadata[2] == 1 && first_metadata[3] == 0 &&
|
||||
second_metadata[1] == second.slot && first_metadata[1] != second_metadata[1],
|
||||
"New tracked instances need distinct stable GPU identities without history");
|
||||
tracker.finish_frame();
|
||||
|
||||
const auto reordered_second = tracker.update("second", mesh, identity, bounds, "game");
|
||||
const auto reordered_first = tracker.update("first", mesh, identity, bounds, "game");
|
||||
const auto reordered_metadata = gpu_instance_metadata(reordered_first, true);
|
||||
require(reordered_metadata[0] == 1 && reordered_metadata[1] == first_metadata[1] &&
|
||||
reordered_metadata[2] == first_metadata[2] &&
|
||||
reordered_metadata[3] == first_metadata[3] &&
|
||||
gpu_instance_metadata(reordered_second, true)[1] == second_metadata[1],
|
||||
"Reordering must preserve stable GPU identity and enable valid history");
|
||||
require(gpu_instance_metadata(reordered_first, false)[0] == 0,
|
||||
"Incompatible history must clear only the history-valid lane");
|
||||
|
||||
const auto changed = tracker.update("first", replacement, identity, bounds, "game");
|
||||
const auto changed_metadata = gpu_instance_metadata(changed, true);
|
||||
require(changed_metadata[1] == first_metadata[1] &&
|
||||
changed_metadata[2] != first_metadata[2] && changed_metadata[0] == 0,
|
||||
"Replacing a mesh must advance the stable GPU identity generation");
|
||||
|
||||
InstanceUpdate wide_generation{};
|
||||
wide_generation.slot = 17;
|
||||
wide_generation.generation = 0x12345678abcdef01ULL;
|
||||
const auto wide_metadata = gpu_instance_metadata(wide_generation, true);
|
||||
require(wide_metadata[1] == 17 && wide_metadata[2] == 0xabcdef01U &&
|
||||
wide_metadata[3] == 0x12345678U,
|
||||
"GPU metadata must preserve all 64 generation bits");
|
||||
require(gpu_instance_metadata({}, true) == std::array<std::uint32_t, 4>{0, 0, 0, 0},
|
||||
"Anonymous draws have generation zero and are untracked");
|
||||
}
|
||||
|
||||
RendererConfig config(VisibilityMode mode) {
|
||||
RendererConfig result;
|
||||
result.width = 320;
|
||||
@@ -60,12 +106,15 @@ std::size_t different_pixels(const std::vector<std::uint8_t>& a,
|
||||
|
||||
int main() {
|
||||
try {
|
||||
stable_gpu_identity_metadata();
|
||||
Renderer direct(config(VisibilityMode::Direct));
|
||||
Renderer gpu(config(VisibilityMode::GpuFrustum));
|
||||
auto frame = scene();
|
||||
direct.render(frame);
|
||||
gpu.render(frame);
|
||||
require(gpu.stats().gpu_visibility_active, "GPU visibility path did not run");
|
||||
require(gpu.stats().effective_visibility_mode == VisibilityMode::GpuFrustum,
|
||||
"GPU frustum request did not use the frustum path");
|
||||
require(gpu.stats().gpu_bins == 1 && gpu.stats().gpu_visible_instances == 1 &&
|
||||
gpu.stats().gpu_frustum_rejected == 1,
|
||||
"GPU frustum/indirect counts are wrong");
|
||||
@@ -94,6 +143,8 @@ int main() {
|
||||
"Visibility counters can be enabled for diagnostics");
|
||||
Renderer occlusion(config(VisibilityMode::GpuOcclusion));
|
||||
occlusion.render(scene());
|
||||
require(occlusion.stats().effective_visibility_mode == VisibilityMode::GpuOcclusion,
|
||||
"GPU occlusion request silently selected another path");
|
||||
auto hzb = occlusion.hzb_debug_image(0);
|
||||
if (occlusion.stats().gpu_ms > 0)
|
||||
require(occlusion.stats().gpu_hzb_ms > 0 &&
|
||||
|
||||
@@ -119,6 +119,23 @@ void lod_thresholds_have_hysteresis_and_fallback() {
|
||||
rejects([&] { select_lod(std::numeric_limits<float>::quiet_NaN(), 0, thresholds, .1f); },
|
||||
"Nonfinite projected size cannot silently select a level");
|
||||
}
|
||||
void effective_mode_exposes_device_fallback() {
|
||||
require(select_effective_visibility_mode(VisibilityMode::Direct, true, true) ==
|
||||
VisibilityMode::Direct,
|
||||
"Direct request stays Direct even on a fully capable device");
|
||||
require(select_effective_visibility_mode(VisibilityMode::GpuFrustum, false, true) ==
|
||||
VisibilityMode::Direct,
|
||||
"Missing GPU culling profile falls back to Direct");
|
||||
require(select_effective_visibility_mode(VisibilityMode::GpuFrustum, true, false) ==
|
||||
VisibilityMode::GpuFrustum,
|
||||
"GPU frustum does not depend on HZB support");
|
||||
require(select_effective_visibility_mode(VisibilityMode::GpuOcclusion, true, false) ==
|
||||
VisibilityMode::GpuFrustum,
|
||||
"Missing HZB exposes a frustum-only effective mode");
|
||||
require(select_effective_visibility_mode(VisibilityMode::GpuOcclusion, true, true) ==
|
||||
VisibilityMode::GpuOcclusion,
|
||||
"Supported occlusion keeps the requested effective mode");
|
||||
}
|
||||
} // namespace
|
||||
int main() {
|
||||
try {
|
||||
@@ -126,6 +143,7 @@ int main() {
|
||||
ids_track_rendered_frames_and_generation();
|
||||
owned_mesh_identity_outlives_reimport_gap();
|
||||
lod_thresholds_have_hysteresis_and_fallback();
|
||||
effective_mode_exposes_device_fallback();
|
||||
std::cout << "Visibility bounds, instance identity and LOD policy passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
Reference in New Issue
Block a user