#ifdef ENABLE_EDITOR
#define IMGUI_DEFINE_MATH_OPERATORS
#include "editor.h"
#include <imgui.h>
#include <imgui_internal.h>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
#include "backend/imgui_backend.h"
#include "quick_submit.h"
namespace editor {
enum class Channel : u8 {
RGB,
R,
G,
B,
A,
};
constexpr const char *CHANNEL_NAMES[] = {"RGB", "R", "G", "B", "A"};
struct InspectorState {
ImGuiID id;
const rhi::Image *image = nullptr;
f32 scale = 1.0f;
u64 desc = UINT64_MAX;
bool y_flip = false;
u32 mip_level = 0;
u32 array_layer = 0;
rhi::ImageView custom_view;
bool owns_view = false;
const rhi::Image *view_image = nullptr;
u32 view_mip = 0;
u32 view_layer = 0;
Channel view_channel = Channel::RGB;
Channel channel = Channel::RGB;
bool show_checker = false;
bool show_grid = false;
bool reset_scroll = false;
// Hover readback cache
const rhi::Image *read_image = nullptr;
i64 read_tx = -1;
i64 read_ty = -1;
u32 read_mip = 0;
u32 read_layer = 0;
f32 read_uv_x = 0.0f;
f32 read_uv_y = 0.0f;
bool read_valid = false;
char hover_text[256];
};
std::vector<InspectorState> INSPECTORS;
InspectorState *find_or_create(ImGuiID id) {
for (auto &insp : INSPECTORS) {
if (insp.id == id) {
return &insp;
}
}
INSPECTORS.push_back({});
INSPECTORS.back().id = id;
return &INSPECTORS.back();
}
struct ReadbackScratch {
rhi::Device *device = nullptr;
rhi::Buffer stage{};
};
ReadbackScratch RB;
void ensure_scratch(rhi::Device &device) {
if (RB.device == &device) {
return;
}
RB.device = &device;
rhi::BufferDesc bd{};
bd.size = 256;
bd.usage = rhi::BufferUsage::CopyDst;
bd.memory = rhi::BufferMemType::Readback;
bd.name = "rb/stage";
rhi::create_buffer(device, bd, RB.stage);
}
void destroy_scratch(rhi::Device &device) {
if (RB.device == nullptr) {
return;
}
rhi::destroy_buffer(device, RB.stage);
RB = {};
}
rhi::Image CHECKER_IMG;
rhi::ImageView CHECKER_VIEW;
u64 CHECKER_DESC = UINT64_MAX;
bool create_checkerboard(rhi::Device &device, QuickSubmit &qs, const rhi::Sampler *sampler) {
if (CHECKER_DESC != UINT64_MAX || sampler == nullptr) {
return CHECKER_DESC != UINT64_MAX;
}
ensure_scratch(device);
constexpr u32 checker_size = 16;
constexpr u32 checker_cell = 8;
std::vector<u8> px(checker_size * checker_size * 4);
for (u32 y = 0; y < checker_size; ++y) {
for (u32 x = 0; x < checker_size; ++x) {
bool light = (((x / checker_cell) + (y / checker_cell)) & 1) == 0;
u8 c = light ? 130 : 85;
u32 i = (y * checker_size + x) * 4;
px[i + 0] = c;
px[i + 1] = c;
px[i + 2] = c;
px[i + 3] = 255;
}
}
rhi::ImageDesc img_desc{};
img_desc.width = checker_size;
img_desc.height = checker_size;
img_desc.format = rhi::ImageFormat::RGBA8_UNORM;
img_desc.usage = rhi::ImageUsage::Sampled | rhi::ImageUsage::TransferDst;
img_desc.name = "tex/checker";
if (!rhi::create_image(device, img_desc, CHECKER_IMG)) {
return false;
}
rhi::ImageViewDesc view_desc{};
view_desc.image = &CHECKER_IMG;
if (!rhi::create_image_view(device, view_desc, CHECKER_VIEW)) {
rhi::destroy_image(device, CHECKER_IMG);
return false;
}
rhi::BufferDesc bd{};
bd.size = px.size();
bd.usage = rhi::BufferUsage::CopySrc;
bd.memory = rhi::BufferMemType::Upload;
bd.name = "rb/checker-stage";
rhi::Buffer stage{};
if (!rhi::create_buffer(device, bd, stage)) {
rhi::destroy_image_view(device, CHECKER_VIEW);
rhi::destroy_image(device, CHECKER_IMG);
return false;
}
std::memcpy(stage.mapped, px.data(), px.size());
rhi::buffer_flush(device, stage, 0, px.size());
QsCmd *qcmd = qs_acquire(qs);
{
rhi::ImageRange range{};
rhi::barrier(qcmd->cmd, CHECKER_IMG, range, CHECKER_IMG.state, rhi::ResourceState::TransferTo);
}
rhi::BufferTextureCopyRegion region{};
region.buffer_offset = 0;
region.texture_extent_width = checker_size;
region.texture_extent_height = checker_size;
region.texture_extent_depth = 1;
region.base_array_layer = 0;
region.mip_level = 0;
region.layer_count = 1;
rhi::copy_buffer_to_texture(qcmd->cmd, stage, CHECKER_VIEW, region);
{
rhi::ImageRange range{};
rhi::barrier(qcmd->cmd, CHECKER_IMG, range, rhi::ResourceState::TransferTo, rhi::ResourceState::TextureSample);
}
qs_submit_and_wait(qs, *qcmd);
rhi::destroy_buffer(device, stage);
CHECKER_DESC = imgui_backend::texture(CHECKER_VIEW);
return CHECKER_DESC != UINT64_MAX;
}
void destroy_checkerboard(rhi::Device &device) {
CHECKER_DESC = UINT64_MAX;
if (rhi::valid(CHECKER_VIEW)) {
rhi::destroy_image_view(device, CHECKER_VIEW);
}
if (rhi::valid(CHECKER_IMG)) {
rhi::destroy_image(device, CHECKER_IMG);
}
}
bool format_readback_supported(rhi::ImageFormat fmt) {
switch (fmt) {
case rhi::ImageFormat::R8_UNORM:
case rhi::ImageFormat::RGBA8_UNORM:
case rhi::ImageFormat::RGBA8_SRGB:
case rhi::ImageFormat::BGRA8_UNORM:
case rhi::ImageFormat::R10G10B10A2_UNORM:
case rhi::ImageFormat::RGBA16_FLOAT:
case rhi::ImageFormat::RG16_FLOAT:
case rhi::ImageFormat::R16_FLOAT:
case rhi::ImageFormat::R32_FLOAT:
case rhi::ImageFormat::R32_UINT:
case rhi::ImageFormat::RGBA32_FLOAT:
case rhi::ImageFormat::D32_FLOAT:
return true;
default:
return false;
}
}
static f32 half_to_float(u16 h) {
u32 sign = (u32)((h >> 15) & 1);
u32 exp = (u32)((h >> 10) & 0x1f);
u32 man = (u32)(h & 0x3ff);
u32 f;
if (exp == 0) {
if (man == 0) {
f = sign << 31;
} else {
exp = 127 - 15 + 1;
while ((man & 0x400) == 0) {
man <<= 1;
exp--;
}
man &= 0x3ff;
f = (sign << 31) | (exp << 23) | (man << 13);
}
} else if (exp == 31) {
f = (sign << 31) | 0x7f800000u | (man << 13);
} else {
f = (sign << 31) | ((exp - 15 + 127) << 23) | (man << 13);
}
f32 out;
std::memcpy(&out, &f, sizeof(f32));
return out;
}
void decode_texel(rhi::ImageFormat fmt, const void *data, char *out, usize out_size) {
switch (fmt) {
case rhi::ImageFormat::R8_UNORM: {
u8 v = ((const u8 *)data)[0];
(void)std::snprintf(out, out_size, "R: %u (%.3f)", v, v / 255.0f);
break;
}
case rhi::ImageFormat::RGBA8_UNORM:
case rhi::ImageFormat::RGBA8_SRGB: {
const u8 *p = (const u8 *)data;
(void)std::snprintf(out, out_size, "R: %u G: %u B: %u A: %u", p[0], p[1], p[2], p[3]);
break;
}
case rhi::ImageFormat::BGRA8_UNORM: {
const u8 *p = (const u8 *)data;
(void)std::snprintf(out, out_size, "B: %u G: %u R: %u A: %u", p[0], p[1], p[2], p[3]);
break;
}
case rhi::ImageFormat::R10G10B10A2_UNORM: {
u32 p = *(const u32 *)data;
(void)std::snprintf(
out,
out_size,
"R: %.3f G: %.3f B: %.3f A: %.3f",
(f32)(p & 0x3ff) / 1023.0f,
(f32)((p >> 10) & 0x3ff) / 1023.0f,
(f32)((p >> 20) & 0x3ff) / 1023.0f,
(f32)((p >> 30) & 0x3) / 3.0f
);
break;
}
case rhi::ImageFormat::RGBA16_FLOAT: {
const u16 *p = (const u16 *)data;
(void)std::snprintf(
out,
out_size,
"R: %.4g G: %.4g B: %.4g A: %.4g",
half_to_float(p[0]),
half_to_float(p[1]),
half_to_float(p[2]),
half_to_float(p[3])
);
break;
}
case rhi::ImageFormat::RG16_FLOAT: {
const u16 *p = (const u16 *)data;
(void)std::snprintf(out, out_size, "R: %.4g G: %.4g", half_to_float(p[0]), half_to_float(p[1]));
break;
}
case rhi::ImageFormat::R16_FLOAT: {
(void)std::snprintf(out, out_size, "R: %.4g", half_to_float(*(const u16 *)data));
break;
}
case rhi::ImageFormat::R32_FLOAT:
case rhi::ImageFormat::D32_FLOAT: {
(void)std::snprintf(out, out_size, "V: %.6g", *(const f32 *)data);
break;
}
case rhi::ImageFormat::R32_UINT: {
(void)std::snprintf(out, out_size, "ID: %u", *(const u32 *)data);
break;
}
case rhi::ImageFormat::RGBA32_FLOAT: {
const f32 *p = (const f32 *)data;
(void)std::snprintf(out, out_size, "R: %.4g G: %.4g B: %.4g A: %.4g", p[0], p[1], p[2], p[3]);
break;
}
default:
(void)std::snprintf(out, out_size, "readback unsupported");
break;
}
}
bool readback_texel_raw(
rhi::Device &device,
QuickSubmit &qs,
rhi::ImageView &view,
u32 mip,
u32 layer,
i64 tx,
i64 ty,
void *out,
usize out_size
) {
if (out == nullptr || out_size == 0) {
return false;
}
rhi::Image &img = *view.desc.image;
// multisampling, cannot be copied directly
if (img.desc.sample_count != rhi::SampleCount::Sample1) {
return false;
}
ensure_scratch(device);
rhi::ResourceState saved = img.state;
rhi::ImageRange vr{};
vr.mip = view.desc.mip_start;
vr.mip_count = view.desc.mip_count;
vr.layer = view.desc.layer_start;
vr.layer_count = view.desc.layer_count;
vr.aspect = view.desc.aspect;
QsCmd *qcmd = qs_acquire(qs);
rhi::barrier(qcmd->cmd, img, vr, saved, rhi::ResourceState::TransferFrom);
rhi::BufferTextureCopyRegion region{};
region.buffer_offset = 0;
region.mip_level = mip;
region.base_array_layer = layer;
region.layer_count = 1;
region.texture_offset_x = (u32)tx;
region.texture_offset_y = (u32)ty;
region.texture_offset_z = 0;
region.texture_extent_width = 1;
region.texture_extent_height = 1;
region.texture_extent_depth = 1;
rhi::copy_image_to_buffer(qcmd->cmd, view, RB.stage, region);
// restore the subrange to its exact prior state so the framegraph's
// tracked state stays consistent with the GPU. img.state was untouched
// (sub-range barriers never write it), so this is TransferFrom -> saved.
rhi::barrier(qcmd->cmd, img, vr, rhi::ResourceState::TransferFrom, saved);
qs_submit_and_wait(qs, *qcmd);
if (RB.stage.mapped == nullptr) {
return false;
}
rhi::buffer_invalidate(device, RB.stage, 0, out_size);
std::memcpy(out, RB.stage.mapped, out_size);
return true;
}
bool readback_texel(
rhi::Device &device,
QuickSubmit &qs,
rhi::ImageView &view,
u32 mip,
u32 layer,
i64 tx,
i64 ty,
char *out,
usize out_size
) {
if (!format_readback_supported(view.desc.image->desc.format)) {
(void)std::snprintf(out, out_size, "readback unsupported for this format");
return false;
}
u8 raw[16];
if (!readback_texel_raw(device, qs, view, mip, layer, tx, ty, raw, sizeof(raw))) {
(void)std::snprintf(out, out_size, "readback failed");
return false;
}
decode_texel(view.desc.image->desc.format, raw, out, out_size);
return true;
}
void build_swizzle(
Channel channel,
rhi::ImageFormat fmt,
rhi::ComponentSwizzle &sr,
rhi::ComponentSwizzle &sg,
rhi::ComponentSwizzle &sb,
rhi::ComponentSwizzle &sa
) {
sr = rhi::ComponentSwizzle::Identity;
sg = rhi::ComponentSwizzle::Identity;
sb = rhi::ComponentSwizzle::Identity;
sa = rhi::ComponentSwizzle::Identity;
if (fmt == rhi::ImageFormat::D32_FLOAT) {
// depth views restrict swizzle: G/B/A may only be R/IDENTITY/ZERO.
// Grayscale display: depth -> RGB, alpha stays Identity (sampled as 1).
if (channel == Channel::R) {
sr = sg = sb = rhi::ComponentSwizzle::R;
sa = rhi::ComponentSwizzle::Identity;
}
return;
}
switch (channel) {
case Channel::R:
sr = sg = sb = rhi::ComponentSwizzle::R;
sa = rhi::ComponentSwizzle::One;
break;
case Channel::G:
sr = sg = sb = rhi::ComponentSwizzle::G;
sa = rhi::ComponentSwizzle::One;
break;
case Channel::B:
sr = sg = sb = rhi::ComponentSwizzle::B;
sa = rhi::ComponentSwizzle::One;
break;
case Channel::A:
sr = sg = sb = rhi::ComponentSwizzle::A;
sa = rhi::ComponentSwizzle::One;
break;
default:
break;
}
}
bool draw_texture_inspector(rhi::Device &device, QuickSubmit &qs, const char *id, const TextureViewerDesc &desc) {
ImGuiID imgui_id = ImGui::GetID(id);
const ImGuiIO &io = ImGui::GetIO();
InspectorState *state = find_or_create(imgui_id);
if (!desc.image || !rhi::valid(*desc.image)) {
if (state->owns_view) {
rhi::destroy_image_view(device, state->custom_view);
state->owns_view = false;
}
ImGui::TextUnformatted("No texture");
return false;
}
if (desc.image != state->image) {
state->image = desc.image;
state->scale = 1.0f;
state->y_flip = false;
state->mip_level = 0;
state->array_layer = 0;
state->channel = Channel::RGB;
// Auto-fit the newly opened texture.
f32 w = (f32)ImMax(desc.image->desc.width, 1u);
f32 h = (f32)ImMax(desc.image->desc.height, 1u);
ImVec2 a = ImGui::GetContentRegionAvail();
a.y -= 60;
state->scale = ImMax(ImMin(a.x / w, a.y / h), 0.0625f);
state->read_valid = false;
}
// Depth sources display as grayscale; the channel selector is disabled.
bool is_depth = desc.image->desc.format == rhi::ImageFormat::D32_FLOAT;
if (is_depth) {
state->channel = Channel::R;
}
u32 max_mip = (desc.image->desc.mip_levels > 1) ? desc.image->desc.mip_levels - 1 : 0;
u32 max_layer = (desc.image->desc.layers > 1) ? desc.image->desc.layers - 1 : 0;
if (state->mip_level > max_mip) {
state->mip_level = max_mip;
}
if (state->array_layer > max_layer) {
state->array_layer = max_layer;
}
// Rebuild the owned view only when its parameters actually change.
const bool want_custom =
is_depth || state->mip_level > 0 || state->array_layer > 0 || state->channel != Channel::RGB;
const bool view_changed = state->view_image != desc.image || state->view_mip != state->mip_level ||
state->view_layer != state->array_layer || state->view_channel != state->channel;
if (state->owns_view && view_changed) {
rhi::destroy_image_view(device, state->custom_view);
state->owns_view = false;
}
if (!state->owns_view) {
rhi::ImageAspect aspect =
desc.image->desc.format == rhi::ImageFormat::D32_FLOAT ? rhi::ImageAspect::Depth : rhi::ImageAspect::Color;
rhi::ImageViewDesc vd{};
vd.image = desc.image;
vd.aspect = aspect;
vd.dimension = rhi::TextureViewDimension::TEXTURE_2D;
vd.mip_start = want_custom ? state->mip_level : 0;
vd.mip_count = 1;
vd.layer_start = want_custom ? state->array_layer : 0;
vd.layer_count = 1;
if (want_custom) {
build_swizzle(
state->channel, desc.image->desc.format, vd.swizzle_r, vd.swizzle_g, vd.swizzle_b, vd.swizzle_a
);
}
state->owns_view = rhi::create_image_view(device, vd, state->custom_view);
if (!state->owns_view) {
return false;
}
}
rhi::ImageView &view = state->custom_view;
state->view_image = desc.image;
state->view_mip = state->mip_level;
state->view_layer = state->array_layer;
state->view_channel = state->channel;
state->desc = imgui_backend::texture(view);
if (state->desc == UINT64_MAX) {
return false;
}
f32 &scale = state->scale;
f32 tex_w = (f32)ImMax(desc.image->desc.width >> state->mip_level, 1u);
f32 tex_h = (f32)ImMax(desc.image->desc.height >> state->mip_level, 1u);
ImVec2 tex_size = ImVec2(tex_w, tex_h);
if (desc.name) {
ImGui::Text("%s", desc.name);
if (desc.format_name) {
ImGui::SameLine();
ImGui::Text(" %s", desc.format_name);
}
ImGui::SameLine();
ImGui::Text(" %ux%u", desc.image->desc.width, desc.image->desc.height);
}
// Row 1: view controls
if (ImGui::Button("1:1")) {
scale = 1.0f;
state->reset_scroll = true;
}
ImGui::SameLine();
if (ImGui::Button("Fit")) {
ImVec2 a = ImGui::GetContentRegionAvail();
a.y -= 60;
scale = ImMax(ImMin(a.x / tex_size.x, a.y / tex_size.y), 0.0625f);
state->reset_scroll = true;
}
ImGui::SameLine();
ImGui::SetNextItemWidth(100);
ImGui::SliderFloat("##zoom", &scale, 0.0625f, 64.0f, "%.2fx");
if (max_mip > 0) {
ImGui::SameLine();
ImGui::SetNextItemWidth(80);
int m = (int)state->mip_level;
if (ImGui::SliderInt("##mip", &m, 0, (int)max_mip, "Mip %d")) {
state->mip_level = (u32)m;
}
}
if (max_layer > 0) {
ImGui::SameLine();
ImGui::SetNextItemWidth(80);
int l = (int)state->array_layer;
if (ImGui::SliderInt("##layer", &l, 0, (int)max_layer, "Lyr %d")) {
state->array_layer = (u32)l;
}
}
ImGui::SameLine();
if (ImGui::Button(state->y_flip ? "Y-Flip ON" : "Y-Flip OFF")) {
state->y_flip = !state->y_flip;
}
// Row 2: display controls
if (is_depth) {
ImGui::TextUnformatted("Depth");
} else {
ImGui::SetNextItemWidth(70);
int ch = (int)state->channel;
if (ImGui::Combo("##channel", &ch, CHANNEL_NAMES, IM_ARRAYSIZE(CHANNEL_NAMES))) {
state->channel = (Channel)ch;
}
}
ImGui::SameLine();
ImGui::Checkbox("Checker", &state->show_checker);
ImGui::SameLine();
ImGui::Checkbox("Grid", &state->show_grid);
ImGui::BeginChild("##img", ImVec2(0, 0), 0, ImGuiWindowFlags_HorizontalScrollbar);
if (state->reset_scroll) {
ImGui::SetScrollX(0.0f);
ImGui::SetScrollY(0.0f);
state->reset_scroll = false;
}
ImVec2 img_size = tex_size * scale;
ImVec2 cursor_tl = ImGui::GetCursorScreenPos();
ImVec2 uv0(0, state->y_flip ? 1 : 0), uv1(1, state->y_flip ? 0 : 1);
ImDrawList *dl = ImGui::GetWindowDrawList();
if (state->show_checker) {
if (create_checkerboard(device, qs, desc.sampler)) {
dl->AddImage((ImTextureID)(u64)CHECKER_DESC, cursor_tl, cursor_tl + img_size, uv0, uv1);
}
}
ImGui::Image((ImTextureID)(u64)state->desc, img_size, uv0, uv1);
bool hovered = ImGui::IsItemHovered();
if (state->show_grid && scale > 4.0f) {
f32 a = ImClamp((scale - 4.0f) / 20.0f, 0.0f, 1.0f);
ImU32 col = IM_COL32(255, 255, 255, (int)(255.0f * (0.2f + 0.4f * a)));
ImVec2 br = cursor_tl + img_size;
for (int x = 1; x < (int)tex_w; ++x) {
float sx = ImFloor(cursor_tl.x + x * scale) + 0.5f;
dl->AddLine(ImVec2(sx, cursor_tl.y), ImVec2(sx, br.y), col);
}
for (int y = 1; y < (int)tex_h; ++y) {
float sy = ImFloor(cursor_tl.y + y * scale) + 0.5f;
dl->AddLine(ImVec2(cursor_tl.x, sy), ImVec2(br.x, sy), col);
}
dl->AddRect(cursor_tl, br, col);
}
if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
ImVec2 a = ImGui::GetContentRegionAvail();
a.y -= 60;
scale = ImMax(ImMin(a.x / tex_size.x, a.y / tex_size.y), 0.0625f);
state->reset_scroll = true;
}
if (hovered && io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_0)) {
scale = 1.0f;
state->reset_scroll = true;
}
if (hovered && io.MouseWheel != 0.0f) {
f32 os = scale;
f32 f = (io.MouseWheel > 0) ? 1.3f : (1 / 1.3f);
f32 ns = ImClamp(os * f, 0.0625f, 64.0f);
if (ns != os) {
ImVec2 mu = (io.MousePos - cursor_tl) / (tex_size * os);
scale = ns;
ImVec2 nm = mu * tex_size * scale;
ImGui::SetScrollX(ImGui::GetScrollX() + nm.x - (io.MousePos.x - cursor_tl.x));
ImGui::SetScrollY(ImGui::GetScrollY() + nm.y - (io.MousePos.y - cursor_tl.y));
}
}
if (hovered) {
ImVec2 mf = (io.MousePos - cursor_tl) / img_size;
f32 vf = state->y_flip ? 1.0f - mf.y : mf.y;
i64 tx = (i64)ImClamp(ImFloor(mf.x * tex_w), 0.0f, tex_w - 1.0f);
i64 ty = (i64)ImClamp(ImFloor(vf * tex_h), 0.0f, tex_h - 1.0f);
state->read_uv_x = mf.x;
state->read_uv_y = vf;
state->read_valid = true;
if (state->read_image != desc.image || state->read_tx != tx || state->read_ty != ty ||
state->read_mip != state->mip_level || state->read_layer != state->array_layer) {
state->read_image = desc.image;
state->read_tx = tx;
state->read_ty = ty;
state->read_mip = state->mip_level;
state->read_layer = state->array_layer;
readback_texel(
device,
qs,
view,
state->mip_level,
state->array_layer,
tx,
ty,
state->hover_text,
sizeof(state->hover_text)
);
}
ImGui::BeginTooltip();
ImGui::Text(
"UV: (%.4f, %.4f) Texel: (%lld, %lld) Mip: %u Layer: %u",
mf.x,
vf,
tx,
ty,
state->mip_level,
state->array_layer
);
ImGui::TextUnformatted(state->hover_text);
ImGui::EndTooltip();
}
ImGui::EndChild();
return true;
}
void shutdown_texture_inspectors(rhi::Device &device) {
for (auto &insp : INSPECTORS) {
if (insp.owns_view) {
rhi::destroy_image_view(device, insp.custom_view);
}
insp.desc = UINT64_MAX;
}
INSPECTORS.clear();
destroy_checkerboard(device);
destroy_scratch(device);
}
} // namespace editor
#endif // ENABLE_EDITOR