#include "VoxelRenderer.h"
#include "wiJobSystem.h"
#include "wiPrimitive.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstring>
#include <unordered_map>

#include "Utility/stb_image.h"
#include "wiTextureHelper.h"

using namespace wi::graphics;

namespace voxel {

// ── VoxelRenderer Implementation ────────────────────────────────

VoxelRenderer::VoxelRenderer() = default;
VoxelRenderer::~VoxelRenderer() { shutdown(); }

void VoxelRenderer::initialize(GraphicsDevice* dev) {
    device_ = dev;
    if (!device_) return;

    createPipeline();
    if (!pso_.IsValid()) {
        wi::backlog::post("VoxelRenderer: pipeline creation failed", wi::backlog::LogLevel::Error);
        initialized_ = false;
        return;
    }
    loadTextures();

    // Create chunk info buffer (SRV for VS chunk lookup)
    GPUBufferDesc infoDesc;
    infoDesc.size = MAX_CHUNKS * sizeof(GPUChunkInfo);
    infoDesc.bind_flags = BindFlag::SHADER_RESOURCE;
    infoDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
    infoDesc.stride = sizeof(GPUChunkInfo);
    infoDesc.usage = Usage::DEFAULT;
    device_->CreateBuffer(&infoDesc, nullptr, &chunkInfoBuffer_);

    // ── GPU Timestamp Queries ──────────────────────────────────────
    GPUQueryHeapDesc queryDesc;
    queryDesc.type = GpuQueryType::TIMESTAMP;
    queryDesc.query_count = TS_COUNT;
    device_->CreateQueryHeap(&queryDesc, &timestampHeap_);

    GPUBufferDesc readbackDesc;
    readbackDesc.size = TS_COUNT * sizeof(uint64_t);
    readbackDesc.usage = Usage::READBACK;
    device_->CreateBuffer(&readbackDesc, nullptr, &timestampReadback_);

    // ── GPU Compute Mesher resources ─────────────────────────────
    wi::renderer::LoadShader(ShaderStage::CS, meshShader_, "voxel/voxelMeshCS.cso");
    gpuMesherAvailable_ = meshShader_.IsValid();
    if (gpuMesherAvailable_) {
        // Voxel data buffer: 1 chunk's worth (32^3 voxels / 2 per uint = 16384 uint)
        GPUBufferDesc voxDesc;
        voxDesc.size = (CHUNK_VOLUME / 2) * sizeof(uint32_t);
        voxDesc.bind_flags = BindFlag::SHADER_RESOURCE;
        voxDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
        voxDesc.stride = sizeof(uint32_t);
        voxDesc.usage = Usage::DEFAULT;
        device_->CreateBuffer(&voxDesc, nullptr, &voxelDataBuffer_);

        // GPU quad output: same capacity as mega-buffer
        GPUBufferDesc gpuQDesc;
        gpuQDesc.size = MEGA_BUFFER_CAPACITY * sizeof(uint64_t); // PackedQuad = 8 bytes
        gpuQDesc.bind_flags = BindFlag::UNORDERED_ACCESS | BindFlag::SHADER_RESOURCE;
        gpuQDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
        gpuQDesc.stride = sizeof(uint64_t); // uint2 = 8 bytes
        gpuQDesc.usage = Usage::DEFAULT;
        device_->CreateBuffer(&gpuQDesc, nullptr, &gpuQuadBuffer_);

        // Quad counter
        GPUBufferDesc cntDesc;
        cntDesc.size = sizeof(uint32_t);
        cntDesc.bind_flags = BindFlag::UNORDERED_ACCESS;
        cntDesc.misc_flags = ResourceMiscFlag::BUFFER_RAW;
        cntDesc.usage = Usage::DEFAULT;
        device_->CreateBuffer(&cntDesc, nullptr, &gpuQuadCounter_);

        // Readback buffer for quad counter (GPU → CPU)
        GPUBufferDesc rbDesc;
        rbDesc.size = sizeof(uint32_t);
        rbDesc.usage = Usage::READBACK;
        device_->CreateBuffer(&rbDesc, nullptr, &meshCounterReadback_);

        wi::backlog::post("VoxelRenderer: GPU compute mesher available");
    } else {
        wi::backlog::post("VoxelRenderer: GPU compute mesher not available", wi::backlog::LogLevel::Warning);
    }

    // ── GPU Smooth Mesher resources (Phase 5.3) ───────────────────
    wi::renderer::LoadShader(ShaderStage::CS, smoothCentroidShader_, "voxel/voxelSmoothCentroidCS.cso");
    wi::renderer::LoadShader(ShaderStage::CS, smoothMeshShader_, "voxel/voxelSmoothCS.cso");
    if (smoothCentroidShader_.IsValid() && smoothMeshShader_.IsValid()) {
        // Centroid grid buffer (34^3 float4, reused per-chunk sequentially)
        GPUBufferDesc cgDesc;
        cgDesc.size = CENTROID_GRID_SIZE * 16; // float4 = 16 bytes
        cgDesc.bind_flags = BindFlag::UNORDERED_ACCESS | BindFlag::SHADER_RESOURCE;
        cgDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
        cgDesc.stride = 16;
        cgDesc.usage = Usage::DEFAULT;
        device_->CreateBuffer(&cgDesc, nullptr, &centroidGridBuffer_);

        // GPU smooth vertex output buffer (GPUSmoothVertex = 32 bytes)
        GPUBufferDesc svDesc;
        svDesc.size = MAX_GPU_SMOOTH_VERTICES * 32;
        svDesc.bind_flags = BindFlag::UNORDERED_ACCESS | BindFlag::SHADER_RESOURCE;
        svDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
        svDesc.stride = 32;
        svDesc.usage = Usage::DEFAULT;
        device_->CreateBuffer(&svDesc, nullptr, &gpuSmoothVertexBuffer_);

        // Atomic counter
        GPUBufferDesc scDesc;
        scDesc.size = sizeof(uint32_t);
        scDesc.bind_flags = BindFlag::UNORDERED_ACCESS;
        scDesc.misc_flags = ResourceMiscFlag::BUFFER_RAW;
        scDesc.usage = Usage::DEFAULT;
        device_->CreateBuffer(&scDesc, nullptr, &gpuSmoothCounter_);

        // Readback
        GPUBufferDesc srbDesc;
        srbDesc.size = sizeof(uint32_t);
        srbDesc.usage = Usage::READBACK;
        device_->CreateBuffer(&srbDesc, nullptr, &smoothCounterReadback_);

        wi::backlog::post("VoxelRenderer: GPU smooth mesher available (2-pass with smooth normals)");
    }

    // ── Ray Tracing (Phase 6) ────────────────────────────────────────
    rt_.initialize(device_, MAX_BLAS_VERTICES);

    cpuChunkInfo_.reserve(MAX_CHUNKS);
    chunkSlots_.reserve(MAX_CHUNKS);

    initialized_ = true;
    wi::backlog::post("VoxelRenderer: initialized (GPU mesh pipeline)");
}

void VoxelRenderer::shutdown() {
    chunkSlots_.clear();
    cpuChunkInfo_.clear();
    initialized_ = false;
}

void VoxelRenderer::createPipeline() {
    // Constant buffer for per-frame data
    GPUBufferDesc cbDesc;
    cbDesc.size = sizeof(VoxelConstants);
    cbDesc.bind_flags = BindFlag::CONSTANT_BUFFER;
    cbDesc.usage = Usage::DEFAULT;
    device_->CreateBuffer(&cbDesc, nullptr, &constantBuffer_);

    // Anisotropic wrap sampler
    SamplerDesc samplerDesc;
    samplerDesc.filter = Filter::ANISOTROPIC;
    samplerDesc.address_u = TextureAddressMode::WRAP;
    samplerDesc.address_v = TextureAddressMode::WRAP;
    samplerDesc.address_w = TextureAddressMode::WRAP;
    samplerDesc.max_anisotropy = 16;
    device_->CreateSampler(&samplerDesc, &sampler_);

    // Load shaders
    wi::renderer::LoadShader(ShaderStage::VS, vertexShader_, "voxel/voxelVS.cso");
    wi::renderer::LoadShader(ShaderStage::PS, pixelShader_, "voxel/voxelPS.cso");
    if (!vertexShader_.IsValid() || !pixelShader_.IsValid()) {
        wi::backlog::post("VoxelRenderer: shader loading failed", wi::backlog::LogLevel::Error);
        return;
    }

    // Pipeline: backface cull, depth test, opaque blend, triangle list
    PipelineStateDesc psoDesc;
    psoDesc.vs = &vertexShader_;
    psoDesc.ps = &pixelShader_;
    psoDesc.rs = wi::renderer::GetRasterizerState(wi::enums::RSTYPE_FRONT);
    psoDesc.dss = wi::renderer::GetDepthStencilState(wi::enums::DSSTYPE_DEFAULT);
    psoDesc.bs = wi::renderer::GetBlendState(wi::enums::BSTYPE_OPAQUE);
    psoDesc.pt = PrimitiveTopology::TRIANGLELIST;

    device_->CreatePipelineState(&psoDesc, &pso_);

    // ── Toping pipeline (Phase 4) ────────────────────────────────
    wi::renderer::LoadShader(ShaderStage::VS, topingVS_, "voxel/voxelTopingVS.cso");
    wi::renderer::LoadShader(ShaderStage::PS, topingPS_, "voxel/voxelTopingPS.cso");

    if (topingVS_.IsValid() && topingPS_.IsValid()) {
        PipelineStateDesc topingPsoDesc;
        topingPsoDesc.vs = &topingVS_;
        topingPsoDesc.ps = &topingPS_;
        topingPsoDesc.rs = wi::renderer::GetRasterizerState(wi::enums::RSTYPE_FRONT);
        topingPsoDesc.dss = wi::renderer::GetDepthStencilState(wi::enums::DSSTYPE_DEFAULT);
        topingPsoDesc.bs = wi::renderer::GetBlendState(wi::enums::BSTYPE_OPAQUE);
        topingPsoDesc.pt = PrimitiveTopology::TRIANGLELIST;
        device_->CreatePipelineState(&topingPsoDesc, &topingPso_);
        wi::backlog::post("VoxelRenderer: toping pipeline created");
    } else {
        wi::backlog::post("VoxelRenderer: toping shader loading failed", wi::backlog::LogLevel::Warning);
    }

    // ── Smooth surface pipeline (Phase 5) ────────────────────────
    wi::renderer::LoadShader(ShaderStage::VS, smoothVS_, "voxel/voxelSmoothVS.cso");
    wi::renderer::LoadShader(ShaderStage::PS, smoothPS_, "voxel/voxelSmoothPS.cso");

    if (smoothVS_.IsValid() && smoothPS_.IsValid()) {
        // Custom rasterizer with depth bias to resolve z-fighting at smooth↔blocky boundaries
        smoothRasterizer_ = *wi::renderer::GetRasterizerState(wi::enums::RSTYPE_FRONT);
        smoothRasterizer_.depth_bias = 2;             // small integer bias
        smoothRasterizer_.slope_scaled_depth_bias = 1.0f; // scale with surface slope

        PipelineStateDesc smoothPsoDesc;
        smoothPsoDesc.vs = &smoothVS_;
        smoothPsoDesc.ps = &smoothPS_;
        smoothPsoDesc.rs = &smoothRasterizer_;
        smoothPsoDesc.dss = wi::renderer::GetDepthStencilState(wi::enums::DSSTYPE_DEFAULT);
        smoothPsoDesc.bs = wi::renderer::GetBlendState(wi::enums::BSTYPE_OPAQUE);
        smoothPsoDesc.pt = PrimitiveTopology::TRIANGLELIST;
        device_->CreatePipelineState(&smoothPsoDesc, &smoothPso_);
        wi::backlog::post("VoxelRenderer: smooth surface pipeline created");
    } else {
        wi::backlog::post("VoxelRenderer: smooth shader loading failed", wi::backlog::LogLevel::Warning);
    }
}

// ── Texture loading from PNG files ──────────────────────────────

void VoxelRenderer::loadTextures() {
    const int TEX_SIZE = 512;
    const int NUM_MATERIALS = 6;

    // Material texture files (RGBA PNG: RGB=albedo, A=heightmap)
    // CWD is the project root (see CLAUDE.md), assets/voxel/ is the source directory
    static const char* texturePaths[NUM_MATERIALS] = {
        "assets/voxel/grass_albedo.png",       // 1: Grass
        "assets/voxel/dirt_albedo.png",         // 2: Dirt
        "assets/voxel/stone_albedo.png",        // 3: Stone (blocky)
        "assets/voxel/sand_albedo.png",         // 4: Sand
        "assets/voxel/snow_albedo.png",         // 5: Snow
        "assets/voxel/smoothstone_albedo.png",  // 6: SmoothStone
    };

    std::vector<uint8_t> allPixels(TEX_SIZE * TEX_SIZE * 4 * NUM_MATERIALS, 128);

    for (int i = 0; i < NUM_MATERIALS; i++) {
        std::vector<uint8_t> fileData;
        if (!wi::helper::FileRead(texturePaths[i], fileData)) {
            wi::backlog::post(std::string("VoxelRenderer: failed to read ") + texturePaths[i],
                              wi::backlog::LogLevel::Warning);
            continue;
        }

        int w, h, channels;
        uint8_t* pixels = stbi_load_from_memory(
            fileData.data(), (int)fileData.size(),
            &w, &h, &channels, 4  // force RGBA
        );

        if (!pixels) {
            wi::backlog::post(std::string("VoxelRenderer: failed to decode ") + texturePaths[i],
                              wi::backlog::LogLevel::Warning);
            continue;
        }

        // Copy into the texture array slice, resizing if needed
        uint8_t* dst = allPixels.data() + i * TEX_SIZE * TEX_SIZE * 4;
        if (w == TEX_SIZE && h == TEX_SIZE) {
            memcpy(dst, pixels, TEX_SIZE * TEX_SIZE * 4);
        } else {
            // Nearest-neighbor resize (textures should already be 512x512 from prepare_textures.py)
            for (int dy = 0; dy < TEX_SIZE; dy++) {
                int sy = dy * h / TEX_SIZE;
                for (int dx = 0; dx < TEX_SIZE; dx++) {
                    int sx = dx * w / TEX_SIZE;
                    int srcIdx = (sy * w + sx) * 4;
                    int dstIdx = (dy * TEX_SIZE + dx) * 4;
                    dst[dstIdx + 0] = pixels[srcIdx + 0];
                    dst[dstIdx + 1] = pixels[srcIdx + 1];
                    dst[dstIdx + 2] = pixels[srcIdx + 2];
                    dst[dstIdx + 3] = pixels[srcIdx + 3];
                }
            }
        }

        stbi_image_free(pixels);
        wi::backlog::post(std::string("VoxelRenderer: loaded ") + texturePaths[i] +
                          " (" + std::to_string(w) + "x" + std::to_string(h) + ")");
    }

    TextureDesc texDesc;
    texDesc.type = TextureDesc::Type::TEXTURE_2D;
    texDesc.width = TEX_SIZE;
    texDesc.height = TEX_SIZE;
    texDesc.array_size = NUM_MATERIALS;
    texDesc.mip_levels = 1;
    texDesc.format = Format::R8G8B8A8_UNORM;
    texDesc.bind_flags = BindFlag::SHADER_RESOURCE;
    texDesc.usage = Usage::DEFAULT;

    std::vector<SubresourceData> subData(NUM_MATERIALS);
    for (int i = 0; i < NUM_MATERIALS; i++) {
        subData[i].data_ptr = allPixels.data() + i * TEX_SIZE * TEX_SIZE * 4;
        subData[i].row_pitch = TEX_SIZE * 4;
        subData[i].slice_pitch = TEX_SIZE * TEX_SIZE * 4;
    }

    device_->CreateTexture(&texDesc, subData.data(), &textureArray_);

    // ── Normal map texture array (RGB, t7) ──
    static const char* normalPaths[NUM_MATERIALS] = {
        "assets/voxel/grass_normal.png",
        "assets/voxel/dirt_normal.png",
        "assets/voxel/stone_normal.png",
        "assets/voxel/sand_normal.png",
        "assets/voxel/snow_normal.png",
        "assets/voxel/smoothstone_normal.png",
    };

    // Default normal = (128,128,255) = tangent-space flat normal (0,0,1)
    std::vector<uint8_t> normalPixels(TEX_SIZE * TEX_SIZE * 4 * NUM_MATERIALS);
    for (size_t j = 0; j < normalPixels.size(); j += 4) {
        normalPixels[j + 0] = 128;
        normalPixels[j + 1] = 128;
        normalPixels[j + 2] = 255;
        normalPixels[j + 3] = 255;
    }

    for (int i = 0; i < NUM_MATERIALS; i++) {
        std::vector<uint8_t> fileData;
        if (!wi::helper::FileRead(normalPaths[i], fileData))
            continue;

        int w, h, channels;
        uint8_t* pixels = stbi_load_from_memory(
            fileData.data(), (int)fileData.size(), &w, &h, &channels, 3
        );
        if (!pixels) continue;

        uint8_t* dst = normalPixels.data() + i * TEX_SIZE * TEX_SIZE * 4;
        for (int dy = 0; dy < TEX_SIZE; dy++) {
            int sy = (w == TEX_SIZE) ? dy : dy * h / TEX_SIZE;
            for (int dx = 0; dx < TEX_SIZE; dx++) {
                int sx = (w == TEX_SIZE) ? dx : dx * w / TEX_SIZE;
                int srcIdx = (sy * w + sx) * 3;
                int dstIdx = (dy * TEX_SIZE + dx) * 4;
                dst[dstIdx + 0] = pixels[srcIdx + 0]; // R
                dst[dstIdx + 1] = pixels[srcIdx + 1]; // G
                dst[dstIdx + 2] = pixels[srcIdx + 2]; // B
                dst[dstIdx + 3] = 255;
            }
        }

        stbi_image_free(pixels);
        wi::backlog::post(std::string("VoxelRenderer: loaded normal ") + normalPaths[i]);
    }

    // Reuse same texDesc but for normal array
    std::vector<SubresourceData> normalSub(NUM_MATERIALS);
    for (int i = 0; i < NUM_MATERIALS; i++) {
        normalSub[i].data_ptr = normalPixels.data() + i * TEX_SIZE * TEX_SIZE * 4;
        normalSub[i].row_pitch = TEX_SIZE * 4;
        normalSub[i].slice_pitch = TEX_SIZE * TEX_SIZE * 4;
    }
    device_->CreateTexture(&texDesc, normalSub.data(), &normalArray_);
}

// ── Mega-buffer rebuild ─────────────────────────────────────────
// Packs all chunk quads contiguously into a single buffer.
// Build chunkInfoBuffer (chunk metadata for GPU mesh path)
void VoxelRenderer::rebuildChunkInfoOnly(VoxelWorld& world) {
    chunkSlots_.clear();
    cpuChunkInfo_.clear();

    // First pass: build position → index map and chunk info
    std::unordered_map<uint64_t, uint32_t> posToIdx;
    auto posKey = [](const ChunkPos& p) -> uint64_t {
        return ((uint64_t)(uint16_t)p.x) | ((uint64_t)(uint16_t)p.y << 16) | ((uint64_t)(uint16_t)p.z << 32);
    };

    uint32_t idx = 0;
    float debugFlag = debugFaceColors_ ? 1.0f : 0.0f;

    world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
        ChunkSlot slot;
        slot.pos = pos;
        slot.quadOffset = 0;
        slot.quadCount = 0;
        chunkSlots_.push_back(slot);

        GPUChunkInfo info = {};
        info.worldPos = XMFLOAT4(
            (float)(pos.x * CHUNK_SIZE),
            (float)(pos.y * CHUNK_SIZE),
            (float)(pos.z * CHUNK_SIZE),
            debugFlag
        );
        info.quadOffset = 0;
        info.quadCount = 0;
        for (int i = 0; i < 6; i++) info.neighbors[i] = 0xFFFFFFFF;
        cpuChunkInfo_.push_back(info);
        posToIdx[posKey(pos)] = idx;
        idx++;
    });

    // Second pass: fill neighbor indices
    static const int offsets[6][3] = {
        {1,0,0}, {-1,0,0}, {0,1,0}, {0,-1,0}, {0,0,1}, {0,0,-1}
    };
    for (uint32_t i = 0; i < (uint32_t)chunkSlots_.size(); i++) {
        const auto& pos = chunkSlots_[i].pos;
        for (int f = 0; f < 6; f++) {
            ChunkPos npos = { pos.x + offsets[f][0], pos.y + offsets[f][1], pos.z + offsets[f][2] };
            auto it = posToIdx.find(posKey(npos));
            if (it != posToIdx.end()) {
                cpuChunkInfo_[i].neighbors[f] = it->second;
            }
        }
    }

    chunkCount_ = (uint32_t)chunkSlots_.size();
}

