renderer.cpp

cross platform rendering playground

src/renderer.cpp

54.27 KB
#include "renderer.h"

#include <cmath>
#include <cstdlib>
#include <vector>

#include "passes/cluster/cluster_bounds.h"
#include "passes/cluster/light_cull.h"
#include "passes/ddgi/ddgi_classify.h"
#include "passes/ddgi/ddgi_probes_dbg.h"
#include "passes/ddgi/ddgi_relocate.h"
#include "passes/ddgi/ddgi_trace.h"
#include "passes/ddgi/ddgi_update.h"
#include "passes/debug/debug_lines.h"
#include "passes/depth_pre/depth_normal.h"
#include "passes/dispatch_mdi/build_indirect.h"
#include "passes/fg.h"
#include "passes/fg_utils.h"
#include "passes/forward/forward.h"
#include "passes/fullscreen/fullscreen.h"
#include "passes/grid/grid.h"
#include "passes/hiz/hiz.h"
#include "passes/id_blit/id_blit.h"
#include "passes/rt_build.h"
#include "passes/shadow/shadow.h"
#include "passes/skybox/skybox.h"
#include "passes/ssr/ssr.h"
#include "shaders/renderer_types.h"

#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

#include "asset/asset.h"
#include "asset/texture.h"
#include "core/globals.h"
#include "core/logger.h"
#include "core/math_rtm.h"
#include "core/platform.h"
#include "core/profile.h"

#ifdef ENABLE_EDITOR
#include "editor/editor.h"
#endif

#include "pipelines.h"
#include "quick_submit.h"
#include "scene/camera.h"
#include "scene/ibl.h"
#include "scene/scene.h"
#include "shaders/renderer_types.h"

struct DefaultTexture {
    rhi::Image image;
    rhi::ImageView view{};
    u32 slot = UINT32_MAX;
};

struct DefaultTextures {
    DefaultTexture white_srgb;
    DefaultTexture white_linear;
    DefaultTexture black_srgb;
    DefaultTexture flat_normal;
    DefaultTexture error;
};

static void build_ubo(Scene &scene, RenderData &rd, rhi::Device &device, FrameData &frame, rhi::Extent2D &extent) {
    FrameUBO out{};

    Camera camera = scene.cameras[scene.active_camera];

    out.view = camera.view;
    out.proj = camera.proj;
    out.view_proj = camera.view_proj;
    out.inv_view = camera.inv_view;
    out.inv_proj = camera.inv_proj;
    out.inv_view_proj = camera.inv_view_proj;
    out.position_ws = vec4(camera.position.x, camera.position.y, camera.position.z, 1.0);
    out.forward_ws = camera.forward();
    out.near_plane = camera.near_plane;
    out.far_plane = camera.far_plane;

    out.render_items = rhi::buffer_device_address(device, rd.render_item_buffer);
    out.submeshes = rhi::buffer_device_address(device, rd.submesh_buffer);
    out.transforms = rhi::buffer_device_address(device, frame.transform_buffer);

    out.screen_w = extent.width;
    out.screen_h = extent.height;

    out.materials = rhi::buffer_device_address(device, rd.material_buffer);

    Light light = scene.directional_lights[0];
    out.light_dir = light.direction;
    out.light_color = light.color;
    out.light_intensity = light.intensity;

    out.env = scene.env.environmentIdx;
    out.env_brdf = scene.env.brdfLutIdx;
    out.env_irradiance = scene.env.irradianceIdx;
    out.env_prefiltered = scene.env.prefilteredIdx;
    out.env_prefilter_mip = scene.env.prefilter_mip_count;

    out.main_light_view_proj = light.view_proj;

    out.point_lights = rhi::buffer_device_address(device, frame.point_light_buffer);
    out.point_light_count = (u32)scene.point_lights.size();
    out.tlas_address =
        rhi::valid(rd.rt_scene.tlas) ? rhi::get_acceleration_structure_device_address(device, rd.rt_scene.tlas) : 0;
    out.debug = rd.debug;

    out.aniso_wrap_mips = static_cast<u32>(rd.aniso_wrap_mips);
    out.linear_clamp_mips = static_cast<u32>(rd.linear_clamp_mips);
    out.linear_clamp = static_cast<u32>(rd.linear_clamp);
    out.cmp_greater = static_cast<u32>(rd.cmp_greater);

    out.ddgi_grid = rd.ddgi_grid;
    out.ddgi_cfg = rd.ddgi_cfg;

    memcpy(frame.frame_ubo.mapped, &out, sizeof(out));
}

static void create_gpu_buffer_and_upload(
    rhi::Device &device,
    Arena &staging,
    rhi::Buffer &out_buffer,
    const void *data,
    usize element_count,
    usize element_stride,
    QsCmd &cmd,
    rhi::BufferUsage usage = rhi::BufferUsage::StorageBuffer,
    const char *name = nullptr
) {
    const usize total_size = element_count * element_stride;

    rhi::BufferDesc info = {};
    info.usage = rhi::BufferUsage::CopyDst | rhi::BufferUsage::ShaderAddr | usage;
    info.size = total_size;
    if (name != nullptr) {
        info.name = name;
    }
    rhi::create_buffer(device, info, out_buffer);

    u64 offset = stage_to_arena(staging, data, total_size);
    if (offset != UINT64_MAX) {
        rhi::buffer_flush(device, staging.buffer, offset, total_size);
        rhi::copy_buffer_to_device(device, staging.buffer, offset, out_buffer, 0, total_size, cmd.cmd);
    }
}

static void create_frame_data(rhi::Device &device, FrameData *frames, u32 max_transform_count) {
    constexpr u32 runtime_transform_headroom = 4096;

    u64 ubo_align = device.min_ubo_alignment;
    if (ubo_align < 16) {
        ubo_align = 16;
    }

    for (u32 i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
        FrameData &frame = frames[i];
        {
            // FrameUBO
            rhi::BufferDesc frame_ubo_desc{};
            frame_ubo_desc.size = 64 * 1024; //  2 * 1024 * 1024;
            frame_ubo_desc.usage =
                rhi::BufferUsage::CopySrc | rhi::BufferUsage::CopyDst | rhi::BufferUsage::UniformBuffer;
            frame_ubo_desc.memory = rhi::BufferMemType::Upload;
            frame_ubo_desc.name = ("Frame UBO [" + std::to_string(i) + "]").c_str();
            rhi::create_buffer(device, frame_ubo_desc, frame.frame_ubo);
        }
        {
            // point lights
            rhi::BufferDesc pl_buffer_desc{};
            pl_buffer_desc.size = 64 * 1024; // 2 * 1024 * 1024;
            pl_buffer_desc.usage = rhi::BufferUsage::CopySrc | rhi::BufferUsage::CopyDst |
                                   rhi::BufferUsage::StorageBuffer | rhi::BufferUsage::ShaderAddr;
            pl_buffer_desc.memory = rhi::BufferMemType::Upload;
            pl_buffer_desc.name = ("Point Light Buffer [" + std::to_string(i) + "]").c_str();
            rhi::create_buffer(device, pl_buffer_desc, frame.point_light_buffer);
        }
        {
            frame.transform_capacity = max_transform_count + runtime_transform_headroom;

            rhi::BufferDesc xform_info{};
            xform_info.size = frame.transform_capacity * sizeof(TransformGPU);
            xform_info.usage = rhi::BufferUsage::StorageBuffer | rhi::BufferUsage::ShaderAddr;
            xform_info.memory = rhi::BufferMemType::Upload;
            xform_info.name = "Transform Buffer [" + std::to_string(i) + "]";
            rhi::create_buffer(device, xform_info, frame.transform_buffer);
        }
    }
}

