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

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;
    }
    generateTextures();

    // 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.1) ────────────────────────────────────
    rtAvailable_ = device_->CheckCapability(GraphicsDeviceCapability::RAYTRACING);
    if (rtAvailable_) {
        wi::renderer::LoadShader(ShaderStage::CS, blasExtractShader_, "voxel/voxelBLASExtractCS.cso");
        if (blasExtractShader_.IsValid()) {
            // BLAS position buffer: 6 float3 per quad (non-indexed triangles)
            // Use BUFFER_RAW (ByteAddressBuffer) — structured buffers may not work as BLAS vertex input
            GPUBufferDesc posDesc;
            posDesc.size = (uint64_t)MAX_BLAS_VERTICES * sizeof(float) * 3; // float3 per vertex
            posDesc.bind_flags = BindFlag::UNORDERED_ACCESS | BindFlag::SHADER_RESOURCE;
            posDesc.misc_flags = ResourceMiscFlag::BUFFER_RAW;
            posDesc.stride = 0; // raw buffer, no stride
            posDesc.usage = Usage::DEFAULT;
            bool ok = device_->CreateBuffer(&posDesc, nullptr, &blasPositionBuffer_);
            // Sequential index buffer for BLAS (DX12 requires valid index buffer,
            // Wicked always writes IndexBuffer GPU address even for "non-indexed").
            GPUBufferDesc idxDesc;
            idxDesc.size = (uint64_t)MAX_BLAS_VERTICES * sizeof(uint32_t);
            idxDesc.bind_flags = BindFlag::SHADER_RESOURCE;
            idxDesc.usage = Usage::DEFAULT;
            auto fillIndices = [](void* dest) {
                uint32_t* p = (uint32_t*)dest;
                for (uint32_t i = 0; i < MAX_BLAS_VERTICES; i++)
                    p[i] = i;
            };
            bool okIdx = device_->CreateBuffer2(&idxDesc, fillIndices, &blasIndexBuffer_);

            if (ok && blasPositionBuffer_.IsValid() && okIdx && blasIndexBuffer_.IsValid()) {
                device_->SetName(&blasPositionBuffer_, "VoxelRenderer::blasPositionBuffer");
                device_->SetName(&blasIndexBuffer_, "VoxelRenderer::blasIndexBuffer");
                wi::backlog::post("VoxelRenderer: RT available (BLAS pos "
                    + std::to_string(posDesc.size / (1024*1024)) + " MB + idx "
                    + std::to_string(idxDesc.size / (1024*1024)) + " MB)");
            } else {
                rtAvailable_ = false;
                wi::backlog::post("VoxelRenderer: RT buffer creation failed", wi::backlog::LogLevel::Warning);
            }
        } else {
            rtAvailable_ = false;
            wi::backlog::post("VoxelRenderer: RT available but BLAS extraction shader failed", wi::backlog::LogLevel::Warning);
        }
        // ── Toping BLAS CS (replaces 196ms CPU loop) ─────────────────
        wi::renderer::LoadShader(ShaderStage::CS, topingBLASShader_, "voxel/voxelTopingBLASCS.cso");
        if (topingBLASShader_.IsValid()) {
            // Pre-allocate small group table buffer (max 64 groups × 20 bytes = 1.25 KB)
            GPUBufferDesc grpDesc;
            grpDesc.size = MAX_TOPING_BLAS_GROUPS * sizeof(TopingBLASGroupGPU);
            grpDesc.bind_flags = BindFlag::SHADER_RESOURCE;
            grpDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
            grpDesc.stride = sizeof(TopingBLASGroupGPU);
            grpDesc.usage = Usage::DEFAULT;
            device_->CreateBuffer(&grpDesc, nullptr, &topingBLASGroupBuffer_);
            wi::backlog::post("VoxelRenderer: toping BLAS CS available");
        } else {
            wi::backlog::post("VoxelRenderer: toping BLAS CS failed", wi::backlog::LogLevel::Warning);
        }

        // ── RT Shadows + AO (Phase 6.2 + 6.3) ────────────────────────
        wi::renderer::LoadShader(ShaderStage::CS, shadowShader_, "voxel/voxelShadowCS.cso",
            wi::graphics::ShaderModel::SM_6_5);
        wi::renderer::LoadShader(ShaderStage::CS, aoBlurShader_, "voxel/voxelAOBlurCS.cso");
        wi::renderer::LoadShader(ShaderStage::CS, aoApplyShader_, "voxel/voxelAOApplyCS.cso");
        if (shadowShader_.IsValid() && aoBlurShader_.IsValid() && aoApplyShader_.IsValid()) {
            rtShadowsEnabled_ = true;
            wi::backlog::post("VoxelRenderer: RT shadows + AO blur available");
        } else {
            wi::backlog::post("VoxelRenderer: RT shadow/AO shader(s) failed to compile",
                wi::backlog::LogLevel::Warning);
        }
    } else {
        wi::backlog::post("VoxelRenderer: RT not available (GPU does not support ray tracing)");
    }

    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);
    }
}

// ── Procedural texture generation ───────────────────────────────

static void generateNoiseTexture(uint8_t* pixels, int w, int h,
    uint8_t r0, uint8_t g0, uint8_t b0,
    uint8_t r1, uint8_t g1, uint8_t b1,
    uint32_t seed, float heightFreq = 1.0f, float heightContrast = 1.0f)
{
    uint32_t s = seed;
    uint32_t s2 = seed * 7919u + 104729u; // separate seed for heightmap
    for (int y = 0; y < h; y++) {
        for (int x = 0; x < w; x++) {
            s = s * 1664525u + 1013904223u;
            float noise = (float)(s & 0xFFFF) / 65535.0f;
            float fx = (float)x / w;
            float fy = (float)y / h;
            float pattern = 0.5f + 0.5f * std::sin(fx * 20.0f + noise * 3.0f) *
                            std::cos(fy * 20.0f + noise * 3.0f);
            float t = noise * 0.6f + pattern * 0.4f;

            int idx = (y * w + x) * 4;
            pixels[idx + 0] = (uint8_t)(r0 + (r1 - r0) * t);
            pixels[idx + 1] = (uint8_t)(g0 + (g1 - g0) * t);
            pixels[idx + 2] = (uint8_t)(b0 + (b1 - b0) * t);

            // Heightmap in alpha: separate noise for height-based material blending
            s2 = s2 * 1664525u + 1013904223u;
            float hn = (float)(s2 & 0xFFFF) / 65535.0f;
            float hPattern = 0.5f + 0.5f * std::sin(fx * 12.0f * heightFreq + hn * 2.0f) *
                             std::cos(fy * 12.0f * heightFreq + hn * 2.0f);
            float heightVal = hn * 0.5f + hPattern * 0.5f;
            heightVal = std::clamp(heightVal * heightContrast, 0.0f, 1.0f);
            pixels[idx + 3] = (uint8_t)(heightVal * 255.0f);
        }
    }
}

