Add on-demand HZB preview to editor diagnostics
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -9,3 +9,13 @@ The implementation uses the prototype's compact three-mode visibility selector,
|
||||

|
||||
|
||||
The second image is the actual 640 × 420 test capture with a deliberately empty scene. It verifies the selected GPU frustum button and the real counters, with the panel scrolling on a small viewport.
|
||||
|
||||
## Current-HZB preview extension
|
||||
|
||||

|
||||
|
||||
The updated prototype was made with the built-in image editing tool before adding the preview control. Its HZB thumbnail and status values are illustrative. The actual overlay exposes **Show HZB** only as an on-demand diagnostic, offers a mip selector, and uses a real GPU depth-pyramid readback. It does not read back HZB while the preview is off.
|
||||
|
||||

|
||||
|
||||
This 640 × 700 capture is the real developer overlay drawing mip 3 of a cube scene's current HZB. The first frame has already completed, so the displayed visibility counters are actual GPU-read values. The default panel grows when the preview is enabled; on a shorter display it remains scrollable.
|
||||
|
||||
@@ -12,7 +12,9 @@ On Windows, use `windows-debug` for both presets and `build/windows-debug/faset_
|
||||
|
||||
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.
|
||||
|
||||
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, and counts per prepared LOD level. A zero may mean either that there were no candidates or that the selected path did not run; check the active-path indicator. **HZB history: unavailable / invalid** is expected after a camera cut or resize until compatible depth history is available. 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.
|
||||
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.
|
||||
|
||||
In **GPU occlusion** mode, enable **Show HZB** to inspect the current grayscale depth pyramid. The **Mip** slider selects a pyramid level; the preview starts at mip 3 to keep its readback small. A larger mip number shows coarser depth. The preview reads the HZB only while the panel and toggle are open, and only once per completed frame or mip change. Opening diagnostics also enables readback of GPU visibility counters, which is disabled again when the panel closes. Disable the HZB preview for performance comparisons: its diagnostic copy and texture upload add GPU and CPU work. **Freeze counters** does not freeze the HZB image.
|
||||
|
||||
The Vulkan backend emits `VK_EXT_debug_utils` labels for `ShadowMap`, `ForwardAndUI`, `Readback`, and, when presenting, `Presentation`. A graphics capture tool that supports this extension can identify those command-buffer regions. Labels remain available without the Khronos validation layer when the extension is exposed; unsupported systems continue rendering and report labels unavailable. A submitted-label count confirms calls were emitted, not that an external capture tool was tested.
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <faset/editor/debug_overlay.hpp>
|
||||
#include <imgui.h>
|
||||
@@ -38,13 +39,18 @@ ImGuiKey key(std::string_view name) {
|
||||
} // namespace
|
||||
struct DebugOverlay::Impl {
|
||||
ImGuiContext* context{};
|
||||
bool visible{}, freeze{};
|
||||
bool visible{}, freeze{}, show_hzb{};
|
||||
bool hzb_sampled{}, hzb_available{};
|
||||
int hzb_mip{3}, hzb_last_mip{-1};
|
||||
std::uint64_t hzb_frame{};
|
||||
std::string hzb_error;
|
||||
std::uint32_t overlay_buttons{}, editor_buttons{};
|
||||
std::array<float, 2> pointer{-1, -1};
|
||||
float scale{};
|
||||
std::array<float, 4> window_rect{};
|
||||
render::FrameStats displayed;
|
||||
std::shared_ptr<render::Texture> atlas;
|
||||
std::shared_ptr<render::Texture> hzb_preview;
|
||||
Impl() {
|
||||
IMGUI_CHECKVERSION();
|
||||
auto* previous = ImGui::GetCurrentContext();
|
||||
@@ -257,12 +263,92 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("GPU visibility");
|
||||
ImGui::Text("Path: %s", stats.gpu_visibility_active ? "active" : "inactive");
|
||||
ImGui::Text("Indirect bins: %u Visible: %u", stats.gpu_bins,
|
||||
stats.gpu_visible_instances);
|
||||
ImGui::Text("Frustum rejected: %u", stats.gpu_frustum_rejected);
|
||||
ImGui::Text("HZB history: %s", stats.hzb_valid ? "valid" : "unavailable / invalid");
|
||||
ImGui::Text("Deferred: %u Post visible: %u", stats.gpu_occlusion_deferred,
|
||||
stats.gpu_post_visible);
|
||||
ImGui::Text("Indirect bins: %u", stats.gpu_bins);
|
||||
if (stats.gpu_visibility_active && stats.visibility_counters_valid) {
|
||||
ImGui::Text("Visible: %u Frustum rejected: %u",
|
||||
stats.gpu_visible_instances, stats.gpu_frustum_rejected);
|
||||
ImGui::Text("Deferred: %u Post visible: %u", stats.gpu_occlusion_deferred,
|
||||
stats.gpu_post_visible);
|
||||
} else {
|
||||
ImGui::TextDisabled("Visibility counters: %s",
|
||||
stats.gpu_visibility_active
|
||||
? state.freeze ? "frozen before sample" : "awaiting sample"
|
||||
: "GPU path inactive");
|
||||
}
|
||||
if (stats.gpu_visibility_active && stats.gpu_ms > 0) {
|
||||
ImGui::Text("Pass ms: cull %.2f raster %.2f", stats.gpu_main_cull_ms,
|
||||
stats.gpu_main_raster_ms);
|
||||
if (stats.gpu_hzb_ms > 0 || stats.gpu_post_cull_ms > 0 ||
|
||||
stats.gpu_post_raster_ms > 0)
|
||||
ImGui::Text("HZB %.2f post cull %.2f raster %.2f",
|
||||
stats.gpu_hzb_ms, stats.gpu_post_cull_ms,
|
||||
stats.gpu_post_raster_ms);
|
||||
}
|
||||
ImGui::Text("Previous HZB history: %s", stats.hzb_valid ? "valid" : "invalid");
|
||||
if (ImGui::Checkbox("Show HZB", &state.show_hzb)) {
|
||||
state.hzb_sampled = false;
|
||||
state.hzb_error.clear();
|
||||
if (state.show_hzb) {
|
||||
const float expanded = std::min(
|
||||
620.f * scale, std::max(220.f, io.DisplaySize.y - 60.f * scale));
|
||||
if (ImGui::GetWindowSize().y < expanded)
|
||||
ImGui::SetWindowSize({ImGui::GetWindowSize().x, expanded});
|
||||
} else {
|
||||
state.hzb_available = false;
|
||||
}
|
||||
}
|
||||
if (state.show_hzb && state.visible && current_mode == render::VisibilityMode::GpuOcclusion) {
|
||||
const auto max_extent = std::max(renderer.width(), renderer.height());
|
||||
const int max_mip = static_cast<int>(std::bit_width(std::bit_ceil(max_extent))) - 1;
|
||||
state.hzb_mip = std::clamp(state.hzb_mip, 0, max_mip);
|
||||
ImGui::SliderInt("Mip", &state.hzb_mip, 0, max_mip);
|
||||
const auto frame = renderer.stats().frame;
|
||||
if (!state.hzb_sampled || state.hzb_frame != frame ||
|
||||
state.hzb_last_mip != state.hzb_mip) {
|
||||
state.hzb_sampled = true;
|
||||
state.hzb_frame = frame;
|
||||
state.hzb_last_mip = state.hzb_mip;
|
||||
try {
|
||||
if (auto image = renderer.hzb_debug_image(
|
||||
static_cast<std::uint32_t>(state.hzb_mip))) {
|
||||
if (!state.hzb_preview)
|
||||
state.hzb_preview = std::make_shared<render::Texture>();
|
||||
state.hzb_preview->width = image->width;
|
||||
state.hzb_preview->height = image->height;
|
||||
state.hzb_preview->rgba = std::move(image->rgba);
|
||||
++state.hzb_preview->revision;
|
||||
state.hzb_available = true;
|
||||
} else {
|
||||
state.hzb_available = false;
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
state.hzb_available = false;
|
||||
state.hzb_error = error.what();
|
||||
state.show_hzb = false;
|
||||
}
|
||||
}
|
||||
if (state.hzb_available && state.hzb_preview) {
|
||||
float width = ImGui::GetContentRegionAvail().x;
|
||||
float height = width * float(state.hzb_preview->height) /
|
||||
float(state.hzb_preview->width);
|
||||
if (height > 180.f * scale) {
|
||||
height = 180.f * scale;
|
||||
width = height * float(state.hzb_preview->width) /
|
||||
float(state.hzb_preview->height);
|
||||
}
|
||||
ImGui::Image(ImTextureID{2}, {width, height});
|
||||
ImGui::TextDisabled("Current HZB | mip %d (%u x %u)", state.hzb_mip,
|
||||
state.hzb_preview->width, state.hzb_preview->height);
|
||||
} else if (state.show_hzb) {
|
||||
ImGui::TextDisabled("Current HZB unavailable for this frame");
|
||||
}
|
||||
} else if (state.show_hzb) {
|
||||
state.hzb_available = false;
|
||||
state.hzb_sampled = false;
|
||||
ImGui::TextDisabled("Switch to GPU occlusion to view the HZB");
|
||||
}
|
||||
if (!state.hzb_error.empty())
|
||||
ImGui::TextWrapped("HZB preview error: %s", state.hzb_error.c_str());
|
||||
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();
|
||||
@@ -273,9 +359,18 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
|
||||
stats.validation_errors);
|
||||
ImGui::Text("GPU pass labels: %s (%u)", stats.gpu_labels_enabled ? "on" : "unavailable",
|
||||
stats.gpu_label_count);
|
||||
const auto current_position = ImGui::GetWindowPos();
|
||||
const auto current_size = ImGui::GetWindowSize();
|
||||
state.window_rect = {current_position.x, current_position.y,
|
||||
current_size.x, current_size.y};
|
||||
}
|
||||
ImGui::End();
|
||||
} else {
|
||||
state.hzb_available = false;
|
||||
state.hzb_sampled = false;
|
||||
}
|
||||
// GPU counters are a diagnostics readback, never a normal renderer dependency.
|
||||
renderer.set_visibility_diagnostics(state.visible);
|
||||
ImGui::Render();
|
||||
const auto* data = ImGui::GetDrawData();
|
||||
if (!data || !data->Valid)
|
||||
@@ -287,7 +382,11 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
|
||||
command.UserCallback(list, &command);
|
||||
continue;
|
||||
}
|
||||
if (command.GetTexID() != ImTextureID{1})
|
||||
const auto texture = command.GetTexID() == ImTextureID{1}
|
||||
? state.atlas
|
||||
: command.GetTexID() == ImTextureID{2} ? state.hzb_preview
|
||||
: nullptr;
|
||||
if (!texture)
|
||||
throw std::runtime_error("Unsupported texture in Faset diagnostic overlay");
|
||||
const float x = command.ClipRect.x - data->DisplayPos.x;
|
||||
const float y = command.ClipRect.y - data->DisplayPos.y;
|
||||
@@ -296,7 +395,7 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
|
||||
if (width <= 0 || height <= 0)
|
||||
continue;
|
||||
render::UiTriangles batch;
|
||||
batch.texture = state.atlas;
|
||||
batch.texture = texture;
|
||||
batch.clip_rect = {x, y, width, height};
|
||||
batch.vertices.reserve(command.ElemCount);
|
||||
for (unsigned i = 0; i < command.ElemCount; ++i) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <faset/editor/debug_overlay.hpp>
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -91,6 +92,88 @@ int main(int argc, char** argv) {
|
||||
renderer.render(scene);
|
||||
require(renderer.pixels() == clean, "Hiding diagnostics restores the underlying frame");
|
||||
require(renderer.stats().validation_errors == 0, "Diagnostic overlay Vulkan validation");
|
||||
|
||||
render::RendererConfig hzb_config;
|
||||
hzb_config.width = 640;
|
||||
hzb_config.height = 700;
|
||||
hzb_config.title = "Faset HZB diagnostics test";
|
||||
hzb_config.headless = true;
|
||||
hzb_config.validation = true;
|
||||
hzb_config.visibility_mode = render::VisibilityMode::GpuOcclusion;
|
||||
render::Renderer hzb_renderer(hzb_config);
|
||||
render::Snapshot hzb_scene;
|
||||
hzb_scene.view_id = "overlay-hzb-scene";
|
||||
hzb_scene.eye = {4, 3, 6};
|
||||
hzb_scene.view_projection = render::multiply(
|
||||
render::perspective(.85f, 640.f / 700.f, .1f, 100.f),
|
||||
render::look_at(hzb_scene.eye, {0, 0, 0}));
|
||||
render::DrawItem cube;
|
||||
cube.mesh = render::cube_mesh();
|
||||
cube.instance_key = "hzb-cube";
|
||||
hzb_scene.draws.push_back(cube);
|
||||
hzb_renderer.render(hzb_scene);
|
||||
require(hzb_renderer.hzb_debug_image(3).has_value(),
|
||||
"Fixture creates a current HZB for the preview");
|
||||
editor::DebugOverlay hzb_overlay;
|
||||
hzb_overlay.set_visible(true);
|
||||
hzb_overlay.append(hzb_scene, hzb_renderer, 1.f / 60.f);
|
||||
require(!hzb_scene.ui_triangles.empty(), "HZB panel emits its normal font texture");
|
||||
const auto font_texture = hzb_scene.ui_triangles.front().texture;
|
||||
require(std::all_of(hzb_scene.ui_triangles.begin(), hzb_scene.ui_triangles.end(),
|
||||
[&](const auto& batch) { return batch.texture == font_texture; }),
|
||||
"HZB image is absent while preview toggle is off");
|
||||
render::Event preview_click;
|
||||
preview_click.button = 1;
|
||||
preview_click.x = 34;
|
||||
preview_click.y = 405;
|
||||
const auto click_preview = [&] {
|
||||
preview_click.type = render::Event::Type::MouseDown;
|
||||
require(hzb_overlay.process_events(std::span(&preview_click, 1)).empty(),
|
||||
"HZB preview checkbox captures pointer down");
|
||||
hzb_scene.ui_triangles.clear();
|
||||
hzb_overlay.append(hzb_scene, hzb_renderer, 1.f / 60.f);
|
||||
preview_click.type = render::Event::Type::MouseUp;
|
||||
require(hzb_overlay.process_events(std::span(&preview_click, 1)).empty(),
|
||||
"HZB preview checkbox captures pointer up");
|
||||
hzb_scene.ui_triangles.clear();
|
||||
hzb_overlay.append(hzb_scene, hzb_renderer, 1.f / 60.f);
|
||||
};
|
||||
click_preview();
|
||||
hzb_scene.ui_triangles.clear();
|
||||
hzb_overlay.append(hzb_scene, hzb_renderer, 1.f / 60.f);
|
||||
const auto preview = std::find_if(hzb_scene.ui_triangles.begin(),
|
||||
hzb_scene.ui_triangles.end(),
|
||||
[&](const auto& batch) {
|
||||
return batch.texture != font_texture;
|
||||
});
|
||||
require(preview != hzb_scene.ui_triangles.end() && preview->texture->width == 128 &&
|
||||
preview->texture->height == 128 && preview->texture->revision > 0,
|
||||
"Enabled HZB preview emits the selected mip as a revised texture");
|
||||
click_preview();
|
||||
hzb_scene.ui_triangles.clear();
|
||||
hzb_overlay.append(hzb_scene, hzb_renderer, 1.f / 60.f);
|
||||
require(std::all_of(hzb_scene.ui_triangles.begin(), hzb_scene.ui_triangles.end(),
|
||||
[&](const auto& batch) { return batch.texture == font_texture; }),
|
||||
"Disabling HZB preview removes its texture");
|
||||
click_preview();
|
||||
hzb_scene.ui_triangles.clear();
|
||||
hzb_overlay.append(hzb_scene, hzb_renderer, 1.f / 60.f);
|
||||
hzb_renderer.render(hzb_scene);
|
||||
require(hzb_renderer.stats().validation_errors == 0 &&
|
||||
hzb_renderer.stats().visibility_counters_valid,
|
||||
"HZB preview renders cleanly and open diagnostics enable GPU counters");
|
||||
hzb_scene.ui_triangles.clear();
|
||||
hzb_overlay.append(hzb_scene, hzb_renderer, 1.f / 60.f);
|
||||
hzb_renderer.render(hzb_scene);
|
||||
if (argc > 2)
|
||||
hzb_renderer.capture(argv[2]);
|
||||
hzb_overlay.set_visible(false);
|
||||
hzb_scene.ui_triangles.clear();
|
||||
hzb_overlay.append(hzb_scene, hzb_renderer, 1.f / 60.f);
|
||||
require(hzb_scene.ui_triangles.empty(), "Hidden panel emits no HZB preview");
|
||||
hzb_renderer.render(hzb_scene);
|
||||
require(!hzb_renderer.stats().visibility_counters_valid,
|
||||
"Closing diagnostics disables GPU visibility readback");
|
||||
std::cout << "ImGui diagnostics, F12, font atlas, clipping and event isolation passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
Reference in New Issue
Block a user