static void upload_transforms(Scene &scene, FrameData &frame) {
    TransformGPU *dst = (TransformGPU *)frame.transform_buffer.mapped;
    const u32 capacity = frame.transform_capacity;
    SceneGraph &sg = scene.scene_graph;

    for (u32 n : scene.mesh_nodes) {
        const u32 base = sg.instance_base[n];
        const u32 ic = sg.instance_count[n];

        if (ic > 0) {
            for (u32 k = 0; k < ic; ++k) {
                u32 slot = base + k;
                if (slot >= capacity) {
                    break;
                }

                // instance TRS under the node's own locals under the parent world.
                mat4 instance_world = scene.instance_world(n, k);
                dst[slot].active = 1;
                dst[slot].world = instance_world;
                dst[slot].normal_world = inverse(instance_world);
            }
        } else {
            if (base >= capacity) {
                continue;
            }
            dst[base].active = sg.is_node_alive(n) ? 1 : 0;
            dst[base].world = sg.worlds[n];
            dst[base].normal_world = inverse(sg.worlds[n]);
        }
    }
}

static void upload_scene_data(Scene &scene, FrameData &frame) {
    upload_transforms(scene, frame);

    u32 pl_count = (u32)std::min(scene.point_lights.size(), (usize)MAX_POINT_LIGHTS);
    PointLightGPU *pl_dst = (PointLightGPU *)frame.point_light_buffer.mapped;
    for (u32 i = 0; i < pl_count; ++i) {
        pl_dst[i].position = scene.point_lights[i].position;
        pl_dst[i].range = scene.point_lights[i].range;
        pl_dst[i].color = scene.point_lights[i].color;
        pl_dst[i].intensity = scene.point_lights[i].intensity;
    }
}

static void create_render_data(
    rhi::Device &device,
    QuickSubmit &qs,
    AssetManager &assets,
    const Scene &scene,
    RenderData &out,
    DefaultTextures &default_textures,
    FrameGraph &fg
) {
    const u32 asset_count = assets.count();

    std::vector<u32> submesh_offset(asset_count, 0);
    std::vector<u32> material_offset(asset_count, 0);
    u32 total_submeshes = 0;
    u32 total_materials = 0;
    for (u32 ai = 0; ai < asset_count; ++ai) {
        const AssetScene *in = assets.get({ai});
        submesh_offset[ai] = total_submeshes;
        material_offset[ai] = total_materials;
        total_submeshes += (u32)in->submeshes.size();
        total_materials += (u32)in->materials.size();
    }

    out.submeshes.resize(total_submeshes);

    for (u32 ai = 0; ai < asset_count; ++ai) {
        const AssetScene &in = *assets.get({ai});
        for (usize i = 0; i < in.submeshes.size(); ++i) {
            const AssetSubmesh &src = in.submeshes[i];
            const AssetGeometry &geom = in.geometries[src.geometry];

            SubmeshGPU &dst = out.submeshes[submesh_offset[ai] + i];
            dst.first_index = (u32)out.indices.size();
            dst.index_count = geom.index_count;
            dst.base_vertex = (u32)out.vertices.size();
            dst.local_aabb_min = src.aabb_min;
            dst.local_aabb_max = src.aabb_max;
            dst.local_sphere = src.sphere;

            const Vertex *vsrc = in.vertices.data() + geom.first_vertex;
            out.vertices.insert(out.vertices.end(), vsrc, vsrc + geom.vertex_count);

            const u32 *isrc = in.indices.data() + geom.first_index;
            out.indices.insert(out.indices.end(), isrc, isrc + geom.index_count);
        }
    }

    auto load = [&](const AssetScene &in, u32 tex_idx, ImageColorSpace cs, u64 fallback) -> u64 {
        if (tex_idx == ASSET_INVALID) {
            return fallback;
        }
        assert(tex_idx < in.textures.size());

        const AssetTexture &tex = in.textures[tex_idx];

        rhi::Image img;
        rhi::ImageView view{};
        bool ok = false;

        std::string tex_name = "tex/" + (!tex.path.empty() ? tex.path : ("blob#" + std::to_string(tex_idx)));
        if (tex.blob && tex.blob_size > 0) {
            ok = tex.is_ktx ? load_texture_ktx2_from_memory(
                                  device, qs, out.staging, img, view, tex.blob, tex.blob_size, cs, tex_name.c_str()
                              )
                            : load_texture_from_memory(
                                  device, qs, out.staging, img, view, tex.blob, tex.blob_size, cs, tex_name.c_str()
                              );
        } else if (!tex.path.empty()) {
            ok = load_texture(device, qs, out.staging, tex.path.c_str(), img, view, cs);
        }

        if (!ok) {
            return rhi::handle_id(device, default_textures.error.view);
        }

        const u64 handle = rhi::handle_id(device, view);
        out.loaded_textures.emplace_back(std::move(img), std::move(view));
        auto &stored = out.loaded_textures.back();
        stored.second.desc.image = &stored.first;
        return handle;
    };

    out.materials.reserve(std::max<usize>(total_materials, 1));
    for (u32 ai = 0; ai < asset_count; ++ai) {
        const AssetScene &in = *assets.get({ai});
        for (const AssetMaterial &mat : in.materials) {
            MaterialGPU gpu{};
            gpu.base_color = mat.base_color;
            gpu.metallic_factor = mat.metallic;
            gpu.roughness_factor = mat.roughness;
            gpu.emissive_factor = mat.emissive;
            gpu.albedo = load(
                in, mat.base_color_tex, ImageColorSpace::SRGB, rhi::handle_id(device, default_textures.white_srgb.view)
            );
            gpu.normal = load(
                in, mat.normal_tex, ImageColorSpace::Linear, rhi::handle_id(device, default_textures.flat_normal.view)
            );
            gpu.metal_rough = load(
                in, mat.mr_tex, ImageColorSpace::Linear, rhi::handle_id(device, default_textures.white_linear.view)
            );
            gpu.emissive = load(
                in, mat.emissive_tex, ImageColorSpace::SRGB, rhi::handle_id(device, default_textures.black_srgb.view)
            );
            out.materials.push_back(gpu);
        }
    }
    if (out.materials.empty()) {
        MaterialGPU mat{};
        mat.base_color = vec4(0.5f, 0.5f, 0.5f, 1.0f);
        mat.metallic_factor = 0.5f;
        mat.roughness_factor = 0.5f;
        mat.emissive_factor = vec3(0.0f, 0.0f, 0.0f);
        mat.albedo = rhi::handle_id(device, default_textures.white_srgb.view);
        mat.normal = rhi::handle_id(device, default_textures.flat_normal.view);
        mat.metal_rough = rhi::handle_id(device, default_textures.white_linear.view);
        mat.emissive = rhi::handle_id(device, default_textures.black_srgb.view);
        out.materials.push_back(mat);
    }

    const SceneGraph &sg = scene.scene_graph;
    for (u32 n : scene.mesh_nodes) {
        AssetHandle ah = sg.asset[n];
        if (!ah.valid()) {
            continue;
        }

        const AssetScene *asset = assets.get(ah);
        if (!asset) {
            continue;
        }

        u32 ai = sg.asset_index[n];
        assert(ai < asset->nodes.size());
        const AssetNode &node = asset->nodes[ai];
        if (node.mesh == ASSET_INVALID) {
            continue;
        }

        assert(node.mesh < asset->meshes.size());
        const AssetMesh &mesh = asset->meshes[node.mesh];
        u32 inst_count = sg.instance_count[n];
        if (inst_count == 0) {
            inst_count = 1;
        }
        u32 base = sg.instance_base[n];
        for (u32 si = 0; si < mesh.submesh_count; ++si) {
            u32 sm_idx = asset->mesh_submeshes[mesh.first_submesh + si];
            assert(sm_idx < asset->submeshes.size());
            u32 mat = asset->submeshes[sm_idx].material;
            u32 material_index = material_offset[ah.id] + ((mat == ASSET_INVALID) ? 0 : mat);
            u32 submesh_index = submesh_offset[ah.id] + sm_idx;

            for (u32 k = 0; k < inst_count; ++k) {
                RenderItemGPU item{};
                item.instance_id = base + k;
                item.submesh_index = submesh_index;
                item.material_index = material_index;
                // +1 so the ID-blit clear value (0) means "no hit"; editor subtracts 1.
                // I might just get rid of this, and raycast instead
                item.entity_id = n + 1;
                out.render_items.push_back(item);
            }
        }
    }
    out.render_item_count = (u32)out.render_items.size();
}