void VoxelRenderer::generateTextures() {
    const int TEX_SIZE = 256;
    const int NUM_MATERIALS = 6;

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

    struct MatColor {
        uint8_t r0,g0,b0, r1,g1,b1;
        uint32_t seed;
        float heightFreq;     // heightmap noise frequency
        float heightContrast; // heightmap contrast (higher = more defined peaks)
    };
    MatColor colors[NUM_MATERIALS] = {
        { 50, 140, 35,   80, 180, 55,  101, 1.5f, 0.8f },  // 1: Grass: natural rich green
        { 100, 70, 40,  140, 100, 60,  202, 0.8f, 0.6f },  // 2: Dirt: smooth mounds
        { 80, 80, 90,   120, 120, 130, 303, 2.5f, 0.5f },  // 3: Stone (blocky): darker blue-gray
        { 220, 200, 130, 245, 230, 160, 404, 3.0f, 0.4f },  // 4: Sand: warmer yellow, fine
        { 220, 225, 230, 245, 248, 252, 505, 1.0f, 0.5f },  // 5: Snow: smooth, soft
        { 100, 100, 110, 145, 145, 155, 606, 2.0f, 0.6f },  // 6: SmoothStone: lighter blue-gray, distinct from blocky stone
    };

    for (int i = 0; i < NUM_MATERIALS; i++) {
        auto& c = colors[i];
        generateNoiseTexture(
            allPixels.data() + i * TEX_SIZE * TEX_SIZE * 4,
            TEX_SIZE, TEX_SIZE,
            c.r0, c.g0, c.b0, c.r1, c.g1, c.b1, c.seed,
            c.heightFreq, c.heightContrast
        );
    }

    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_);
}

// ── 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: BLAS extraction + AS build (Phase 6.1) ──────────

void VoxelRenderer::dispatchBLASExtract(CommandList cmd) const {
    if (!rtAvailable_ || !blasExtractShader_.IsValid()) return;

    auto* dev = device_;
    uint32_t quadCount = gpuMeshQuadCount_;
    if (quadCount == 0) return;

    // Pre-barriers: blasPositionBuffer_ UNDEFINED → UAV
    GPUBarrier preBarriers[] = {
        GPUBarrier::Buffer(&blasPositionBuffer_,
            ResourceState::UNDEFINED, ResourceState::UNORDERED_ACCESS),
    };
    dev->Barrier(preBarriers, 1, cmd);

    // Bind compute shader
    dev->BindComputeShader(&blasExtractShader_, cmd);

    // Bind resources: t0 = gpuQuadBuffer (SRV), t2 = chunkInfoBuffer (SRV), u0 = blasPositionBuffer (UAV)
    dev->BindResource(&gpuQuadBuffer_, 0, cmd);     // t0
    dev->BindResource(&chunkInfoBuffer_, 2, cmd);   // t2
    dev->BindUAV(&blasPositionBuffer_, 0, cmd);     // u0

    // Push constants: quadCount
    struct BLASPush {
        uint32_t quadCount;
        uint32_t pad[11];
    } pushData = {};
    pushData.quadCount = quadCount;
    dev->PushConstants(&pushData, sizeof(pushData), cmd);

    // Dispatch: 64 threads per group
    uint32_t groupCount = (quadCount + 63) / 64;
    dev->Dispatch(groupCount, 1, 1, cmd);

    // Post-barrier: blasPositionBuffer_ UAV → SHADER_RESOURCE (for BLAS build)
    GPUBarrier postBarriers[] = {
        GPUBarrier::Buffer(&blasPositionBuffer_,
            ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE),
    };
    dev->Barrier(postBarriers, 1, cmd);

    rtBlockyVertexCount_ = quadCount * 6;
}

// ── GPU compute toping BLAS position extraction ─────────────────
// Replaces the 196ms CPU nested loop. Reads vertex templates (t4) +
// instance positions (t5) + group table (t7), writes positions to u0.
void VoxelRenderer::dispatchTopingBLASExtract(CommandList cmd) const {
    if (!topingBLASShader_.IsValid() || !topingBLASGroupBuffer_.IsValid() ||
        !topingBLASPositionBuffer_.IsValid() || !topingVertexBuffer_.IsValid() ||
        !topingInstanceBuffer_.IsValid() || topingBLASTotalVertices_ == 0 ||
        topingBLASGroupsGPU_.empty()) return;

    auto* dev = device_;

    // Upload group table (tiny: ~32 × 20 bytes = 640 bytes)
    size_t groupUploadSize = topingBLASGroupsGPU_.size() * sizeof(TopingBLASGroupGPU);
    dev->UpdateBuffer(&topingBLASGroupBuffer_,
        topingBLASGroupsGPU_.data(), cmd, groupUploadSize);

    // Pre-barriers
    GPUBarrier preBarriers[] = {
        GPUBarrier::Buffer(&topingBLASGroupBuffer_,
            ResourceState::COPY_DST, ResourceState::SHADER_RESOURCE),
        GPUBarrier::Buffer(&topingBLASPositionBuffer_,
            ResourceState::UNDEFINED, ResourceState::UNORDERED_ACCESS),
    };
    dev->Barrier(preBarriers, 2, cmd);

    // Bind compute shader + resources
    dev->BindComputeShader(&topingBLASShader_, cmd);
    dev->BindResource(&topingVertexBuffer_, 4, cmd);    // t4
    dev->BindResource(&topingInstanceBuffer_, 5, cmd);  // t5
    dev->BindResource(&topingBLASGroupBuffer_, 7, cmd); // t7
    dev->BindUAV(&topingBLASPositionBuffer_, 0, cmd);   // u0

    struct {
        uint32_t totalVertices;
        uint32_t groupCount;
        uint32_t pad[10];
    } pushData = {};
    pushData.totalVertices = topingBLASTotalVertices_;
    pushData.groupCount = (uint32_t)topingBLASGroupsGPU_.size();
    dev->PushConstants(&pushData, sizeof(pushData), cmd);

    // Dispatch: 64 threads per group
    uint32_t threadGroups = (topingBLASTotalVertices_ + 63) / 64;
    dev->Dispatch(threadGroups, 1, 1, cmd);

    // Post-barrier: UAV → SHADER_RESOURCE (for BLAS build)
    GPUBarrier postBarriers[] = {
        GPUBarrier::Buffer(&topingBLASPositionBuffer_,
            ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE),
    };
    dev->Barrier(postBarriers, 1, cmd);

    rtTopingVertexCount_ = topingBLASTotalVertices_;
    rtDirty_ = true;
    topingBLASDirty_ = false;
}