void VoxelRenderer::updateMeshes(VoxelWorld& world) {
    if (!device_) return;

    // Rebuild chunk info when any chunks are dirty
    bool anyDirty = false;
    world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
        if (chunk.dirty) { anyDirty = true; chunk.dirty = false; }
    });
    if (anyDirty || megaBufferDirty_) {
        rebuildChunkInfoOnly(world);
        // If cache wasn't already filled by fused regen+pack, mark for repack
        if (!gpuMeshDirty_) {
            voxelCacheDirty_ = true;
            gpuMeshDirty_ = true;
        }
        chunkInfoDirty_ = true;
        megaBufferDirty_ = false;
    }
}

// ── GPU Mesh Dispatch ────────────────────────────────────────────
// Dispatches GPU mesher for ALL chunks every frame. Replaces CPU greedy meshing.
// Uses the atomic quad counter for 1-frame-delayed readback of total quad count.

void VoxelRenderer::dispatchGpuMesh(CommandList cmd, const VoxelWorld& world,
    ProfileAccum* profPack, ProfileAccum* profUpload, ProfileAccum* profDispatch) const {
    auto* dev = device_;

    // Zero the quad counter
    uint32_t zero = 0;
    dev->UpdateBuffer(&gpuQuadCounter_, &zero, cmd, sizeof(uint32_t));

    // Barrier: COPY_DST → UAV for counter, UNDEFINED → UAV for output buffer
    GPUBarrier preBarriers[] = {
        GPUBarrier::Buffer(&gpuQuadCounter_, ResourceState::COPY_DST, ResourceState::UNORDERED_ACCESS),
        GPUBarrier::Buffer(&gpuQuadBuffer_, ResourceState::UNDEFINED, ResourceState::UNORDERED_ACCESS),
    };
    dev->Barrier(preBarriers, 2, cmd);

    dev->BindComputeShader(&meshShader_, cmd);

    // Pack and upload all chunks' voxel data
    // Each chunk = 32^3/2 = 16384 uint32 (two voxels per uint)
    const uint32_t wordsPerChunk = CHUNK_VOLUME / 2;
    uint32_t totalWords = chunkCount_ * wordsPerChunk;

    // Resize voxel data buffer if needed
    if (totalWords > voxelDataCapacity_) {
        voxelDataCapacity_ = totalWords;
        GPUBufferDesc voxDesc;
        voxDesc.size = totalWords * sizeof(uint32_t);
        voxDesc.bind_flags = BindFlag::SHADER_RESOURCE;
        voxDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
        voxDesc.stride = sizeof(uint32_t);
        voxDesc.usage = Usage::DEFAULT;
        dev->CreateBuffer(&voxDesc, nullptr, const_cast<GPUBuffer*>(&voxelDataBuffer_));
    }

    // Pack voxel data — use cached copy, only update when dirty.
    // VoxelData is exactly uint16_t, so voxels[] is a packed uint16 array.
    // Two consecutive uint16 = one uint32 → direct memcpy, no bit manipulation.
    static_assert(sizeof(VoxelData) == sizeof(uint16_t),
        "VoxelData must be 2 bytes for direct memcpy to GPU buffer");

    auto tPack0 = std::chrono::high_resolution_clock::now();
    if (voxelCacheDirty_) {
        packedVoxelCache_.resize(totalWords);
        uint32_t chunkI = 0;
        world.forEachChunk([&](const ChunkPos& pos, const Chunk& chunk) {
            std::memcpy(
                packedVoxelCache_.data() + chunkI * wordsPerChunk,
                chunk.voxels,
                wordsPerChunk * sizeof(uint32_t) // = CHUNK_VOLUME * 2 bytes
            );
            chunkI++;
        });
        voxelCacheDirty_ = false;
    }
    auto tPack1 = std::chrono::high_resolution_clock::now();
    if (profPack) profPack->add(std::chrono::duration<float, std::milli>(tPack1 - tPack0).count());

    // Upload all voxel data at once
    auto tUpload0 = std::chrono::high_resolution_clock::now();
    dev->UpdateBuffer(&voxelDataBuffer_, packedVoxelCache_.data(), cmd,
        totalWords * sizeof(uint32_t));
    auto tUpload1 = std::chrono::high_resolution_clock::now();
    if (profUpload) profUpload->add(std::chrono::duration<float, std::milli>(tUpload1 - tUpload0).count());

    // Bind resources (shared across all chunk dispatches)
    dev->BindResource(&voxelDataBuffer_, 0, cmd);
    dev->BindUAV(&gpuQuadBuffer_, 0, cmd);
    dev->BindUAV(&gpuQuadCounter_, 1, cmd);

    // Dispatch for each chunk
    struct MeshPush {
        uint32_t chunkIndex;
        uint32_t voxelBufferOffset;
        uint32_t quadBufferOffset;
        uint32_t maxOutputQuads;
        uint32_t pad[8];
    };

    auto tDisp0 = std::chrono::high_resolution_clock::now();
    uint32_t chunkIdx = 0;
    world.forEachChunk([&](const ChunkPos& pos, const Chunk& chunk) {
        MeshPush pushData = {};
        pushData.chunkIndex = chunkIdx;
        pushData.voxelBufferOffset = chunkIdx * wordsPerChunk;
        pushData.quadBufferOffset = 0;  // global atomic counter handles offsets
        pushData.maxOutputQuads = MEGA_BUFFER_CAPACITY;
        dev->PushConstants(&pushData, sizeof(pushData), cmd);

        // Dispatch: 32/8 = 4 groups per axis → 64 groups per chunk
        dev->Dispatch(4, 4, 4, cmd);
        chunkIdx++;
    });
    auto tDisp1 = std::chrono::high_resolution_clock::now();
    if (profDispatch) profDispatch->add(std::chrono::duration<float, std::milli>(tDisp1 - tDisp0).count());

    // Barriers: UAV → COPY_SRC for counter readback, UAV → SRV for quad buffer (rendering)
    GPUBarrier postBarriers[] = {
        GPUBarrier::Buffer(&gpuQuadCounter_, ResourceState::UNORDERED_ACCESS, ResourceState::COPY_SRC),
        GPUBarrier::Buffer(&gpuQuadBuffer_, ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE),
    };
    dev->Barrier(postBarriers, 2, cmd);

    // Copy quad counter to readback buffer (result available next frame)
    dev->CopyBuffer(&meshCounterReadback_, 0, &gpuQuadCounter_, 0, sizeof(uint32_t), cmd);

    totalQuads_ = gpuMeshQuadCount_; // display previous frame's count in HUD
    gpuMeshDirty_ = false;
}