static bool create_rt_scene(rhi::Device &device, rhi::CmdBuffer *cmd, RenderData &rd) {
    RtScene &rt = rd.rt_scene;

    u32 total_verts = (u32)rd.vertices.size();
    u32 total_indices = (u32)rd.indices.size();
    if (total_verts == 0 || total_indices == 0 || rd.submeshes.empty()) {
        VEL_WARN("create_rt_scene: skipped (no vertex/index/submesh data)");
        return false;
    }

    u64 vb_addr = rhi::buffer_device_address(device, rd.vertex_buffer);
    u64 ib_addr = rhi::buffer_device_address(device, rd.index_buffer);

    u32 max_instances = rd.render_item_count;

    rd.mesh_rt.clear();
    rd.mesh_rt.resize(rd.submeshes.size());

    std::vector<rhi::AccelStructBuildSizesInfo> blas_sizes(rd.submeshes.size());
    std::vector<u32> primitive_counts(rd.submeshes.size());

    u64 max_blas_scratch_size = 0;
    auto blas_build_flags = [](bool dynamic) -> rhi::AccelStructBuildFlags {
        return dynamic ? rhi::AccelStructBuildFlags::AllowUpdate : rhi::AccelStructBuildFlags::PreferFastTrace;
    };

    for (u32 mesh_id = 0; mesh_id < (u32)rd.submeshes.size(); ++mesh_id) {
        const SubmeshGPU &submesh = rd.submeshes[mesh_id];
        primitive_counts[mesh_id] = submesh.index_count / 3;
        if (primitive_counts[mesh_id] == 0) {
            continue;
        }

        MeshRT &mesh_rt = rd.mesh_rt[mesh_id];

        rhi::AccelStructGeometryTrianglesData triangles{
            .vertex_format = rhi::VertexFormat::R32G32B32_FLOAT,
            .vertex_data = vb_addr + offsetof(Vertex, pos),
            .vertex_stride = sizeof(Vertex),
            .max_vertex = total_verts - 1,
            .index_type = rhi::IndexType::UINT32,
            .index_data = ib_addr + (u64)submesh.first_index * sizeof(u32),
        };

        rhi::AccelStructGeometry as_geom{
            .type = rhi::AccelStructGeometryType::Triangles,
            .flags = rhi::AccelStructGeometryFlags::Opaque,
            .triangles = triangles,
        };

        rhi::AccelStructBuildGeometryInfo build_info{
            .type = rhi::AccelStructType::BottomLevel,
            .flags = blas_build_flags(mesh_rt.dynamic),
            .geometries = &as_geom,
            .geometry_count = 1,
        };

        rhi::get_acceleration_structure_build_sizes(
            device, build_info, &primitive_counts[mesh_id], blas_sizes[mesh_id]
        );
        max_blas_scratch_size = std::max(max_blas_scratch_size, blas_sizes[mesh_id].build_scratch_size);
    }

    if (max_blas_scratch_size == 0) {
        VEL_WARN("create_rt_scene: skipped (no BLAS geometry)");
        return false;
    }

    u64 tlas_build_scratch_size = 0;
    if (max_instances > 0) {
        rhi::AccelStructGeometry tlas_size_geom{
            .type = rhi::AccelStructGeometryType::Instances,
            .flags = rhi::AccelStructGeometryFlags::Opaque,
        };
        rhi::AccelStructBuildGeometryInfo tlas_size_info{
            .type = rhi::AccelStructType::TopLevel,
            .flags = rhi::AccelStructBuildFlags::PreferFastTrace,
            .geometries = &tlas_size_geom,
            .geometry_count = 1,
        };
        rhi::AccelStructBuildSizesInfo tlas_size_query{};
        rhi::get_acceleration_structure_build_sizes(device, tlas_size_info, &max_instances, tlas_size_query);
        tlas_build_scratch_size = tlas_size_query.build_scratch_size;
    }
    const u64 scratch_size = std::max(max_blas_scratch_size, tlas_build_scratch_size);

    rhi::BufferDesc scratch_desc{};
    scratch_desc.size = scratch_size;
    scratch_desc.usage = rhi::BufferUsage::StorageBuffer | rhi::BufferUsage::ShaderAddr;
    scratch_desc.memory = rhi::BufferMemType::Device;
    scratch_desc.dedicated = true;
    scratch_desc.name = "rt/accel-scratch";
    rhi::create_buffer(device, scratch_desc, rt.scratch_buffer);
    rt.scratch_size = scratch_size;
    rt.scratch_device_addr = rhi::buffer_device_address(device, rt.scratch_buffer);

    rhi::barrier(*cmd, rd.vertex_buffer, rhi::ResourceState::TransferTo, rhi::ResourceState::AccelBuild);
    rhi::barrier(*cmd, rd.index_buffer, rhi::ResourceState::TransferTo, rhi::ResourceState::AccelBuild);

    // BLAS: one acceleration structure per submesh
    for (u32 mesh_id = 0; mesh_id < (u32)rd.submeshes.size(); ++mesh_id) {
        if (primitive_counts[mesh_id] == 0) {
            continue;
        }

        const SubmeshGPU &submesh = rd.submeshes[mesh_id];
        MeshRT &mesh_rt = rd.mesh_rt[mesh_id];

        rhi::BufferDesc blas_desc{};
        blas_desc.size = blas_sizes[mesh_id].acceleration_structure_size;
        blas_desc.usage = rhi::BufferUsage::AccelStructStorage | rhi::BufferUsage::ShaderAddr;
        blas_desc.memory = rhi::BufferMemType::Device;
        blas_desc.dedicated = true;
        blas_desc.name = "rt/blas[submesh#" + std::to_string(mesh_id) + "]";
        rhi::create_buffer(device, blas_desc, mesh_rt.blas_buffer);

        rhi::create_acceleration_structure(
            device,
            {.buffer = &mesh_rt.blas_buffer,
             .size = blas_sizes[mesh_id].acceleration_structure_size,
             .type = rhi::AccelStructType::BottomLevel,
             .name = "rt/blas[submesh#" + std::to_string(mesh_id) + "]"},
            mesh_rt.blas
        );

        rhi::AccelStructGeometryTrianglesData triangles{
            .vertex_format = rhi::VertexFormat::R32G32B32_FLOAT,
            .vertex_data = vb_addr + offsetof(Vertex, pos),
            .vertex_stride = sizeof(Vertex),
            .max_vertex = total_verts - 1,
            .index_type = rhi::IndexType::UINT32,
            .index_data = ib_addr + (u64)submesh.first_index * sizeof(u32),
        };

        rhi::AccelStructGeometry as_geom{
            .type = rhi::AccelStructGeometryType::Triangles,
            .flags = rhi::AccelStructGeometryFlags::Opaque,
            .triangles = triangles,
        };

        rhi::AccelStructBuildGeometryInfo build_info{
            .type = rhi::AccelStructType::BottomLevel,
            .flags = blas_build_flags(mesh_rt.dynamic),
            .dst_acceleration_structure = &mesh_rt.blas,
            .geometries = &as_geom,
            .geometry_count = 1,
            .scratch_data = rt.scratch_device_addr,
        };

        rhi::AccelStructBuildRangeInfo range{
            .primitive_count = primitive_counts[mesh_id],
            .primitive_offset = 0,
            .first_vertex = submesh.base_vertex,
            .transform_offset = 0,
        };

        const rhi::AccelStructBuildRangeInfo *p_range = &range;
        rhi::cmd_build_acceleration_structures(*cmd, &build_info, &p_range, 1);

        mesh_rt.blas_device_address = rhi::get_acceleration_structure_device_address(device, mesh_rt.blas);

        if (mesh_id + 1 < (u32)rd.submeshes.size()) {
            rhi::barrier(*cmd, rhi::ResourceState::AccelBuild, rhi::ResourceState::AccelBuild);
        }
    }
    rhi::barrier(*cmd, rhi::ResourceState::AccelBuild, rhi::ResourceState::AccelBuild);

    // Instance buffer for per-frame TLAS build (CPU-writable)
    u64 inst_buf_size = std::max(1u, max_instances) * sizeof(rhi::AccelStructInstance);
    rhi::BufferDesc inst_desc{};
    inst_desc.size = inst_buf_size;
    inst_desc.usage = rhi::BufferUsage::AccelStructBuildInput | rhi::BufferUsage::ShaderAddr;
    inst_desc.memory = rhi::BufferMemType::Upload;
    inst_desc.name = "rt/tlas-instances";
    rhi::create_buffer(device, inst_desc, rt.instance_buffer);
    rt.instance_buffer_size = inst_buf_size;

    // Instance metadata buffer (GPU-readable)
    u64 inst_data_size = std::max(1u, max_instances) * sizeof(InstanceData);
    rhi::BufferDesc inst_data_desc{};
    inst_data_desc.size = inst_data_size;
    inst_data_desc.usage = rhi::BufferUsage::StorageBuffer | rhi::BufferUsage::ShaderAddr;
    inst_data_desc.memory = rhi::BufferMemType::Upload;
    inst_data_desc.name = "rt/instance-data";
    rhi::create_buffer(device, inst_data_desc, rt.instance_data_buffer);

    // TLAS backing buffer and acceleration structure (pre-allocated, reused every frame)
    if (max_instances > 0) {
        u64 inst_dev_addr = rhi::buffer_device_address(device, rt.instance_buffer);

        rhi::AccelStructGeometryInstancesData instances_data{
            .data = inst_dev_addr,
            .array_of_pointers = false,
        };
        rhi::AccelStructGeometry tlas_geom{
            .type = rhi::AccelStructGeometryType::Instances,
            .flags = rhi::AccelStructGeometryFlags::Opaque,
            .instances = instances_data,
        };

        rhi::AccelStructBuildGeometryInfo tlas_info{
            .type = rhi::AccelStructType::TopLevel,
            .flags = rhi::AccelStructBuildFlags::PreferFastTrace,
            .geometries = &tlas_geom,
            .geometry_count = 1,
        };

        rhi::AccelStructBuildSizesInfo tlas_sizes{};
        rhi::get_acceleration_structure_build_sizes(device, tlas_info, &max_instances, tlas_sizes);

        rhi::BufferDesc tlas_desc{};
        tlas_desc.size = tlas_sizes.acceleration_structure_size;
        tlas_desc.usage = rhi::BufferUsage::AccelStructStorage | rhi::BufferUsage::ShaderAddr;
        tlas_desc.memory = rhi::BufferMemType::Device;
        tlas_desc.dedicated = true;
        tlas_desc.name = "rt/tlas";
        rhi::create_buffer(device, tlas_desc, rt.tlas_buffer);
        rt.tlas_buffer_size = tlas_sizes.acceleration_structure_size;

        rhi::create_acceleration_structure(
            device,
            {.buffer = &rt.tlas_buffer,
             .size = tlas_sizes.acceleration_structure_size,
             .type = rhi::AccelStructType::TopLevel,
             .name = "rt/tlas"},
            rt.tlas
        );

        rhi::barrier(*cmd, rt.tlas_buffer, rhi::ResourceState::Idle, rhi::ResourceState::AccelBuild);
        {
            rhi::AccelStructGeometryInstancesData init_instances{
                .data = inst_dev_addr,
                .array_of_pointers = false,
            };
            rhi::AccelStructGeometry init_geom{
                .type = rhi::AccelStructGeometryType::Instances,
                .flags = rhi::AccelStructGeometryFlags::Opaque,
                .instances = init_instances,
            };
            rhi::AccelStructBuildGeometryInfo init_build{
                .type = rhi::AccelStructType::TopLevel,
                .flags = rhi::AccelStructBuildFlags::PreferFastTrace,
                .dst_acceleration_structure = &rt.tlas,
                .geometries = &init_geom,
                .geometry_count = 1,
                .scratch_data = rt.scratch_device_addr,
            };
            rhi::AccelStructBuildRangeInfo init_range{
                .primitive_count = 0,
                .primitive_offset = 0,
                .first_vertex = 0,
                .transform_offset = 0,
            };
            const rhi::AccelStructBuildRangeInfo *p_init_range = &init_range;
            rhi::cmd_build_acceleration_structures(*cmd, &init_build, &p_init_range, 1);
        }
        rhi::barrier(*cmd, rhi::ResourceState::AccelBuild, rhi::ResourceState::AccelBuild);
    }

    // TLAS now BDA via FrameUBO.tlas_address — no descriptor arena
    return true;
};