void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const {
    if (!rtAvailable_) return;

    auto* dev = device_;

    // ── Blocky BLAS ──────────────────────────────────────────────
    uint32_t blockyVertCount = rtBlockyVertexCount_;
    if (blockyVertCount < 3) blockyVertCount = 0; // Need at least 1 triangle
    if (blockyVertCount > 0 && blasPositionBuffer_.IsValid()) {
        // Only (re)create BLAS when vertex count exceeds allocated capacity.
        // CreateRaytracingAccelerationStructure allocates GPU memory — calling it per-frame leaks VRAM.
        // We allocate with headroom and update desc.vertex_count before each Build.
        if (!blockyBLAS_.IsValid() || blockyVertCount > blockyBLASCapacity_) {
            blockyBLASCapacity_ = blockyVertCount + blockyVertCount / 4; // 25% headroom

            RaytracingAccelerationStructureDesc desc;
            desc.type = RaytracingAccelerationStructureDesc::Type::BOTTOMLEVEL;
            desc.flags = RaytracingAccelerationStructureDesc::FLAG_PREFER_FAST_BUILD;

            desc.bottom_level.geometries.resize(1);
            auto& geom = desc.bottom_level.geometries[0];
            geom.type = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::Type::TRIANGLES;
            geom.flags = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::FLAG_OPAQUE;
            geom.triangles.vertex_buffer = blasPositionBuffer_;
            geom.triangles.vertex_byte_offset = 0;
            geom.triangles.vertex_count = blockyBLASCapacity_; // allocate for capacity
            geom.triangles.vertex_stride = sizeof(float) * 3;
            geom.triangles.vertex_format = Format::R32G32B32_FLOAT;
            geom.triangles.index_buffer = blasIndexBuffer_;
            geom.triangles.index_count = blockyBLASCapacity_;
            geom.triangles.index_format = IndexBufferFormat::UINT32;
            geom.triangles.index_offset = 0;

            bool ok = dev->CreateRaytracingAccelerationStructure(&desc,
                &blockyBLAS_);
            if (ok) {
                dev->SetName(&blockyBLAS_, "VoxelRenderer::blockyBLAS");
                wi::backlog::post("VoxelRenderer: blocky BLAS created (capacity "
                    + std::to_string(blockyBLASCapacity_ / 3) + " tris)");
            } else {
                wi::backlog::post("VoxelRenderer: failed to create blocky BLAS", wi::backlog::LogLevel::Error);
                rtAvailable_ = false;
                return;
            }
        }

        // Update actual vertex count in descriptor before Build
        blockyBLAS_.desc.bottom_level.geometries[0].triangles.vertex_count = blockyVertCount;
        blockyBLAS_.desc.bottom_level.geometries[0].triangles.index_count = blockyVertCount;
        dev->BuildRaytracingAccelerationStructure(&blockyBLAS_, cmd, nullptr);
    }

    // ── Smooth BLAS ──────────────────────────────────────────────
    // Smooth vertex buffer: float3 position at offset 0, stride 32 bytes
    uint32_t smoothVertCount = gpuSmoothVertexCount_;
    if (smoothVertCount < 3) smoothVertCount = 0; // Need at least 1 triangle
    bool useGpuSmooth = smoothCentroidShader_.IsValid() && smoothMeshShader_.IsValid();
    const GPUBuffer& smoothVB = useGpuSmooth ? gpuSmoothVertexBuffer_ : smoothVertexBuffer_;

    if (smoothVertCount > 0 && smoothVB.IsValid()) {
        // Capacity-based: only recreate when exceeding allocated capacity
        if (!smoothBLAS_.IsValid() || smoothVertCount > smoothBLASCapacity_) {
            smoothBLASCapacity_ = smoothVertCount + smoothVertCount / 4; // 25% headroom

            RaytracingAccelerationStructureDesc desc;
            desc.type = RaytracingAccelerationStructureDesc::Type::BOTTOMLEVEL;
            desc.flags = RaytracingAccelerationStructureDesc::FLAG_PREFER_FAST_BUILD;

            desc.bottom_level.geometries.resize(1);
            auto& geom = desc.bottom_level.geometries[0];
            geom.type = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::Type::TRIANGLES;
            geom.flags = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::FLAG_OPAQUE;
            geom.triangles.vertex_buffer = smoothVB;
            geom.triangles.vertex_byte_offset = 0;
            geom.triangles.vertex_count = smoothBLASCapacity_;
            geom.triangles.vertex_stride = 32; // SmoothVtx struct = 32 bytes, position at offset 0
            geom.triangles.index_buffer = blasIndexBuffer_;
            geom.triangles.index_count = smoothBLASCapacity_;
            geom.triangles.index_format = IndexBufferFormat::UINT32;
            geom.triangles.index_offset = 0;
            geom.triangles.vertex_format = Format::R32G32B32_FLOAT;

            bool ok = dev->CreateRaytracingAccelerationStructure(&desc,
                &smoothBLAS_);
            if (ok) {
                dev->SetName(&smoothBLAS_, "VoxelRenderer::smoothBLAS");
                wi::backlog::post("VoxelRenderer: smooth BLAS created (capacity "
                    + std::to_string(smoothBLASCapacity_ / 3) + " tris)");
            } else {
                wi::backlog::post("VoxelRenderer: failed to create smooth BLAS", wi::backlog::LogLevel::Error);
            }
        }

        if (smoothBLAS_.IsValid()) {
            // Update actual vertex count before Build
            smoothBLAS_.desc.bottom_level.geometries[0].triangles.vertex_count = smoothVertCount;
            smoothBLAS_.desc.bottom_level.geometries[0].triangles.index_count = smoothVertCount;
            dev->BuildRaytracingAccelerationStructure(&smoothBLAS_, cmd, nullptr);
        }

        rtSmoothVertexCount_ = smoothVertCount;
    }

    // ── Toping BLAS ──────────────────────────────────────────────
    uint32_t topingVertCount = rtTopingVertexCount_;
    if (topingVertCount >= 3 && topingBLASPositionBuffer_.IsValid()) {
        // Capacity-based: only recreate when exceeding allocated capacity
        if (!topingBLAS_.IsValid() || topingVertCount > topingBLASASCapacity_) {
            topingBLASASCapacity_ = topingVertCount + topingVertCount / 4; // 25% headroom

            RaytracingAccelerationStructureDesc desc;
            desc.type = RaytracingAccelerationStructureDesc::Type::BOTTOMLEVEL;
            desc.flags = RaytracingAccelerationStructureDesc::FLAG_PREFER_FAST_BUILD; // fast rebuild for animation

            desc.bottom_level.geometries.resize(1);
            auto& geom = desc.bottom_level.geometries[0];
            geom.type = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::Type::TRIANGLES;
            geom.flags = RaytracingAccelerationStructureDesc::BottomLevel::Geometry::FLAG_OPAQUE;
            geom.triangles.vertex_buffer = topingBLASPositionBuffer_;
            geom.triangles.vertex_byte_offset = 0;
            geom.triangles.vertex_count = topingBLASASCapacity_;
            geom.triangles.vertex_stride = sizeof(float) * 3;
            geom.triangles.vertex_format = Format::R32G32B32_FLOAT;
            geom.triangles.index_buffer = topingBLASIndexBuffer_;
            geom.triangles.index_count = topingBLASASCapacity_;
            geom.triangles.index_format = IndexBufferFormat::UINT32;
            geom.triangles.index_offset = 0;

            bool ok = dev->CreateRaytracingAccelerationStructure(&desc, &topingBLAS_);
            if (ok) {
                dev->SetName(&topingBLAS_, "VoxelRenderer::topingBLAS");
                wi::backlog::post("VoxelRenderer: toping BLAS created (capacity "
                    + std::to_string(topingBLASASCapacity_ / 3) + " tris)");
            } else {
                wi::backlog::post("VoxelRenderer: failed to create toping BLAS", wi::backlog::LogLevel::Error);
            }
        }

        if (topingBLAS_.IsValid()) {
            // Update actual vertex count before Build
            topingBLAS_.desc.bottom_level.geometries[0].triangles.vertex_count = topingVertCount;
            topingBLAS_.desc.bottom_level.geometries[0].triangles.index_count = topingVertCount;
            dev->BuildRaytracingAccelerationStructure(&topingBLAS_, cmd, nullptr);
        }
    }

    // ── Memory barrier: sync BLAS builds before TLAS ──────────────
    // Without this, TLAS build can execute before BLASes are complete.
    // (Same pattern as wiRenderer.cpp line 5788)
    {
        GPUBarrier barriers[] = { GPUBarrier::Memory() };
        dev->Barrier(barriers, 1, cmd);
    }

    // ── TLAS (up to 3 instances: blocky + smooth + topings) ────
    // Only recreate TLAS when instance count changes. BLASes are capacity-based
    // (never recreated during animation), so BLAS pointers remain stable.
    // For subsequent frames, just rebuild the TLAS to pick up rebuilt BLASes.
    uint32_t instanceCount = 0;
    if (blockyBLAS_.IsValid()) instanceCount++;
    if (smoothBLAS_.IsValid() && smoothVertCount > 0) instanceCount++;
    if (topingBLAS_.IsValid() && topingVertCount >= 3) instanceCount++;
    if (instanceCount == 0) { rtDirty_ = false; return; }

    // Recreate TLAS only when instance count changes (avoids per-frame GPU allocation)
    if (!tlas_.IsValid() || instanceCount != tlasInstanceCount_) {
        const size_t instSize = dev->GetTopLevelAccelerationStructureInstanceSize();

        auto setIdentity = [](float transform[3][4]) {
            std::memset(transform, 0, sizeof(float) * 12);
            transform[0][0] = 1.0f;
            transform[1][1] = 1.0f;
            transform[2][2] = 1.0f;
        };

        const RaytracingAccelerationStructure* blockyBLASPtr = blockyBLAS_.IsValid() ? &blockyBLAS_ : nullptr;
        const RaytracingAccelerationStructure* smoothBLASPtr = (smoothBLAS_.IsValid() && smoothVertCount > 0) ? &smoothBLAS_ : nullptr;
        const RaytracingAccelerationStructure* topingBLASPtr = (topingBLAS_.IsValid() && topingVertCount >= 3) ? &topingBLAS_ : nullptr;

        RaytracingAccelerationStructureDesc desc;
        desc.flags = RaytracingAccelerationStructureDesc::FLAG_PREFER_FAST_BUILD;
        desc.type = RaytracingAccelerationStructureDesc::Type::TOPLEVEL;
        desc.top_level.count = instanceCount;

        GPUBufferDesc bufdesc;
        bufdesc.misc_flags = ResourceMiscFlag::RAY_TRACING;
        bufdesc.stride = (uint32_t)instSize;
        bufdesc.size = bufdesc.stride * desc.top_level.count;

        auto initInstances = [&](void* dest) {
            uint32_t idx = 0;
            if (blockyBLASPtr) {
                RaytracingAccelerationStructureDesc::TopLevel::Instance inst;
                setIdentity(inst.transform);
                inst.instance_id = 0;  inst.instance_mask = 0xFF;
                inst.instance_contribution_to_hit_group_index = 0;  inst.flags = 0;
                inst.bottom_level = blockyBLASPtr;
                dev->WriteTopLevelAccelerationStructureInstance(&inst, (uint8_t*)dest + idx * instSize);
                idx++;
            }
            if (smoothBLASPtr) {
                RaytracingAccelerationStructureDesc::TopLevel::Instance inst;
                setIdentity(inst.transform);
                inst.instance_id = 1;  inst.instance_mask = 0xFF;
                inst.instance_contribution_to_hit_group_index = 0;  inst.flags = 0;
                inst.bottom_level = smoothBLASPtr;
                dev->WriteTopLevelAccelerationStructureInstance(&inst, (uint8_t*)dest + idx * instSize);
                idx++;
            }
            if (topingBLASPtr) {
                RaytracingAccelerationStructureDesc::TopLevel::Instance inst;
                setIdentity(inst.transform);
                inst.instance_id = 2;  inst.instance_mask = 0xFF;
                inst.instance_contribution_to_hit_group_index = 0;  inst.flags = 0;
                inst.bottom_level = topingBLASPtr;
                dev->WriteTopLevelAccelerationStructureInstance(&inst, (uint8_t*)dest + idx * instSize);
                idx++;
            }
        };

        bool ok = dev->CreateBuffer2(&bufdesc, initInstances, &desc.top_level.instance_buffer);
        if (!ok) {
            wi::backlog::post("VoxelRenderer: failed to create TLAS instance buffer", wi::backlog::LogLevel::Error);
            rtDirty_ = false;
            return;
        }

        ok = dev->CreateRaytracingAccelerationStructure(&desc, &tlas_);
        if (!ok) {
            wi::backlog::post("VoxelRenderer: failed to create TLAS", wi::backlog::LogLevel::Error);
            rtDirty_ = false;
            return;
        }

        tlasInstanceCount_ = instanceCount;
        wi::backlog::post("VoxelRenderer: TLAS created (" + std::to_string(instanceCount) + " instances)");
    }

    // Rebuild TLAS (picks up rebuilt BLASes with new vertex data)
    dev->BuildRaytracingAccelerationStructure(&tlas_, cmd, nullptr);

    // Memory barrier: sync TLAS build before ray queries can use it
    // (Same pattern as wiRenderer.cpp line 5808)
    {
        GPUBarrier barriers[] = { GPUBarrier::Memory(&tlas_) };
        dev->Barrier(barriers, 1, cmd);
    }

    rtDirty_ = false;
}

