Files
Faset_Engine/src/editor/debug_overlay.cpp
T
Emil 7297d01436
Native and manual checks / native (ubuntu-24.04) (push) Failing after 37s
Native and manual checks / manual (push) Successful in 27s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
Expose and package temporal rendering with guarded edge history
2026-09-24 03:59:22 +03:00

560 lines
27 KiB
C++

#include <algorithm>
#include <bit>
#include <cmath>
#include <faset/editor/debug_overlay.hpp>
#include <imgui.h>
#include <stdexcept>
#include <string_view>
namespace faset::editor {
namespace {
struct CurrentContext {
ImGuiContext* previous{ImGui::GetCurrentContext()};
explicit CurrentContext(ImGuiContext* context) {
ImGui::SetCurrentContext(context);
}
~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";
}
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');
if (name.size() == 1 && name[0] >= '0' && name[0] <= '9')
return static_cast<ImGuiKey>(ImGuiKey_0 + name[0] - '0');
const std::pair<std::string_view, ImGuiKey> names[] = {
{"Tab", ImGuiKey_Tab}, {"Left", ImGuiKey_LeftArrow},
{"Right", ImGuiKey_RightArrow}, {"Up", ImGuiKey_UpArrow},
{"Down", ImGuiKey_DownArrow}, {"PageUp", ImGuiKey_PageUp},
{"PageDown", ImGuiKey_PageDown}, {"Home", ImGuiKey_Home},
{"End", ImGuiKey_End}, {"Insert", ImGuiKey_Insert},
{"Delete", ImGuiKey_Delete}, {"Backspace", ImGuiKey_Backspace},
{"Space", ImGuiKey_Space}, {"Return", ImGuiKey_Enter},
{"Escape", ImGuiKey_Escape}};
for (const auto& [label, value] : names)
if (label == name)
return value;
return ImGuiKey_None;
}
} // namespace
struct DebugOverlay::Impl {
ImGuiContext* context{};
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{};
float last_upscale_scale{.67f};
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();
context = ImGui::CreateContext();
auto& io = ImGui::GetIO();
io.IniFilename = nullptr;
io.LogFilename = nullptr;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset;
io.BackendPlatformName = "faset_events";
io.BackendRendererName = "faset_ui_triangles";
unsigned char* pixels{};
int width{}, height{};
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
atlas = std::make_shared<render::Texture>();
atlas->width = width;
atlas->height = height;
atlas->rgba.assign(pixels, pixels + std::size_t(width) * height * 4);
io.Fonts->SetTexID(ImTextureID{1});
ImGui::SetCurrentContext(previous);
}
~Impl() {
ImGui::DestroyContext(context);
}
};
DebugOverlay::DebugOverlay() : impl_(std::make_unique<Impl>()) {}
DebugOverlay::~DebugOverlay() = default;
bool DebugOverlay::visible() const {
return impl_->visible;
}
void DebugOverlay::set_visible(bool value) {
impl_->visible = value;
if (!value) {
impl_->hzb_preview.reset();
impl_->hzb_available = impl_->hzb_sampled = false;
}
}
std::vector<render::Event> DebugOverlay::process_events(std::span<const render::Event> events) {
CurrentContext current(impl_->context);
auto& io = ImGui::GetIO();
std::vector<render::Event> forwarded;
const auto pointer_in_overlay = [&] {
return impl_->visible && impl_->pointer[0] >= impl_->window_rect[0] &&
impl_->pointer[1] >= impl_->window_rect[1] &&
impl_->pointer[0] < impl_->window_rect[0] + impl_->window_rect[2] &&
impl_->pointer[1] < impl_->window_rect[1] + impl_->window_rect[3];
};
const auto capture_pointer = [&] {
if (impl_->overlay_buttons)
return true;
if (impl_->editor_buttons)
return false;
return pointer_in_overlay();
};
for (const auto& event : events) {
using Type = render::Event::Type;
if ((event.type == Type::KeyDown || event.type == Type::KeyUp) && event.key == "F12") {
if (event.type == Type::KeyDown && !event.repeat) {
impl_->visible = !impl_->visible;
if (!impl_->visible) {
impl_->hzb_preview.reset();
impl_->hzb_available = impl_->hzb_sampled = false;
}
}
continue;
}
io.AddKeyEvent(ImGuiMod_Ctrl, event.control);
io.AddKeyEvent(ImGuiMod_Shift, event.shift);
io.AddKeyEvent(ImGuiMod_Alt, event.alt);
bool captured{};
bool pointer_event{};
switch (event.type) {
case Type::MouseMove:
io.AddMousePosEvent(event.x, event.y);
impl_->pointer = {event.x, event.y};
pointer_event = true;
captured = capture_pointer();
break;
case Type::MouseDown:
case Type::MouseUp:
io.AddMousePosEvent(event.x, event.y);
impl_->pointer = {event.x, event.y};
pointer_event = true;
captured = capture_pointer();
if (event.button >= 1 && event.button <= 5) {
const int buttons[] = {0, 2, 1, 3, 4};
io.AddMouseButtonEvent(buttons[event.button - 1], event.type == Type::MouseDown);
const auto mask = std::uint32_t{1} << (event.button - 1);
if (event.type == Type::MouseDown) {
if (captured)
impl_->overlay_buttons |= mask;
else
impl_->editor_buttons |= mask;
} else {
if (impl_->overlay_buttons & mask)
captured = true;
else if (impl_->editor_buttons & mask)
captured = false;
impl_->overlay_buttons &= ~mask;
impl_->editor_buttons &= ~mask;
}
}
break;
case Type::Wheel:
io.AddMouseWheelEvent(event.x, event.y);
pointer_event = true;
captured = capture_pointer();
break;
case Type::KeyDown:
case Type::KeyUp:
if (const auto mapped = key(event.key); mapped != ImGuiKey_None)
io.AddKeyEvent(mapped, event.type == Type::KeyDown);
captured = io.WantCaptureKeyboard;
break;
case Type::TextInput:
io.AddInputCharactersUTF8(event.text.c_str());
captured = io.WantCaptureKeyboard;
break;
case Type::FocusGained:
case Type::FocusLost:
io.AddFocusEvent(event.type == Type::FocusGained);
if (event.type == Type::FocusLost)
impl_->overlay_buttons = impl_->editor_buttons = 0;
break;
default:
break;
}
if (!captured || (!pointer_event && !impl_->visible))
forwarded.push_back(event);
}
return forwarded;
}
void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer, float delta) {
CurrentContext current(impl_->context);
auto& state = *impl_;
auto& io = ImGui::GetIO();
io.DisplaySize = {float(renderer.width()), float(renderer.height())};
io.DisplayFramebufferScale = {1, 1}; // Faset events and geometry already use drawable pixels.
io.DeltaTime = std::isfinite(delta) && delta > 0 ? std::clamp(delta, .0001f, .1f) : 1.f / 60.f;
const float scale = renderer.display_scale();
if (state.scale != scale) {
auto& style = ImGui::GetStyle();
style = ImGuiStyle{};
ImGui::StyleColorsDark();
// Developer tooling follows the editor's neutral, low-contrast dark palette.
style.Colors[ImGuiCol_WindowBg] = {0.11f, 0.11f, 0.115f, 0.98f};
style.Colors[ImGuiCol_TitleBg] = {0.085f, 0.085f, 0.087f, 1.f};
style.Colors[ImGuiCol_TitleBgActive] = {0.11f, 0.11f, 0.115f, 1.f};
style.Colors[ImGuiCol_FrameBg] = {0.15f, 0.15f, 0.16f, 1.f};
style.Colors[ImGuiCol_FrameBgHovered] = {0.19f, 0.19f, 0.20f, 1.f};
style.Colors[ImGuiCol_Button] = {0.15f, 0.15f, 0.16f, 1.f};
style.Colors[ImGuiCol_ButtonHovered] = {0.21f, 0.21f, 0.22f, 1.f};
style.Colors[ImGuiCol_ButtonActive] = {0.24f, 0.23f, 0.28f, 1.f};
style.Colors[ImGuiCol_Border] = {0.23f, 0.23f, 0.24f, 1.f};
style.Colors[ImGuiCol_Separator] = style.Colors[ImGuiCol_Border];
style.Colors[ImGuiCol_Text] = {0.88f, 0.88f, 0.89f, 1.f};
style.Colors[ImGuiCol_TextDisabled] = {0.60f, 0.60f, 0.62f, 1.f};
style.Colors[ImGuiCol_CheckMark] = {0.66f, 0.63f, 0.76f, 1.f};
style.WindowRounding = 5.f;
style.FrameRounding = 4.f;
style.WindowBorderSize = 1.f;
style.ScaleAllSizes(scale);
style.FontScaleMain = scale;
if (state.window_rect[2] == 0)
state.window_rect = {16 * scale, 44 * scale, 420 * scale, 455 * scale};
state.scale = scale;
}
// Complete an ImGui frame while hidden too, so queued input cannot accumulate.
ImGui::NewFrame();
if (state.visible) {
ImGui::SetNextWindowPos({16 * scale, 44 * scale}, ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize({420 * scale,
std::min(455 * scale, std::max(220.f, io.DisplaySize.y - 60.f * scale))},
ImGuiCond_FirstUseEver);
if (ImGui::Begin("Faset diagnostics (F12)", &state.visible,
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse)) {
const auto position = ImGui::GetWindowPos(), size = ImGui::GetWindowSize();
state.window_rect = {position.x, position.y, size.x, size.y};
if (!state.freeze)
state.displayed = renderer.stats();
const auto& stats = state.displayed;
ImGui::TextUnformatted("Visibility");
const auto current_mode = renderer.visibility_mode();
const struct {
const char* label;
render::VisibilityMode value;
} modes[] = {{"Direct", render::VisibilityMode::Direct},
{"GPU frustum", render::VisibilityMode::GpuFrustum},
{"GPU occlusion", render::VisibilityMode::GpuOcclusion}};
const float button_width =
(ImGui::GetContentRegionAvail().x - 2.f * ImGui::GetStyle().ItemSpacing.x) / 3.f;
for (int i = 0; i < 3; ++i) {
if (i)
ImGui::SameLine();
const bool selected = current_mode == modes[i].value;
if (selected)
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{.35f, .33f, .43f, 1.f});
if (ImGui::Button(modes[i].label, {button_width, 0}))
renderer.set_visibility_mode(modes[i].value);
if (selected)
ImGui::PopStyleColor();
}
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();
ImGui::Checkbox("Freeze counters", &state.freeze);
ImGui::TextWrapped("%s", stats.device.c_str());
ImGui::Text("Frame: %llu", static_cast<unsigned long long>(stats.frame));
ImGui::Text("Render call (wall): %.3f ms", stats.cpu_ms);
if (stats.gpu_ms > 0)
ImGui::Text("GPU: %.3f ms", stats.gpu_ms);
else
ImGui::TextUnformatted("GPU timestamps: unavailable");
ImGui::Text("Readback (wall): %.3f ms", stats.readback_cpu_ms);
ImGui::Text("Draws: %u Packed vertices: %u", stats.draw_calls, stats.vertices);
ImGui::Text("Culled meshes: %u Textures: %u", stats.culled_meshes,
stats.texture_count);
ImGui::Separator();
ImGui::TextUnformatted("GPU visibility");
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",
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;
state.hzb_preview.reset();
}
}
if (state.show_hzb && state.visible &&
selected_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;
state.hzb_preview.reset();
}
} catch (const std::exception& error) {
state.hzb_available = false;
state.hzb_preview.reset();
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_preview.reset();
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();
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("Local lights: %u submitted, %u omitted",
stats.submitted_local_lights, stats.omitted_local_lights);
ImGui::Text("Sun cascades: %u / %u effective",
stats.requested_sun_cascades, stats.effective_sun_cascades);
ImGui::Text("Local faces: %u requested, %u rasterized (%u tiles)",
stats.requested_local_shadow_faces, stats.local_shadow_faces,
stats.local_shadow_tiles);
ImGui::Text("Dropped faces: %u (point %u, atlas %u, draw budget %u, unavailable %u)",
stats.dropped_shadow_faces, stats.dropped_point_shadow_faces,
stats.shadow_atlas_full_drops, stats.shadow_caster_budget_drops,
stats.shadow_unavailable_drops);
ImGui::Text("Shadow caster draws: %u / 4096", stats.shadow_caster_draws);
ImGui::Text("Atlas memory: sun %.1f MiB, local %.1f MiB",
double(stats.sun_shadow_atlas_bytes) / 1048576.0,
double(stats.local_shadow_atlas_bytes) / 1048576.0);
if (stats.gpu_ms > 0)
ImGui::Text("Shadow GPU: sun %.2f ms, local %.2f ms",
stats.gpu_sun_shadow_ms, stats.gpu_local_shadow_ms);
ImGui::Separator();
ImGui::Text("Vulkan allocations: %.2f MiB",
double(stats.gpu_allocated_bytes) / 1048576.0);
ImGui::Text("Validation: %s Errors: %u",
stats.validation_enabled ? "on" : "unavailable/off",
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();
}
if (!state.visible) {
state.hzb_available = false;
state.hzb_preview.reset();
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)
return;
for (const auto* list : data->CmdLists) {
for (const auto& command : list->CmdBuffer) {
if (command.UserCallback) {
if (command.UserCallback != ImDrawCallback_ResetRenderState)
command.UserCallback(list, &command);
continue;
}
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;
const float width = command.ClipRect.z - command.ClipRect.x;
const float height = command.ClipRect.w - command.ClipRect.y;
if (width <= 0 || height <= 0)
continue;
render::UiTriangles batch;
batch.texture = texture;
batch.clip_rect = {x, y, width, height};
batch.vertices.reserve(command.ElemCount);
for (unsigned i = 0; i < command.ElemCount; ++i) {
const auto index = list->IdxBuffer[command.IdxOffset + i] + command.VtxOffset;
const auto& vertex = list->VtxBuffer[index];
const auto channel = [&](unsigned shift) {
return float((vertex.col >> shift) & 0xffu) / 255.f;
};
batch.vertices.push_back(
{{vertex.pos.x - data->DisplayPos.x, vertex.pos.y - data->DisplayPos.y, 0},
{},
{channel(IM_COL32_R_SHIFT), channel(IM_COL32_G_SHIFT),
channel(IM_COL32_B_SHIFT), channel(IM_COL32_A_SHIFT)},
{vertex.uv.x, vertex.uv.y}});
}
output.ui_triangles.push_back(std::move(batch));
}
}
}
} // namespace faset::editor