mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
Vulkan never read the material's texture addressing, so MR_FORCE_CLAMP skyboxes were sampled with a wrapping sampler, producing seams. Set it on every material draw, as OpenGL does in opengl_tnl_set_material(). Co-Authored-By: Claude Opus 5.5 <[email protected]>
1650 lines
56 KiB
C++
1650 lines
56 KiB
C++
#include "VulkanDraw.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
|
|
#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<TransformBufferState, MAX_FRAMES_IN_FLIGHT> 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<size_t>(4096));
|
|
|
|
auto* bufferManager = getBufferManager();
|
|
vk::Device device = bufferManager->getDevice();
|
|
|
|
vk::BufferCreateInfo bufferInfo;
|
|
bufferInfo.size = static_cast<vk::DeviceSize>(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<char*>(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<uint32_t>(gr_screen.clip_width),
|
|
static_cast<uint32_t>(gr_screen.clip_height));
|
|
} else {
|
|
clearRect.rect.offset = vk::Offset2D(0, 0);
|
|
clearRect.rect.extent = vk::Extent2D(static_cast<uint32_t>(gr_screen.max_w),
|
|
static_cast<uint32_t>(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<ubyte>(r);
|
|
gr_screen.current_clear_color.green = static_cast<ubyte>(g);
|
|
gr_screen.current_clear_color.blue = static_cast<ubyte>(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<uint32_t>(w), static_cast<uint32_t>(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<uint32_t>(gr_screen.max_w),
|
|
static_cast<uint32_t>(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<uint32_t>(gr_screen.max_w),
|
|
static_cast<uint32_t>(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<vk::DeviceSize>(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<int32_t>(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<uint32_t>(datap->index_offset / idxSize);
|
|
p.indexCount = static_cast<uint32_t>(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<int>(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, 0.0f);
|
|
|
|
// 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<size_t>(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<vk::DeviceSize>(ubo_offset), static_cast<vk::DeviceSize>(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<vk::DeviceSize>(tf.lastUploadOffset),
|
|
static_cast<vk::DeviceSize>(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<uint32_t>(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<size_t>(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<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> textureInfos;
|
|
textureInfos.fill(fallbackTexInfo);
|
|
|
|
// Check for movie material - needs special YUV texture handling
|
|
auto* movieMat = dynamic_cast<movie_material*>(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<int>(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());
|
|
|
|
// 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<int>(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<int>(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<size_t>(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<size_t>(uniform_block_type::ModelData)];
|
|
const auto& dg = m_pendingUniformBindings[static_cast<size_t>(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<vk::DeviceSize>(matInputs.transformOffset),
|
|
static_cast<vk::DeviceSize>(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<size_t>(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);
|
|
}
|
|
|
|
// Set depth bias if needed
|
|
stateTracker->setDepthBias(static_cast<float>(mat->get_depth_bias()), 0.0f);
|
|
|
|
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<vk::DeviceSize>(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<vk::DeviceSize>(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<int>(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 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<uint32_t>(vertex_count),
|
|
1,
|
|
static_cast<uint32_t>(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<int>(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<uint32_t>(index_count),
|
|
1,
|
|
static_cast<uint32_t>(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<int>(m_sphereIndexCount), 0, 0);
|
|
}
|
|
|
|
} // namespace graphics::vulkan
|
|
|