// ── RT Shadow + AO dispatch (Phase 6.2 + 6.3) ──────────────────
void VoxelRenderer::dispatchShadows(CommandList cmd,
    const Texture& depthBuffer,
    const Texture& renderTarget,
    const Texture& normalTarget) const
{
    if (!rtShadowsEnabled_ || !shadowShader_.IsValid() || !tlas_.IsValid())
        return;

    auto* dev = device_;
    uint32_t w = renderTarget.GetDesc().width;
    uint32_t h = renderTarget.GetDesc().height;
    uint32_t gx = (w + 7) / 8;
    uint32_t gy = (h + 7) / 8;

    // ── Pass 1: Shadow + raw AO (with temporal accumulation) ───────
    {
        GPUBarrier preBarriers[] = {
            GPUBarrier::Image(&const_cast<Texture&>(depthBuffer),
                ResourceState::DEPTHSTENCIL, ResourceState::SHADER_RESOURCE),
            GPUBarrier::Image(&const_cast<Texture&>(renderTarget),
                ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS),
            GPUBarrier::Image(&aoRawTexture_,
                ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS),
        };
        dev->Barrier(preBarriers, 3, cmd);

        dev->BindComputeShader(&shadowShader_, cmd);
        dev->BindResource(&depthBuffer, 0, cmd);        // t0 = depth
        dev->BindResource(&normalTarget, 1, cmd);       // t1 = normals
        dev->BindResource(&tlas_, 2, cmd);               // t2 = TLAS
        dev->BindResource(&aoHistoryTexture_, 3, cmd);   // t3 = AO history (prev frame)
        dev->BindUAV(&renderTarget, 0, cmd);             // u0 = color
        dev->BindUAV(&aoRawTexture_, 1, cmd);            // u1 = raw AO output
        dev->BindConstantBuffer(&constantBuffer_, 0, cmd);

        struct ShadowPush {
            uint32_t width, height;
            float normalBias, shadowMaxDist;
            uint32_t debugMode;
            float aoRadius;
            uint32_t aoRayCount;
            float aoStrength;
            uint32_t frameIndex;
            uint32_t historyValid;
            uint32_t pad[2];
        } pushData = {};
        pushData.width = w;
        pushData.height = h;
        pushData.normalBias = 0.15f;
        pushData.shadowMaxDist = 512.0f;
        pushData.debugMode = rtShadowDebug_;
        pushData.aoRadius = 8.0f;
        pushData.aoRayCount = 4;
        pushData.aoStrength = 0.7f;
        pushData.frameIndex = frameCounter_++;
        pushData.historyValid = aoHistoryValid_ ? 1u : 0u;
        dev->PushConstants(&pushData, sizeof(pushData), cmd);
        dev->Dispatch(gx, gy, 1, cmd);
    }

    // ── Pass 1.5: Copy raw AO → history (before blur, for next frame) ──
    {
        GPUBarrier copyBarriers[] = {
            GPUBarrier::Image(&aoRawTexture_,
                ResourceState::UNORDERED_ACCESS, ResourceState::COPY_SRC),
            GPUBarrier::Image(&aoHistoryTexture_,
                ResourceState::SHADER_RESOURCE, ResourceState::COPY_DST),
        };
        dev->Barrier(copyBarriers, 2, cmd);
        dev->CopyResource(&aoHistoryTexture_, &aoRawTexture_, cmd);

        GPUBarrier postCopyBarriers[] = {
            GPUBarrier::Image(&aoRawTexture_,
                ResourceState::COPY_SRC, ResourceState::SHADER_RESOURCE),
            GPUBarrier::Image(&aoHistoryTexture_,
                ResourceState::COPY_DST, ResourceState::SHADER_RESOURCE),
        };
        dev->Barrier(postCopyBarriers, 2, cmd);
        aoHistoryValid_ = true;
    }

    // ── Pass 2: Bilateral blur horizontal (aoRaw → aoBlurred) ──────
    {
        // aoRawTexture_ already in SHADER_RESOURCE from copy pass
        GPUBarrier barriers[] = {
            GPUBarrier::Image(&aoBlurredTexture_,
                ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS),
        };
        dev->Barrier(barriers, 1, cmd);

        dev->BindComputeShader(&aoBlurShader_, cmd);
        dev->BindResource(&aoRawTexture_, 0, cmd);   // t0 = AO input
        dev->BindResource(&depthBuffer, 1, cmd);     // t1 = depth
        dev->BindResource(&normalTarget, 2, cmd);    // t2 = normals
        dev->BindUAV(&aoBlurredTexture_, 0, cmd);    // u0 = AO output

        struct BlurPush {
            uint32_t width, height, direction, radius;
            float depthThreshold, normalThreshold;
            uint32_t pad[6];
        } blurPush = {};
        blurPush.width = w;
        blurPush.height = h;
        blurPush.direction = 0; // horizontal
        blurPush.radius = 6;
        blurPush.depthThreshold = 0.001f;
        blurPush.normalThreshold = 0.9f;
        dev->PushConstants(&blurPush, sizeof(blurPush), cmd);
        dev->Dispatch(gx, gy, 1, cmd);
    }

    // ── Pass 3: Bilateral blur vertical (aoBlurred → aoRaw) ────────
    {
        GPUBarrier barriers[] = {
            GPUBarrier::Image(&aoBlurredTexture_,
                ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE),
            GPUBarrier::Image(&aoRawTexture_,
                ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS),
        };
        dev->Barrier(barriers, 2, cmd);

        dev->BindComputeShader(&aoBlurShader_, cmd);
        dev->BindResource(&aoBlurredTexture_, 0, cmd); // t0 = AO input (H-blurred)
        dev->BindResource(&depthBuffer, 1, cmd);
        dev->BindResource(&normalTarget, 2, cmd);
        dev->BindUAV(&aoRawTexture_, 0, cmd);          // u0 = AO output (fully blurred)

        struct BlurPush {
            uint32_t width, height, direction, radius;
            float depthThreshold, normalThreshold;
            uint32_t pad[6];
        } blurPush = {};
        blurPush.width = w;
        blurPush.height = h;
        blurPush.direction = 1; // vertical
        blurPush.radius = 6;
        blurPush.depthThreshold = 0.001f;
        blurPush.normalThreshold = 0.9f;
        dev->PushConstants(&blurPush, sizeof(blurPush), cmd);
        dev->Dispatch(gx, gy, 1, cmd);
    }

    // ── Pass 4: Apply blurred AO to color ──────────────────────────
    {
        GPUBarrier barriers[] = {
            GPUBarrier::Image(&aoRawTexture_,
                ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE),
        };
        dev->Barrier(barriers, 1, cmd);

        dev->BindComputeShader(&aoApplyShader_, cmd);
        dev->BindResource(&aoRawTexture_, 0, cmd);   // t0 = blurred AO
        dev->BindResource(&depthBuffer, 1, cmd);      // t1 = depth (for sky detection)
        dev->BindUAV(&renderTarget, 0, cmd);          // u0 = color

        struct ApplyPush {
            uint32_t width, height, debugMode;
            uint32_t pad[9];
        } applyPush = {};
        applyPush.width = w;
        applyPush.height = h;
        applyPush.debugMode = rtShadowDebug_;
        dev->PushConstants(&applyPush, sizeof(applyPush), cmd);
        dev->Dispatch(gx, gy, 1, cmd);
    }

    // ── Restore resource states ────────────────────────────────────
    GPUBarrier postBarriers[] = {
        GPUBarrier::Image(&const_cast<Texture&>(depthBuffer),
            ResourceState::SHADER_RESOURCE, ResourceState::DEPTHSTENCIL),
        GPUBarrier::Image(&const_cast<Texture&>(renderTarget),
            ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE),
    };
    dev->Barrier(postBarriers, 2, cmd);
}