// ── GPU Smooth Mesh Dispatch (Phase 5.3) ─────────────────────────
// Dispatches GPU Surface Nets compute shader for all chunks.
// Uses voxelDataBuffer_ (already uploaded by dispatchGpuMesh).

void VoxelRenderer::dispatchGpuSmoothMesh(CommandList cmd, const VoxelWorld& world) const {
    if (!smoothCentroidShader_.IsValid() || !smoothMeshShader_.IsValid()) return;
    auto* dev = device_;

    // ── Collect smooth chunk indices (chunks that contain smooth OR neighbor smooth) ──
    struct SmoothChunkEntry { uint32_t chunkIdx; };
    std::vector<SmoothChunkEntry> smoothChunks;
    smoothChunks.reserve(256);

    {
        // Build chunk index list + check containsSmooth for neighbors
        std::vector<std::pair<ChunkPos, uint32_t>> allChunks;
        allChunks.reserve(chunkCount_);
        uint32_t ci = 0;
        world.forEachChunk([&](const ChunkPos& pos, const Chunk& chunk) {
            allChunks.push_back({pos, ci});
            ci++;
        });

        // Build position→index map for neighbor lookup
        std::unordered_map<uint64_t, uint32_t> posToLocal;
        auto posKey = [](const ChunkPos& p) -> uint64_t {
            return ((uint64_t)(uint16_t)p.x) | ((uint64_t)(uint16_t)p.y << 16) | ((uint64_t)(uint16_t)p.z << 32);
        };
        for (uint32_t i = 0; i < (uint32_t)allChunks.size(); i++) {
            posToLocal[posKey(allChunks[i].first)] = i;
        }

        static const int offs[6][3] = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}};
        for (auto& [pos, idx] : allChunks) {
            const Chunk* c = world.getChunk(pos);
            if (!c) continue;
            bool needed = c->containsSmooth;
            if (!needed) {
                for (int f = 0; f < 6 && !needed; f++) {
                    ChunkPos np = {pos.x + offs[f][0], pos.y + offs[f][1], pos.z + offs[f][2]};
                    const Chunk* nc = world.getChunk(np);
                    if (nc && nc->containsSmooth) needed = true;
                }
            }
            if (needed) smoothChunks.push_back({idx});
        }
    }

    if (smoothChunks.empty()) {
        gpuSmoothMeshDirty_ = false;
        return;
    }

    uint32_t smoothCount = (uint32_t)smoothChunks.size();

    // ── Resize centroid grid buffer if needed (one slot per smooth chunk) ──
    uint32_t requiredGridSize = smoothCount * CENTROID_GRID_SIZE * 16; // bytes
    if (!centroidGridBuffer_.IsValid() || centroidGridBuffer_.desc.size < requiredGridSize) {
        GPUBufferDesc cgDesc;
        cgDesc.size = requiredGridSize;
        cgDesc.bind_flags = BindFlag::UNORDERED_ACCESS | BindFlag::SHADER_RESOURCE;
        cgDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
        cgDesc.stride = 16;
        cgDesc.usage = Usage::DEFAULT;
        dev->CreateBuffer(&cgDesc, nullptr, const_cast<GPUBuffer*>(&centroidGridBuffer_));
        wi::backlog::post("VoxelRenderer: resized centroid grid for " + std::to_string(smoothCount)
            + " smooth chunks (" + std::to_string(requiredGridSize / 1024) + " KB)");
    }

    // Zero the smooth vertex counter
    uint32_t zero = 0;
    dev->UpdateBuffer(const_cast<GPUBuffer*>(&gpuSmoothCounter_), &zero, cmd, sizeof(uint32_t));

    // Pre-barriers
    GPUBarrier preBarriers[] = {
        GPUBarrier::Buffer(const_cast<GPUBuffer*>(&gpuSmoothCounter_), ResourceState::COPY_DST, ResourceState::UNORDERED_ACCESS),
        GPUBarrier::Buffer(const_cast<GPUBuffer*>(&gpuSmoothVertexBuffer_), ResourceState::UNDEFINED, ResourceState::UNORDERED_ACCESS),
        GPUBarrier::Buffer(const_cast<GPUBuffer*>(&centroidGridBuffer_), ResourceState::UNDEFINED, ResourceState::UNORDERED_ACCESS),
    };
    dev->Barrier(preBarriers, 3, cmd);

    struct SmoothPush {
        uint32_t chunkIndex;
        uint32_t voxelBufferOffset;
        uint32_t maxOutputVerts;
        uint32_t centroidGridOffset;
        uint32_t pad[8];
    };

    const uint32_t wordsPerChunk = CHUNK_VOLUME / 2;

    // ── Pass 1: Dispatch ALL centroid computations (batched, no barriers) ──
    dev->BindComputeShader(&smoothCentroidShader_, cmd);
    dev->BindResource(&voxelDataBuffer_, 0, cmd);    // t0
    dev->BindResource(&chunkInfoBuffer_, 1, cmd);     // t1
    dev->BindUAV(const_cast<GPUBuffer*>(&centroidGridBuffer_), 0, cmd);  // u0

    for (uint32_t i = 0; i < smoothCount; i++) {
        uint32_t ci = smoothChunks[i].chunkIdx;
        SmoothPush pushData = {};
        pushData.chunkIndex = ci;
        pushData.voxelBufferOffset = ci * wordsPerChunk;
        pushData.maxOutputVerts = MAX_GPU_SMOOTH_VERTICES;
        pushData.centroidGridOffset = i * CENTROID_GRID_SIZE;
        dev->PushConstants(&pushData, sizeof(pushData), cmd);
        dev->Dispatch(5, 5, 5, cmd);
    }

    // ── Single barrier: centroid grid UAV → SRV ──
    GPUBarrier midBarrier = GPUBarrier::Buffer(
        const_cast<GPUBuffer*>(&centroidGridBuffer_),
        ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE);
    dev->Barrier(&midBarrier, 1, cmd);

    // ── Pass 2: Dispatch ALL emit passes (batched, no barriers) ──
    // Emit shader reads ONLY from centroid grid (no voxelData access)
    dev->BindComputeShader(&smoothMeshShader_, cmd);
    dev->BindResource(&chunkInfoBuffer_, 1, cmd);     // t1
    dev->BindResource(&centroidGridBuffer_, 2, cmd);  // t2: centroid grid (SRV)
    dev->BindUAV(const_cast<GPUBuffer*>(&gpuSmoothVertexBuffer_), 0, cmd);  // u0
    dev->BindUAV(const_cast<GPUBuffer*>(&gpuSmoothCounter_), 1, cmd);       // u1

    for (uint32_t i = 0; i < smoothCount; i++) {
        uint32_t ci = smoothChunks[i].chunkIdx;
        SmoothPush pushData = {};
        pushData.chunkIndex = ci;
        pushData.voxelBufferOffset = ci * wordsPerChunk;
        pushData.maxOutputVerts = MAX_GPU_SMOOTH_VERTICES;
        pushData.centroidGridOffset = i * CENTROID_GRID_SIZE;
        dev->PushConstants(&pushData, sizeof(pushData), cmd);
        dev->Dispatch(4, 4, 4, cmd);
    }

    // Post-barriers
    GPUBarrier postBarriers[] = {
        GPUBarrier::Buffer(const_cast<GPUBuffer*>(&gpuSmoothCounter_), ResourceState::UNORDERED_ACCESS, ResourceState::COPY_SRC),
        GPUBarrier::Buffer(const_cast<GPUBuffer*>(&gpuSmoothVertexBuffer_), ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE),
    };
    dev->Barrier(postBarriers, 2, cmd);

    // Readback counter (result available next frame)
    dev->CopyBuffer(const_cast<GPUBuffer*>(&smoothCounterReadback_), 0,
        const_cast<GPUBuffer*>(&gpuSmoothCounter_), 0, sizeof(uint32_t), cmd);

    gpuSmoothMeshDirty_ = false;
}

// ── Ray Tracing methods now in VoxelRTManager.cpp ────────────────


// ── Frustum plane extraction (Gribb-Hartmann method) ────────────
static void extractFrustumPlanes(const XMMATRIX& vp, XMFLOAT4 planes[6]) {
    XMFLOAT4X4 m;
    XMStoreFloat4x4(&m, vp);

    // Left
    planes[0] = XMFLOAT4(m._14 + m._11, m._24 + m._21, m._34 + m._31, m._44 + m._41);
    // Right
    planes[1] = XMFLOAT4(m._14 - m._11, m._24 - m._21, m._34 - m._31, m._44 - m._41);
    // Bottom
    planes[2] = XMFLOAT4(m._14 + m._12, m._24 + m._22, m._34 + m._32, m._44 + m._42);
    // Top
    planes[3] = XMFLOAT4(m._14 - m._12, m._24 - m._22, m._34 - m._32, m._44 - m._42);
    // Near
    planes[4] = XMFLOAT4(m._13, m._23, m._33, m._43);
    // Far
    planes[5] = XMFLOAT4(m._14 - m._13, m._24 - m._23, m._34 - m._33, m._44 - m._43);

    // Normalize each plane
    for (int i = 0; i < 6; i++) {
        float len = std::sqrt(planes[i].x * planes[i].x +
                              planes[i].y * planes[i].y +
                              planes[i].z * planes[i].z);
        if (len > 0.0001f) {
            planes[i].x /= len;
            planes[i].y /= len;
            planes[i].z /= len;
            planes[i].w /= len;
        }
    }
}

// ── Render pass ─────────────────────────────────────────────────