static void destroy_rt_scene(rhi::Device &device, RenderData &rd) {
    RtScene &rt = rd.rt_scene;

    rhi::destroy_acceleration_structure(device, rt.tlas);

    for (MeshRT &mesh_rt : rd.mesh_rt) {
        rhi::destroy_acceleration_structure(device, mesh_rt.blas);
        if (rhi::valid(mesh_rt.blas_buffer)) {
            rhi::destroy_buffer(device, mesh_rt.blas_buffer);
            mesh_rt.blas_buffer = {};
        }
    }
    rd.mesh_rt.clear();

    if (rhi::valid(rt.scratch_buffer)) {
        rhi::destroy_buffer(device, rt.scratch_buffer);
        rt.scratch_buffer = {};
    }
    if (rhi::valid(rt.instance_buffer)) {
        rhi::destroy_buffer(device, rt.instance_buffer);
        rt.instance_buffer = {};
    }
    if (rhi::valid(rt.instance_data_buffer)) {
        rhi::destroy_buffer(device, rt.instance_data_buffer);
        rt.instance_data_buffer = {};
    }
    if (rhi::valid(rt.tlas_buffer)) {
        rhi::destroy_buffer(device, rt.tlas_buffer);
        rt.tlas_buffer = {};
    }
    rt = {};
}

static void destroy_default_textures(rhi::Device &device, DefaultTextures &tex) {
    auto destroy = [&](DefaultTexture &t) {
        rhi::destroy_image_view(device, t.view);
        rhi::destroy_image(device, t.image);
    };
    destroy(tex.white_srgb);
    destroy(tex.white_linear);
    destroy(tex.black_srgb);
    destroy(tex.flat_normal);
    destroy(tex.error);
}