// ── 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 = prevViewProjection_; // from last frame
        cb.cameraPosition = XMFLOAT4(camera.Eye.x, camera.Eye.y, camera.Eye.z, 1.0f);
        cb.sunDirection = XMFLOAT4(-0.7f, -0.4f, -0.3f, 0.0f); // lower sun = longer cast shadows
        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_;
        cb.bleedMask = (1u << 1) | (1u << 2) | (1u << 4) | (1u << 5);
        cb.resistBleedMask = (1u << 1);
        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, 0.0f, 0.0f);     // natural saturation, balanced exposure
        dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb));
        // Save current VP for next frame's temporal reprojection
        XMStoreFloat4x4(&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->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.
    topingSorted_.resize(instances.size());
    for (size_t i = 0; i < instances.size(); i++) {
        topingSorted_[i] = { instances[i].wx, instances[i].wy, instances[i].wz,
                       instances[i].topingType, instances[i].variant };
    }
    std::sort(topingSorted_.begin(), topingSorted_.end(), [](const TopingSortedInst& a, const TopingSortedInst& b) {
        if (a.type != b.type) return a.type < b.type;
        return a.variant < b.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 UpdateBuffer (needs CommandList).
    if (!topingInstanceBuffer_.IsValid() || topingInstanceCapacity_ < instCount) {
        topingInstanceCapacity_ = instCount + instCount / 4; // 25% headroom
        GPUBufferDesc ibDesc;
        ibDesc.size = topingInstanceCapacity_ * sizeof(TopingGPUInst);
        ibDesc.bind_flags = BindFlag::SHADER_RESOURCE;
        ibDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
        ibDesc.stride = sizeof(TopingGPUInst);
        ibDesc.usage = Usage::DEFAULT;
        // Create WITHOUT data — Wicked copies desc.size bytes from data ptr,
        // which would overread our vector (instCount < topingInstanceCapacity_).
        // Actual data upload deferred to Render() via UpdateBuffer.
        device_->CreateBuffer(&ibDesc, nullptr, &topingInstanceBuffer_);
        topingInstanceDirty_ = true; // upload data in Render()
        char msg[128];
        snprintf(msg, sizeof(msg), "Toping: allocated instance buffer (%u capacity, %.1f KB)",
            topingInstanceCapacity_, topingInstanceCapacity_ * sizeof(TopingGPUInst) / 1024.0f);
        wi::backlog::post(msg);
    } else {
        topingInstanceDirty_ = true; // 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;
    topingBLASVertexCount_ = totalTopingVerts;

    // Pre-allocate GPU BLAS position buffer (UAV+SRV, raw for RWByteAddressBuffer).
    // Same pattern as blocky blasPositionBuffer_.
    if (totalTopingVerts > 0) {
        if (!topingBLASPositionBuffer_.IsValid() || topingBLASPositionCapacity_ < totalTopingVerts) {
            topingBLASPositionCapacity_ = totalTopingVerts + totalTopingVerts / 4; // 25% headroom
            GPUBufferDesc posDesc;
            posDesc.size = (size_t)topingBLASPositionCapacity_ * 3 * sizeof(float);
            posDesc.bind_flags = BindFlag::UNORDERED_ACCESS | BindFlag::SHADER_RESOURCE;
            posDesc.misc_flags = ResourceMiscFlag::BUFFER_RAW;
            posDesc.stride = 0;
            posDesc.usage = Usage::DEFAULT;
            device_->CreateBuffer(&posDesc, nullptr, &topingBLASPositionBuffer_);

            char msg[256];
            snprintf(msg, sizeof(msg), "Toping BLAS: allocated pos buffer (%u capacity, %.1f MB)",
                topingBLASPositionCapacity_, posDesc.size / (1024.0 * 1024.0));
            wi::backlog::post(msg);
        }

        // Pre-allocate index buffer (sequential [0,1,2,...]) — grow only when needed
        if (topingBLASIndexCount_ < topingBLASPositionCapacity_) {
            uint32_t idxCount = topingBLASPositionCapacity_;
            std::vector<uint32_t> indices(idxCount);
            for (uint32_t j = 0; j < idxCount; j++) indices[j] = j;

            GPUBufferDesc idxDesc;
            idxDesc.size = (size_t)idxCount * sizeof(uint32_t);
            idxDesc.bind_flags = BindFlag::SHADER_RESOURCE;
            idxDesc.misc_flags = ResourceMiscFlag::NONE;
            idxDesc.usage = Usage::DEFAULT;
            device_->CreateBuffer(&idxDesc, indices.data(), &topingBLASIndexBuffer_);
            topingBLASIndexCount_ = idxCount;
        }

        topingBLASDirty_ = true; // GPU compute dispatch + BLAS rebuild in Render()
    }
}

void VoxelRenderer::renderTopings(
    CommandList cmd,
    const TopingSystem& topingSystem,
    const Texture& depthBuffer,
    const Texture& renderTarget,
    const Texture& normalTarget
) const {
    if (!topingPso_.IsValid() || !topingVertexBuffer_.IsValid() ||
        !topingInstanceBuffer_.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(&topingInstanceBuffer_, 5, cmd); // t5
    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 upload + rendering ─────────────

void VoxelRenderer::uploadSmoothData(VoxelWorld& world) {
    if (!device_ || !smoothPso_.IsValid()) return;

    // Collect all smooth vertices from all chunks, stamping each with its chunkIndex.
    // The chunkIndex must match the order in chunkInfoBuffer_ (assigned by forEachChunk).
    // Reuse a persistent staging vector to avoid per-frame allocations.
    smoothStagingVerts_.clear();
    if (smoothStagingVerts_.capacity() < 64 * 1024)
        smoothStagingVerts_.reserve(64 * 1024);

    uint32_t chunkIdx = 0;
    world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
        if (chunk.hasSmooth && chunk.smoothVertexCount > 0) {
            for (auto& sv : chunk.smoothVertices) {
                sv.chunkIndex = (uint16_t)chunkIdx;
            }
            smoothStagingVerts_.insert(smoothStagingVerts_.end(),
                chunk.smoothVertices.begin(),
                chunk.smoothVertices.end());
        }
        chunkIdx++;
    });

    smoothVertexCount_ = (uint32_t)std::min(smoothStagingVerts_.size(), (size_t)MAX_SMOOTH_VERTICES);

    if (smoothVertexCount_ == 0) {
        smoothDirty_ = false;
        return;
    }

    // Pre-allocate smooth buffer; only recreate when capacity needs to grow.
    if (!smoothVertexBuffer_.IsValid() || smoothVertexCapacity_ < smoothVertexCount_) {
        smoothVertexCapacity_ = smoothVertexCount_ + smoothVertexCount_ / 4; // 25% headroom
        GPUBufferDesc vbDesc;
        vbDesc.size = smoothVertexCapacity_ * sizeof(SmoothVertex);
        vbDesc.bind_flags = BindFlag::SHADER_RESOURCE;
        vbDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
        vbDesc.stride = sizeof(SmoothVertex);
        vbDesc.usage = Usage::DEFAULT;
        // Create WITHOUT data — capacity > vertex count, Wicked would overread.
        device_->CreateBuffer(&vbDesc, nullptr, &smoothVertexBuffer_);
        smoothVertexDirty_ = true; // upload data in Render()
        wi::backlog::post("Smooth: allocated vertex buffer (" + std::to_string(smoothVertexCapacity_)
            + " capacity, " + std::to_string(smoothVertexCapacity_ * sizeof(SmoothVertex) / 1024) + " KB)");
    } else {
        smoothVertexDirty_ = true; // deferred upload in Render()
    }

    smoothDirty_ = false;
}

void VoxelRenderer::uploadSmoothDataFast(VoxelWorld& world) {
    if (!device_ || !smoothPso_.IsValid()) return;

    // Fast path: chunkIndex already stamped during parallel meshChunk.
    // Just collect vertices (no per-vertex stamping needed).
    smoothStagingVerts_.clear();
    if (smoothStagingVerts_.capacity() < 64 * 1024)
        smoothStagingVerts_.reserve(64 * 1024);

    world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
        if (chunk.hasSmooth && chunk.smoothVertexCount > 0) {
            smoothStagingVerts_.insert(smoothStagingVerts_.end(),
                chunk.smoothVertices.begin(),
                chunk.smoothVertices.end());
        }
    });

    smoothVertexCount_ = (uint32_t)std::min(smoothStagingVerts_.size(), (size_t)MAX_SMOOTH_VERTICES);

    if (smoothVertexCount_ == 0) {
        smoothDirty_ = false;
        return;
    }

    // Pre-allocate smooth buffer; only recreate when capacity needs to grow.
    if (!smoothVertexBuffer_.IsValid() || smoothVertexCapacity_ < smoothVertexCount_) {
        smoothVertexCapacity_ = smoothVertexCount_ + smoothVertexCount_ / 4;
        GPUBufferDesc vbDesc;
        vbDesc.size = smoothVertexCapacity_ * sizeof(SmoothVertex);
        vbDesc.bind_flags = BindFlag::SHADER_RESOURCE;
        vbDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
        vbDesc.stride = sizeof(SmoothVertex);
        vbDesc.usage = Usage::DEFAULT;
        // Create WITHOUT data — capacity > vertex count, Wicked would overread.
        device_->CreateBuffer(&vbDesc, nullptr, &smoothVertexBuffer_);
        smoothVertexDirty_ = true; // upload data in Render()
    } else {
        smoothVertexDirty_ = true; // deferred upload in Render()
    }

    smoothDirty_ = false;
}

void VoxelRenderer::renderSmooth(
    CommandList cmd,
    const Texture& depthBuffer,
    const Texture& renderTarget,
    const Texture& normalTarget
) const {
    // Use GPU-generated smooth buffer if available, otherwise CPU buffer
    const bool useGpuSmooth = smoothCentroidShader_.IsValid() && smoothMeshShader_.IsValid();
    const auto& smoothBuf = useGpuSmooth ? gpuSmoothVertexBuffer_ : smoothVertexBuffer_;
    uint32_t vertCount = useGpuSmooth ? gpuSmoothVertexCount_ : smoothVertexCount_;

    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->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();
        cameraPos = { 15.0f, 12.0f, -5.0f };
        cameraPitch = -0.5f;
        cameraYaw = 0.8f;
    } else if (debugMode) {
        world.generateDebug();
        cameraPos = { 10.0f, 10.0f, 0.0f };
        cameraPitch = -0.4f;
        cameraYaw = 0.5f;
    } else {
        world.generateAround(cameraPos.x, cameraPos.y, cameraPos.z, 4);
    }
    // Screenshot mode: fixed camera with good framing of terrain
    if (screenshotMode) {
        cameraPos = { 270.0f, 50.0f, 240.0f };  // above terrain, below sky
        cameraPitch = -0.25f;                      // slight downward look
        cameraYaw = 0.6f;                          // angled view for depth
    }
    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 path or CPU fallback
    if (renderer.isInitialized()) {
        if (renderer.smoothCentroidShader_.IsValid() && renderer.smoothMeshShader_.IsValid()) {
            // GPU smooth mesher available — will dispatch in first Render()
            renderer.gpuSmoothMeshDirty_ = true;
            wi::backlog::post("SmoothMesher: GPU path active, dispatch deferred to Render()");
        } else {
            // CPU fallback: Surface Nets mesh for smooth voxels (parallelized)
            std::vector<Chunk*> chunkPtrs;
            world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
                chunkPtrs.push_back(&chunk);
            });
            const VoxelWorld& worldRef = world;
            wi::jobsystem::context smoothCtx;
            wi::jobsystem::Dispatch(smoothCtx, (uint32_t)chunkPtrs.size(), 1,
                [&chunkPtrs, &worldRef](wi::jobsystem::JobArgs args) {
                    SmoothMesher::meshChunk(*chunkPtrs[args.jobIndex], worldRef);
                });
            wi::jobsystem::Wait(smoothCtx);

            uint32_t totalSmooth = 0;
            uint32_t smoothChunks = 0;
            for (auto* c : chunkPtrs) {
                if (c->smoothVertexCount > 0) {
                    totalSmooth += c->smoothVertexCount;
                    smoothChunks++;
                }
            }
            renderer.uploadSmoothData(world);
            char msg[256];
            snprintf(msg, sizeof(msg),
                "SmoothMesher: %u vertices (%u tris) in %u chunks",
                totalSmooth, totalSmooth / 3, smoothChunks);
            wi::backlog::post(msg);
        }
    }

    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.aoRawTexture_);
    device->CreateTexture(&aoDesc, nullptr, &renderer.aoBlurredTexture_);
    device->CreateTexture(&aoDesc, nullptr, &renderer.aoHistoryTexture_);
    renderer.aoHistoryValid_ = false; // no history on first frame

    rtCreated_ = voxelRT_.IsValid() && voxelNormalRT_.IsValid() && voxelDepth_.IsValid()
              && renderer.aoRawTexture_.IsValid() && renderer.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_A = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('A' - '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 VoxelRenderPath::handleInput(float dt) {
    // F2: toggle backlog console
    if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F2)) {
        wi::backlog::Toggle();
    }
    // F3: toggle animated terrain
    if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F3)) {
        animatedTerrain_ = !animatedTerrain_;
        wi::backlog::post(animatedTerrain_ ? "Animation: ON (30 Hz)" : "Animation: OFF");
    }
    // F4: toggle blend debug visualization
    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_F5)) {
        // Cycle: OFF → ON → DBG_SHADOW → DBG_AO → OFF
        if (!renderer.rtShadowsEnabled_) {
            renderer.rtShadowsEnabled_ = true;
            renderer.rtShadowDebug_ = 0;
            wi::backlog::post("RT Shadows+AO: ON");
        } else if (renderer.rtShadowDebug_ == 0) {
            renderer.rtShadowDebug_ = 1;
            wi::backlog::post("RT Debug: SHADOWS (red=shadow, green=lit, blue=backface)");
        } else if (renderer.rtShadowDebug_ == 1) {
            renderer.rtShadowDebug_ = 2;
            wi::backlog::post("RT Debug: AO (white=open, black=occluded)");
        } else {
            renderer.rtShadowsEnabled_ = false;
            renderer.rtShadowDebug_ = 0;
            wi::backlog::post("RT Shadows+AO: OFF");
        }
    }
    if (wi::input::Press(wi::input::MOUSE_BUTTON_RIGHT)) {
        mouseCaptured = !mouseCaptured;
        wi::input::HidePointer(mouseCaptured);
    }

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

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

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

    if (wi::input::Down(KEY_W)) { cameraPos.x += forward.x * speed; cameraPos.y += forward.y * speed; cameraPos.z += forward.z * speed; }
    if (wi::input::Down(KEY_S)) { cameraPos.x -= forward.x * speed; cameraPos.y -= forward.y * speed; cameraPos.z -= forward.z * speed; }
    if (wi::input::Down(KEY_A)) { cameraPos.x -= right.x * speed; cameraPos.z -= right.z * speed; }
    if (wi::input::Down(KEY_D)) { cameraPos.x += right.x * speed; cameraPos.z += right.z * speed; }
    if (wi::input::Down(wi::input::KEYBOARD_BUTTON_SPACE))    cameraPos.y += speed;
    if (wi::input::Down(wi::input::KEYBOARD_BUTTON_LCONTROL)) cameraPos.y -= speed;

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