void VoxelRenderer::render(
    CommandList cmd,
    const wi::scene::CameraComponent& camera,
    const Texture& depthBuffer,
    const Texture& renderTarget,
    const Texture& normalTarget
) const {
    if (!initialized_ || chunkCount_ == 0 || !pso_.IsValid()) return;

    auto* dev = device_;

    // Upload chunk info only when chunks changed
    if (!cpuChunkInfo_.empty() && chunkInfoDirty_) {
        dev->UpdateBuffer(&chunkInfoBuffer_, cpuChunkInfo_.data(), cmd,
            cpuChunkInfo_.size() * sizeof(GPUChunkInfo));
        chunkInfoDirty_ = false;
    }

        // Per-frame constants
        VoxelConstants cb = {};
        XMMATRIX vpMatrix = camera.GetViewProjection();
        XMStoreFloat4x4(&cb.viewProjection, vpMatrix);
        XMMATRIX invVP = XMMatrixInverse(nullptr, vpMatrix);
        XMStoreFloat4x4(&cb.inverseViewProjection, invVP);
        cb.prevViewProjection = rt_.prevViewProjection; // from last frame
        cb.cameraPosition = XMFLOAT4(camera.Eye.x, camera.Eye.y, camera.Eye.z, 1.0f);
        cb.sunDirection = sunDirection_;
        cb.sunColor = XMFLOAT4(1.35f, 1.15f, 0.75f, 1.0f); // warm golden sun
        cb.chunkSize = (float)CHUNK_SIZE;
        cb.textureTiling = 0.25f;
        cb.blendEnabled = 1.0f; // Phase 3: PS-based blending enabled in GPU mesh path
        cb.debugBlend = debugBlend_ ? 1.0f : 0.0f;
        cb.chunkCount = chunkCount_;
        // bleedMask: bit N = material N can bleed onto neighbors
        // resistBleedMask: bit N = material N resists being bled onto
        cb.bleedMask = (1u << 1) | (1u << 2) | (1u << 4) | (1u << 5) | (1u << 6); // no stone (3)
        cb.resistBleedMask = (1u << 1); // grass resists bleed
        cb.windTime = windTime_;
        // Stylized lighting (Phase 7) — Wonderbox-inspired
        cb.skyAmbient    = XMFLOAT4(0.50f, 0.55f, 0.65f, 0.0f);   // cool sky fill
        cb.groundAmbient = XMFLOAT4(0.25f, 0.24f, 0.14f, 0.0f);   // warm green-brown ground bounce
        cb.shadowTint    = XMFLOAT4(0.50f, 0.42f, 0.70f, 0.0f);   // purple-blue shadows
        cb.fogColor      = XMFLOAT4(0.78f, 0.73f, 0.60f, 1.0f);   // warm sandy fog
        cb.fogParams     = XMFLOAT4(0.004f, 0.0f, 0.0f, 0.0f);    // fog density
        cb.rimColor      = XMFLOAT4(0.90f, 0.80f, 0.55f, 0.0f);   // warm golden rim
        cb.rimParams     = XMFLOAT4(2.5f, 0.45f, 0.0f, 0.0f);     // exponent, intensity
        cb.toneMapParams = XMFLOAT4(1.10f, 1.8f, normalStrength_, (float)debugLighting_); // saturation, exposure, normalStrength, debugLighting
        dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb));
        // Save current VP for next frame's temporal reprojection
        XMStoreFloat4x4(&rt_.prevViewProjection, vpMatrix);

        // Render pass (MRT: color + normals + depth)
        RenderPassImage rp[] = {
            RenderPassImage::RenderTarget(
                &renderTarget,
                RenderPassImage::LoadOp::CLEAR,
                RenderPassImage::StoreOp::STORE,
                ResourceState::SHADER_RESOURCE,
                ResourceState::SHADER_RESOURCE
            ),
            RenderPassImage::RenderTarget(
                &normalTarget,
                RenderPassImage::LoadOp::CLEAR,
                RenderPassImage::StoreOp::STORE,
                ResourceState::SHADER_RESOURCE,
                ResourceState::SHADER_RESOURCE
            ),
            RenderPassImage::DepthStencil(
                &depthBuffer,
                RenderPassImage::LoadOp::CLEAR,
                RenderPassImage::StoreOp::STORE,
                ResourceState::DEPTHSTENCIL,
                ResourceState::DEPTHSTENCIL,
                ResourceState::DEPTHSTENCIL
            ),
        };
        dev->RenderPassBegin(rp, 3, cmd);

        Viewport vp;
        vp.width = (float)renderTarget.GetDesc().width;
        vp.height = (float)renderTarget.GetDesc().height;
        vp.min_depth = 0.0f;
        vp.max_depth = 1.0f;
        dev->BindViewports(1, &vp, cmd);

        Rect scissor = { 0, 0, (int)vp.width, (int)vp.height };
        dev->BindScissorRects(1, &scissor, cmd);

        dev->BindPipelineState(&pso_, cmd);
        dev->BindConstantBuffer(&constantBuffer_, 0, cmd);
        dev->BindResource(&gpuQuadBuffer_, 0, cmd);  // GPU quads, not mega-buffer
        dev->BindResource(&textureArray_, 1, cmd);
        dev->BindResource(&chunkInfoBuffer_, 2, cmd);
        dev->BindResource(&voxelDataBuffer_, 3, cmd); // Phase 3: voxel data for PS neighbor lookups
        dev->BindResource(&normalArray_, 7, cmd);     // t7: normal maps
        dev->BindSampler(&sampler_, 0, cmd);

        // GPU mesh mode: flags=2, MUST be after BindPipelineState
        struct VoxelPush {
            uint32_t chunkIndex;
            uint32_t quadOffset;
            uint32_t flags;
            uint32_t pad[9];
        };
        VoxelPush pushData = {};
        pushData.flags = 2; // GPU mesh mode
        pushData.quadOffset = 0;
        dev->PushConstants(&pushData, sizeof(pushData), cmd);

        // Draw using previous frame's quad count (1-frame delay)
        if (gpuMeshQuadCount_ > 0) {
            dev->DrawInstanced(gpuMeshQuadCount_ * 6, 1, 0, 0, cmd);
            drawCalls_ = 1;
        }

    dev->RenderPassEnd(cmd);
    visibleChunks_ = chunkCount_;
}


// ── Phase 4: Toping GPU upload + rendering ─────────────────────

void VoxelRenderer::uploadTopingData(const TopingSystem& topingSystem) {
    if (!device_ || !topingPso_.IsValid()) return;

    // Upload mesh vertices (done once, meshes are static)
    const auto& verts = topingSystem.getVertices();
    if (!verts.empty() && !topingVertexBuffer_.IsValid()) {
        GPUBufferDesc vbDesc;
        vbDesc.size = verts.size() * sizeof(TopingVertex);
        vbDesc.bind_flags = BindFlag::SHADER_RESOURCE;
        vbDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
        vbDesc.stride = sizeof(TopingVertex);
        vbDesc.usage = Usage::DEFAULT;
        device_->CreateBuffer(&vbDesc, verts.data(), &topingVertexBuffer_);

        char msg[128];
        snprintf(msg, sizeof(msg), "Toping: uploaded %zu vertices (%zu bytes)",
            verts.size(), verts.size() * sizeof(TopingVertex));
        wi::backlog::post(msg);
    }

    // Upload instance positions (re-upload when world changes)
    const auto& instances = topingSystem.getInstances();
    if (instances.empty()) return;

    // Log a few grass toping positions (one-time, for camera placement debug)
    static bool loggedPositions = false;
    if (!loggedPositions) {
        loggedPositions = true;
        int count = 0;
        for (const auto& inst : instances) {
            if (inst.topingType == 1 && count < 5) { // type 1 = grass
                char msg[128];
                snprintf(msg, sizeof(msg), "GRASS toping: pos=(%.0f, %.0f, %.0f) variant=%d",
                    inst.wx, inst.wy, inst.wz, inst.variant);
                wi::backlog::post(msg);
                count++;
            }
        }
    }

    // GPU instances are just float3 (12 bytes), sorted by (type, variant) for batched draws.
    // We sort a copy and build a draw group table.
    // Reuse persistent vectors to avoid per-frame allocations.
    // Counting sort by (type, variant) — O(n) since key space is tiny
    // (types < ~10, variants 0-15 → max ~160 buckets).
    {
        const size_t n = instances.size();
        topingSorted_.resize(n);
        if (n > 0) {
            // Find max composite key to size the count array
            uint32_t maxKey = 0;
            for (size_t i = 0; i < n; i++) {
                uint32_t key = ((uint32_t)instances[i].topingType << 4) | instances[i].variant;
                if (key > maxKey) maxKey = key;
            }

            // Count occurrences
            std::vector<uint32_t> counts(maxKey + 2, 0);
            for (size_t i = 0; i < n; i++) {
                uint32_t key = ((uint32_t)instances[i].topingType << 4) | instances[i].variant;
                counts[key + 1]++;
            }

            // Prefix sum → write offsets
            for (uint32_t k = 1; k < (uint32_t)counts.size(); k++)
                counts[k] += counts[k - 1];

            // Scatter into sorted output
            for (size_t i = 0; i < n; i++) {
                uint32_t key = ((uint32_t)instances[i].topingType << 4) | instances[i].variant;
                uint32_t pos = counts[key]++;
                topingSorted_[pos] = { instances[i].wx, instances[i].wy, instances[i].wz,
                                       instances[i].topingType, instances[i].variant };
            }
        }
    }

    // Pack GPU instance data (just float3 positions)
    uint32_t instCount = (uint32_t)std::min(topingSorted_.size(), (size_t)MAX_TOPING_INSTANCES);
    topingGpuInsts_.resize(instCount);
    for (uint32_t i = 0; i < instCount; i++) {
        topingGpuInsts_[i] = { topingSorted_[i].wx, topingSorted_[i].wy, topingSorted_[i].wz };
    }

    // Pre-allocate instance buffer; only recreate when capacity needs to grow.
    // Data upload deferred to Render() via DeferredGPUBuffer::upload().
    if (topingInstanceBuf_.ensureCapacity(device_, instCount, sizeof(TopingGPUInst),
            BindFlag::SHADER_RESOURCE)) {
        char msg[128];
        snprintf(msg, sizeof(msg), "Toping: allocated instance buffer (%u capacity, %.1f KB)",
            topingInstanceBuf_.capacity, topingInstanceBuf_.capacity * sizeof(TopingGPUInst) / 1024.0f);
        wi::backlog::post(msg);
    } else {
        topingInstanceBuf_.markDirty(); // deferred upload in Render()
    }

    // ── Build draw groups + BLAS group table ───────────────────
    // Draw groups are built once here from sorted instances and reused by:
    // 1. renderTopings() — for instanced draw calls
    // 2. dispatchTopingBLASExtract() — GPU compute shader fills BLAS positions
    // This replaces the 196ms CPU loop that computed world-space positions.
    const auto& defs = topingSystem.getDefs();
    topingDrawGroups_.clear();
    if (instCount > 0) {
        TopingDrawGroup g;
        g.type = topingSorted_[0].type;
        g.variant = topingSorted_[0].variant;
        g.instanceOffset = 0;
        g.instanceCount = 1;
        g.vertexTemplateOffset = 0;
        g.vertexCount = 0;
        if (g.type < defs.size() && g.variant < 16) {
            const auto& slice = defs[g.type].variants[g.variant];
            g.vertexTemplateOffset = slice.offset;
            g.vertexCount = slice.count;
        }
        for (uint32_t i = 1; i < instCount; i++) {
            if (topingSorted_[i].type == g.type && topingSorted_[i].variant == g.variant) {
                g.instanceCount++;
            } else {
                if (g.vertexCount > 0) topingDrawGroups_.push_back(g);
                g.type = topingSorted_[i].type;
                g.variant = topingSorted_[i].variant;
                g.instanceOffset = i;
                g.instanceCount = 1;
                g.vertexTemplateOffset = 0;
                g.vertexCount = 0;
                if (g.type < defs.size() && g.variant < 16) {
                    const auto& slice = defs[g.type].variants[g.variant];
                    g.vertexTemplateOffset = slice.offset;
                    g.vertexCount = slice.count;
                }
            }
        }
        if (g.vertexCount > 0) topingDrawGroups_.push_back(g);
    }

    // Build GPU group table with prefix sums for the BLAS CS
    topingBLASGroupsGPU_.resize(topingDrawGroups_.size());
    uint32_t totalTopingVerts = 0;
    for (size_t i = 0; i < topingDrawGroups_.size(); i++) {
        const auto& dg = topingDrawGroups_[i];
        topingBLASGroupsGPU_[i] = {
            totalTopingVerts,          // globalVertexOffset (prefix sum)
            dg.vertexTemplateOffset,
            dg.vertexCount,
            dg.instanceOffset,
            dg.instanceCount
        };
        totalTopingVerts += dg.vertexCount * dg.instanceCount;
    }
    topingBLASTotalVertices_ = totalTopingVerts;

    // Delegate BLAS buffer allocation to VoxelRTManager
    if (totalTopingVerts > 0) {
        rt_.ensureTopingBLASCapacity(totalTopingVerts);
    }
}