static f32 randf(f32 min, f32 max) {
    return min + (max - min) * (f32(rand()) / f32(RAND_MAX));
}

static u32 scatter_instances(AssetManager &assets, Scene &scene, AssetHandle ah, u32 count, f32 spread) {
    const AssetScene *asset = assets.get(ah);
    if (!asset) {
        return SCENE_INVALID;
    }

    u32 base = scene.scene_graph.node_count;
    u32 root = scene.instantiate(assets, ah);
    if (root == SCENE_INVALID) {
        return SCENE_INVALID;
    }

    // asset_node_index that has the first mesh
    u32 mesh_asset_node = SCENE_INVALID;
    for (u32 i = 0; i < (u32)asset->nodes.size(); ++i) {
        if (asset->nodes[i].mesh != ASSET_INVALID) {
            mesh_asset_node = i;
            break;
        }
    }
    if (mesh_asset_node == SCENE_INVALID) {
        return SCENE_INVALID;
    }

    u32 mesh_node = base + mesh_asset_node;

    // instance locals
    u32 offset = (u32)scene.instance_locals.size();
    scene.instance_locals.reserve(offset + count);
    for (u32 i = 0; i < count; ++i) {
        TRS trs;
        trs.translation = vec3(randf(-spread, spread), randf(-spread, spread), randf(-spread, spread));
        trs.rotation =
            quat::from_axis_angle(vec3(randf(-1, 1), randf(-1, 1), randf(-1, 1)).normalized(), randf(0, 6.283f));
        scene.instance_locals.push_back(trs);
    }

    SceneGraph &sg = scene.scene_graph;
    sg.instance_count[mesh_node] = count;
    sg.instance_base[mesh_node] = offset;

    sg.update_world_transforms();
    return root;
}

static void scatter_point_lights(Scene &scene, u32 count) {
    scene.point_lights.reserve(count);
    for (u32 i = 0; i < count; ++i) {
        vec3 pos = {randf(-8.0f, 8.0f), randf(-12.0f, 12.0f), randf(0.2f, 9.5f)};
        vec3 col = {randf(0.0f, 1.0f), randf(0.0f, 1.0f), randf(0.0f, 1.0f)};
        NodeHandle pl_node = scene.scene_graph.add_node(-1, TRS{.translation = pos});
        PointLight pl;
        pl.node = pl_node;
        pl.color = col;
        pl.intensity = 5.0f;
        pl.range = 2.0f;
        scene.point_lights.push_back(pl);
    }
}