void VoxelRenderPath::Update(float dt) {
    auto frameStart = std::chrono::high_resolution_clock::now();
    lastDt_ = dt;
    float instantFps = (dt > 0.0f) ? (1.0f / dt) : 0.0f;
    smoothFps_ = smoothFps_ * 0.95f + instantFps * 0.05f;
    if (camera) handleInput(dt);
    windTime_ += dt;
    renderer.windTime_ = windTime_;

    // Animated terrain: regenerate at 30 Hz with time-shifted noise
    // Fused: regenerate + pack voxel data in the same parallel pass
    if (animatedTerrain_ && renderer.isInitialized()) {
        animAccum_ += dt;
        if (animAccum_ >= ANIM_INTERVAL) {
            animAccum_ -= ANIM_INTERVAL;
            animTime_ += ANIM_INTERVAL;

            // 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(animTime_,
                renderer.packedVoxelCache_.data(), totalWords);
            auto t1 = std::chrono::high_resolution_clock::now();
            profRegenerate_.add(std::chrono::duration<float, std::milli>(t1 - t0).count());

            renderer.voxelCacheDirty_ = false;    // cache already filled by fused pack
            renderer.gpuMeshDirty_ = true;        // GPU still needs upload + dispatch
            renderer.aoHistoryValid_ = false;     // invalidate temporal AO (geometry changed)

            // Re-mesh smooth surfaces — GPU path or CPU fallback
            if (renderer.smoothCentroidShader_.IsValid() && renderer.smoothMeshShader_.IsValid()) {
                renderer.gpuSmoothMeshDirty_ = true; // will dispatch in Render()
            } else {
                // CPU fallback (Surface Nets) — parallelized
                auto ts0 = std::chrono::high_resolution_clock::now();
                std::vector<Chunk*> chunkPtrs;
                world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
                    chunkPtrs.push_back(&chunk);
                });
                const VoxelWorld& worldRef = world;
                wi::jobsystem::context ctx;
                wi::jobsystem::Dispatch(ctx, (uint32_t)chunkPtrs.size(), 1,
                    [&chunkPtrs, &worldRef](wi::jobsystem::JobArgs args) {
                        uint32_t idx = args.jobIndex;
                        SmoothMesher::meshChunk(*chunkPtrs[idx], worldRef);
                        // Stamp chunkIndex during parallel pass (avoids sequential loop in upload)
                        for (auto& sv : chunkPtrs[idx]->smoothVertices)
                            sv.chunkIndex = (uint16_t)idx;
                    });
                wi::jobsystem::Wait(ctx);
                auto ts1 = std::chrono::high_resolution_clock::now();
                profSmoothMesh_.add(std::chrono::duration<float, std::milli>(ts1 - ts0).count());
                renderer.uploadSmoothDataFast(world);
                auto ts2 = std::chrono::high_resolution_clock::now();
                profSmoothUpload_.add(std::chrono::duration<float, std::milli>(ts2 - ts1).count());
            }

            // Re-collect toping instances — parallelized
            {
                auto tt0 = std::chrono::high_resolution_clock::now();
                topingSystem.collectInstancesParallel(world);
                auto tt1 = std::chrono::high_resolution_clock::now();
                profTopingCollect_.add(std::chrono::duration<float, std::milli>(tt1 - tt0).count());
                renderer.uploadTopingData(topingSystem);
                auto tt2 = std::chrono::high_resolution_clock::now();
                profTopingUpload_.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();
        profUpdateMeshes_.add(std::chrono::duration<float, std::milli>(t1 - t0).count());
    }
    RenderPath3D::Update(dt);

    // Profiling: accumulate frame time (will be completed in Compose)
    auto frameEnd = std::chrono::high_resolution_clock::now();
    profFrame_.add(std::chrono::duration<float, std::milli>(frameEnd - frameStart).count());

    // Log averages every 5 seconds
    profTimer_ += dt;
    if (profTimer_ >= PROF_INTERVAL) {
        logProfilingAverages();
        profTimer_ -= PROF_INTERVAL;
    }
}