void VoxelRenderer::renderTopings(
    CommandList cmd,
    const TopingSystem& topingSystem,
    const Texture& depthBuffer,
    const Texture& renderTarget,
    const Texture& normalTarget
) const {
    if (!topingPso_.IsValid() || !topingVertexBuffer_.IsValid() ||
        !topingInstanceBuf_.isValid()) return;

    const auto& instances = topingSystem.getInstances();
    const auto& defs = topingSystem.getDefs();
    if (instances.empty()) return;

    auto* dev = device_;

    // Open render pass with LOAD (preserve voxel render output)
    RenderPassImage rp[] = {
        RenderPassImage::RenderTarget(
            &renderTarget,
            RenderPassImage::LoadOp::LOAD,
            RenderPassImage::StoreOp::STORE,
            ResourceState::SHADER_RESOURCE,
            ResourceState::SHADER_RESOURCE
        ),
        RenderPassImage::RenderTarget(
            &normalTarget,
            RenderPassImage::LoadOp::LOAD,
            RenderPassImage::StoreOp::STORE,
            ResourceState::SHADER_RESOURCE,
            ResourceState::SHADER_RESOURCE
        ),
        RenderPassImage::DepthStencil(
            &depthBuffer,
            RenderPassImage::LoadOp::LOAD,
            RenderPassImage::StoreOp::STORE,
            ResourceState::DEPTHSTENCIL,
            ResourceState::DEPTHSTENCIL,
            ResourceState::DEPTHSTENCIL
        ),
    };
    dev->RenderPassBegin(rp, 3, cmd);

    // Viewport & scissor
    Viewport vp;
    vp.top_left_x = 0; vp.top_left_y = 0;
    vp.width  = (float)renderTarget.GetDesc().width;
    vp.height = (float)renderTarget.GetDesc().height;
    vp.min_depth = 0.0f; vp.max_depth = 1.0f;
    Rect scissor = { 0, 0, (int)renderTarget.GetDesc().width, (int)renderTarget.GetDesc().height };
    dev->BindViewports(1, &vp, cmd);
    dev->BindScissorRects(1, &scissor, cmd);

    // Bind toping pipeline (MUST be before PushConstants!)
    dev->BindPipelineState(&topingPso_, cmd);
    dev->BindConstantBuffer(&constantBuffer_, 0, cmd);
    dev->BindResource(&textureArray_, 1, cmd);
    dev->BindResource(&topingVertexBuffer_, 4, cmd);   // t4
    dev->BindResource(&topingInstanceBuf_.gpu, 5, cmd); // t5
    dev->BindResource(&normalArray_, 7, cmd);           // t7: normal maps
    dev->BindSampler(&sampler_, 0, cmd);

    // Reuse draw groups built in uploadTopingData (avoids redundant sort)
    topingDrawCalls_ = 0;
    struct TopingPush {
        uint32_t vertexOffset;
        uint32_t instanceOffset;
        uint32_t materialID;
        uint32_t pad[9];
    };

    for (const auto& g : topingDrawGroups_) {
        if (g.type >= defs.size()) continue;
        if (g.vertexCount == 0) continue;

        TopingPush pushData = {};
        pushData.vertexOffset = g.vertexTemplateOffset;
        pushData.instanceOffset = g.instanceOffset;
        pushData.materialID = defs[g.type].materialID;
        dev->PushConstants(&pushData, sizeof(pushData), cmd);

        dev->DrawInstanced(g.vertexCount, g.instanceCount, 0, 0, cmd);
        topingDrawCalls_++;
    }

    dev->RenderPassEnd(cmd);
}

// ── Phase 5: Smooth Surface Nets rendering (GPU compute only) ───

void VoxelRenderer::renderSmooth(
    CommandList cmd,
    const Texture& depthBuffer,
    const Texture& renderTarget,
    const Texture& normalTarget
) const {
    // GPU compute smooth buffer only (CPU fallback removed)
    const auto& smoothBuf = gpuSmoothVertexBuffer_;
    uint32_t vertCount = gpuSmoothVertexCount_;

    if (!smoothPso_.IsValid() || !smoothBuf.IsValid() || vertCount == 0) return;

    auto* dev = device_;

    // Open render pass with LOAD (preserve voxel + toping render output)
    RenderPassImage rp[] = {
        RenderPassImage::RenderTarget(
            &renderTarget,
            RenderPassImage::LoadOp::LOAD,
            RenderPassImage::StoreOp::STORE,
            ResourceState::SHADER_RESOURCE,
            ResourceState::SHADER_RESOURCE
        ),
        RenderPassImage::RenderTarget(
            &normalTarget,
            RenderPassImage::LoadOp::LOAD,
            RenderPassImage::StoreOp::STORE,
            ResourceState::SHADER_RESOURCE,
            ResourceState::SHADER_RESOURCE
        ),
        RenderPassImage::DepthStencil(
            &depthBuffer,
            RenderPassImage::LoadOp::LOAD,
            RenderPassImage::StoreOp::STORE,
            ResourceState::DEPTHSTENCIL,
            ResourceState::DEPTHSTENCIL,
            ResourceState::DEPTHSTENCIL
        ),
    };
    dev->RenderPassBegin(rp, 3, cmd);

    // Viewport & scissor
    Viewport vp;
    vp.top_left_x = 0; vp.top_left_y = 0;
    vp.width  = (float)renderTarget.GetDesc().width;
    vp.height = (float)renderTarget.GetDesc().height;
    vp.min_depth = 0.0f; vp.max_depth = 1.0f;
    Rect scissor = { 0, 0, (int)renderTarget.GetDesc().width, (int)renderTarget.GetDesc().height };
    dev->BindViewports(1, &vp, cmd);
    dev->BindScissorRects(1, &scissor, cmd);

    // Bind smooth pipeline (MUST be before PushConstants!)
    dev->BindPipelineState(&smoothPso_, cmd);
    dev->BindConstantBuffer(&constantBuffer_, 0, cmd);
    dev->BindResource(&textureArray_, 1, cmd);
    dev->BindResource(&chunkInfoBuffer_, 2, cmd);    // t2: chunk info for PS voxel lookups
    dev->BindResource(&voxelDataBuffer_, 3, cmd);    // t3: voxel data for PS neighbor blending
    dev->BindResource(&smoothBuf, 6, cmd); // t6: smooth vertices (GPU or CPU buffer)
    dev->BindResource(&normalArray_, 7, cmd);        // t7: normal maps
    dev->BindSampler(&sampler_, 0, cmd);

    // Push constants (unused by smooth VS, but must be valid 48 bytes)
    struct SmoothPush {
        uint32_t pad[12];
    };
    SmoothPush pushData = {};
    dev->PushConstants(&pushData, sizeof(pushData), cmd);

    // Single draw call for all smooth vertices
    dev->DrawInstanced(vertCount, 1, 0, 0, cmd);
    smoothDrawCalls_ = 1;

    dev->RenderPassEnd(cmd);
}

// ── VoxelRenderPath (custom RenderPath3D) ───────────────────────

void VoxelRenderPath::Start() {
    RenderPath3D::Start();

    auto* device = wi::graphics::GetDevice();
    renderer.initialize(device);
    renderer.debugFaceColors_ = debugMode;

    // Generate world
    if (debugSmooth) {
        world.generateDebugSmooth();
        camera_.set(15.0f, 12.0f, -5.0f, -0.5f, 0.8f);
    } else if (debugMode) {
        world.generateDebug();
        camera_.set(10.0f, 10.0f, 0.0f, -0.4f, 0.5f);
    } else {
        world.generateAround(camera_.pos.x, camera_.pos.y, camera_.pos.z, 4);
    }
    // Screenshot mode: fixed camera with good framing of terrain
    if (screenshotMode) {
        camera_.set(270.0f, 50.0f, 240.0f, -0.25f, 0.6f);
    }
    if (renderer.isInitialized()) {
        renderer.updateMeshes(world);
    }

    // Phase 4: Initialize toping system, collect instances, upload to GPU
    topingSystem.initialize();
    topingSystem.collectInstances(world);
    if (renderer.isInitialized()) {
        renderer.uploadTopingData(topingSystem);
    }
    {
        char msg[256];
        snprintf(msg, sizeof(msg),
            "TopingSystem: %zu defs, %zu vertices, %zu instances",
            topingSystem.getDefCount(),
            topingSystem.getVertexCount(),
            topingSystem.getInstanceCount());
        wi::backlog::post(msg);
    }

    // Phase 5: Smooth surface mesh — GPU compute only, dispatched in first Render()
    if (renderer.isInitialized()) {
        renderer.gpuSmoothMeshDirty_ = true;
        wi::backlog::post("SmoothMesher: GPU path active, dispatch deferred to Render()");
    }

    worldGenerated_ = true;

    setAO(AO_DISABLED);
    setFXAAEnabled(true);
    setBloomEnabled(false);

    createRenderTargets();
}

void VoxelRenderPath::createRenderTargets() {
    auto* device = wi::graphics::GetDevice();
    if (!device) return;

    uint32_t w = GetPhysicalWidth();
    uint32_t h = GetPhysicalHeight();
    if (w == 0 || h == 0) { w = 1920; h = 1080; }

    wi::graphics::TextureDesc rtDesc;
    rtDesc.type = wi::graphics::TextureDesc::Type::TEXTURE_2D;
    rtDesc.width = w;
    rtDesc.height = h;
    rtDesc.format = wi::graphics::Format::R8G8B8A8_UNORM;
    rtDesc.bind_flags = wi::graphics::BindFlag::RENDER_TARGET | wi::graphics::BindFlag::SHADER_RESOURCE
                      | wi::graphics::BindFlag::UNORDERED_ACCESS; // RT shadows modify in-place
    rtDesc.mip_levels = 1;
    rtDesc.sample_count = 1;
    rtDesc.layout = wi::graphics::ResourceState::SHADER_RESOURCE;
    device->CreateTexture(&rtDesc, nullptr, &voxelRT_);

    // Normal render target (world-space normals for RT shadows/AO)
    wi::graphics::TextureDesc normalDesc;
    normalDesc.type = wi::graphics::TextureDesc::Type::TEXTURE_2D;
    normalDesc.width = w;
    normalDesc.height = h;
    normalDesc.format = wi::graphics::Format::R16G16B16A16_SNORM;
    normalDesc.bind_flags = wi::graphics::BindFlag::RENDER_TARGET | wi::graphics::BindFlag::SHADER_RESOURCE;
    normalDesc.mip_levels = 1;
    normalDesc.sample_count = 1;
    normalDesc.layout = wi::graphics::ResourceState::SHADER_RESOURCE;
    device->CreateTexture(&normalDesc, nullptr, &voxelNormalRT_);

    wi::graphics::TextureDesc depthDesc;
    depthDesc.type = wi::graphics::TextureDesc::Type::TEXTURE_2D;
    depthDesc.width = w;
    depthDesc.height = h;
    depthDesc.format = wi::graphics::Format::D32_FLOAT;
    depthDesc.bind_flags = wi::graphics::BindFlag::DEPTH_STENCIL | wi::graphics::BindFlag::SHADER_RESOURCE;
    depthDesc.mip_levels = 1;
    depthDesc.sample_count = 1;
    depthDesc.layout = wi::graphics::ResourceState::DEPTHSTENCIL;
    device->CreateTexture(&depthDesc, nullptr, &voxelDepth_);

    // AO textures (R8_UNORM) for bilateral blur pipeline
    wi::graphics::TextureDesc aoDesc;
    aoDesc.type = wi::graphics::TextureDesc::Type::TEXTURE_2D;
    aoDesc.width = w;
    aoDesc.height = h;
    aoDesc.format = wi::graphics::Format::R8_UNORM;
    aoDesc.bind_flags = wi::graphics::BindFlag::SHADER_RESOURCE | wi::graphics::BindFlag::UNORDERED_ACCESS;
    aoDesc.mip_levels = 1;
    aoDesc.sample_count = 1;
    aoDesc.layout = wi::graphics::ResourceState::SHADER_RESOURCE;
    device->CreateTexture(&aoDesc, nullptr, &renderer.rt_.aoRawTexture);
    device->CreateTexture(&aoDesc, nullptr, &renderer.rt_.aoBlurredTexture);
    device->CreateTexture(&aoDesc, nullptr, &renderer.rt_.aoHistoryTexture);
    renderer.rt_.aoHistoryValid = false; // no history on first frame

    rtCreated_ = voxelRT_.IsValid() && voxelNormalRT_.IsValid() && voxelDepth_.IsValid()
              && renderer.rt_.aoRawTexture.IsValid() && renderer.rt_.aoBlurredTexture.IsValid();
    wi::backlog::post("VoxelRenderPath: render targets " + std::string(rtCreated_ ? "OK" : "FAILED")
        + " (" + std::to_string(w) + "x" + std::to_string(h) + ")");
}

// ── WASD camera input ───────────────────────────────────────────

static constexpr wi::input::BUTTON KEY_W = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('W' - 'A'));
static constexpr wi::input::BUTTON KEY_Z = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('Z' - 'A'));
static constexpr wi::input::BUTTON KEY_A = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('A' - 'A'));
static constexpr wi::input::BUTTON KEY_Q = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('Q' - 'A'));
static constexpr wi::input::BUTTON KEY_S = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('S' - 'A'));
static constexpr wi::input::BUTTON KEY_D = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('D' - 'A'));

