debug_draw.h

cross platform rendering playground

src/passes/debug/debug_draw.h

7.28 KB
#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);
    }
};