void VoxelRenderPath::Render() const {
    RenderPath3D::Render();

    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_;
            }
            // Only re-dispatch compute mesher when data changed
            if (renderer.gpuMeshDirty_) {
                renderer.dispatchGpuMesh(cmd, world,
                    &profVoxelPack_, &profGpuUpload_, &profGpuDispatch_);
            }

            // 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()) {
                renderer.dispatchGpuSmoothMesh(cmd, world);
            }
            // 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;
            }

            // ── GPU compute toping BLAS extraction (replaces 196ms CPU loop) ──
            // Must happen BEFORE BLAS build. Fills topingBLASPositionBuffer_ via CS,
            // sets rtTopingVertexCount_ and rtDirty_ to trigger BLAS/TLAS rebuild.
            if (renderer.topingBLASDirty_ && renderer.topingBLASShader_.IsValid()) {
                renderer.dispatchTopingBLASExtract(cmd);
            }

            // Phase 6.1: BLAS extraction + acceleration structure build
            if (renderer.rtAvailable_ && renderer.blasExtractShader_.IsValid() &&
                renderer.gpuMeshQuadCount_ > 0 &&
                (renderer.rtDirty_ || renderer.gpuMeshQuadCount_ != renderer.rtBlockyVertexCount_ / 6)) {
                renderer.dispatchBLASExtract(cmd);
                renderer.buildAccelerationStructures(cmd);
            }
        }

        // ── Deferred GPU uploads (dirty flags set in Update(), need CommandList) ──
        if (renderer.topingInstanceDirty_ && renderer.topingInstanceBuffer_.IsValid() &&
            !renderer.topingGpuInsts_.empty()) {
            size_t uploadSize = renderer.topingGpuInsts_.size() * sizeof(VoxelRenderer::TopingGPUInst);
            size_t bufferSize = renderer.topingInstanceCapacity_ * sizeof(VoxelRenderer::TopingGPUInst);
            if (uploadSize <= bufferSize) {
                device->UpdateBuffer(&renderer.topingInstanceBuffer_,
                    renderer.topingGpuInsts_.data(), cmd, uploadSize);
            }
            renderer.topingInstanceDirty_ = false;
        }
        if (renderer.smoothVertexDirty_ && renderer.smoothVertexBuffer_.IsValid() &&
            renderer.smoothVertexCount_ > 0 &&
            renderer.smoothVertexCount_ <= renderer.smoothStagingVerts_.size()) {
            size_t uploadSize = renderer.smoothVertexCount_ * sizeof(SmoothVertex);
            size_t bufferSize = renderer.smoothVertexCapacity_ * sizeof(SmoothVertex);
            if (uploadSize <= bufferSize) {
                device->UpdateBuffer(&renderer.smoothVertexBuffer_,
                    renderer.smoothStagingVerts_.data(), cmd, uploadSize);
            }
            renderer.smoothVertexDirty_ = false;
        }

        auto tRender0 = std::chrono::high_resolution_clock::now();
        renderer.render(cmd, *camera, voxelDepth_, voxelRT_, voxelNormalRT_);

        // Phase 4: render topings (separate render pass, preserves voxel output)
        renderer.renderTopings(cmd, topingSystem, voxelDepth_, voxelRT_, voxelNormalRT_);

        // Phase 5: render smooth surfaces (separate render pass, preserves all prior output)
        renderer.renderSmooth(cmd, voxelDepth_, voxelRT_, voxelNormalRT_);

        // Phase 6.2: RT Shadows (modulates voxelRT_ in-place after all geometry is rendered)
        if (renderer.isRTShadowsEnabled() && renderer.isRTReady()) {
            renderer.dispatchShadows(cmd, voxelDepth_, voxelRT_, voxelNormalRT_);
        }
        auto tRender1 = std::chrono::high_resolution_clock::now();
        profRender_.add(std::chrono::duration<float, std::milli>(tRender1 - tRender0).count());
    }
}

