#include "VulkanDraw.h" #include #include #include "VulkanState.h" #include "VulkanBuffer.h" #include "VulkanPipeline.h" #include "VulkanShader.h" #include "VulkanTexture.h" #include "VulkanRenderer.h" #include "VulkanPostProcessing.h" #include "VulkanDescriptorManager.h" #include "VulkanDeletionQueue.h" #include "VulkanMemory.h" #include "VulkanConstants.h" #include "gr_vulkan.h" #include "VulkanVertexFormat.h" #include "bmpman/bmpman.h" #include "ddsutils/ddsutils.h" #include "graphics/grinternal.h" #include "graphics/material.h" #include "graphics/matrix.h" #include "graphics/util/primitives.h" #include "graphics/util/uniform_structs.h" #include "graphics/shadows.h" #include "lighting/lighting.h" #include "graphics/util/UniformBuffer.h" #include "ktxutils/ktxutils.h" #define MODEL_SDR_FLAG_MODE_CPP #include "def_files/data/effects/model_shader_flags.h" namespace graphics::vulkan { // Convert FSO texture addressing mode to Vulkan sampler address mode static vk::SamplerAddressMode convertTextureAddressing(int mode) { switch (mode) { case TMAP_ADDRESS_MIRROR: return vk::SamplerAddressMode::eMirroredRepeat; case TMAP_ADDRESS_CLAMP: return vk::SamplerAddressMode::eClampToEdge; case TMAP_ADDRESS_WRAP: default: return vk::SamplerAddressMode::eRepeat; } } // Global draw manager pointer static VulkanDrawManager* g_drawManager = nullptr; // ========== Transform buffer for batched submodel rendering ========== // Per-frame sub-allocating buffer. Multiple draw lists may upload transforms // in a single frame (e.g. space view + HUD targeting). Because Vulkan defers // command submission until flip(), each upload must be preserved — we append // rather than overwrite, and bind the SSBO with the per-upload byte offset. // SSBO descriptor offsets must be aligned to minStorageBufferOffsetAlignment. // The Vulkan spec guarantees this value is <= 256, so 256 is always safe. static constexpr size_t SSBO_OFFSET_ALIGNMENT = 256; struct TransformBufferState { vk::Buffer buffer; VulkanAllocation allocation; size_t capacity = 0; // allocated bytes size_t writeOffset = 0; // append cursor (resets each frame) size_t lastUploadOffset = 0; // byte offset of most recent upload size_t lastUploadSize = 0; // byte size of most recent upload }; static std::array g_transformBuffers; static uint32_t g_lastTransformWriteFrame = UINT32_MAX; void vulkan_update_transform_buffer(void* data, size_t size) { if (!data || size == 0) { return; } auto* descManager = getDescriptorManager(); uint32_t frameIdx = descManager->getCurrentFrame(); auto& tb = g_transformBuffers[frameIdx]; // Reset write cursor on first call of each frame if (g_lastTransformWriteFrame != frameIdx) { tb.writeOffset = 0; g_lastTransformWriteFrame = frameIdx; } // Align the write offset for SSBO descriptor binding size_t alignedOffset = (tb.writeOffset + SSBO_OFFSET_ALIGNMENT - 1) & ~(SSBO_OFFSET_ALIGNMENT - 1); size_t needed = alignedOffset + size; auto* memManager = getMemoryManager(); // Resize if needed, preserving data already written this frame if (needed > tb.capacity) { size_t newCapacity = std::max(needed * 2, static_cast(4096)); auto* bufferManager = getBufferManager(); vk::Device device = bufferManager->getDevice(); vk::BufferCreateInfo bufferInfo; bufferInfo.size = static_cast(newCapacity); bufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; bufferInfo.sharingMode = vk::SharingMode::eExclusive; vk::Buffer newBuffer; VulkanAllocation newAllocation; try { newBuffer = device.createBuffer(bufferInfo); } catch (const vk::SystemError& e) { nprintf(("vulkan", "vulkan_update_transform_buffer: Failed to create buffer: %s\n", e.what())); return; } Verification(memManager->allocateBufferMemory(newBuffer, MemoryUsage::CpuToGpu, newAllocation), "Failed to allocate memory for Vulkan transform buffer"); // Copy data already written this frame from old buffer if (tb.buffer && tb.writeOffset > 0) { void* oldMapped = memManager->mapMemory(tb.allocation); void* newMapped = memManager->mapMemory(newAllocation); Assert(oldMapped); Assert(newMapped); memcpy(newMapped, oldMapped, tb.writeOffset); memManager->unmapMemory(tb.allocation); memManager->unmapMemory(newAllocation); } // Defer destruction of old buffer if (tb.buffer) { auto* deletionQueue = getDeletionQueue(); deletionQueue->queueBuffer(tb.buffer, tb.allocation); } tb.buffer = newBuffer; tb.allocation = newAllocation; tb.capacity = newCapacity; } // Upload new data at the aligned offset void* mapped = memManager->mapMemory(tb.allocation); Assert(mapped); memcpy(static_cast(mapped) + alignedOffset, data, size); memManager->flushMemory(tb.allocation, alignedOffset, size); memManager->unmapMemory(tb.allocation); tb.lastUploadOffset = alignedOffset; tb.lastUploadSize = size; tb.writeOffset = alignedOffset + size; } VulkanDrawManager* getDrawManager() { Assertion(g_drawManager != nullptr, "Vulkan DrawManager not initialized!"); return g_drawManager; } void setDrawManager(VulkanDrawManager* manager) { g_drawManager = manager; } bool VulkanDrawManager::init(vk::Device device) { if (m_initialized) { return true; } m_device = device; initSphereBuffers(); m_initialized = true; nprintf(("vulkan", "VulkanDrawManager: Initialized\n")); return true; } void VulkanDrawManager::shutdown() { if (!m_initialized) { return; } // Destroy transform SSBO buffers (static globals, not tracked by deletion queue) auto* bufferManager = getBufferManager(); auto* memManager = getMemoryManager(); if (bufferManager && memManager) { vk::Device device = bufferManager->getDevice(); for (auto& tb : g_transformBuffers) { if (tb.buffer) { device.destroyBuffer(tb.buffer); memManager->freeAllocation(tb.allocation); tb.buffer = nullptr; tb.capacity = 0; tb.writeOffset = 0; } } } shutdownSphereBuffers(); m_initialized = false; nprintf(("vulkan", "VulkanDrawManager: Shutdown complete\n")); } void VulkanDrawManager::clear() { (void)this; auto* stateTracker = getStateTracker(); // Use the current clip/scissor region for clearing, matching OpenGL behavior. // In OpenGL, glClear() respects the scissor test - if a clip region is set, // only that region is cleared. Without this, HUD code that does // gr_set_clip(panel) + gr_clear() would wipe the entire screen in Vulkan. vk::ClearAttachment clearAttachment; clearAttachment.aspectMask = vk::ImageAspectFlagBits::eColor; clearAttachment.colorAttachment = 0; clearAttachment.clearValue.color = graphics::vulkan::VulkanStateTracker::getClearColor(); vk::ClearRect clearRect; if (stateTracker->isScissorEnabled()) { // Respect the current clip region (matches OpenGL scissor behavior) clearRect.rect.offset = vk::Offset2D(gr_screen.offset_x + gr_screen.clip_left, gr_screen.offset_y + gr_screen.clip_top); clearRect.rect.extent = vk::Extent2D(static_cast(gr_screen.clip_width), static_cast(gr_screen.clip_height)); } else { clearRect.rect.offset = vk::Offset2D(0, 0); clearRect.rect.extent = vk::Extent2D(static_cast(gr_screen.max_w), static_cast(gr_screen.max_h)); } // gr_screen dimensions can exceed the active render pass when rendering to a // smaller off-screen target; vkCmdClearAttachments requires rects inside the // render area. clearRect.rect = stateTracker->clampToRenderArea(clearRect.rect); if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) { return; } clearRect.baseArrayLayer = 0; clearRect.layerCount = 1; auto cmdBuffer = stateTracker->getCommandBuffer(); cmdBuffer.clearAttachments(1, &clearAttachment, 1, &clearRect); } void VulkanDrawManager::setClearColor(int r, int g, int b) { // Also update gr_screen for compatibility gr_screen.current_clear_color.red = static_cast(r); gr_screen.current_clear_color.green = static_cast(g); gr_screen.current_clear_color.blue = static_cast(b); gr_screen.current_clear_color.alpha = 255; } void VulkanDrawManager::setClip(int x, int y, int w, int h, int resize_mode) { (void)this; auto* stateTracker = getStateTracker(); // Clamp values x = std::max(x, 0); y = std::max(y, 0); int to_resize = (resize_mode != GR_RESIZE_NONE && resize_mode != GR_RESIZE_REPLACE && (gr_screen.custom_size || (gr_screen.rendering_to_texture != -1))); int max_w = (to_resize) ? gr_screen.max_w_unscaled : gr_screen.max_w; int max_h = (to_resize) ? gr_screen.max_h_unscaled : gr_screen.max_h; if ((gr_screen.rendering_to_texture != -1) && to_resize) { gr_unsize_screen_pos(&max_w, &max_h); } if (resize_mode != GR_RESIZE_REPLACE) { if (x >= max_w) x = max_w - 1; if (y >= max_h) y = max_h - 1; if (x + w > max_w) w = max_w - x; if (y + h > max_h) h = max_h - y; w = std::min(w, max_w); h = std::min(h, max_h); } // Store unscaled values gr_screen.offset_x_unscaled = x; gr_screen.offset_y_unscaled = y; gr_screen.clip_left_unscaled = 0; gr_screen.clip_right_unscaled = w - 1; gr_screen.clip_top_unscaled = 0; gr_screen.clip_bottom_unscaled = h - 1; gr_screen.clip_width_unscaled = w; gr_screen.clip_height_unscaled = h; if (to_resize) { gr_resize_screen_pos(&x, &y, &w, &h, resize_mode); } else { gr_unsize_screen_pos(&gr_screen.offset_x_unscaled, &gr_screen.offset_y_unscaled); gr_unsize_screen_pos(&gr_screen.clip_right_unscaled, &gr_screen.clip_bottom_unscaled); gr_unsize_screen_pos(&gr_screen.clip_width_unscaled, &gr_screen.clip_height_unscaled); } // Update gr_screen clip state (scaled values) gr_screen.offset_x = x; gr_screen.offset_y = y; gr_screen.clip_left = 0; gr_screen.clip_top = 0; gr_screen.clip_right = w - 1; gr_screen.clip_bottom = h - 1; gr_screen.clip_width = w; gr_screen.clip_height = h; gr_screen.clip_aspect = i2fl(w) / i2fl(h); gr_screen.clip_center_x = (gr_screen.clip_left + gr_screen.clip_right) * 0.5f; gr_screen.clip_center_y = (gr_screen.clip_top + gr_screen.clip_bottom) * 0.5f; // Check if full screen (disable scissor) if ((x == 0) && (y == 0) && (w == max_w) && (h == max_h)) { stateTracker->setScissorEnabled(false); return; } // Enable scissor test stateTracker->setScissorEnabled(true); stateTracker->setScissor(x, y, static_cast(w), static_cast(h)); } void VulkanDrawManager::resetClip() { (void)this; auto* stateTracker = getStateTracker(); int max_w = gr_screen.max_w; int max_h = gr_screen.max_h; gr_screen.offset_x = gr_screen.offset_x_unscaled = 0; gr_screen.offset_y = gr_screen.offset_y_unscaled = 0; gr_screen.clip_left = gr_screen.clip_left_unscaled = 0; gr_screen.clip_top = gr_screen.clip_top_unscaled = 0; gr_screen.clip_right = gr_screen.clip_right_unscaled = max_w - 1; gr_screen.clip_bottom = gr_screen.clip_bottom_unscaled = max_h - 1; gr_screen.clip_width = gr_screen.clip_width_unscaled = max_w; gr_screen.clip_height = gr_screen.clip_height_unscaled = max_h; if (gr_screen.custom_size) { gr_unsize_screen_pos(&gr_screen.clip_right_unscaled, &gr_screen.clip_bottom_unscaled); gr_unsize_screen_pos(&gr_screen.clip_width_unscaled, &gr_screen.clip_height_unscaled); } gr_screen.clip_aspect = i2fl(max_w) / i2fl(max_h); gr_screen.clip_center_x = (gr_screen.clip_left + gr_screen.clip_right) * 0.5f; gr_screen.clip_center_y = (gr_screen.clip_top + gr_screen.clip_bottom) * 0.5f; stateTracker->setScissorEnabled(false); } int VulkanDrawManager::zbufferGet() const { if (!gr_global_zbuffering) { return GR_ZBUFF_NONE; } return m_zbufferMode; } int VulkanDrawManager::zbufferSet(int mode) { auto* stateTracker = getStateTracker(); int prev = m_zbufferMode; m_zbufferMode = mode; // Update FSO global state if (mode == GR_ZBUFF_NONE) { gr_zbuffering = 0; } else { gr_zbuffering = 1; } gr_zbuffering_mode = mode; gr_zbuffer_type zbufType; switch (mode) { case GR_ZBUFF_NONE: zbufType = ZBUFFER_TYPE_NONE; break; case GR_ZBUFF_READ: zbufType = ZBUFFER_TYPE_READ; break; case GR_ZBUFF_WRITE: zbufType = ZBUFFER_TYPE_WRITE; break; case GR_ZBUFF_FULL: default: zbufType = ZBUFFER_TYPE_FULL; break; } stateTracker->setZBufferMode(zbufType); return prev; } void VulkanDrawManager::zbufferClear(int mode) { auto* stateTracker = getStateTracker(); if (mode) { // Enable zbuffering and clear gr_zbuffering = 1; gr_zbuffering_mode = GR_ZBUFF_FULL; gr_global_zbuffering = 1; m_zbufferMode = GR_ZBUFF_FULL; stateTracker->setZBufferMode(ZBUFFER_TYPE_FULL); // Clear depth buffer (clamped: gr_screen can exceed a smaller render target) vk::ClearAttachment clearAttachment; clearAttachment.aspectMask = vk::ImageAspectFlagBits::eDepth; clearAttachment.clearValue.depthStencil.depth = 1.0f; clearAttachment.clearValue.depthStencil.stencil = 0; vk::ClearRect clearRect; clearRect.rect.offset = vk::Offset2D(0, 0); clearRect.rect.extent = vk::Extent2D(static_cast(gr_screen.max_w), static_cast(gr_screen.max_h)); clearRect.rect = stateTracker->clampToRenderArea(clearRect.rect); clearRect.baseArrayLayer = 0; clearRect.layerCount = 1; if (clearRect.rect.extent.width > 0 && clearRect.rect.extent.height > 0) { stateTracker->getCommandBuffer().clearAttachments(1, &clearAttachment, 1, &clearRect); } } else { // Disable zbuffering gr_zbuffering = 0; gr_zbuffering_mode = GR_ZBUFF_NONE; gr_global_zbuffering = 0; m_zbufferMode = GR_ZBUFF_NONE; stateTracker->setZBufferMode(ZBUFFER_TYPE_NONE); } } int VulkanDrawManager::stencilSet(int mode) { auto* stateTracker = getStateTracker(); int prev = m_stencilMode; m_stencilMode = mode; gr_stencil_mode = mode; stateTracker->setStencilMode(mode); // Set stencil reference based on mode if (mode == GR_STENCIL_READ || mode == GR_STENCIL_WRITE) { stateTracker->setStencilReference(1); } else { stateTracker->setStencilReference(0); } return prev; } void VulkanDrawManager::stencilClear() { (void)this; auto* stateTracker = getStateTracker(); // Clear stencil buffer (clamped: gr_screen can exceed a smaller render target) vk::ClearAttachment clearAttachment; clearAttachment.aspectMask = vk::ImageAspectFlagBits::eStencil; clearAttachment.clearValue.depthStencil.depth = 1.0f; clearAttachment.clearValue.depthStencil.stencil = 0; vk::ClearRect clearRect; clearRect.rect.offset = vk::Offset2D(0, 0); clearRect.rect.extent = vk::Extent2D(static_cast(gr_screen.max_w), static_cast(gr_screen.max_h)); clearRect.rect = stateTracker->clampToRenderArea(clearRect.rect); clearRect.baseArrayLayer = 0; clearRect.layerCount = 1; if (clearRect.rect.extent.width > 0 && clearRect.rect.extent.height > 0) { stateTracker->getCommandBuffer().clearAttachments(1, &clearAttachment, 1, &clearRect); } } int VulkanDrawManager::setCull(int cull) { auto* stateTracker = getStateTracker(); int prev = m_cullEnabled ? 1 : 0; m_cullEnabled = (cull != 0); stateTracker->setCullMode(m_cullEnabled); return prev; } void VulkanDrawManager::renderPrimitivesCommon(material* material_info, primitive_type prim_type, vertex_layout* layout, int offset, int n_verts, gr_buffer_handle buffer_handle, size_t buffer_offset, int* statCounter) { GR_DEBUG_SCOPE("Render primitives"); if (!material_info || !layout || n_verts <= 0) { return; } if (statCounter != nullptr) { (*statCounter)++; } // Apply material state and bind pipeline if (!applyMaterial(material_info, prim_type, layout)) { return; } // Bind vertex buffer and issue the draw call bindVertexBuffer(buffer_handle, buffer_offset); draw(prim_type, offset, n_verts); } void VulkanDrawManager::renderPrimitives(material* material_info, primitive_type prim_type, vertex_layout* layout, int offset, int n_verts, gr_buffer_handle buffer_handle, size_t buffer_offset) { renderPrimitivesCommon(material_info, prim_type, layout, offset, n_verts, buffer_handle, buffer_offset, &m_frameStats.renderPrimitiveCalls); } void VulkanDrawManager::renderPrimitivesBatched(batched_bitmap_material* material_info, primitive_type prim_type, vertex_layout* layout, int offset, int n_verts, gr_buffer_handle buffer_handle) { GR_DEBUG_SCOPE("Render batched primitives"); renderPrimitivesCommon(material_info, prim_type, layout, offset, n_verts, buffer_handle, 0, &m_frameStats.renderBatchedCalls); } void VulkanDrawManager::renderPrimitivesParticle(particle_material* material_info, primitive_type prim_type, vertex_layout* layout, int offset, int n_verts, gr_buffer_handle buffer_handle) { renderPrimitivesCommon(material_info, prim_type, layout, offset, n_verts, buffer_handle, 0, &m_frameStats.renderParticleCalls); } void VulkanDrawManager::renderPrimitivesDistortion(distortion_material* material_info, primitive_type prim_type, vertex_layout* layout, int offset, int n_verts, gr_buffer_handle buffer_handle) { // Distortion intentionally tracks no dedicated frame stat counter. renderPrimitivesCommon(material_info, prim_type, layout, offset, n_verts, buffer_handle, 0, nullptr); } void VulkanDrawManager::renderMovie(movie_material* material_info, primitive_type prim_type, vertex_layout* layout, int n_verts, gr_buffer_handle buffer_handle, size_t buffer_offset) { GR_DEBUG_SCOPE("Render movie frame"); renderPrimitivesCommon(material_info, prim_type, layout, 0, n_verts, buffer_handle, buffer_offset, &m_frameStats.renderMovieCalls); } void VulkanDrawManager::renderNanoVG(nanovg_material* material_info, primitive_type prim_type, vertex_layout* layout, int offset, int n_verts, gr_buffer_handle buffer_handle) { GR_DEBUG_SCOPE("Render NanoVG primitives"); renderPrimitivesCommon(material_info, prim_type, layout, offset, n_verts, buffer_handle, 0, &m_frameStats.renderNanoVGCalls); } void VulkanDrawManager::renderRocketPrimitives(interface_material* material_info, primitive_type prim_type, vertex_layout* layout, int n_indices, gr_buffer_handle vertex_buffer, gr_buffer_handle index_buffer) { if (!material_info || !layout || n_indices <= 0) { return; } GR_DEBUG_SCOPE("Render rocket ui primitives"); m_frameStats.renderRocketCalls++; if (!applyMaterial(material_info, prim_type, layout)) { return; } bindVertexBuffer(vertex_buffer, 0); bindIndexBuffer(index_buffer); drawIndexed(prim_type, n_indices, 0, 0); } // Shared indexed-draw parameters for model + shadow draws. renderModel() and // renderShadowDraw() need these computed identically from the same three // sources; the only per-call differences are the instance count (1 vs shadow // cascade count) and the frame-stats bookkeeping, which stay at the call sites. namespace { struct ModelDrawParams { vk::IndexType indexType; vk::DeviceSize indexBufferOffset; // vert_source->Index_offset (heap allocation offset) uint32_t firstIndex; int32_t baseVertex; uint32_t indexCount; }; ModelDrawParams computeModelDrawParams(const indexed_vertex_source* vs, const vertex_buffer* vb, const buffer_data* datap) { ModelDrawParams p; p.indexType = (datap->flags & VB_FLAG_LARGE_INDEX) ? vk::IndexType::eUint32 : vk::IndexType::eUint16; p.indexBufferOffset = static_cast(vs->Index_offset); // Base vertex: heap allocation position + per-mesh vertex offset (matches // OpenGL's glDrawElementsBaseVertex). firstIndex converts the byte index // offset to an index count for the active index width. p.baseVertex = static_cast(vs->Base_vertex_offset + vb->vertex_num_offset); const size_t idxSize = (p.indexType == vk::IndexType::eUint32) ? sizeof(uint32_t) : sizeof(uint16_t); p.firstIndex = static_cast(datap->index_offset / idxSize); p.indexCount = static_cast(datap->n_verts); return p; } } // namespace void VulkanDrawManager::renderModel(model_material* material_info, indexed_vertex_source* vert_source, vertex_buffer* bufferp, size_t texi) { if (!material_info || !vert_source || !bufferp) { return; } m_frameStats.renderModelCalls++; // Validate buffers if (!vert_source->Vbuffer_handle.isValid() || !vert_source->Ibuffer_handle.isValid()) { nprintf(("Vulkan", "VulkanDrawManager: renderModel called with invalid buffer handles\n")); return; } if (texi >= bufferp->tex_buf.size()) { nprintf(("Vulkan", "VulkanDrawManager: renderModel texi out of range\n")); return; } auto* stateTracker = getStateTracker(); // Get buffer data for this texture/draw buffer_data* datap = &bufferp->tex_buf[texi]; if (datap->n_verts == 0) { return; // Nothing to draw } // Apply model material state and bind pipeline // Model rendering always uses triangles if (!applyMaterial(material_info, PRIM_TYPE_TRIS, &bufferp->layout)) { return; } // Bind vertex buffer with the model's vertex offset auto* bufferManager = getBufferManager(); vk::Buffer vbuffer = bufferManager->getVkBuffer(vert_source->Vbuffer_handle); vk::Buffer ibuffer = bufferManager->getVkBuffer(vert_source->Ibuffer_handle); Assertion(vbuffer, "VulkanDrawManager::renderModel got null vertex buffer from valid handle!"); Assertion(ibuffer, "VulkanDrawManager::renderModel got null index buffer from valid handle!"); // Bind vertex buffer at offset 0 (start of heap buffer), matching OpenGL behavior. // The Base_vertex_offset in drawIndexed handles the heap allocation offset. stateTracker->bindVertexBuffer(0, vbuffer, 0); const ModelDrawParams dp = computeModelDrawParams(vert_source, bufferp, datap); stateTracker->bindIndexBuffer(ibuffer, dp.indexBufferOffset, dp.indexType); // Issue indexed draw call (one instance) m_frameStats.drawIndexedCalls++; m_frameStats.totalIndices += static_cast(dp.indexCount); // Flush any dirty dynamic state before draw stateTracker->applyDynamicState(); stateTracker->getCommandBuffer().drawIndexed(dp.indexCount, 1, dp.firstIndex, dp.baseVertex, 0); } void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle, size_t ubo_offset, size_t ubo_size, vertex_buffer* buffer, indexed_vertex_source* vert_src, size_t texi) const { if (!buffer || !vert_src) { return; } if (texi >= buffer->tex_buf.size()) { return; } buffer_data* datap = &buffer->tex_buf[texi]; if (datap->n_verts == 0) { return; } auto* stateTracker = getStateTracker(); auto* pipelineManager = getPipelineManager(); auto* descManager = getDescriptorManager(); auto* bufferManager = getBufferManager(); int shaderHandle = gr_maybe_create_shader(SDR_TYPE_SHADOW_MAP_GEN, gr_is_capable(gr_capability::CAPABILITY_FAST_SHADOWS) ? 0 : SDR_FLAG_SHADOW_FALLBACK); auto* shaderManager = getShaderManager(); const VulkanShaderModule* shaderModule = shaderHandle >= 0 ? shaderManager->getShaderByHandle(shaderHandle) : nullptr; if (!shaderModule) { return; } PipelineConfig config; config.shaderType = shaderModule->type; config.shaderFlags = shaderModule->flags; config.primitiveType = PRIM_TYPE_TRIS; config.depthMode = ZBUFFER_TYPE_FULL; config.depthWriteEnabled = true; config.cullEnabled = true; config.frontFaceCW = true; config.blendMode = ALPHA_BLEND_NONE; config.colorWriteMask = {false, false, false, false}; config.depthBiasEnabled = true; // Clamp depth in the shadow pass so geometry beyond the light frustum's far // plane still writes depth instead of being clipped away (without this, // nearby shadows break up). Always enabled here: depth clamp is a hard // requirement for correct shadows, so CAPABILITY_SHADOWS is gated on the // device feature being present (see vulkan_is_capable). If it were missing, // gr_init would have disabled shadows and this path would be unreachable. config.depthClampEnabled = true; config.renderPass = stateTracker->getCurrentRenderPass(); config.colorAttachmentCount = stateTracker->getColorAttachmentCount(); config.sampleCount = stateTracker->getCurrentSampleCount(); if (!config.renderPass) { return; } vk::Pipeline pipeline = pipelineManager->getPipeline(config, buffer->layout); if (!pipeline) { nprintf(("vulkan", "VulkanDrawManager::renderShadowDraw: Failed to get pipeline!\n")); return; } stateTracker->bindPipeline(pipeline, pipelineManager->getPipelineLayout()); stateTracker->setDepthBias(1024.0f, 1.0f); // GL shadow pass: gr_zbias(-1024) = polygon offset factor 1, units 1024 // Bind descriptor sets: shadowMapData (per-draw) and shadowCascadeParams // (bound separately, per-frame, via shadow_cascade_params_bind) both live in // the fixed 3-tier layout alongside the batched-submodel transform buffer; // no material textures are needed for depth-only rendering. { DescriptorWriter writer; writer.reset(descManager->getDevice(), descManager->getFallbacks()); vk::DescriptorSet globalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global); Assert(globalSet); writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global)); { const auto& pending = getPendingUniformBinding(static_cast(uniform_block_type::ShadowCascadeParams)); if (pending.valid) { vk::Buffer buf = bufferManager->getVkBuffer(pending.bufferHandle); if (buf) { writer.setBuffer(GlobalBinding::ShadowCascadeParams, {buf, pending.offset, pending.size}); } } } vk::DescriptorSet materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material); Assert(materialSet); writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material)); { vk::Buffer buf = bufferManager->getVkBuffer(ubo_handle); if (buf) { writer.setBuffer(MaterialBinding::ShadowMapData, {buf, static_cast(ubo_offset), static_cast(ubo_size)}); } } { uint32_t tfIdx = descManager->getCurrentFrame(); auto& tf = g_transformBuffers[tfIdx]; if (tf.buffer && tf.lastUploadSize > 0) { writer.setBuffer(MaterialBinding::TransformSSBO, {tf.buffer, static_cast(tf.lastUploadOffset), static_cast(tf.lastUploadSize)}); } } vk::DescriptorSet perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw); Assert(perDrawSet); writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw)); writer.flush(); stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet); stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet); stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet); } vk::Buffer vbuffer = bufferManager->getVkBuffer(vert_src->Vbuffer_handle); vk::Buffer ibuffer = bufferManager->getVkBuffer(vert_src->Ibuffer_handle); if (!vbuffer || !ibuffer) { return; } stateTracker->bindVertexBuffer(0, vbuffer, 0); const ModelDrawParams dp = computeModelDrawParams(vert_src, buffer, datap); stateTracker->bindIndexBuffer(ibuffer, dp.indexBufferOffset, dp.indexType); stateTracker->applyDynamicState(); // One instance per active shadow cascade, routed via gl_InstanceIndex → gl_Layer // in the vertex shader (CAPABILITY_FAST_SHADOWS path -- Vulkan always writes // gl_Layer directly, no geometry-shader fallback needed). uint32_t instanceCount = static_cast(std::max(Shadow_cascade_count, 1)); stateTracker->getCommandBuffer().drawIndexed(dp.indexCount, instanceCount, dp.firstIndex, dp.baseVertex, 0); } void VulkanDrawManager::setFillMode(int mode) { m_fillMode = mode; } int VulkanDrawManager::setColorBuffer(int mode) { int prev = m_colorBufferEnabled ? 1 : 0; m_colorBufferEnabled = (mode != 0); return prev; } void VulkanDrawManager::setTextureAddressing(int mode) { m_textureAddressing = mode; } void VulkanDrawManager::setDepthBiasEnabled(bool enabled) { m_depthBiasEnabled = enabled; } void VulkanDrawManager::setDepthTextureOverride(vk::DescriptorImageInfo info) { m_depthTextureInfo = info; } void VulkanDrawManager::clearDepthTextureOverride() { m_depthTextureInfo = vk::DescriptorImageInfo(); } void VulkanDrawManager::setSceneColorOverride(vk::DescriptorImageInfo info) { m_sceneColorInfo = info; } void VulkanDrawManager::setDistMapOverride(vk::DescriptorImageInfo info) { m_distMapInfo = info; } void VulkanDrawManager::clearDistortionOverrides() { m_sceneColorInfo = vk::DescriptorImageInfo(); m_distMapInfo = vk::DescriptorImageInfo(); } void VulkanDrawManager::onResize() { // The cached override infos can reference views destroyed by the swap chain // / post-processor resize; drop them so applyMaterial() falls back until the // next frame sets fresh ones. clearDepthTextureOverride(); clearDistortionOverrides(); } void VulkanDrawManager::clearStates() { auto* stateTracker = getStateTracker(); // Match OpenGL's gr_opengl_clear_states() behavior: // gr_zbias(0), gr_zbuffer_set(ZBUFFER_TYPE_READ), gr_set_cull(0), // gr_set_fill_mode(GR_FILL_MODE_SOLID) m_zbufferMode = GR_ZBUFF_READ; m_stencilMode = GR_STENCIL_NONE; m_cullEnabled = false; m_fillMode = GR_FILL_MODE_SOLID; m_colorBufferEnabled = true; m_textureAddressing = TMAP_ADDRESS_WRAP; m_depthBiasEnabled = false; gr_zbuffering = 1; gr_zbuffering_mode = GR_ZBUFF_READ; gr_global_zbuffering = 1; gr_stencil_mode = GR_STENCIL_NONE; stateTracker->setZBufferMode(ZBUFFER_TYPE_READ); stateTracker->setStencilMode(GR_STENCIL_NONE); stateTracker->setCullMode(false); stateTracker->setScissorEnabled(false); stateTracker->setDepthBias(0.0f, 0.0f); stateTracker->setLineWidth(1.0f); // Clear pending uniform bindings clearPendingUniformBindings(); // NOTE: Do NOT call resetClip() here. OpenGL's gr_opengl_clear_states() does // not reset the clip region, and callers (e.g. model_render_immediate) rely on // the clip/offset state surviving through clear_states for subsequent 2D draws. } void VulkanDrawManager::setPendingUniformBinding(uniform_block_type blockType, gr_buffer_handle bufferHandle, vk::DeviceSize offset, vk::DeviceSize size) { auto index = static_cast(blockType); if (index >= NUM_UNIFORM_BLOCK_TYPES) { return; } m_pendingUniformBindings[index].bufferHandle = bufferHandle; m_pendingUniformBindings[index].offset = offset; m_pendingUniformBindings[index].size = size; m_pendingUniformBindings[index].valid = bufferHandle.isValid(); // The memoized Global (Set 0) descriptor set binds these three block types; a // change to any of them must force a rebuild. if (blockType == uniform_block_type::Lights || blockType == uniform_block_type::DeferredGlobals || blockType == uniform_block_type::ShadowCascadeParams) { m_globalSetDirty = true; } } void VulkanDrawManager::clearPendingUniformBindings() { for (auto& binding : m_pendingUniformBindings) { binding.valid = false; binding.bufferHandle = gr_buffer_handle(); binding.offset = 0; binding.size = 0; } } void VulkanDrawManager::resetFrameStats() { m_frameStats = {}; // Per-frame reset of the memoized descriptor sets. Called from setupFrame() // AFTER the descriptor manager reset its frame pool, so the cached // VkDescriptorSets have just been freed -- drop them and force a rebuild on // the frame's first applyMaterial(). m_cachedGlobalSet = nullptr; m_globalSetDirty = true; m_cachedMaterialSet = nullptr; m_cachedMaterialValid = false; m_cachedPerDrawSet = nullptr; m_cachedPerDrawValid = false; } void VulkanDrawManager::printFrameStats() { // Print summary every frame for the first 200 frames, then every 60 frames bool shouldPrint = (m_frameStatsFrameNum < 200) || (m_frameStatsFrameNum % 60 == 0); if (shouldPrint) { nprintf(("vulkanframe", "FRAME %d STATS: draws=%d indexed=%d verts=%d idxs=%d | applyMat=%d/%d fails | noPipeline=%d sdrNeg1=%d\n", m_frameStatsFrameNum, m_frameStats.drawCalls, m_frameStats.drawIndexedCalls, m_frameStats.totalVertices, m_frameStats.totalIndices, m_frameStats.applyMaterialFailures, m_frameStats.applyMaterialCalls, m_frameStats.noPipelineSkips, m_frameStats.shaderHandleNeg1)); nprintf(("vulkanframe", " CALLS: prim=%d batch=%d model=%d particle=%d nanovg=%d rocket=%d movie=%d\n", m_frameStats.renderPrimitiveCalls, m_frameStats.renderBatchedCalls, m_frameStats.renderModelCalls, m_frameStats.renderParticleCalls, m_frameStats.renderNanoVGCalls, m_frameStats.renderRocketCalls, m_frameStats.renderMovieCalls)); // Descriptor/pipeline pressure: baseline for the descriptor-reuse work and // regression canary for pipeline-cache misses (unexpected growth = new // pipeline permutations being created mid-mission). auto* descMgr = getDescriptorManager(); auto* pipeMgr = getPipelineManager(); nprintf(("vulkanframe", " DESC: sets=%u writes=%u | pipelines total=%zu | onDemandTexUploads=%d\n", descMgr->getSetsAllocatedThisFrame(), descMgr->getWritesThisFrame(), pipeMgr->getPipelineCount(), m_frameStats.onDemandTextureUploads)); } m_frameStatsFrameNum++; } PipelineConfig VulkanDrawManager::buildPipelineConfig(material* mat, primitive_type prim_type) const { PipelineConfig config; // Get shader info from material int shaderHandle = mat->get_shader_handle(); auto* shaderManager = getShaderManager(); if (shaderHandle >= 0) { const auto* shaderModule = shaderManager->getShaderByHandle(shaderHandle); if (shaderModule) { config.shaderType = shaderModule->type; config.shaderFlags = shaderModule->flags; } } // Primitive type config.primitiveType = prim_type; // Depth mode config.depthMode = mat->get_depth_mode(); // Blend mode config.blendMode = mat->get_blend_mode(); // Cull mode config.cullEnabled = mat->get_cull_mode(); // Fill mode config.fillMode = mat->get_fill_mode(); // Front face winding: match OpenGL which defaults to CCW and only switches to CW // for model rendering (opengl_tnl_set_model_material sets GL_CW). config.frontFaceCW = (config.shaderType == SDR_TYPE_MODEL); // Depth write config.depthWriteEnabled = (config.depthMode == ZBUFFER_TYPE_FULL || config.depthMode == ZBUFFER_TYPE_WRITE); // Stencil state config.stencilEnabled = mat->is_stencil_enabled(); if (config.stencilEnabled) { config.stencilFunc = mat->get_stencil_func().compare; config.stencilMask = mat->get_stencil_func().mask; config.frontStencilOp = mat->get_front_stencil_op(); config.backStencilOp = mat->get_back_stencil_op(); } // Color write mask config.colorWriteMask = mat->get_color_mask(); // Override color write mask if color buffer writes are disabled if (!m_colorBufferEnabled) { config.colorWriteMask = {false, false, false, false}; } // Fill mode and depth bias from draw manager state config.depthBiasEnabled = m_depthBiasEnabled; // Get current render pass, attachment count, and sample count from state tracker auto* stateTracker = getStateTracker(); config.renderPass = stateTracker->getCurrentRenderPass(); config.colorAttachmentCount = stateTracker->getColorAttachmentCount(); config.sampleCount = stateTracker->getCurrentSampleCount(); return config; } bool VulkanDrawManager::bindMaterialTextures(material* mat, DescriptorWriter* writer) const { auto* texManager = getTextureManager(); // Get sampler matching current texture addressing mode and fallback texture vk::SamplerAddressMode addressMode = convertTextureAddressing(m_textureAddressing); vk::Sampler sampler = texManager->getSampler( vk::Filter::eLinear, vk::Filter::eLinear, addressMode, true, 0.0f, true); // OpenGL skips applying texture addressing for AABITMAP, INTERFACE, and CUBEMAP // types - they always stay clamped. We need a clamp sampler for those cases. vk::Sampler clampSampler = texManager->getSampler( vk::Filter::eLinear, vk::Filter::eLinear, vk::SamplerAddressMode::eClampToEdge, true, 0.0f, true); auto fallbackTexInfo = texManager->getFallbackTextureInfo2DArray(); fallbackTexInfo.sampler = sampler; std::array textureInfos; textureInfos.fill(fallbackTexInfo); // Check for movie material - needs special YUV texture handling auto* movieMat = dynamic_cast(mat); if (movieMat) { auto loadYuvTexture = [&](int handle, uint32_t slot) { if (handle < 0 || slot >= textureInfos.size()) return; auto* texSlot = texManager->getTextureSlot(handle); if (!texSlot || !texSlot->imageView) { // Load on demand - YUV planes are 8bpp grayscale bitmap* bmp = bm_lock(handle, 8, BMP_AABITMAP); if (bmp) { texManager->bm_data(handle, bmp, bm_is_compressed(handle)); bm_unlock(handle); texSlot = texManager->getTextureSlot(handle); m_frameStats.onDemandTextureUploads++; } } if (texSlot && texSlot->imageView) { textureInfos[slot].imageView = texSlot->imageView; } }; loadYuvTexture(movieMat->getYtex(), 0); // Y at index 0 loadYuvTexture(movieMat->getUtex(), 1); // U at index 1 loadYuvTexture(movieMat->getVtex(), 2); // V at index 2 writer->setImageArray(MaterialBinding::TextureArray, textureInfos); return true; } // Helper to set texture at a specific slot - loads on-demand if not present // Get material's expected texture type for the base map int materialTextureType = mat->get_texture_type(); auto setTexture = [&](int textureHandle, uint32_t slot, bool isBaseMap = false) { if (textureHandle < 0 || slot >= textureInfos.size()) { return; } // Determine bitmap type - match OpenGL's gr_opengl_tcache_set logic: // Override material texture type with bitmap's own type if not NORMAL int bitmapType = isBaseMap ? materialTextureType : TCACHE_TYPE_NORMAL; int overrideType = bm_get_tcache_type(textureHandle); if (overrideType != TCACHE_TYPE_NORMAL) { bitmapType = overrideType; } // OpenGL skips applying texture addressing for AABITMAP, INTERFACE, and // CUBEMAP types - they always stay clamped (gropengltexture.cpp:1140-1141). // Match that behavior by using a clamp sampler for these types. if (bitmapType == TCACHE_TYPE_AABITMAP || bitmapType == TCACHE_TYPE_INTERFACE || bitmapType == TCACHE_TYPE_CUBEMAP) { textureInfos[slot].sampler = clampSampler; } auto* texSlot = texManager->getTextureSlot(textureHandle); // If texture isn't loaded, try to load it on-demand (like OpenGL does) if (!texSlot || !texSlot->imageView) { // Determine bpp and flags - matches OpenGL's opengl_determine_bpp_and_flags uint lockFlags = 0; int bpp = 16; switch (bitmapType) { case TCACHE_TYPE_AABITMAP: lockFlags = BMP_AABITMAP; bpp = 8; break; case TCACHE_TYPE_INTERFACE: case TCACHE_TYPE_XPARENT: lockFlags = BMP_TEX_XPARENT; if (bm_get_type(textureHandle) == BM_TYPE_PCX) { bpp = 16; } else { bpp = 32; } break; case TCACHE_TYPE_COMPRESSED: switch (bm_is_compressed(textureHandle)) { case DDS_DXT1: bpp = 24; lockFlags = BMP_TEX_DXT1; break; case DDS_DXT3: bpp = 32; lockFlags = BMP_TEX_DXT3; break; case DDS_DXT5: bpp = 32; lockFlags = BMP_TEX_DXT5; break; case KTX_ETC2_RGB: case KTX_ETC2_SRGB: bpp = 24; lockFlags = BMP_TEX_ETC2_RGB8; break; case KTX_ETC2_RGBA_EAC: case KTX_ETC2_SRGBA_EAC: bpp = 32; lockFlags = BMP_TEX_ETC2_RGBA8; break; case KTX_ETC2_RGB_A1: case KTX_ETC2_SRGB_A1: bpp = 24; lockFlags = BMP_TEX_ETC2_RGBA1; break; default: bpp = 32; lockFlags = BMP_TEX_OTHER; break; } break; case TCACHE_TYPE_NORMAL: default: lockFlags = BMP_TEX_OTHER; if (bm_get_type(textureHandle) == BM_TYPE_PCX) { bpp = 16; // PCX locking only works with bpp=16 } else { if (bm_has_alpha_channel(textureHandle)) { bpp = 32; } else { bpp = 24; } } break; } // Lock bitmap with appropriate flags bitmap* bmp = bm_lock(textureHandle, bpp, lockFlags); if (bmp) { // Upload texture texManager->bm_data(textureHandle, bmp, bm_is_compressed(textureHandle)); bm_unlock(textureHandle); m_frameStats.onDemandTextureUploads++; // Re-get the slot after upload texSlot = texManager->getTextureSlot(textureHandle); if (m_texBindLogCount < 20) { nprintf(("vulkan", "bindMaterialTextures: loaded tex %d (type=%d bpp=%d lockFlags=0x%x bmType=%d), slot=%p\n", textureHandle, bitmapType, bpp, lockFlags, static_cast(bm_get_type(textureHandle)), texSlot)); m_texBindLogCount++; } } } if (texSlot && texSlot->imageView) { textureInfos[slot].imageView = texSlot->imageView; } else { if (m_texBindLogCount < 20) { nprintf(("vulkan", "bindMaterialTextures: slot %u handle %d FAILED to load\n", slot, textureHandle)); m_texBindLogCount++; } } }; // Bind material textures to their slots // Base map uses material's texture type (may be AABITMAP for fonts) setTexture(mat->get_texture_map(TM_BASE_TYPE), TextureSlot::BaseMap, true); setTexture(mat->get_texture_map(TM_GLOW_TYPE), TextureSlot::GlowMap); // Specular - prefer spec_gloss if available int specMap = mat->get_texture_map(TM_SPEC_GLOSS_TYPE); if (specMap < 0) { specMap = mat->get_texture_map(TM_SPECULAR_TYPE); } setTexture(specMap, TextureSlot::SpecMap); setTexture(mat->get_texture_map(TM_NORMAL_TYPE), TextureSlot::NormalMap); setTexture(mat->get_texture_map(TM_HEIGHT_TYPE), TextureSlot::HeightMap); setTexture(mat->get_texture_map(TM_AMBIENT_TYPE), TextureSlot::AmbientMap); setTexture(mat->get_texture_map(TM_MISC_TYPE), TextureSlot::MiscMap); // Update the texture array in the descriptor set // All slots now have valid views (either actual texture or fallback) writer->setImageArray(MaterialBinding::TextureArray, textureInfos); return true; } bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, vertex_layout* layout) { auto* stateTracker = getStateTracker(); auto* pipelineManager = getPipelineManager(); auto* descManager = getDescriptorManager(); auto* bufferManager = getBufferManager(); if (!mat || !layout) { return false; } m_frameStats.applyMaterialCalls++; // Like opengl_tnl_set_material(), every material draw sets the texture addressing (e.g. MR_FORCE_CLAMP skyboxes) setTextureAddressing(mat->get_texture_addressing()); vulkan_zbias(mat->get_depth_bias()); // Build pipeline configuration from material PipelineConfig config = buildPipelineConfig(mat, prim_type); // Track shader handle issues if (mat->get_shader_handle() < 0) { m_frameStats.shaderHandleNeg1++; } // Check if we have a valid render pass if (!config.renderPass) { m_frameStats.applyMaterialFailures++; nprintf(("vulkan", "VulkanDrawManager: applyMaterial FAIL - no render pass (shaderType=%d)\n", static_cast(config.shaderType))); return false; } // Get or create pipeline vk::Pipeline pipeline = pipelineManager->getPipeline(config, *layout); if (!pipeline) { m_frameStats.applyMaterialFailures++; nprintf(("vulkan", "VulkanDrawManager: applyMaterial FAIL - no pipeline (shaderType=%d handle=%d)\n", static_cast(config.shaderType), mat->get_shader_handle())); return false; } // Bind pipeline with layout stateTracker->bindPipeline(pipeline, pipelineManager->getPipelineLayout()); // Bind fallback vertex buffers for attributes the layout doesn't provide but the shader needs if (pipelineManager->needsFallbackAttribute(*layout, config.shaderType, VATTRIB_COLOR)) { vk::Buffer fallbackColor = bufferManager->getFallbackColorBuffer(); if (fallbackColor) { stateTracker->bindVertexBuffer(FALLBACK_COLOR_BINDING, fallbackColor, 0); } } if (pipelineManager->needsFallbackAttribute(*layout, config.shaderType, VATTRIB_TEXCOORD)) { vk::Buffer fallbackTexCoord = bufferManager->getFallbackTexCoordBuffer(); if (fallbackTexCoord) { stateTracker->bindVertexBuffer(FALLBACK_TEXCOORD_BINDING, fallbackTexCoord, 0); } } // Allocate and bind descriptor sets for this draw. // Template-based writer pre-fills all bindings with fallbacks, // then we overwrite only the bindings that have real data. { DescriptorWriter writer; writer.reset(descManager->getDevice(), descManager->getFallbacks()); // Bind pending UBOs for a given descriptor set auto bindPendingUBOs = [&](DescriptorSetIndex targetSet) { for (const auto& entry : VulkanDescriptorManager::getUniformBindings(targetSet)) { vk::DescriptorBufferInfo bufInfo; const auto& pending = m_pendingUniformBindings[static_cast(entry.blockType)]; if (pending.valid) { vk::Buffer buf = bufferManager->getVkBuffer(pending.bufferHandle); if (buf) { bufInfo = vk::DescriptorBufferInfo(buf, pending.offset, pending.size); } } writer.setBuffer(entry.binding, bufInfo); } }; // Set 0: Global (memoized per frame — see m_cachedGlobalSet). Rebuilt only // when a Global input changed: a pending Global UBO (m_globalSetDirty set in // setPendingUniformBinding), the shadow TLAS (invalidateGlobalSet from // setCurrentShadowTlas), or the shadow-map lazy-init transition (compared // below). A descriptor set retains its written contents until overwritten // or its pool is reset, so subsequent draws just rebind the cached set. auto* pp = getPostProcessor(); const bool shadowReady = (pp && pp->shadow().isInitialized()); if (m_globalSetDirty || !m_cachedGlobalSet || shadowReady != m_cachedGlobalHadShadow) { m_cachedGlobalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global); Assert(m_cachedGlobalSet); writer.writeSet(m_cachedGlobalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global)); bindPendingUBOs(DescriptorSetIndex::Global); if (shadowReady) { writer.setImage(GlobalBinding::ShadowMap, pp->getShadowTextureInfo()); } m_cachedGlobalHadShadow = shadowReady; m_globalSetDirty = false; } vk::DescriptorSet globalSet = m_cachedGlobalSet; // Set 1: Material (previous-set memoized — see m_cachedMaterialSet). // Snapshot every input; reuse the cached set only on an exact match. MaterialSetInputs matInputs; matInputs.texHandles[0] = mat->get_texture_map(TM_BASE_TYPE); matInputs.texHandles[1] = mat->get_texture_map(TM_GLOW_TYPE); int specMap = mat->get_texture_map(TM_SPEC_GLOSS_TYPE); if (specMap < 0) { specMap = mat->get_texture_map(TM_SPECULAR_TYPE); } matInputs.texHandles[2] = specMap; matInputs.texHandles[3] = mat->get_texture_map(TM_NORMAL_TYPE); matInputs.texHandles[4] = mat->get_texture_map(TM_HEIGHT_TYPE); matInputs.texHandles[5] = mat->get_texture_map(TM_AMBIENT_TYPE); matInputs.texHandles[6] = mat->get_texture_map(TM_MISC_TYPE); matInputs.textureAddressing = m_textureAddressing; { auto& tf = g_transformBuffers[descManager->getCurrentFrame()]; if (tf.buffer && tf.lastUploadSize > 0) { matInputs.transformBuffer = tf.buffer; matInputs.transformOffset = tf.lastUploadOffset; matInputs.transformSize = tf.lastUploadSize; } } matInputs.depthInfo = m_depthTextureInfo; matInputs.sceneColorInfo = m_sceneColorInfo; matInputs.distMapInfo = m_distMapInfo; { const auto& md = m_pendingUniformBindings[static_cast(uniform_block_type::ModelData)]; const auto& dg = m_pendingUniformBindings[static_cast(uniform_block_type::DecalGlobals)]; matInputs.uboHandle[0] = md.bufferHandle.value(); matInputs.uboOffset[0] = md.offset; matInputs.uboSize[0] = md.size; matInputs.uboValid[0] = md.valid; matInputs.uboHandle[1] = dg.bufferHandle.value(); matInputs.uboOffset[1] = dg.offset; matInputs.uboSize[1] = dg.size; matInputs.uboValid[1] = dg.valid; } vk::DescriptorSet materialSet; if (m_cachedMaterialValid && m_cachedMaterialSet && matInputs == m_cachedMaterialInputs) { // Identical to the previous draw: reuse the set (skips allocation, the // template write, texture resolution/upload, and the override/UBO writes). materialSet = m_cachedMaterialSet; } else { materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material); Assert(materialSet); writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material)); bindPendingUBOs(DescriptorSetIndex::Material); if (matInputs.transformBuffer) { writer.setBuffer(MaterialBinding::TransformSSBO, {matInputs.transformBuffer, static_cast(matInputs.transformOffset), static_cast(matInputs.transformSize)}); } writer.setImage(MaterialBinding::DepthMap, m_depthTextureInfo); writer.setImage(MaterialBinding::SceneColor, m_sceneColorInfo); writer.setImage(MaterialBinding::DistortionMap, m_distMapInfo); bindMaterialTextures(mat, &writer); m_cachedMaterialSet = materialSet; m_cachedMaterialInputs = matInputs; m_cachedMaterialValid = true; } // Set 2: PerDraw (previous-set memoized — see m_cachedPerDrawSet) PerDrawSetInputs pdInputs; { static constexpr uniform_block_type pdTypes[NUM_PERDRAW_UBOS] = { uniform_block_type::GenericData, uniform_block_type::Matrices, uniform_block_type::NanoVGData, uniform_block_type::DecalInfo, uniform_block_type::MovieData}; for (int i = 0; i < NUM_PERDRAW_UBOS; ++i) { const auto& p = m_pendingUniformBindings[static_cast(pdTypes[i])]; pdInputs.uboHandle[i] = p.bufferHandle.value(); pdInputs.uboOffset[i] = p.offset; pdInputs.uboSize[i] = p.size; pdInputs.uboValid[i] = p.valid; } } vk::DescriptorSet perDrawSet; if (m_cachedPerDrawValid && m_cachedPerDrawSet && pdInputs == m_cachedPerDrawInputs) { perDrawSet = m_cachedPerDrawSet; } else { perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw); Assert(perDrawSet); writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw)); bindPendingUBOs(DescriptorSetIndex::PerDraw); m_cachedPerDrawSet = perDrawSet; m_cachedPerDrawInputs = pdInputs; m_cachedPerDrawValid = true; } writer.flush(); stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet); stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet); stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet); } // Update tracked state for FSO compatibility stateTracker->setZBufferMode(mat->get_depth_mode()); stateTracker->setCullMode(mat->get_cull_mode()); if (mat->is_stencil_enabled()) { // Record the material's actual stencil intent rather than hardcoding READ: // a WRITE if any of the front-face stencil ops modifies the stencil buffer // (op != Keep), otherwise a READ (test-only). The pipeline itself is // configured directly from the material's full stencil state above; this // only keeps the tracker's GR_STENCIL_* bookkeeping honest. const auto& op = mat->get_front_stencil_op(); const bool writesStencil = op.stencilFailOperation != StencilOperation::Keep || op.depthFailOperation != StencilOperation::Keep || op.successOperation != StencilOperation::Keep; stateTracker->setStencilMode(writesStencil ? GR_STENCIL_WRITE : GR_STENCIL_READ); stateTracker->setStencilReference(mat->get_stencil_func().ref); } else { stateTracker->setStencilMode(GR_STENCIL_NONE); } return true; } void VulkanDrawManager::bindVertexBuffer(gr_buffer_handle handle, size_t offset) { (void)this; auto* bufferManager = getBufferManager(); auto* stateTracker = getStateTracker(); if (!handle.isValid()) { return; } vk::Buffer buffer = bufferManager->getVkBuffer(handle); if (buffer) { // Add frame base offset for ring buffer support // This maps the caller's offset into the current frame's span size_t frameOffset = bufferManager->getFrameBaseOffset(handle); size_t totalOffset = frameOffset + offset; stateTracker->bindVertexBuffer(0, buffer, static_cast(totalOffset)); } } void VulkanDrawManager::bindIndexBuffer(gr_buffer_handle handle) { (void)this; auto* bufferManager = getBufferManager(); auto* stateTracker = getStateTracker(); if (!handle.isValid()) { return; } vk::Buffer buffer = bufferManager->getVkBuffer(handle); if (buffer) { // Add frame base offset for ring buffer support (mirrors bindVertexBuffer) size_t frameOffset = bufferManager->getFrameBaseOffset(handle); stateTracker->bindIndexBuffer(buffer, static_cast(frameOffset), vk::IndexType::eUint32); } } void VulkanDrawManager::draw(primitive_type prim_type, int first_vertex, int vertex_count) { auto* stateTracker = getStateTracker(); Assertion(stateTracker->getCurrentPipeline(), "draw() called with no bound pipeline! prim_type=%d first_vertex=%d vertex_count=%d", static_cast(prim_type), first_vertex, vertex_count); if (!stateTracker->getCurrentPipeline()) { m_frameStats.noPipelineSkips++; return; } m_frameStats.drawCalls++; m_frameStats.totalVertices += vertex_count; // Flush any dirty dynamic state (viewport, scissor, depth bias, stencil ref) // before issuing the draw command. applyMaterial sets some of these AFTER bindPipeline, // so they may be dirty even when the pipeline didn't change. stateTracker->applyDynamicState(); auto cmdBuffer = stateTracker->getCommandBuffer(); cmdBuffer.draw(static_cast(vertex_count), 1, static_cast(first_vertex), 0); } void VulkanDrawManager::drawIndexed(primitive_type prim_type, int index_count, int first_index, int vertex_offset) { auto* stateTracker = getStateTracker(); Assertion(stateTracker->getCurrentPipeline(), "drawIndexed() called with no bound pipeline! prim_type=%d index_count=%d first_index=%d vertex_offset=%d", static_cast(prim_type), index_count, first_index, vertex_offset); if (!stateTracker->getCurrentPipeline()) { m_frameStats.noPipelineSkips++; return; } m_frameStats.drawIndexedCalls++; m_frameStats.totalIndices += index_count; // Flush any dirty dynamic state before draw stateTracker->applyDynamicState(); auto cmdBuffer = stateTracker->getCommandBuffer(); cmdBuffer.drawIndexed(static_cast(index_count), 1, static_cast(first_index), vertex_offset, 0); } void VulkanDrawManager::initSphereBuffers() { auto* bufferManager = getBufferManager(); auto mesh = graphics::util::generate_sphere_mesh(16, 16); m_sphereIndexCount = mesh.index_count; m_sphereVBO = bufferManager->createBuffer(BufferType::Vertex, BufferUsageHint::Static); bufferManager->updateBufferData(m_sphereVBO, mesh.vertices.size() * sizeof(float), mesh.vertices.data()); m_sphereIBO = bufferManager->createBuffer(BufferType::Index, BufferUsageHint::Static); bufferManager->updateBufferData(m_sphereIBO, mesh.indices.size() * sizeof(ushort), mesh.indices.data()); m_sphereVertexLayout.add_vertex_component(vertex_format_data::POSITION3, sizeof(float) * 3, 0); nprintf(("vulkan", "VulkanDrawManager: Sphere mesh created (%u vertices, %u indices)\n", mesh.vertex_count, mesh.index_count)); } void VulkanDrawManager::shutdownSphereBuffers() { auto* bufferManager = getBufferManager(); if (m_sphereVBO.isValid()) { bufferManager->deleteBuffer(m_sphereVBO); m_sphereVBO = gr_buffer_handle::invalid(); } if (m_sphereIBO.isValid()) { bufferManager->deleteBuffer(m_sphereIBO); m_sphereIBO = gr_buffer_handle::invalid(); } } void VulkanDrawManager::drawSphere(material* material_def) { if (!material_def || m_sphereIndexCount == 0) { return; } auto* stateTracker = getStateTracker(); auto* bufferManager = getBufferManager(); if (!applyMaterial(material_def, PRIM_TYPE_TRIS, &m_sphereVertexLayout)) { return; } // Bind sphere vertex buffer vk::Buffer vbo = bufferManager->getVkBuffer(m_sphereVBO); if (!vbo) { return; } stateTracker->bindVertexBuffer(0, vbo, 0); // Bind sphere index buffer with uint16 indices (matching the ushort mesh data) vk::Buffer ibo = bufferManager->getVkBuffer(m_sphereIBO); if (!ibo) { return; } stateTracker->bindIndexBuffer(ibo, 0, vk::IndexType::eUint16); drawIndexed(PRIM_TYPE_TRIS, static_cast(m_sphereIndexCount), 0, 0); } } // namespace graphics::vulkan