int main() {
    vel::Logger::init();
    rhi::Config config{};
    rhi::ShaderCompiler sc;
    rhi::Device device{};
    rhi::Swapchain swapchain{};

    rhi::Sync *frame_sync = nullptr;
    rhi::CmdPool frame_pools[MAX_FRAMES_IN_FLIGHT]{};
    rhi::CmdBuffer frame_cmds[MAX_FRAMES_IN_FLIGHT]{};

    FrameGraph fg;

    config.vsync_enabled = true;
    config.enable_validation = true;

    Platform platform{};
    QuickSubmit quick_submit;

    init_compiler(sc);
    init_platform(config.instance_extensions);
    create_context(device, config);
    create_memory_allocator(device);

    qs_init(quick_submit, rhi::graphics_queue(device), device);

    platform.width = 1920;
    platform.height = 1080;
    create_window(device, platform, "gfx-playground", WindowMode::Windowed, true);

    rhi::SwapchainDesc swapchain_desc{};
    swapchain_desc.native_window = platform_native_window(platform);
    swapchain_desc.width = platform.width;
    swapchain_desc.height = platform.height;
    swapchain_desc.vsync_enabled = config.vsync_enabled;
    swapchain_desc.image_count = config.swapchain_image_count;
    create_swapchain(device, swapchain_desc, swapchain);

    frame_sync = rhi::sync_create(device, 0, "frame");
    if (!rhi::valid(frame_sync)) {
        VEL_CRITICAL("Failed to create frame pacing timeline");
        return 1;
    }

    for (u32 i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {

        rhi::create_cmd_pool(
            device,
            rhi::CmdPoolDesc{
                .queue_family_index = rhi::queue_family_index(rhi::graphics_queue(device)),
                .flags = rhi::CmdPoolUsage::Transient | rhi::CmdPoolUsage::ResetCommandBuffer,
            },
            frame_pools[i]
        );
        rhi::CmdBufferDesc cmd_desc;
        cmd_desc.pool = &frame_pools[i];
        cmd_desc.level = rhi::CmdBufferLevel::Primary;
        rhi::create_cmd_buffer(device, cmd_desc, frame_cmds[i]);
    }

#ifdef ENABLE_EDITOR
    editor::Editor editor;
#endif
    Pipelines pipelines(device, sc);
    AssetManager assets;

    RenderData render_data{};
    FrameData frame_data[MAX_FRAMES_IN_FLIGHT]{};

    DefaultTextures textures;
    Scene scene;

    rhi::Extent2D render_size;

    {
        NodeHandle cam_node = scene.scene_graph.add_node(
            -1,
            TRS{
                .translation = vec3(-3.6f, -1.2f, 3.7f),
            }
        );
        Camera cam;
        cam.node = cam_node;
        scene.cameras.push_back(cam);
        scene.active_camera = (u32)scene.cameras.size() - 1;

        NodeHandle cam2_node = scene.scene_graph.add_node(
            -1,
            TRS{
                .translation = vec3(5.0f, 5.0f, 5.0f),
            }
        );
        Camera cam2;
        cam2.node = cam2_node;
        scene.cameras.push_back(cam2);
    }

    {
        vec3 target_dir = vec3(0.2f, 0.2f, 1.0f).normalized();
        vec3 fwd = vec3(1.0f, 0.0f, 0.0f);
        vec3 axis = cross(fwd, target_dir).normalized();
        f32 angle = std::acos(std::clamp(fwd.dot(target_dir), -1.0f, 1.0f));

        NodeHandle light_node = scene.scene_graph.add_node(
            -1,
            TRS{
                .rotation = quat::from_axis_angle(axis, angle),
            }
        );
        Light dl;
        dl.node = light_node;
        scene.directional_lights.push_back(dl);
    }

    fg.device = &device;

    rhi::BufferDesc staging_arena_buffer_desc{};
    staging_arena_buffer_desc.size = 8ull * 1024 * 1024 * 1024;
    staging_arena_buffer_desc.usage = rhi::BufferUsage::CopySrc;
    staging_arena_buffer_desc.memory = rhi::BufferMemType::Upload;
    staging_arena_buffer_desc.name = "Staging Arena";

    rhi::BufferViewDesc staging_arena_view_desc{};
    staging_arena_view_desc.type = rhi::BufferViewType::Storage;
    create_arena(device, render_data.staging, staging_arena_buffer_desc, staging_arena_view_desc);

    {
        rhi::Sampler shadow_sampler;
        rhi::SamplerDesc compare_sampler_info;
        compare_sampler_info.filter = rhi::SamplerFilter::LINEAR;
        compare_sampler_info.address = rhi::SamplerAddressMode::CLAMP;
        compare_sampler_info.comparison_func = rhi::SamplerComparisonFunc::GREATER_EQUAL;
        compare_sampler_info.use_mips = false;
        compare_sampler_info.name = "samp/shadow-compare";
        rhi::create_sampler(device, compare_sampler_info, shadow_sampler);
        render_data.cmp_greater = rhi::handle_id(device, shadow_sampler);

        rhi::Sampler material_sampler;
        rhi::SamplerDesc linear_sampler_info;
        linear_sampler_info.filter = rhi::SamplerFilter::ANISOTROPIC;
        linear_sampler_info.address = rhi::SamplerAddressMode::WRAP;
        linear_sampler_info.comparison_func = rhi::SamplerComparisonFunc::NEVER;
        linear_sampler_info.use_mips = true;
        linear_sampler_info.name = "samp/material-aniso-wrap";
        rhi::create_sampler(device, linear_sampler_info, material_sampler);
        render_data.aniso_wrap_mips = rhi::handle_id(device, material_sampler);

        rhi::Sampler atlas_sampler;
        rhi::SamplerDesc atlas_sampler_info;
        atlas_sampler_info.filter = rhi::SamplerFilter::LINEAR;
        atlas_sampler_info.address = rhi::SamplerAddressMode::CLAMP;
        atlas_sampler_info.comparison_func = rhi::SamplerComparisonFunc::NEVER;
        atlas_sampler_info.use_mips = false;
        atlas_sampler_info.name = "samp/atlas-linear-clamp";
        rhi::create_sampler(device, atlas_sampler_info, atlas_sampler);
        render_data.linear_clamp = rhi::handle_id(device, atlas_sampler);

        rhi::Sampler linear_mip_sampler;
        rhi::SamplerDesc linear_mip_info;
        linear_mip_info.filter = rhi::SamplerFilter::LINEAR;
        linear_mip_info.address = rhi::SamplerAddressMode::CLAMP;
        linear_mip_info.comparison_func = rhi::SamplerComparisonFunc::NEVER;
        linear_mip_info.use_mips = true;
        linear_mip_info.name = "samp/linear-clamp-mips";
        rhi::create_sampler(device, linear_mip_info, linear_mip_sampler);
        render_data.linear_clamp_mips = rhi::handle_id(device, linear_mip_sampler);
    }

    {
        auto push_tex = [&](DefaultTexture &dst,
                            const void *pixels,
                            u32 pixel_count,
                            u32 pixel_stride,
                            rhi::ImageFormat cs,
                            const char *name) {
            if (!create_texture_from_raw_rgba(
                    device,
                    quick_submit,
                    render_data.staging,
                    dst.image,
                    dst.view,
                    pixels,
                    pixel_count,
                    pixel_count,
                    pixel_stride,
                    cs,
                    name
                )) {
                VEL_CRITICAL("Failed to create default texture");
                return;
            }
            dst.slot = (u32)dst.view.slot;
        };

        u8 white[] = {192, 192, 192, 255};
        u8 default_metal_rough[] = {0, 128, 128, 255};
        u8 black[] = {0, 0, 0, 255};
        u8 error[] = {255, 0, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 0, 255, 255};
        u8 flat_normal[] = {128, 128, 255, 255};

        push_tex(textures.white_srgb, white, 1, 4, rhi::ImageFormat::RGBA8_SRGB, "tex/default/white-srgb");
        push_tex(
            textures.white_linear, default_metal_rough, 1, 4, rhi::ImageFormat::RGBA8_UNORM, "tex/default/white-linear"
        );
        push_tex(textures.black_srgb, black, 1, 4, rhi::ImageFormat::RGBA8_SRGB, "tex/default/black");
        push_tex(textures.flat_normal, flat_normal, 1, 4, rhi::ImageFormat::RGBA8_UNORM, "tex/default/flat-normal");
        push_tex(textures.error, error, 4, 4, rhi::ImageFormat::RGBA8_SRGB, "tex/default/error");
    }
#ifdef ENABLE_EDITOR
    editor::init(editor, device, platform, quick_submit, render_data.staging, fg, assets);
    editor::eval_render_size(editor.viewport_width, editor.viewport_height, swapchain, render_size);
#else
    render_size = rhi::swapchain_extent(swapchain);
#endif
    qs_wait_all(quick_submit);
    reset_arena(render_data.staging);

    if (!load_texture(
            device,
            quick_submit,
            render_data.staging,
            "assets/kloofendal_overcast_puresky_4k.hdr",
            render_data.sky,
            render_data.sky_view,
            ImageColorSpace::Linear
        )) {
        VEL_ERROR("Failed to load HDR IMAGE, using fallback");
    }
    GenerateIBL(device, quick_submit, sc, scene.env, fg, render_data.sky_view, render_data.linear_clamp_mips);

    AssetHandle sponza = assets.load("assets/gltf/Sponza-KTX.glb");
    AssetHandle helmet = assets.load("assets/gltf/DamagedHelmet.glb");

    u32 sponza_root = scene.instantiate(assets, sponza);
    u32 helmet_root = scene.instantiate(assets, helmet);

    // scatter_instances(assets, scene, helmet, 5000, 100.0f);
    // scatter_point_lights(scene, 512);
    // scene.scene_graph.set_scale(helmet_root, vec3(50.0, 50.0, 50.0f));
    // scene.scene_graph.set_translation(helmet_root, vec3(0.0, 0.0, -1200.0f));
    // scene.scene_graph.update_world_transforms();

    create_render_data(device, quick_submit, assets, scene, render_data, textures, fg);
    create_frame_data(device, frame_data, (u32)scene.instance_locals.size());

    VEL_INFO("node_count={}, render_items={}", scene.scene_graph.node_count, render_data.render_items.size());
    render_data.scene_graph = &scene.scene_graph;

    upload_transforms(scene, frame_data[0]);

    QsCmd *cmd = qs_acquire(quick_submit);
    create_gpu_buffer_and_upload(
        device,
        render_data.staging,
        render_data.vertex_buffer,
        render_data.vertices.data(),
        render_data.vertices.size(),
        sizeof(Vertex),
        *cmd,
        rhi::BufferUsage::VertexBuffer | rhi::BufferUsage::StorageBuffer | rhi::BufferUsage::ShaderAddr |
            rhi::BufferUsage::AccelStructBuildInput,
        "buf/scene-vertices"
    );
    create_gpu_buffer_and_upload(
        device,
        render_data.staging,
        render_data.index_buffer,
        render_data.indices.data(),
        render_data.indices.size(),
        sizeof(u32),
        *cmd,
        rhi::BufferUsage::IndexBuffer | rhi::BufferUsage::AccelStructBuildInput,
        "buf/scene-indices"
    );
    create_gpu_buffer_and_upload(
        device,
        render_data.staging,
        render_data.submesh_buffer,
        render_data.submeshes.data(),
        render_data.submeshes.size(),
        sizeof(SubmeshGPU),
        *cmd,
        rhi::BufferUsage::StorageBuffer,
        "buf/scene-submeshes"
    );
    create_gpu_buffer_and_upload(
        device,
        render_data.staging,
        render_data.material_buffer,
        render_data.materials.data(),
        render_data.materials.size(),
        sizeof(MaterialGPU),
        *cmd,
        rhi::BufferUsage::StorageBuffer,
        "buf/scene-materials"
    );
    create_gpu_buffer_and_upload(
        device,
        render_data.staging,
        render_data.render_item_buffer,
        render_data.render_items.data(),
        render_data.render_items.size(),
        sizeof(RenderItemGPU),
        *cmd,
        rhi::BufferUsage::StorageBuffer,
        "buf/scene-render-items"
    );
    qs_submit(quick_submit, *cmd);
    reset_arena(render_data.staging);

    render_data.vertex_bda = rhi::buffer_device_address(device, render_data.vertex_buffer);
    render_data.index_bda = rhi::buffer_device_address(device, render_data.index_buffer);

    {
        if (!rhi::create_buffer_view(
                device,
                {.buffer = &render_data.vertex_buffer, .type = rhi::BufferViewType::Storage},
                render_data.vertex_view
            )) {
            VEL_CRITICAL("Failed to create vertex buffer view");
        }
        for (u32 i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
            if (!rhi::create_buffer_view(
                    device,
                    {.buffer = &frame_data[i].frame_ubo, .type = rhi::BufferViewType::Uniform},
                    render_data.frame_ubo_view[i]
                )) {
                VEL_CRITICAL("Failed to create frame UBO view");
            }
        }
    }
    u64 frame_count = 0;

    PassTimings pass_timings;

    u32 cascade_sizes[Light::CASCADE_COUNT] = {};
    resolve_cascade_sizes(render_data.cascade_size, cascade_sizes);

    u32 render_item_count = (u32)render_data.render_items.size();

    BufferHandle render_items_buf = fg.import_buffer(&render_data.render_item_buffer);
    BufferHandle submesh_buf = fg.import_buffer(&render_data.submesh_buffer);

    ImageHandle env_cubemap = fg.import_image(&scene.env.environment_img, rhi::ResourceState::TextureSample);
    // should handle this better instead of forcing some initial state?
    scene.env.environment_img.state = rhi::ResourceState::TextureSample;

    auto hiz_res = hiz::create(fg);
    auto ddgi_res = ddgi_trace::create(fg);
    render_data.ddgi_grid = ddgi_res.grid;
    render_data.ddgi_cfg = ddgi_res.cfg;
    auto main_bd = build_indirect::create(fg, render_item_count, "main");
    auto shadow_bd = build_indirect::create(fg, render_item_count, "shadow");

    build_indirect::record(
        device, pipelines, sc, fg, render_items_buf, submesh_buf, main_bd, hiz_res.hiz, true, "Main Build Indirect"
    );
    build_indirect::record(
        device,
        pipelines,
        sc,
        fg,
        render_items_buf,
        submesh_buf,
        shadow_bd,
        ImageHandle{},
        false,
        "Shadow Build Indirect"
    );
    auto shadow_map = shadow::record(
        device, pipelines, sc, fg, shadow_bd.indirect, shadow_bd.draw_count, shadow_bd.visible, cascade_sizes
    );
    auto dn = depth_normal::record(device, pipelines, sc, fg, main_bd.indirect, main_bd.draw_count, main_bd.visible);
    auto clusters = cluster_bounds::record(device, pipelines, sc, fg, swapchain.extent.width, swapchain.extent.height);
    auto light_cull = light_cull::record(device, pipelines, sc, fg, clusters.cluster_bounds, clusters.cluster_records);
    auto forward = forward::record(
        device,
        pipelines,
        sc,
        fg,
        main_bd.indirect,
        main_bd.draw_count,
        main_bd.visible,
        shadow_map.cascade,
        clusters.cluster_bounds,
        clusters.cluster_records,
        light_cull.light_index_list,
        ddgi_res,
        dn.depth
    );

    { // ddgi (deegee)
        QsCmd *cmd = qs_acquire(quick_submit);
        create_rt_scene(device, &cmd->cmd, render_data);
        qs_submit(quick_submit, *cmd);
        qs_wait_all(quick_submit);

        BufferHandle tlas_buf = fg.import_buffer(&render_data.rt_scene.tlas_buffer, rhi::ResourceState::AccelBuild);
        rt_build::record(fg, tlas_buf);

        ddgi_trace::record(device, pipelines, sc, fg, ddgi_res, env_cubemap, tlas_buf);

        rhi::ComputePipelineDesc ddgi_update_irr;
        ddgi_update_irr.device = &device;
        ddgi_update_irr.set_shader(sc, "src/passes/ddgi/ddgi_update_irradiance.slang", "csMain");
        pipelines.add_compute("DdgiUpdateIrr", ddgi_update_irr);
        ddgi_update::record(
            fg, ddgi_res, ddgi_res.irradiance_img, pipelines.get_compute("DdgiUpdateIrr"), "Ddgi Update Irradiance"
        );

        rhi::ComputePipelineDesc ddgi_update_dist;
        ddgi_update_dist.device = &device;
        ddgi_update_dist.set_shader(sc, "src/passes/ddgi/ddgi_update_distance.slang", "csMain");
        pipelines.add_compute("DdgiUpdateDist", ddgi_update_dist);
        ddgi_update::record(
            fg, ddgi_res, ddgi_res.distance_img, pipelines.get_compute("DdgiUpdateDist"), "Ddgi Update Distance"
        );

        // NOTE: silently ordered, relocate writes the probe offset, then classify
        // reads that offset and the fixed-ray backface ratio to ease the probe state
        ddgi_relocate::record(device, pipelines, sc, fg, ddgi_res);

        ddgi_classify::record(device, pipelines, sc, fg, ddgi_res);

        ddgi_probes::record(device, pipelines, sc, fg, forward.hdr, forward.depth, ddgi_res, &render_data.debug);
    }

    hiz::record(device, pipelines, sc, fg, hiz_res, forward.depth);
    // ssr::record(device, pipelines, sc, fg, ssr_res, forward.hdr, dn.depth, dn.normal, hiz_res.hiz);
    skybox::record(device, pipelines, sc, fg, forward.hdr, forward.depth, env_cubemap);
    grid::record(device, pipelines, sc, fg, forward.hdr, forward.depth);

#ifdef ENABLE_EDITOR
    debug_lines::record(
        device, pipelines, sc, fg, forward.hdr, forward.depth, debug_lines::Settings{.draw = &render_data.debug_draw}
    );

    ImageHandle display_target =
        fg.add_image({.desc = {.format = rhi::swapchain_format(swapchain)}, .name = "fg/present/display"});
    ImageHandle id_blit_target = id_blit::record(
        device, pipelines, sc, fg, main_bd.indirect, main_bd.draw_count, main_bd.visible, forward.depth
    );
#endif

    ImageHandle swapchain_target = fg.import_image(rhi::swapchain_image(swapchain, 0), rhi::ResourceState::ColorDraw);

    fullscreen_fg::record(
        device,
        pipelines,
        sc,
        fg,
        forward.hdr,
        forward.depth,
        rhi::swapchain_format(swapchain),
#ifdef ENABLE_EDITOR
        display_target
#else
        swapchain_target
#endif
        // , ssr_res.output
    );

#ifdef ENABLE_EDITOR
    editor::PresentTargets present_targets{};
    present_targets.display = display_target;
    present_targets.id_blit = id_blit_target;
    present_targets.irradiance = ddgi_res.irradiance_img;
    present_targets.distance = ddgi_res.distance_img;
    for (u32 i = 0; i < Light::CASCADE_COUNT; ++i) {
        present_targets.cascade[i] = shadow_map.cascade[i];
    }
    present_targets.swapchain = swapchain_target;
    editor::record_present(editor, pipelines, sc, fg, swapchain, present_targets, pass_timings);
#endif

    fg.compile(device, render_size);

#ifdef ENABLE_EDITOR
    pass_timings.enable(fg, device, MAX_FRAMES_IN_FLIGHT);
#endif

    while (platform_update(platform)) {
        VEL_PROFILE_FRAME();

        render_data.frame_stats.record((f32)(platform.raw_dt * 1000.0));

        const u32 frame_slot = frame_count % MAX_FRAMES_IN_FLIGHT;
        const u64 frame_wait_value =
            (frame_count + 1 > MAX_FRAMES_IN_FLIGHT) ? (frame_count + 1 - MAX_FRAMES_IN_FLIGHT) : 0;

        if (frame_wait_value != 0 && !rhi::sync_host_wait(frame_sync, frame_wait_value)) {
            // GPU wedged (device lost / timeout)
            VEL_CRITICAL("FRAME SYNC WAIT MISS");
            rhi::device_wait_idle(device);
        }

        FrameData &frame = frame_data[frame_slot];

        bool recreate_swap = platform.framebuffer_resized || rhi::swapchain_needs_recreate(swapchain);
        platform.framebuffer_resized = false;

        if (recreate_swap) {
            if (!platform_wait_for_valid_framebuffer(platform)) {
                continue;
            }
            rhi::queue_wait_idle(device);
            destroy_swapchain(device, swapchain);
            rhi::SwapchainDesc swapchain_desc{};
            swapchain_desc.native_window = platform_native_window(platform);
            swapchain_desc.width = platform.width;
            swapchain_desc.height = platform.height;
            swapchain_desc.vsync_enabled = config.vsync_enabled;
            swapchain_desc.image_count = config.swapchain_image_count;
            create_swapchain(device, swapchain_desc, swapchain);

#ifdef ENABLE_EDITOR
            editor::eval_render_size(editor.viewport_width, editor.viewport_height, swapchain, render_size);
#else
            render_size = rhi::swapchain_extent(swapchain);
#endif

            fg.overwrite_imported_image(swapchain_target, rhi::swapchain_image(swapchain, 0));
            fg.compile(device, render_size);

            Camera &cam = scene.cameras[scene.active_camera];
            cam.aspect = static_cast<f32>(render_size.width) / static_cast<f32>(render_size.height);

            rhi::Extent2D sc_extent = rhi::swapchain_extent(swapchain);
            platform.width = sc_extent.width;
            platform.height = sc_extent.height;

            continue;
        }

#ifdef ENABLE_EDITOR
        editor::handle_frame_begin(
            editor,
            render_data,
            device,
            swapchain,
            fg,
            swapchain_target,
            render_size,
            scene,
            pipelines,
            pass_timings,
            frame_slot
        );
#endif
        Camera &cam = scene.cameras[scene.active_camera];
        cam.position = scene.scene_graph.translations[cam.node];
        cam.aspect = static_cast<f32>(render_size.width) / static_cast<f32>(render_size.height);
        cam.aspect = static_cast<f32>(render_size.width) / static_cast<f32>(render_size.height);
        update_camera(cam, platform.input);
        scene.scene_graph.set_translation(cam.node, cam.position);

        update_light(scene.directional_lights[0], scene.cameras[scene.active_camera]);
        update_light_cascades(scene.directional_lights[0], scene.cameras[scene.active_camera], cascade_sizes);

        build_ubo(scene, render_data, device, frame, render_size);

        scene.evaluate();
        upload_scene_data(scene, frame);

#ifdef ENABLE_EDITOR
        render_data.debug_draw.clear();
        editor::apply_debug_settings(editor, render_data.debug_draw);
        if (editor.show_debug_lines) {
            ddgi_trace::debug_draw(render_data.debug_draw, render_data);
            editor::debug_draw_selection(render_data.debug_draw, scene, render_data, editor.selected_node);
            editor::update_gizmo(editor, scene, render_data.debug_draw);
        }
#endif

        rhi::reset_cmd_pool(device, frame_pools[frame_slot]);

        rhi::begin_cmd_buffer(
            frame_cmds[frame_slot], rhi::CmdBufferBeginDesc{.usage = rhi::CmdBufferUsage::OneTimeSubmit}
        );

        u32 image_index;
        if (!rhi::acquire_swapchain_image(device, swapchain, image_index, frame_cmds[frame_slot])) {
            rhi::reset_cmd_buffer(frame_cmds[frame_slot]);
            continue;
        }

        rhi::Image &swap_image = *rhi::swapchain_image(swapchain, image_index);
        fg.overwrite_imported_image(swapchain_target, &swap_image);

        fg.execute(
            frame_cmds[frame_slot],
            GraphExecInfo{
                .frame_data = frame_data,
                .render_data = &render_data,
                .scene = &scene,
                .frame_index = frame_slot,
                .frame_count = frame_count,
            }
        );

        {
            rhi::ImageRange range{};
            rhi::barrier(frame_cmds[frame_slot], swap_image, range, swap_image.state, rhi::ResourceState::Display);
        }

        rhi::end_cmd_buffer(frame_cmds[frame_slot]);

        rhi::QueueSignal frame_signal{{frame_sync, frame_count + 1}};
        rhi::QueueSubmitDesc submit_desc{};
        submit_desc.cmd_count = 1;
        submit_desc.cmds = &frame_cmds[frame_slot];
        submit_desc.signal_count = 1;
        submit_desc.signals = &frame_signal;
        rhi::queue_submit(device, rhi::graphics_queue(device), submit_desc);
        rhi::present_swapchain(device, swapchain, image_index, {frame_sync, frame_count + 1});
        frame_count++;
    }
    rhi::device_wait_idle(device);
#ifdef ENABLE_EDITOR
    pass_timings.destroy(device);
#endif
    fg.destroy();

#ifdef ENABLE_EDITOR
    editor::shutdown(editor, device);
#endif
    destroy_rt_scene(device, render_data);
    pipelines.clear();
    destroy_default_textures(device, textures);
    rhi::destroy_image_view(device, render_data.sky_view);
    rhi::destroy_image(device, render_data.sky);
    rhi::sync_destroy(frame_sync);
    frame_sync = nullptr;
    for (u32 i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
        rhi::destroy_cmd_buffer(device, frame_pools[i], frame_cmds[i]);
        rhi::destroy_cmd_pool(device, frame_pools[i]);
    }
    destroy_swapchain(device, swapchain);
    qs_shutdown(quick_submit);

    auto destroy_buf = [&](rhi::Buffer &b) {
        if (rhi::valid(b)) {
            rhi::destroy_buffer(device, b);
        }
    };
    auto destroy_img = [&](rhi::Image &i) {
        if (rhi::valid(i)) {
            rhi::destroy_image(device, i);
        }
    };
    auto destroy_iv = [&](rhi::ImageView &v) {
        if (rhi::valid(v)) {
            rhi::destroy_image_view(device, v);
        }
    };
    auto destroy_view = [&](rhi::ImageView &v) { rhi::destroy_image_view(device, v); };

    destroy_view(scene.env.environment_view);
    destroy_img(scene.env.environment_img);
    destroy_view(scene.env.irradiance_view);
    destroy_img(scene.env.irradiance_img);
    destroy_view(scene.env.prefiltered_view);
    destroy_img(scene.env.prefiltered_img);
    destroy_view(scene.env.brdf_lut_view);
    destroy_img(scene.env.brdf_lut_img);

    for (auto &[img, view] : render_data.loaded_textures) {
        destroy_iv(view);
        destroy_img(img);
    }
    render_data.loaded_textures.clear();

    rhi::destroy_buffer_view(device, render_data.vertex_view);
    for (u32 i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
        rhi::destroy_buffer_view(device, render_data.frame_ubo_view[i]);
    }
    destroy_buf(render_data.vertex_buffer);
    destroy_buf(render_data.index_buffer);
    destroy_buf(render_data.submesh_buffer);
    destroy_buf(render_data.material_buffer);
    destroy_buf(render_data.render_item_buffer);

    for (u32 i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
        destroy_buf(frame_data[i].frame_ubo);
        destroy_buf(frame_data[i].point_light_buffer);
        destroy_buf(frame_data[i].transform_buffer);
    }

    destroy_arena(device, render_data.staging);
    destroy_memory_allocator(device);
    destroy_window(device, platform);
    vel::Logger::shutdown();
    return 0;
}