#pragma once
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <vector>
#include "core/globals.h"
#include "core/math_rtm.h"
enum DebugCategory : u8 {
DEBUG_CAT_DEFAULT = 0,
DEBUG_CAT_GRID = 1,
DEBUG_CAT_PROXY = 2,
DEBUG_CAT_SELECTION = 3,
DEBUG_CAT_LIGHT = 4,
DEBUG_CAT_DDGI = 5,
DEBUG_CAT_GIZMO = 6,
};
enum DebugDepthMode : u8 {
DEBUG_DEPTH_TEST = 0,
DEBUG_DEPTH_XRAY = 1,
};
struct DebugLineVertex {
vec3 pos = vec3(0.0f);
u32 packed = 0; // [category:8][depth:8][reserved:16]
vec4 color = vec4(1.0f);
u32 width_bits = 0; // f32 screen-space width in px (thick quad expansion)
u32 _pad1 = 0; // reserved: gizmo id
static u32 pack(u8 category, u8 depth) {
return (u32)category | ((u32)depth << 8u);
}
static u32 width_to_bits(f32 w) {
u32 b = 0;
memcpy(&b, &w, sizeof(b));
return b;
}
f32 width() const {
f32 w = 2.0f;
memcpy(&w, &width_bits, sizeof(w));
return w;
}
u8 category() const {
return (u8)(packed & 0xFFu);
}
u8 depth_mode() const {
return (u8)((packed >> 8u) & 0xFFu);
}
};
#ifndef __SLANG__
static_assert(sizeof(DebugLineVertex) == 40, "DebugLineVertex must be 40B");
static_assert(offsetof(DebugLineVertex, color) == 16, "DebugLineVertex color offset drift");
#endif
constexpr u32 MAX_DEBUG_LINES = 65536; // verts; 64k * 40B ~= 2.6MB upload
inline bool debug_finite_vec3(vec3 v) {
return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z);
}
struct DebugDraw {
std::vector<DebugLineVertex> test_lines; // depth-tested
std::vector<DebugLineVertex> xray_lines; // depth-ALWAYS overlay
bool overflow_warned = false;
bool force_xray = false;
// category bitmask
u32 category_mask = 0xFFFFFFFFu; // not reset by clear()
static u32 category_bit(u8 category) {
return (category < 32u) ? (1u << category) : 0u;
}
void clear() {
test_lines.clear();
xray_lines.clear();
overflow_warned = false;
// NOTE: force_xray/category_mask are set by the frame loop after clear(), not reset here
}
usize total_count() const {
return test_lines.size() + xray_lines.size();
}
void line(
vec3 a, vec3 b, vec4 color, u8 category = DEBUG_CAT_DEFAULT, u8 depth = DEBUG_DEPTH_TEST, f32 width_px = 1.5f
) {
if (!debug_finite_vec3(a) || !debug_finite_vec3(b)) {
return;
}
if (!(category_mask & category_bit(category))) {
return;
}
if (force_xray && depth == DEBUG_DEPTH_TEST) {
depth = DEBUG_DEPTH_XRAY;
}
if (!(width_px > 0.0f) || !std::isfinite(width_px)) {
width_px = 1.5f;
}
if (width_px > 16.0f) {
width_px = 16.0f;
}
std::vector<DebugLineVertex> &dst = (depth == DEBUG_DEPTH_XRAY) ? xray_lines : test_lines;
if (dst.size() + 2 > MAX_DEBUG_LINES) {
return; // truncated; pass logs once per frame (has frame context)
}
u32 packed = DebugLineVertex::pack(category, depth);
u32 wb = DebugLineVertex::width_to_bits(width_px);
dst.push_back({a, packed, color, wb, 0});
dst.push_back({b, packed, color, wb, 0});
}
void cross(
vec3 p, f32 size, vec4 color, u8 category = DEBUG_CAT_DEFAULT, u8 depth = DEBUG_DEPTH_TEST, f32 width_px = 2.0f
) {
f32 h = size * 0.5f;
line(vec3(p.x - h, p.y, p.z), vec3(p.x + h, p.y, p.z), color, category, depth, width_px);
line(vec3(p.x, p.y - h, p.z), vec3(p.x, p.y + h, p.z), color, category, depth, width_px);
line(vec3(p.x, p.y, p.z - h), vec3(p.x, p.y, p.z + h), color, category, depth, width_px);
}
// 8 corners -> 12 edges. The proxy primitive: AABB/OBB/cascade/cluster
// boxes all lower to this. `world` maps unit-box corners into place.
void box_mat(
mat4 world, vec4 color, u8 category = DEBUG_CAT_PROXY, u8 depth = DEBUG_DEPTH_TEST, f32 width_px = 2.0f
) {
auto xform = [](vec3 p, const mat4 &m) {
vec4 h = transform_point(p, m);
return vec3(h.x, h.y, h.z);
};
vec3 c[8] = {
xform(vec3(-0.5f, -0.5f, -0.5f), world),
xform(vec3(0.5f, -0.5f, -0.5f), world),
xform(vec3(0.5f, 0.5f, -0.5f), world),
xform(vec3(-0.5f, 0.5f, -0.5f), world),
xform(vec3(-0.5f, -0.5f, 0.5f), world),
xform(vec3(0.5f, -0.5f, 0.5f), world),
xform(vec3(0.5f, 0.5f, 0.5f), world),
xform(vec3(-0.5f, 0.5f, 0.5f), world),
};
static const u8 edges[12][2] = {
{0, 1}, {1, 2}, {2, 3}, {3, 0}, {4, 5}, {5, 6}, {6, 7}, {7, 4}, {0, 4}, {1, 5}, {2, 6}, {3, 7}
};
for (u32 i = 0; i < 12; ++i) {
line(c[edges[i][0]], c[edges[i][1]], color, category, depth, width_px);
}
}
void aabb(
vec3 mn, vec3 mx, vec4 color, u8 category = DEBUG_CAT_PROXY, u8 depth = DEBUG_DEPTH_TEST, f32 width_px = 2.0f
) {
if (!debug_finite_vec3(mn) || !debug_finite_vec3(mx)) {
return;
}
vec3 center = (mn + mx) * 0.5f;
vec3 extent = vec3(mx.x - mn.x, mx.y - mn.y, mx.z - mn.z);
if (extent.x <= 0.0f || extent.y <= 0.0f || extent.z <= 0.0f) {
return;
}
mat4 world = scale(extent) * translate(center);
box_mat(world, color, category, depth, width_px);
}
// 3 great circles, CPU-tessellated. `segments` is per-circle.
void sphere(
vec3 center,
f32 radius,
vec4 color,
u32 segments = 24,
u8 category = DEBUG_CAT_PROXY,
u8 depth = DEBUG_DEPTH_TEST,
f32 width_px = 2.0f
) {
if (!debug_finite_vec3(center) || !std::isfinite(radius) || radius <= 0.0f) {
return;
}
segments = segments < 8 ? 8 : (segments > 64 ? 64 : segments);
const f32 PI = 3.14159265358979323846f;
for (u32 axis = 0; axis < 3; ++axis) {
vec3 prev{};
for (u32 i = 0; i <= segments; ++i) {
f32 t = (f32)i / (f32)segments * 2.0f * PI;
vec3 p{};
if (axis == 0) {
p = vec3(center.x, center.y + cosf(t) * radius, center.z + sinf(t) * radius);
} else if (axis == 1) {
p = vec3(center.x + cosf(t) * radius, center.y, center.z + sinf(t) * radius);
} else {
p = vec3(center.x + cosf(t) * radius, center.y + sinf(t) * radius, center.z);
}
if (i > 0) {
line(prev, p, color, category, depth, width_px);
}
prev = p;
}
}
}
// screen-constant-size helper for future gizmo handles: world size for
// `pixels` at `dist` given vertical fov. Gizmo handles should call this
// so they don't shrink with distance (unlike fixed world sizes)
static f32 pixels_to_world(f32 dist, f32 pixels, f32 fov_y_deg, f32 viewport_h) {
if (viewport_h <= 0.0f || dist <= 0.0f) {
return 0.0f;
}
const f32 PI = 3.14159265358979323846f;
f32 fov_rad = fov_y_deg * PI / 180.0f;
return 2.0f * dist * tanf(fov_rad * 0.5f) * (pixels / viewport_h);
}
};