#include "VoxelRenderer.h" #include "wiPrimitive.h" #include #include 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 mega quad buffer (SRV for vertex pulling) GPUBufferDesc megaDesc; megaDesc.size = MEGA_BUFFER_CAPACITY * sizeof(PackedQuad); megaDesc.bind_flags = BindFlag::SHADER_RESOURCE; megaDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED; megaDesc.stride = sizeof(PackedQuad); megaDesc.usage = Usage::DEFAULT; device_->CreateBuffer(&megaDesc, nullptr, &megaQuadBuffer_); // 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_); // Create indirect args buffer (for DrawInstancedIndirectCount, up to 6 draws per chunk) // UAV bind flag needed for GPU cull compute shader to write args GPUBufferDesc argsDesc; argsDesc.size = MAX_DRAWS * sizeof(IndirectDrawArgs); argsDesc.bind_flags = BindFlag::UNORDERED_ACCESS; argsDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED | ResourceMiscFlag::INDIRECT_ARGS; argsDesc.stride = sizeof(IndirectDrawArgs); argsDesc.usage = Usage::DEFAULT; device_->CreateBuffer(&argsDesc, nullptr, &indirectArgsBuffer_); // Create draw count buffer (single uint32, raw for RWByteAddressBuffer) // UAV bind flag needed for GPU cull compute shader atomic counter GPUBufferDesc countDesc; countDesc.size = sizeof(uint32_t); countDesc.bind_flags = BindFlag::UNORDERED_ACCESS; countDesc.misc_flags = ResourceMiscFlag::BUFFER_RAW | ResourceMiscFlag::INDIRECT_ARGS; countDesc.usage = Usage::DEFAULT; device_->CreateBuffer(&countDesc, nullptr, &drawCountBuffer_); // ── GPU Timestamp Queries ────────────────────────────────────── GPUQueryHeapDesc queryDesc; queryDesc.type = GpuQueryType::TIMESTAMP; queryDesc.query_count = TS_COUNT; device_->CreateQueryHeap(&queryDesc, ×tampHeap_); GPUBufferDesc readbackDesc; readbackDesc.size = TS_COUNT * sizeof(uint64_t); readbackDesc.usage = Usage::READBACK; device_->CreateBuffer(&readbackDesc, nullptr, ×tampReadback_); // ── 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; 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_); wi::backlog::post("VoxelRenderer: GPU compute mesher available"); } else { wi::backlog::post("VoxelRenderer: GPU compute mesher not available", wi::backlog::LogLevel::Warning); } cpuMegaQuads_.reserve(MEGA_BUFFER_CAPACITY); cpuChunkInfo_.reserve(MAX_CHUNKS); chunkSlots_.reserve(MAX_CHUNKS); cpuIndirectArgs_.reserve(MAX_CHUNKS); initialized_ = true; wi::backlog::post("VoxelRenderer: initialized (mega-buffer: " + std::to_string(MEGA_BUFFER_CAPACITY) + " quads capacity)"); } void VoxelRenderer::shutdown() { chunkSlots_.clear(); cpuChunkInfo_.clear(); cpuMegaQuads_.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"); wi::renderer::LoadShader(ShaderStage::CS, cullShader_, "voxel/voxelCullCS.cso"); if (!vertexShader_.IsValid() || !pixelShader_.IsValid()) { wi::backlog::post("VoxelRenderer: shader loading failed", wi::backlog::LogLevel::Error); return; } gpuCullingEnabled_ = cullShader_.IsValid(); if (!gpuCullingEnabled_) { wi::backlog::post("VoxelRenderer: cull compute shader not available, using CPU culling", wi::backlog::LogLevel::Warning); } else { wi::backlog::post("VoxelRenderer: GPU frustum+backface culling enabled"); } // 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_); } // ── 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) { uint32_t s = seed; 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); pixels[idx + 3] = 255; } } } void VoxelRenderer::generateTextures() { const int TEX_SIZE = 256; const int NUM_MATERIALS = 5; std::vector allPixels(TEX_SIZE * TEX_SIZE * 4 * NUM_MATERIALS); struct MatColor { uint8_t r0,g0,b0, r1,g1,b1; uint32_t seed; }; MatColor colors[NUM_MATERIALS] = { { 60, 140, 40, 80, 180, 60, 101 }, // Grass { 100, 70, 40, 140, 100, 60, 202 }, // Dirt { 110, 110, 105, 140, 140, 130, 303 }, // Stone { 200, 190, 140, 230, 220, 170, 404 }, // Sand { 220, 225, 230, 245, 248, 252, 505 }, // Snow }; 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 ); } 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 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. // Simple strategy: full rebuild whenever any chunk is dirty. void VoxelRenderer::rebuildMegaBuffer(VoxelWorld& world) { cpuMegaQuads_.clear(); chunkSlots_.clear(); cpuChunkInfo_.clear(); uint32_t offset = 0; float debugFlag = debugFaceColors_ ? 1.0f : 0.0f; world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) { if (chunk.quadCount == 0) return; if (offset + chunk.quadCount > MEGA_BUFFER_CAPACITY) return; // overflow guard ChunkSlot slot; slot.pos = pos; slot.quadOffset = offset; slot.quadCount = chunk.quadCount; 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 = offset; info.quadCount = chunk.quadCount; for (int f = 0; f < 6; f++) { info.faceOffsets[f] = chunk.faceOffsets[f]; info.faceCounts[f] = chunk.faceCounts[f]; } cpuChunkInfo_.push_back(info); cpuMegaQuads_.insert(cpuMegaQuads_.end(), chunk.quads.begin(), chunk.quads.end()); offset += chunk.quadCount; }); chunkCount_ = (uint32_t)chunkSlots_.size(); totalQuads_ = offset; } void VoxelRenderer::updateMeshes(VoxelWorld& world) { if (!device_) return; // Re-mesh dirty chunks bool anyDirty = false; world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) { if (chunk.dirty) { VoxelMesher::meshChunk(chunk, world); anyDirty = true; } }); if (anyDirty || megaBufferDirty_) { rebuildMegaBuffer(world); megaBufferDirty_ = false; } } // ── Render pass ───────────────────────────────────────────────── void VoxelRenderer::render( CommandList cmd, const wi::scene::CameraComponent& camera, const Texture& depthBuffer, const Texture& renderTarget ) const { if (!initialized_ || chunkCount_ == 0 || !pso_.IsValid()) return; auto* dev = device_; // Upload mega-buffer and chunk info to GPU if (!cpuMegaQuads_.empty()) { dev->UpdateBuffer(&megaQuadBuffer_, cpuMegaQuads_.data(), cmd, cpuMegaQuads_.size() * sizeof(PackedQuad)); } if (!cpuChunkInfo_.empty()) { dev->UpdateBuffer(&chunkInfoBuffer_, cpuChunkInfo_.data(), cmd, cpuChunkInfo_.size() * sizeof(GPUChunkInfo)); } // Per-frame constants VoxelConstants cb = {}; XMStoreFloat4x4(&cb.viewProjection, camera.GetViewProjection()); cb.cameraPosition = XMFLOAT4(camera.Eye.x, camera.Eye.y, camera.Eye.z, 1.0f); cb.sunDirection = XMFLOAT4(-0.5f, -0.8f, -0.3f, 0.0f); cb.sunColor = XMFLOAT4(1.2f, 1.1f, 0.9f, 1.0f); cb.chunkSize = (float)CHUNK_SIZE; cb.textureTiling = 0.25f; cb.chunkCount = chunkCount_; dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb)); // CPU frustum culling wi::primitive::Frustum frustum; frustum.Create(camera.GetViewProjection()); // ── Render pass: color + depth ──────────────────────────────── RenderPassImage rp[] = { RenderPassImage::RenderTarget( &renderTarget, 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, 2, 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(&megaQuadBuffer_, 0, cmd); // t0: mega quad buffer dev->BindResource(&textureArray_, 1, cmd); // t1: material textures dev->BindResource(&chunkInfoBuffer_, 2, cmd); // t2: chunk info dev->BindSampler(&sampler_, 0, cmd); visibleChunks_ = 0; drawCalls_ = 0; // Push constant structure (must be 48 bytes = 12 x uint32, matches b999) struct VoxelPush { uint32_t chunkIndex; uint32_t quadOffset; // offset into mega quad buffer (in quads) uint32_t pad[10]; }; // Simple DrawInstanced loop with frustum culling + push constants for (uint32_t i = 0; i < chunkCount_; i++) { const auto& slot = chunkSlots_[i]; if (slot.quadCount == 0) continue; XMFLOAT3 aabbMin( (float)(slot.pos.x * CHUNK_SIZE), (float)(slot.pos.y * CHUNK_SIZE), (float)(slot.pos.z * CHUNK_SIZE) ); XMFLOAT3 aabbMax( aabbMin.x + CHUNK_SIZE, aabbMin.y + CHUNK_SIZE, aabbMin.z + CHUNK_SIZE ); wi::primitive::AABB aabb(aabbMin, aabbMax); if (!frustum.CheckBoxFast(aabb)) continue; visibleChunks_++; // Pass chunk index AND quad offset via push constants // (SV_VertexID/SV_InstanceID offsets unreliable across drivers) VoxelPush pushData = {}; pushData.chunkIndex = i; pushData.quadOffset = slot.quadOffset; dev->PushConstants(&pushData, sizeof(pushData), cmd); // startVertexLocation = 0: the VS computes quad address from push.quadOffset dev->DrawInstanced(slot.quadCount * 6, 1, 0, 0, cmd); drawCalls_++; } 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 (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); } if (renderer.isInitialized()) { renderer.updateMeshes(world); } 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; rtDesc.mip_levels = 1; rtDesc.sample_count = 1; rtDesc.layout = wi::graphics::ResourceState::SHADER_RESOURCE; device->CreateTexture(&rtDesc, nullptr, &voxelRT_); 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_); rtCreated_ = voxelRT_.IsValid() && voxelDepth_.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) { 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) { lastDt_ = dt; float instantFps = (dt > 0.0f) ? (1.0f / dt) : 0.0f; smoothFps_ = smoothFps_ * 0.95f + instantFps * 0.05f; if (camera) handleInput(dt); if (renderer.isInitialized()) renderer.updateMeshes(world); RenderPath3D::Update(dt); } void VoxelRenderPath::Render() const { RenderPath3D::Render(); if (renderer.isInitialized() && camera && rtCreated_) { auto* device = wi::graphics::GetDevice(); CommandList cmd = device->BeginCommandList(); renderer.render(cmd, *camera, voxelDepth_, voxelRT_); } } 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); } // 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 2 — GPU-driven)\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()) + " (DrawInstanced + CPU cull + backface)\n"; char cullStr[16], drawStr[16]; snprintf(cullStr, sizeof(cullStr), "%.3f", renderer.getGpuCullTimeMs()); snprintf(drawStr, sizeof(drawStr), "%.3f", renderer.getGpuDrawTimeMs()); stats += "GPU Cull: " + std::string(cullStr) + " ms | Draw: " + std::string(drawStr) + " ms\n"; stats += "WASD+Space/Ctrl: move | Shift: fast | Right-click: capture mouse"; wi::font::Draw(stats, fp, cmd); } } // namespace voxel