void CameraController::handleInput(float dt, wi::scene::CameraComponent* cam) {
    if (wi::input::Press(wi::input::MOUSE_BUTTON_RIGHT)) {
        mouseCaptured = !mouseCaptured;
        wi::input::HidePointer(mouseCaptured);
    }

    if (mouseCaptured) {
        auto mouseState = wi::input::GetMouseState();
        yaw += mouseState.delta_position.x * sensitivity;
        pitch += mouseState.delta_position.y * sensitivity;
        pitch = std::clamp(pitch, -1.5f, 1.5f);
    }

    float cosPitch = std::cos(pitch);
    XMFLOAT3 forward(
        std::sin(yaw) * cosPitch,
        -std::sin(pitch),
        std::cos(yaw) * cosPitch
    );
    XMFLOAT3 right(std::cos(yaw), 0.0f, -std::sin(yaw));

    float spd = speed * dt;
    if (wi::input::Down(wi::input::KEYBOARD_BUTTON_LSHIFT)) spd *= 3.0f;

    if (wi::input::Down(KEY_Z)) { pos.x += forward.x * spd; pos.y += forward.y * spd; pos.z += forward.z * spd; }
    if (wi::input::Down(KEY_S)) { pos.x -= forward.x * spd; pos.y -= forward.y * spd; pos.z -= forward.z * spd; }
    if (wi::input::Down(KEY_Q)) { pos.x -= right.x * spd; pos.z -= right.z * spd; }
    if (wi::input::Down(KEY_D)) { pos.x += right.x * spd; pos.z += right.z * spd; }
    if (wi::input::Down(wi::input::KEYBOARD_BUTTON_SPACE))    pos.y += spd;
    if (wi::input::Down(wi::input::KEYBOARD_BUTTON_LCONTROL)) pos.y -= spd;

    cam->Eye = pos;
    cam->At = forward;
    cam->Up = XMFLOAT3(0, 1, 0);
    cam->UpdateCamera();
}

void VoxelRenderPath::Update(float dt) {
    auto frameStart = std::chrono::high_resolution_clock::now();
    prof_.frameStart = frameStart;
    // Measure GPU wait: time from last Compose() end to this Update() start
    if (prof_.lastComposeEndValid) {
        float gpuWaitMs = std::chrono::duration<float, std::milli>(frameStart - prof_.lastComposeEnd).count();
        if (gpuWaitMs > 0.01f) prof_.gpuWait.add(gpuWaitMs);
    }
    lastDt_ = dt;
    float instantFps = (dt > 0.0f) ? (1.0f / dt) : 0.0f;
    smoothFps_ = smoothFps_ * 0.95f + instantFps * 0.05f;

    // F-key toggles (kept here: they need access to renderer + anim state)
    if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F2)) {
        wi::backlog::Toggle();
    }
    if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F3)) {
        anim_.terrainAnimated = !anim_.terrainAnimated;
        if (anim_.terrainAnimated) {
            rtWasEnabled_ = renderer.rt_.isShadowsEnabled();
            renderer.rt_.setShadowsEnabled(false);
        } else {
            renderer.rt_.dirty = true;
            renderer.rt_.topingBLASDirty = true;
            renderer.rt_.aoHistoryValid = false;
            renderer.rt_.setShadowsEnabled(rtWasEnabled_);
        }
        wi::backlog::post(anim_.terrainAnimated ? "Animation: ON (30 Hz)" : "Animation: OFF");
    }
    if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F4)) {
        renderer.debugBlend_ = !renderer.debugBlend_;
        wi::backlog::post(renderer.debugBlend_ ? "Blend debug: ON" : "Blend debug: OFF");
    }
    if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F6)) {
        // In-app screenshot: saves voxelRT_ directly (immune to HDR/SDR mismatch)
        static int screenshotIdx = 0;
        char fname[64];
        snprintf(fname, sizeof(fname), "bvle_screenshot_%03d", screenshotIdx++);
        std::string pngName = std::string(fname) + ".png";
        std::string logName = std::string(fname) + ".log";
        wi::helper::saveTextureToFile(voxelRT_, pngName);
        // Write companion .log with debug info
        {
            std::string log = buildDebugLog();
            FILE* f = fopen(logName.c_str(), "w");
            if (f) { fputs(log.c_str(), f); fclose(f); }
        }
        wi::backlog::post(std::string("Screenshot saved: ") + pngName + " + " + logName);
    }
    if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F7)) {
        anim_.sunOrbit = !anim_.sunOrbit;
        wi::backlog::post(anim_.sunOrbit ? "Sun orbit: ON (10s cycle)" : "Sun orbit: OFF");
    }
    if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F8)) {
        anim_.showCrosshair = !anim_.showCrosshair;
        wi::backlog::post(anim_.showCrosshair ? "Crosshair + debug: ON" : "Crosshair + debug: OFF");
    }
    if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F9)) {
        anim_.debugLighting = (anim_.debugLighting + 1) % AnimationState::DEBUG_LIGHTING_MODES;
        static const char* modeNames[] = {
            "F9: ALL ON (normals+lighting+blend)",
            "F9: Normal maps OFF",
            "F9: FLAT lighting (no tex, no blend, no nmap)",
            "F9: ALBEDO only (texture, no lighting)",
            "F9: NdotL only (grayscale, flat normal)",
            "F9: NORMAL viz (RGB = XYZ geometric normal)"
        };
        wi::backlog::post(modeNames[anim_.debugLighting]);
    }
    if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F5)) {
        if (!renderer.rt_.isShadowsEnabled()) {
            renderer.rt_.setShadowsEnabled(true); renderer.rt_.setShadowDebug(0);
            wi::backlog::post("RT Shadows+AO: ON");
        } else if (renderer.rt_.getShadowDebug() == 0) {
            renderer.rt_.setShadowDebug(1);
            wi::backlog::post("RT Debug: SHADOWS (red=shadow, green=lit, blue=backface)");
        } else if (renderer.rt_.getShadowDebug() == 1) {
            renderer.rt_.setShadowDebug(2);
            wi::backlog::post("RT Debug: AO (white=open, black=occluded)");
        } else {
            renderer.rt_.setShadowsEnabled(false); renderer.rt_.setShadowDebug(0);
            wi::backlog::post("RT Shadows+AO: OFF");
        }
    }

    if (camera) camera_.handleInput(dt, camera);
    anim_.windTime += dt;
    renderer.windTime_ = anim_.windTime;
    renderer.normalStrength_ = (anim_.debugLighting == 0) ? 0.7f : 0.0f;
    renderer.debugLighting_ = anim_.debugLighting;

    // Sun direction: fixed or orbiting (F7)
    if (anim_.sunOrbit) {
        float t = anim_.windTime;
        constexpr float CYCLE = 10.0f; // 10 seconds per full orbit
        float angle = t * (2.0f * 3.14159265f / CYCLE);
        float altitude = 0.3f + 0.25f * std::sin(angle * 0.5f); // oscillates 0.05..0.55
        float x = -std::cos(angle);
        float z = -std::sin(angle);
        float len = std::sqrt(x * x + altitude * altitude + z * z);
        renderer.sunDirection_ = XMFLOAT4(x / len, -altitude / len, z / len, 0.0f);
    } else {
        renderer.sunDirection_ = XMFLOAT4(-0.7f, -0.4f, -0.3f, 0.0f);
    }

    // Animated terrain: regenerate at 30 Hz with time-shifted noise
    if (anim_.tick(dt) && renderer.isInitialized()) {
        // Prepare pack cache for fused regenerate+pack
        const uint32_t wordsPerChunk = CHUNK_VOLUME / 2;
        uint32_t totalWords = (uint32_t)world.chunkCount() * wordsPerChunk;
        renderer.packedVoxelCache_.resize(totalWords);

        auto t0 = std::chrono::high_resolution_clock::now();
        world.regenerateAnimated(anim_.time,
            renderer.packedVoxelCache_.data(), totalWords);
        auto t1 = std::chrono::high_resolution_clock::now();
        prof_.regenerate.add(std::chrono::duration<float, std::milli>(t1 - t0).count());

        renderer.voxelCacheDirty_ = false;
        renderer.gpuMeshDirty_ = true;
        renderer.rt_.aoHistoryValid = false;

        // Re-mesh smooth surfaces — GPU compute only
        renderer.gpuSmoothMeshDirty_ = true;

        // Re-collect toping instances — parallelized
        {
            auto tt0 = std::chrono::high_resolution_clock::now();
            topingSystem.collectInstancesParallel(world);
            auto tt1 = std::chrono::high_resolution_clock::now();
            prof_.topingCollect.add(std::chrono::duration<float, std::milli>(tt1 - tt0).count());
            renderer.uploadTopingData(topingSystem);
            auto tt2 = std::chrono::high_resolution_clock::now();
            prof_.topingUpload.add(std::chrono::duration<float, std::milli>(tt2 - tt1).count());
        }
    }

    if (renderer.isInitialized()) {
        auto t0 = std::chrono::high_resolution_clock::now();
        renderer.updateMeshes(world);
        auto t1 = std::chrono::high_resolution_clock::now();
        prof_.updateMeshes.add(std::chrono::duration<float, std::milli>(t1 - t0).count());
    }
    RenderPath3D::Update(dt);

    auto frameEnd = std::chrono::high_resolution_clock::now();
    prof_.frame.add(std::chrono::duration<float, std::milli>(frameEnd - frameStart).count());

    prof_.timer += dt;
    if (prof_.timer >= VoxelProfiler::INTERVAL) {
        prof_.log(renderer);
        prof_.resetAll();
        prof_.timer -= VoxelProfiler::INTERVAL;
    }
}