void VoxelRenderPath::logProfilingAverages() const {
    char msg[1024];
    snprintf(msg, sizeof(msg),
        "=== PERF PROFILE (avg over %.0fs) ===\n"
        "  Regenerate:    %7.2f ms  (%u calls)\n"
        "  UpdateMeshes:  %7.2f ms  (%u calls)\n"
        "  VoxelPack:     %7.2f ms  (%u calls)\n"
        "  GPU Upload:    %7.2f ms  (%u calls)\n"
        "  GPU Dispatch:  %7.2f ms  (%u calls)\n"
        "  SmoothMesh:    %7.2f ms  (%u calls)\n"
        "  SmoothUpload:  %7.2f ms  (%u calls)\n"
        "  TopingCollect: %7.2f ms  (%u calls)\n"
        "  TopingUpload:  %7.2f ms  (%u calls)\n"
        "  Render:        %7.2f ms  (%u calls)\n"
        "  Frame (Upd):   %7.2f ms  (%u calls, %.1f FPS)",
        PROF_INTERVAL,
        profRegenerate_.avg(), profRegenerate_.count,
        profUpdateMeshes_.avg(), profUpdateMeshes_.count,
        profVoxelPack_.avg(), profVoxelPack_.count,
        profGpuUpload_.avg(), profGpuUpload_.count,
        profGpuDispatch_.avg(), profGpuDispatch_.count,
        profSmoothMesh_.avg(), profSmoothMesh_.count,
        profSmoothUpload_.avg(), profSmoothUpload_.count,
        profTopingCollect_.avg(), profTopingCollect_.count,
        profTopingUpload_.avg(), profTopingUpload_.count,
        profRender_.avg(), profRender_.count,
        profFrame_.avg(), profFrame_.count,
        profFrame_.count > 0 ? (1000.0f / profFrame_.avg()) : 0.0f);
    wi::backlog::post(msg);

    profRegenerate_.reset();
    profUpdateMeshes_.reset();
    profVoxelPack_.reset();
    profGpuUpload_.reset();
    profGpuDispatch_.reset();
    profSmoothMesh_.reset();
    profSmoothUpload_.reset();
    profTopingCollect_.reset();
    profTopingUpload_.reset();
    profRender_.reset();
    profFrame_.reset();
}

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

    RenderPath3D::Compose(cmd);

    if (rtCreated_ && voxelRT_.IsValid()) {
        wi::image::Params fx;
        fx.enableFullScreen();
        fx.blendFlag = wi::enums::BLENDMODE_OPAQUE;
        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.rtShadowDebug_ == 1 ? "DBG_SHD" : (renderer.rtShadowDebug_ == 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(animatedTerrain_ ? "ON" : "OFF")
        + "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF")
        + "] | F5: shd+ao [" + std::string(renderer.rtShadowDebug_ == 1 ? "SHD" : (renderer.rtShadowDebug_ == 2 ? "AO" : (renderer.isRTShadowsEnabled() ? "ON" : "OFF"))) + "]";

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

void VoxelRenderPath::setCamera(float x, float y, float z, float pitch, float yaw) {
    cameraPos = { x, y, z };
    cameraPitch = pitch;
    cameraYaw = yaw;
}

void VoxelRenderPath::resetAOHistory() {
    renderer.aoHistoryValid_ = false;
    renderer.frameCounter_ = 0;
}

} // namespace voxel