void VoxelRenderPath::Render() const {
    auto tWicked0 = std::chrono::high_resolution_clock::now();
    RenderPath3D::Render();
    auto tWicked1 = std::chrono::high_resolution_clock::now();
    prof_.wickedRender.add(std::chrono::duration<float, std::milli>(tWicked1 - tWicked0).count());

    if (renderer.isInitialized() && camera && rtCreated_) {
        auto* device = wi::graphics::GetDevice();
        CommandList cmd = device->BeginCommandList();

        // GPU mesh path: only re-dispatch when voxel data changed
        if (renderer.gpuMesherAvailable_) {
            // Always readback previous frame's quad count
            uint32_t* countData = (uint32_t*)renderer.meshCounterReadback_.mapped_data;
            if (countData) {
                renderer.gpuMeshQuadCount_ = *countData;
                renderer.totalQuads_ = renderer.gpuMeshQuadCount_;
            }
            // ── GPU Timestamp readback (previous frame's results) ──
            {
                uint64_t* tsData = (uint64_t*)renderer.timestampReadback_.mapped_data;
                if (tsData) {
                    double freq = (double)device->GetTimestampFrequency();
                    auto readTs = [&](uint32_t begin, uint32_t end) -> float {
                        if (freq > 0.0 && tsData[end] > tsData[begin])
                            return (float)((double)(tsData[end] - tsData[begin]) / freq * 1000.0);
                        return 0.0f;
                    };
                    renderer.gpuMeshTimeMs_ = readTs(VoxelRenderer::TS_GPU_MESH_BEGIN, VoxelRenderer::TS_GPU_MESH_END);
                    renderer.gpuSmoothMeshTimeMs_ = readTs(VoxelRenderer::TS_GPU_SMOOTH_BEGIN, VoxelRenderer::TS_GPU_SMOOTH_END);
                    renderer.gpuBLASExtractTimeMs_ = readTs(VoxelRenderer::TS_BLAS_EXTRACT_BEGIN, VoxelRenderer::TS_BLAS_EXTRACT_END);
                    renderer.gpuBLASBuildTimeMs_ = readTs(VoxelRenderer::TS_BLAS_BUILD_BEGIN, VoxelRenderer::TS_BLAS_BUILD_END);
                    renderer.gpuDrawTimeMs_ = readTs(VoxelRenderer::TS_DRAW_BEGIN, VoxelRenderer::TS_DRAW_END);
                    renderer.gpuRTShadowsTimeMs_ = readTs(VoxelRenderer::TS_RT_SHADOWS_BEGIN, VoxelRenderer::TS_RT_SHADOWS_END);
                }
            }

            // Only re-dispatch compute mesher when data changed
            if (renderer.gpuMeshDirty_) {
                device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_GPU_MESH_BEGIN, cmd);
                auto t0 = std::chrono::high_resolution_clock::now();
                renderer.dispatchGpuMesh(cmd, world,
                    &prof_.voxelPack, &prof_.gpuUpload, &prof_.gpuDispatch);
                auto t1 = std::chrono::high_resolution_clock::now();
                prof_.gpuMeshDispatch.add(std::chrono::duration<float, std::milli>(t1 - t0).count());
                device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_GPU_MESH_END, cmd);
            }

            // GPU smooth mesh: readback previous frame's vertex count
            if (renderer.smoothCounterReadback_.mapped_data) {
                uint32_t* smoothCount = (uint32_t*)renderer.smoothCounterReadback_.mapped_data;
                renderer.gpuSmoothVertexCount_ = *smoothCount;
            }
            // GPU smooth mesh dispatch (uses same voxelDataBuffer_ already uploaded)
            if (renderer.gpuSmoothMeshDirty_ && renderer.smoothCentroidShader_.IsValid() && renderer.smoothMeshShader_.IsValid()) {
                device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_GPU_SMOOTH_BEGIN, cmd);
                renderer.dispatchGpuSmoothMesh(cmd, world);
                device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_GPU_SMOOTH_END, cmd);
            }
            // Re-dispatch next frame if readback not yet available (1-frame delay)
            if (renderer.gpuSmoothVertexCount_ == 0 &&
                renderer.smoothCentroidShader_.IsValid() && renderer.smoothMeshShader_.IsValid()) {
                renderer.gpuSmoothMeshDirty_ = true;
            }

            // ── Deferred GPU uploads BEFORE compute dispatches that read them ──
            // topingInstanceBuf_ must be filled before dispatchTopingBLASExtract reads it (t5)
            renderer.topingInstanceBuf_.upload(device, cmd,
                renderer.topingGpuInsts_.data(), (uint32_t)renderer.topingGpuInsts_.size());

            // ── GPU compute toping BLAS extraction ──
            // Skip during animation (toping BLAS is skipped to save ~130ms GPU)
            auto& rt = renderer.rt_;
            if (rt.topingBLASDirty && !anim_.terrainAnimated) {
                device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_BLAS_EXTRACT_BEGIN, cmd);
                rt.dispatchTopingBLASExtract(cmd,
                    renderer.topingVertexBuffer_, renderer.topingInstanceBuf_.gpu,
                    renderer.topingBLASGroupsGPU_.data(),
                    renderer.topingBLASGroupsGPU_.size() * sizeof(VoxelRenderer::TopingBLASGroupGPU),
                    (uint32_t)renderer.topingBLASGroupsGPU_.size(),
                    renderer.topingBLASTotalVertices_);
                device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_BLAS_EXTRACT_END, cmd);
            }

            // Phase 6.1: BLAS extraction + acceleration structure build
            {
                uint32_t potentialInstances = 0;
                if (renderer.gpuMeshQuadCount_ > 0) potentialInstances++;
                if (renderer.gpuSmoothVertexCount_ >= 3) potentialInstances++;
                if (rt.getTopingVertexCount() >= 3) potentialInstances++;
                bool tlasNeedsMoreInstances = potentialInstances > rt.getTlasInstanceCount();

                bool needsBuild = rt.isAvailable() &&
                    renderer.gpuMeshQuadCount_ > 0 &&
                    (rt.dirty ||
                     renderer.gpuMeshQuadCount_ != rt.getBlockyTriCount() * 3 / 6 ||
                     tlasNeedsMoreInstances);

                if (needsBuild) {
                    device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_BLAS_BUILD_BEGIN, cmd);
                    rt.dispatchBLASExtract(cmd, renderer.gpuQuadBuffer_,
                        renderer.chunkInfoBuffer_, renderer.gpuMeshQuadCount_);

                    const auto& smoothVB = renderer.gpuSmoothVertexBuffer_;

                    if (anim_.terrainAnimated) {
                        uint32_t flags = (rtBuildSkipCounter_ & 1)
                            ? VoxelRTManager::BUILD_BLOCKY
                            : VoxelRTManager::BUILD_SMOOTH;
                        rtBuildSkipCounter_++;
                        rt.buildAccelerationStructures(cmd, flags, smoothVB, renderer.gpuSmoothVertexCount_);
                    } else {
                        rt.buildAccelerationStructures(cmd, VoxelRTManager::BUILD_ALL, smoothVB, renderer.gpuSmoothVertexCount_);
                    }

                    device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_BLAS_BUILD_END, cmd);
                }
            }
        }

        // ── Draw passes ──
        device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_DRAW_BEGIN, cmd);
        renderer.render(cmd, *camera, voxelDepth_, voxelRT_, voxelNormalRT_);
        renderer.renderTopings(cmd, topingSystem, voxelDepth_, voxelRT_, voxelNormalRT_);
        renderer.renderSmooth(cmd, voxelDepth_, voxelRT_, voxelNormalRT_);
        device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_DRAW_END, cmd);

        // Phase 6.2: RT Shadows + AO (skip in F9 debug modes 2-4 for raw diagnostic output)
        if (renderer.isRTShadowsEnabled() && renderer.isRTReady() && renderer.debugLighting_ < 2) {
            device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_RT_SHADOWS_BEGIN, cmd);
            renderer.rt_.dispatchShadows(cmd, voxelDepth_, voxelRT_, voxelNormalRT_,
                renderer.constantBuffer_);
            device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_RT_SHADOWS_END, cmd);
        }

        // Resolve GPU timestamps for readback next frame
        device->QueryResolve(&renderer.timestampHeap_, 0, VoxelRenderer::TS_COUNT,
            &renderer.timestampReadback_, 0, cmd);
    }
}

void VoxelProfiler::log(const VoxelRenderer& r) const {
    char msg[2048];
    snprintf(msg, sizeof(msg),
        "=== PERF PROFILE (avg over %.0fs) ===\n"
        "  ── Update() ──\n"
        "  Regenerate:       %7.2f ms  (%u calls)\n"
        "  UpdateMeshes:     %7.2f ms  (%u calls)\n"
        "  TopingCollect:    %7.2f ms  (%u calls)\n"
        "  TopingUpload:     %7.2f ms  (%u calls)\n"
        "  SmoothMesh(CPU):  %7.2f ms  (%u calls)\n"
        "  SmoothUpload:     %7.2f ms  (%u calls)\n"
        "  Update total:     %7.2f ms  (%u calls)\n"
        "  ── Render() ──\n"
        "  GPU Mesh (pack):  %7.2f ms  (%u calls)\n"
        "  GPU Mesh (up):    %7.2f ms  (%u calls)\n"
        "  GPU Mesh (disp):  %7.2f ms  (%u calls)\n"
        "  GPU MeshTotal:    %7.2f ms  (%u calls)\n"
        "  GPU SmoothMesh:   %7.2f ms  (%u calls)\n"
        "  BLAS Extract:     %7.2f ms  (%u calls)\n"
        "  BLAS/TLAS Build:  %7.2f ms  (%u calls)\n"
        "  Deferred Upload:  %7.2f ms  (%u calls)\n"
        "  Draw (3 passes):  %7.2f ms  (%u calls)\n"
        "  RT Shadows+AO:    %7.2f ms  (%u calls)\n"
        "  ── GPU Timings (hardware timestamps) ──\n"
        "  GPU Mesh:         %7.2f ms\n"
        "  GPU SmoothMesh:   %7.2f ms\n"
        "  GPU BLAS Extract: %7.2f ms\n"
        "  GPU BLAS Build:   %7.2f ms\n"
        "  GPU Draw:         %7.2f ms\n"
        "  GPU RT Shad+AO:   %7.2f ms\n"
        "  GPU Total:        %7.2f ms\n"
        "  ── Totals ──\n"
        "  Wicked Render:    %7.2f ms  (%u calls)\n"
        "  GPU Wait/Sync:    %7.2f ms  (%u calls)\n"
        "  CPU Frame:        %7.2f ms  (Update→Compose start)\n"
        "  True Frame:       %7.2f ms  (Update→Compose end)\n"
        "  Wall FPS:         %7.1f     (%u frames in %.0fs)",
        INTERVAL,
        regenerate.avg(), regenerate.count,
        updateMeshes.avg(), updateMeshes.count,
        topingCollect.avg(), topingCollect.count,
        topingUpload.avg(), topingUpload.count,
        smoothMesh.avg(), smoothMesh.count,
        smoothUpload.avg(), smoothUpload.count,
        frame.avg(), frame.count,
        voxelPack.avg(), voxelPack.count,
        gpuUpload.avg(), gpuUpload.count,
        gpuDispatch.avg(), gpuDispatch.count,
        gpuMeshDispatch.avg(), gpuMeshDispatch.count,
        gpuSmoothDispatch.avg(), gpuSmoothDispatch.count,
        blasExtract.avg(), blasExtract.count,
        blasBuild.avg(), blasBuild.count,
        deferredUpload.avg(), deferredUpload.count,
        render.avg(), render.count,
        rtShadows.avg(), rtShadows.count,
        r.getGpuMeshTimeMs(),
        r.getGpuSmoothMeshTimeMs(),
        r.getGpuBLASExtractTimeMs(),
        r.getGpuBLASBuildTimeMs(),
        r.getGpuDrawTimeMs(),
        r.getGpuRTShadowsTimeMs(),
        r.getGpuMeshTimeMs() + r.getGpuSmoothMeshTimeMs() +
            r.getGpuBLASExtractTimeMs() + r.getGpuBLASBuildTimeMs() +
            r.getGpuDrawTimeMs() + r.getGpuRTShadowsTimeMs(),
        wickedRender.avg(), wickedRender.count,
        gpuWait.avg(), gpuWait.count,
        fullFrame.avg(),
        trueFrame.avg(),
        trueFrame.count > 0 ? (1000.0f / trueFrame.avg()) : 0.0f,
        trueFrame.count, INTERVAL);
    wi::backlog::post(msg);
}

void VoxelProfiler::resetAll() {
    regenerate.reset(); updateMeshes.reset();
    voxelPack.reset(); gpuUpload.reset(); gpuDispatch.reset();
    gpuMeshDispatch.reset(); gpuSmoothDispatch.reset();
    smoothMesh.reset(); smoothUpload.reset();
    topingCollect.reset(); topingUpload.reset();
    blasExtract.reset(); blasBuild.reset();
    deferredUpload.reset(); render.reset(); rtShadows.reset();
    frame.reset(); fullFrame.reset();
    gpuWait.reset(); wickedRender.reset(); trueFrame.reset();
}

void VoxelRenderPath::Compose(CommandList cmd) const {
    frameCount_++;

    // Measure full frame time (Update + Render + Compose start)
    auto composeStart = std::chrono::high_resolution_clock::now();
    float fullFrameMs = std::chrono::duration<float, std::milli>(composeStart - prof_.frameStart).count();
    if (fullFrameMs > 0.1f) prof_.fullFrame.add(fullFrameMs);

    RenderPath3D::Compose(cmd);

    if (rtCreated_ && voxelRT_.IsValid()) {
        wi::image::Params fx;
        fx.enableFullScreen();
        fx.blendFlag = wi::enums::BLENDMODE_OPAQUE;

        // HDR support: when the swapchain is HDR, Compose() targets a linear float buffer
        // (rendertargetPreHDR10). Our voxelRT_ contains tone-mapped sRGB values, so we must
        // decode sRGB → linear and apply HDR scaling (same pattern as wiRenderPath2D).
        // Without this, the HDR10 PQ curve double-transforms our already-gamma'd values.
        if (colorspace != wi::graphics::ColorSpace::SRGB) {
            fx.enableLinearOutputMapping(hdr_scaling);
        }

        wi::image::Draw(&voxelRT_, fx, cmd);
    }

    // No HUD in screenshot mode
    if (screenshotMode) return;

    // HUD overlay
    wi::font::Params fp;
    fp.posX = 10; fp.posY = 10; fp.size = 20;
    fp.color = wi::Color(255, 255, 255, 230);
    fp.shadowColor = wi::Color(0, 0, 0, 180);

    char fpsStr[16];
    snprintf(fpsStr, sizeof(fpsStr), "%.1f", smoothFps_);
    char dtStr[16];
    snprintf(dtStr, sizeof(dtStr), "%.2f", lastDt_ * 1000.0f);

    std::string stats = "BVLE Voxel Engine (Phase 6 — Ray Tracing)\n";
    stats += "FPS: " + std::string(fpsStr) + " (" + std::string(dtStr) + " ms)\n";
    if (debugMode) {
        stats += "=== DEBUG FACE MODE ===\n";
        stats += "+X=Red  -X=DkRed  +Y=Green  -Y=DkGreen  +Z=Blue  -Z=DkBlue\n";
    }
    stats += "Chunks: " + std::to_string(renderer.getVisibleChunks())
           + "/" + std::to_string(renderer.getChunkCount()) + "\n";
    stats += "Quads: " + std::to_string(renderer.getTotalQuads()) + "\n";
    stats += "Draw Calls: " + std::to_string(renderer.getDrawCalls())
           + " (GPU mesh + DrawInstanced)\n";
    stats += "GPU Mesh Quads: " + std::to_string(renderer.getGpuMeshQuadCount()) + "\n";
    stats += "Topings: " + std::to_string(topingSystem.getInstanceCount())
           + " instances, " + std::to_string(renderer.getTopingDrawCalls())
           + " draws (" + std::to_string(topingSystem.getDefCount()) + " types)\n";
    if (renderer.getSmoothVertexCount() > 0) {
        stats += "Smooth: " + std::to_string(renderer.getSmoothVertexCount())
               + " verts (" + std::to_string(renderer.getSmoothVertexCount() / 3)
               + " tris), " + std::to_string(renderer.getSmoothDrawCalls()) + " draws\n";
    }
    if (renderer.isRTAvailable()) {
        if (renderer.isRTReady()) {
            stats += "RT: TLAS ready | Blocky "
                   + std::to_string(renderer.getRTBlockyTriCount()) + " tris | Smooth "
                   + std::to_string(renderer.getRTSmoothTriCount()) + " tris | Topings "
                   + std::to_string(renderer.getRTTopingTriCount()) + " tris"
                   + " | Shadows+AO " + std::string(renderer.rt_.getShadowDebug() == 1 ? "DBG_SHD" : (renderer.rt_.getShadowDebug() == 2 ? "DBG_AO" : (renderer.isRTShadowsEnabled() ? "ON" : "OFF"))) + "\n";
        } else {
            stats += "RT: building...\n";
        }
    } else {
        stats += "RT: not available\n";
    }
    stats += "WASD+Space/Ctrl: move | Shift: fast | Right-click: capture mouse\n";
    stats += "F2: console | F3: anim [" + std::string(anim_.terrainAnimated ? "ON" : "OFF")
        + "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF")
        + "] | F5: shd+ao [" + std::string(renderer.rt_.getShadowDebug() == 1 ? "SHD" : (renderer.rt_.getShadowDebug() == 2 ? "AO" : (renderer.isRTShadowsEnabled() ? "ON" : "OFF")))
        + "] | F7: sun [" + std::string(anim_.sunOrbit ? "ORBIT" : "FIXED")
        + "] | F8: xhair [" + std::string(anim_.showCrosshair ? "ON" : "OFF")
        + "] | F9: " + std::string(
            anim_.debugLighting == 0 ? "ALL" :
            anim_.debugLighting == 1 ? "noNMAP" :
            anim_.debugLighting == 2 ? "FLAT" :
            anim_.debugLighting == 3 ? "ALBEDO" :
            anim_.debugLighting == 4 ? "NdotL" : "NORMAL");

    wi::font::Draw(stats, fp, cmd);

    // ── Crosshair + face debug info (F8) ──
    if (anim_.showCrosshair) {
        float screenW = GetLogicalWidth();
        float screenH = GetLogicalHeight();
        float cx = screenW * 0.5f;
        float cy = screenH * 0.5f;

        // Draw crosshair lines using wi::texturehelper::getWhite() as solid 1x1 texture
        {
            const wi::graphics::Texture* whiteTex = wi::texturehelper::getWhite();
            wi::image::Params cp;
            cp.color = wi::Color(255, 255, 255, 200);
            cp.blendFlag = wi::enums::BLENDMODE_ALPHA;
            // Horizontal bar
            cp.pos = XMFLOAT3(cx - 12, cy - 1, 0);
            cp.siz = XMFLOAT2(24, 2);
            wi::image::Draw(whiteTex, cp, cmd);
            // Vertical bar
            cp.pos = XMFLOAT3(cx - 1, cy - 12, 0);
            cp.siz = XMFLOAT2(2, 24);
            wi::image::Draw(whiteTex, cp, cmd);
        }

        // DDA voxel raycast from camera center → update cached crosshairHit_
        {
            float cosPitch = std::cos(camera_.pitch);
            XMFLOAT3 rayDir(
                std::sin(camera_.yaw) * cosPitch,
                -std::sin(camera_.pitch),
                std::cos(camera_.yaw) * cosPitch
            );
            XMFLOAT3 rayPos = camera_.pos;

            int mapX = (int)std::floor(rayPos.x);
            int mapY = (int)std::floor(rayPos.y);
            int mapZ = (int)std::floor(rayPos.z);
            int stepX = (rayDir.x >= 0) ? 1 : -1;
            int stepY = (rayDir.y >= 0) ? 1 : -1;
            int stepZ = (rayDir.z >= 0) ? 1 : -1;

            float tDeltaX = (rayDir.x != 0.0f) ? std::abs(1.0f / rayDir.x) : 1e30f;
            float tDeltaY = (rayDir.y != 0.0f) ? std::abs(1.0f / rayDir.y) : 1e30f;
            float tDeltaZ = (rayDir.z != 0.0f) ? std::abs(1.0f / rayDir.z) : 1e30f;

            float tMaxX = (rayDir.x >= 0) ? ((mapX + 1) - rayPos.x) * tDeltaX : (rayPos.x - mapX) * tDeltaX;
            float tMaxY = (rayDir.y >= 0) ? ((mapY + 1) - rayPos.y) * tDeltaY : (rayPos.y - mapY) * tDeltaY;
            float tMaxZ = (rayDir.z >= 0) ? ((mapZ + 1) - rayPos.z) * tDeltaZ : (rayPos.z - mapZ) * tDeltaZ;

            crosshairHit_.valid = false;
            constexpr int MAX_STEPS = 200;
            constexpr float MAX_DIST = 200.0f;

            for (int i = 0; i < MAX_STEPS; i++) {
                int lastAxis = 0;
                if (tMaxX < tMaxY) {
                    if (tMaxX < tMaxZ) { mapX += stepX; tMaxX += tDeltaX; lastAxis = 0; }
                    else               { mapZ += stepZ; tMaxZ += tDeltaZ; lastAxis = 2; }
                } else {
                    if (tMaxY < tMaxZ) { mapY += stepY; tMaxY += tDeltaY; lastAxis = 1; }
                    else               { mapZ += stepZ; tMaxZ += tDeltaZ; lastAxis = 2; }
                }

                float dist = std::min({tMaxX - tDeltaX, tMaxY - tDeltaY, tMaxZ - tDeltaZ});
                if (dist > MAX_DIST) break;

                VoxelData v = world.getVoxel(mapX, mapY, mapZ);
                if (!v.isEmpty()) {
                    crosshairHit_.valid = true;
                    crosshairHit_.x = mapX; crosshairHit_.y = mapY; crosshairHit_.z = mapZ;
                    crosshairHit_.matID = v.getMaterialID();
                    crosshairHit_.smooth = v.isSmooth();
                    if (lastAxis == 0) crosshairHit_.face = (stepX > 0) ? 1 : 0;
                    else if (lastAxis == 1) crosshairHit_.face = (stepY > 0) ? 3 : 2;
                    else crosshairHit_.face = (stepZ > 0) ? 5 : 4;
                    break;
                }
            }
        }

        // Display debug info from buildDebugLog()
        std::string dbg = buildDebugLog();
        wi::font::Params dbgFp;
        dbgFp.posX = 10;
        dbgFp.posY = screenH - 180;
        dbgFp.size = 18;
        dbgFp.color = wi::Color(255, 255, 100, 230);
        dbgFp.shadowColor = wi::Color(0, 0, 0, 200);
        wi::font::Draw(dbg, dbgFp, cmd);
    }

    // Save compose end time for GPU wait measurement
    prof_.lastComposeEnd = std::chrono::high_resolution_clock::now();
    prof_.lastComposeEndValid = true;
    // True frame-to-frame time
    float trueFrameMs = std::chrono::duration<float, std::milli>(prof_.lastComposeEnd - prof_.frameStart).count();
    if (trueFrameMs > 0.1f) prof_.trueFrame.add(trueFrameMs);
}

std::string VoxelRenderPath::buildDebugLog() const {
    static const char* matNames[] = { "air", "grass", "dirt", "stone", "sand", "snow", "smoothstone" };
    static const char* faceNames[] = { "+X", "-X", "+Y", "-Y", "+Z", "-Z" };
    static const char* faceNormals[] = { "(1,0,0)", "(-1,0,0)", "(0,1,0)", "(0,-1,0)", "(0,0,1)", "(0,0,-1)" };

    char buf[512];
    float cosPitch = std::cos(camera_.pitch);
    float dirX = std::sin(camera_.yaw) * cosPitch;
    float dirY = -std::sin(camera_.pitch);
    float dirZ = std::cos(camera_.yaw) * cosPitch;

    std::string log;

    // Camera info
    snprintf(buf, sizeof(buf),
        "Cam: (%.1f, %.1f, %.1f) yaw=%.1f deg  pitch=%.1f deg\n"
        "Dir: (%.3f, %.3f, %.3f)\n"
        "Sun: (%.3f, %.3f, %.3f)\n",
        camera_.pos.x, camera_.pos.y, camera_.pos.z,
        camera_.yaw * 57.2958f, camera_.pitch * 57.2958f,
        dirX, dirY, dirZ,
        renderer.sunDirection_.x, renderer.sunDirection_.y, renderer.sunDirection_.z);
    log += buf;

    // Crosshair target
    if (crosshairHit_.valid) {
        const char* mName = (crosshairHit_.matID < 7) ? matNames[crosshairHit_.matID] : "unknown";
        int f = crosshairHit_.face;
        const char* fName = (f >= 0 && f < 6) ? faceNames[f] : "?";
        const char* fNorm = (f >= 0 && f < 6) ? faceNormals[f] : "?";
        snprintf(buf, sizeof(buf),
            "Target: (%d, %d, %d) mat=%d (%s) %s\n"
            "Face: %s  Normal: %s\n"
            "NMap proj: %s-axis\n",
            crosshairHit_.x, crosshairHit_.y, crosshairHit_.z,
            crosshairHit_.matID, mName,
            crosshairHit_.smooth ? "[smooth]" : "[blocky]",
            fName, fNorm,
            (f == 0 || f == 1) ? "X (UV=zy)" :
            (f == 2 || f == 3) ? "Y (UV=xz, GL-flip)" :
            "Z (UV=xy)");
        log += buf;
    } else {
        log += "Target: none (sky)\n";
    }

    // Debug tool states
    snprintf(buf, sizeof(buf),
        "--- Debug states ---\n"
        "FPS: %.1f (%.2f ms)\n"
        "Chunks: %u/%u  Quads: %u  GPU Mesh: %u\n"
        "Smooth verts: %u  Toping instances: %zu\n"
        "Animation: %s | Blend debug: %s | Sun orbit: %s | Crosshair: %s | F9: %s\n"
        "RT available: %s | RT shadows+AO: %s | RT debug: %s\n"
        "Debug face mode: %s | Debug smooth: %s\n",
        smoothFps_, lastDt_ * 1000.0f,
        renderer.getVisibleChunks(), renderer.getChunkCount(),
        renderer.getTotalQuads(), renderer.getGpuMeshQuadCount(),
        renderer.getSmoothVertexCount(), topingSystem.getInstanceCount(),
        anim_.terrainAnimated ? "ON" : "OFF",
        renderer.debugBlend_ ? "ON" : "OFF",
        anim_.sunOrbit ? "ORBIT" : "FIXED",
        anim_.showCrosshair ? "ON" : "OFF",
        anim_.debugLighting == 0 ? "ALL" :
            anim_.debugLighting == 1 ? "noNMAP" :
            anim_.debugLighting == 2 ? "FLAT" :
            anim_.debugLighting == 3 ? "ALBEDO" :
            anim_.debugLighting == 4 ? "NdotL" : "NORMAL",
        renderer.isRTAvailable() ? "yes" : "no",
        renderer.isRTShadowsEnabled() ? "ON" : "OFF",
        renderer.rt_.getShadowDebug() == 1 ? "SHADOWS" :
            (renderer.rt_.getShadowDebug() == 2 ? "AO" : "OFF"),
        debugMode ? "ON" : "OFF",
        debugSmooth ? "ON" : "OFF");
    log += buf;

    if (renderer.isRTAvailable() && renderer.isRTReady()) {
        snprintf(buf, sizeof(buf),
            "RT tris: blocky=%u  smooth=%u  topings=%u\n",
            renderer.getRTBlockyTriCount(),
            renderer.getRTSmoothTriCount(),
            renderer.getRTTopingTriCount());
        log += buf;
    }

    return log;
}

void VoxelRenderPath::resetAOHistory() {
    renderer.rt_.aoHistoryValid = false;
    renderer.rt_.frameCounter = 0;
}

} // namespace voxel
