feat: draw meshes with instanced multi-draw indirect

This commit is contained in:
2026-10-19 00:45:25 +02:00
parent 997844eaa7
commit 92c1daf396
19 changed files with 556 additions and 250 deletions
+2
View File
@@ -64,6 +64,7 @@ set(SRC_FILES
"src/Graphics/GfxDevice.cpp"
"src/Graphics/Caches/ImageCache.cpp"
"src/Graphics/ImageLoader.cpp"
"src/Graphics/IndirectDrawList.cpp"
"src/Graphics/ImmediateExecuter.cpp"
"src/Graphics/Init.cpp"
"src/Graphics/Caches/MaterialCache.cpp"
@@ -232,6 +233,7 @@ if (SPIRV_VAL_EXECUTABLE)
file(RELATIVE_PATH shader_relative "${ASSETS_SRC_DIR}" "${shader_source}")
list(APPEND VALIDATE_SHADER_COMMANDS
COMMAND "${SPIRV_VAL_EXECUTABLE}"
--target-env vulkan1.3 --scalar-block-layout
"${ASSETS_RUNTIME_DIR}/${shader_relative}.spv")
endforeach()
@@ -0,0 +1,21 @@
#ifndef INSTANCE_DATA_GLSL
#define INSTANCE_DATA_GLSL
#extension GL_EXT_buffer_reference : require
#extension GL_EXT_scalar_block_layout : require
#include "vertex.glsl"
// Must match GPUInstanceData in IndirectDrawList.h
struct InstanceData {
mat4 transform;
VertexBuffer vertexBuffer;
uint materialID;
uint padding;
};
layout (buffer_reference, scalar) readonly buffer InstanceBuffer {
InstanceData instances[];
};
#endif // INSTANCE_DATA_GLSL
+2 -1
View File
@@ -12,6 +12,7 @@ layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inUV;
layout (location = 2) in vec3 inNormal;
layout (location = 3) in vec4 inTangent;
layout (location = 4) flat in uint inMaterialID;
layout (location = 0) out vec4 outFragColor;
@@ -62,7 +63,7 @@ float calculateShadow(Light light, vec3 fragPos, vec3 geometryNormal)
void main()
{
MaterialData material = pcs.sceneData.materials.data[pcs.materialID];
MaterialData material = pcs.sceneData.materials.data[inMaterialID];
uint samplerID = material.textureFilteringMode;
vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor;
+9 -5
View File
@@ -8,6 +8,7 @@ layout (location = 0) out vec3 outPos;
layout (location = 1) out vec2 outUV;
layout (location = 2) out vec3 outNormal;
layout (location = 3) out vec4 outTangent;
layout (location = 4) flat out uint outMaterialID;
// The depth pre-pass reuses this shader, positions have to match bit for bit
// so the main pass can depth test against it with LESS_OR_EQUAL
@@ -15,9 +16,11 @@ invariant gl_Position;
void main()
{
Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex];
// gl_InstanceIndex already includes the indirect command's firstInstance
InstanceData instance = pcs.instanceBuffer.instances[gl_InstanceIndex];
Vertex v = instance.vertexBuffer.vertices[gl_VertexIndex];
vec4 worldPos = pcs.transform * vec4(v.position, 1.0f);
vec4 worldPos = instance.transform * vec4(v.position, 1.0f);
gl_Position = pcs.sceneData.viewProj * worldPos;
outPos = worldPos.xyz;
@@ -25,9 +28,10 @@ void main()
// A bit inefficient, but okay - this is needed for non-uniform scale
// models. See: http://www.lighthouse3d.com/tutorials/glsl-12-tutorial/the-normal-matrix/
// Simpler case, when everything is uniform
// outNormal = (pcs.transform * vec4(v.normal, 0.0)).xyz;
outNormal = mat3(transpose(inverse(pcs.transform))) * v.normal;
// outNormal = (instance.transform * vec4(v.normal, 0.0)).xyz;
outNormal = mat3(transpose(inverse(instance.transform))) * v.normal;
// w is the bitangent sign, the fragment shader builds the TBN from the interpolated vectors
outTangent = vec4(mat3(pcs.transform) * v.tangent.xyz, v.tangent.w);
outTangent = vec4(mat3(instance.transform) * v.tangent.xyz, v.tangent.w);
outMaterialID = instance.materialID;
}
+2 -6
View File
@@ -2,14 +2,10 @@
#extension GL_EXT_scalar_block_layout: require
#include "scene_data.glsl"
#include "vertex.glsl"
#include "instance_data.glsl"
layout (push_constant, scalar) uniform constants
{
mat4 transform;
SceneDataBuffer sceneData;
VertexBuffer vertexBuffer;
uint materialID;
uint padding;
InstanceBuffer instanceBuffer;
} pcs;
+6 -5
View File
@@ -4,17 +4,18 @@
#extension GL_EXT_buffer_reference : require
#extension GL_EXT_scalar_block_layout : require
#include "vertex.glsl"
#include "instance_data.glsl"
layout (push_constant, scalar) uniform constants
{
mat4 modelToLightClip;
VertexBuffer vertexBuffer;
mat4 lightViewProj;
InstanceBuffer instanceBuffer;
} pcs;
void main()
{
Vertex v = pcs.vertexBuffer.vertices[gl_VertexIndex];
InstanceData instance = pcs.instanceBuffer.instances[gl_InstanceIndex];
Vertex v = instance.vertexBuffer.vertices[gl_VertexIndex];
gl_Position = pcs.modelToLightClip * vec4(v.position, 1.0f);
gl_Position = pcs.lightViewProj * instance.transform * vec4(v.position, 1.0f);
}
@@ -16,12 +16,24 @@ public:
MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh);
const GPUMesh& getMesh(MeshID id) const;
[[nodiscard]] const std::vector<GPUMesh>& getMeshes() const { return meshes; }
const CPUMesh& getCPUMesh(MeshID id) const;
[[nodiscard]] const std::string& getMeshKey(MeshID id) const;
[[nodiscard]] std::optional<MeshID> findMeshByKey(std::string_view key) const;
[[nodiscard]] const GPUBuffer& getIndexBuffer() const { return indexBuffer; }
[[nodiscard]] std::uint32_t getIndexCount() const { return indexCount; }
[[nodiscard]] std::uint32_t getIndexCapacity() const { return indexCapacity; }
private:
void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const;
void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh);
void ensureIndexCapacity(GfxDevice& gfxDevice, std::uint32_t requiredIndices);
static constexpr std::uint32_t InitialIndexCapacity = 1u << 20;
GPUBuffer indexBuffer;
std::uint32_t indexCount{0};
std::uint32_t indexCapacity{0};
std::vector<GPUMesh> meshes;
std::vector<CPUMesh> cpuMeshes;
@@ -0,0 +1,80 @@
#ifndef INDIRECTDRAWLIST_H
#define INDIRECTDRAWLIST_H
#include <array>
#include <cstddef>
#include <cstdint>
#include <vector>
#include <glm/mat4x4.hpp>
#include <vulkan/vulkan.h>
#include <destrum/Graphics/FrameConfig.h>
#include <destrum/Graphics/Frustum.h>
#include <destrum/Graphics/MeshDrawCommand.h>
#include <destrum/Graphics/Resources/Buffer.h>
class GfxDevice;
struct GPUMesh;
// Must match InstanceData in instance_data.glsl
struct GPUInstanceData {
glm::mat4 transform;
VkDeviceAddress vertexBuffer;
std::uint32_t materialId;
std::uint32_t padding;
};
static_assert(sizeof(GPUInstanceData) == 80);
class IndirectDrawList {
public:
struct View {
std::uint32_t firstCommand{0};
std::uint32_t commandCount{0};
std::uint32_t instanceCount{0};
};
enum class Filter {
All,
ShadowCasters,
};
void cleanup(GfxDevice& gfxDevice);
void begin(std::uint32_t frameIndex);
View addView(
const std::vector<GPUMesh>& meshes,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const Frustum& frustum,
Filter filter);
void upload(GfxDevice& gfxDevice);
void draw(VkCommandBuffer cmd, const GPUBuffer& indexBuffer, const View& view) const;
[[nodiscard]] VkDeviceAddress getInstanceBufferAddress() const;
[[nodiscard]] const std::vector<GPUInstanceData>& getInstances() const { return instances; }
[[nodiscard]] const std::vector<VkDrawIndexedIndirectCommand>& getCommands() const { return commands; }
private:
struct PerFrameBuffers {
GPUBuffer instances;
GPUBuffer commands;
std::size_t instanceCapacity{0};
std::size_t commandCapacity{0};
};
static constexpr std::size_t InitialInstanceCapacity = 1024;
static constexpr std::size_t InitialCommandCapacity = 256;
std::array<PerFrameBuffers, FRAMES_IN_FLIGHT> frames{};
std::uint32_t currentFrame{0};
std::vector<GPUInstanceData> instances;
std::vector<VkDrawIndexedIndirectCommand> commands;
};
#endif // INDIRECTDRAWLIST_H
@@ -27,6 +27,8 @@ public:
VkPhysicalDeviceFeatures device{
.imageCubeArray = VK_TRUE,
.geometryShader = VK_TRUE,
.multiDrawIndirect = VK_TRUE,
.drawIndirectFirstInstance = VK_TRUE,
.depthClamp = VK_TRUE,
.fillModeNonSolid = VK_TRUE,
.samplerAnisotropy = VK_TRUE,
@@ -9,9 +9,8 @@
#include <vulkan/vulkan.h>
#include <destrum/Graphics/Pipeline.h>
#include <destrum/Graphics/Camera.h>
#include <destrum/Graphics/IndirectDrawList.h>
#include <destrum/Graphics/Resources/Buffer.h>
#include <destrum/Graphics/MeshDrawCommand.h>
class GfxDevice;
class RenderResources;
@@ -31,19 +30,17 @@ public:
VkCommandBuffer cmd,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands);
const IndirectDrawList& drawList,
const IndirectDrawList::View& view);
void draw(
VkCommandBuffer cmd,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands);
const IndirectDrawList& drawList,
const IndirectDrawList::View& view);
void cleanup(VkDevice device);
@@ -57,11 +54,8 @@ public:
private:
struct PushConstants {
glm::mat4 transform;
VkDeviceAddress sceneDataBuffer;
VkDeviceAddress vertexBuffer;
std::uint32_t materialId;
std::uint32_t padding;
VkDeviceAddress instanceBuffer;
};
void drawMeshes(
@@ -69,10 +63,9 @@ private:
const Pipeline& pipeline,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands);
const IndirectDrawList& drawList,
const IndirectDrawList::View& view);
private:
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
@@ -11,7 +11,7 @@
#include <destrum/Graphics/Light.h>
#include <destrum/Graphics/Pipeline.h>
#include <destrum/Graphics/ids.h>
#include <destrum/Graphics/MeshDrawCommand.h>
#include <destrum/Graphics/IndirectDrawList.h>
class GfxDevice;
struct GPUImage;
@@ -32,15 +32,15 @@ public:
void drawDirectionalShadow(
VkCommandBuffer cmd,
const RenderResources& resources,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const IndirectDrawList& drawList,
const IndirectDrawList::View& view,
const glm::mat4& lightViewProj);
void drawSpotShadows(
VkCommandBuffer cmd,
const RenderResources& resources,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const IndirectDrawList& drawList,
const std::vector<IndirectDrawList::View>& views,
const std::vector<glm::mat4>& lightViewProjs);
[[nodiscard]] glm::mat4 calculateDirectionalViewProj(const glm::vec3& lightDirection, const glm::vec3& center) const;
@@ -57,8 +57,8 @@ public:
private:
struct PushConstants
{
glm::mat4 modelToLightClip;
VkDeviceAddress vertexBuffer;
glm::mat4 lightViewProj;
VkDeviceAddress instanceBuffer;
};
void beginDepthRendering(VkCommandBuffer cmd, const RenderResources& resources, const GPUImage& depthImage);
@@ -67,8 +67,8 @@ private:
void drawShadowCasters(
VkCommandBuffer cmd,
const RenderResources& resources,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const IndirectDrawList& drawList,
const IndirectDrawList::View& view,
const glm::mat4& lightViewProj);
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
@@ -5,6 +5,7 @@
#include <glm/vec3.hpp>
#include <destrum/Graphics/Camera.h>
#include <destrum/Graphics/IndirectDrawList.h>
#include <destrum/Graphics/Light.h>
#include <destrum/Graphics/Pipelines/LightCullingPipeline.h>
#include <destrum/Graphics/Pipelines/LineRenderingPass.h>
@@ -29,6 +30,15 @@
class GameRenderer
{
public:
struct DrawStats
{
std::uint32_t meshDraws{0};
std::uint32_t indirectCommands{0};
std::uint32_t instances{0};
std::uint32_t mainViewCommands{0};
std::uint32_t mainViewInstances{0};
};
struct SceneData
{
const Camera& camera;
@@ -106,6 +116,8 @@ public:
return resources;
}
[[nodiscard]] const DrawStats& getDrawStats() const { return lastDrawStats; }
void draw2D(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
@@ -125,6 +137,8 @@ private:
std::vector<MeshDrawCommand> meshDrawCommands;
std::vector<std::size_t> sortedMeshDrawCommands;
IndirectDrawList drawList;
DrawStats lastDrawStats;
VkFormat drawImageFormat{VK_FORMAT_R16G16B16A16_SFLOAT};
VkFormat depthImageFormat{VK_FORMAT_D32_SFLOAT};
@@ -20,10 +20,11 @@
struct GPUMesh {
GPUBuffer vertexBuffer;
GPUBuffer indexBuffer;
std::uint32_t numVertices{0};
std::uint32_t numIndices{0};
// offset into MeshCache::getIndexBuffer(), every mesh shares that one index buffer
std::uint32_t firstIndex{0};
// AABB
glm::vec3 minPos;
+7
View File
@@ -208,6 +208,13 @@ if (!isRunning) break;
if (ImGui::Begin("Debug", &m_DebugWindows.stats))
{
ImGui::Text("FPS: %.2f", avgFPS);
const auto& stats = renderer.getDrawStats();
ImGui::SeparatorText("Meshes");
ImGui::Text("Submitted meshes: %u", stats.meshDraws);
ImGui::Text("Camera: %u indirect draws, %u instances", stats.mainViewCommands, stats.mainViewInstances);
ImGui::Text("All views: %u indirect draws, %u instances", stats.indirectCommands, stats.instances);
ImGui::Text("Pending GPU deletions: %zu", gfxDevice.getPendingDeletionCount());
}
ImGui::End();
}
+175
View File
@@ -0,0 +1,175 @@
#include <destrum/Graphics/IndirectDrawList.h>
#include <algorithm>
#include <cassert>
#include <cstring>
#include <volk.h>
#include <tracy/Tracy.hpp>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/Resources/Mesh.h>
#include <destrum/Graphics/Util.h>
namespace {
std::size_t grownCapacity(std::size_t current, std::size_t initial, std::size_t required)
{
std::size_t capacity = std::max(current, initial);
while (capacity < required) {
capacity *= 2;
}
return capacity;
}
}
void IndirectDrawList::cleanup(GfxDevice& gfxDevice)
{
for (auto& frame : frames) {
gfxDevice.destroyBuffer(frame.instances);
gfxDevice.destroyBuffer(frame.commands);
frame.instanceCapacity = 0;
frame.commandCapacity = 0;
}
instances.clear();
commands.clear();
}
void IndirectDrawList::begin(std::uint32_t frameIndex)
{
assert(frameIndex < FRAMES_IN_FLIGHT);
currentFrame = frameIndex;
instances.clear();
commands.clear();
}
IndirectDrawList::View IndirectDrawList::addView(
const std::vector<GPUMesh>& meshes,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const Frustum& frustum,
Filter filter)
{
ZoneScopedN("IndirectDrawList::addView");
View view{
.firstCommand = static_cast<std::uint32_t>(commands.size()),
};
MeshID batchMesh = NULL_MESH_ID;
const auto addDraw = [&](const MeshDrawCommand& dc)
{
if (filter == Filter::ShadowCasters && !dc.castShadow) {
return;
}
if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) {
return;
}
assert(dc.meshId < meshes.size());
assert(dc.materialId != NULL_MATERIAL_ID);
const GPUMesh& mesh = meshes[dc.meshId];
// Draws arrive sorted by mesh, so every run of the same mesh becomes one
// instanced command. Each instance carries its own transform, material and
// vertex buffer (skinned meshes point at their own skinned copy).
if (dc.meshId != batchMesh) {
batchMesh = dc.meshId;
commands.push_back(VkDrawIndexedIndirectCommand{
.indexCount = mesh.numIndices,
.instanceCount = 0,
.firstIndex = mesh.firstIndex,
.vertexOffset = 0,
.firstInstance = static_cast<std::uint32_t>(instances.size()),
});
++view.commandCount;
}
instances.push_back(GPUInstanceData{
.transform = dc.transformMatrix,
.vertexBuffer = dc.skinnedMesh != nullptr
? dc.skinnedMesh->skinnedVertexBuffer.address
: mesh.vertexBuffer.address,
.materialId = dc.materialId,
.padding = 0,
});
++commands.back().instanceCount;
++view.instanceCount;
};
if (!sortedDrawCommands.empty()) {
for (std::size_t drawIndex : sortedDrawCommands) {
assert(drawIndex < drawCommands.size());
addDraw(drawCommands[drawIndex]);
}
} else {
for (const MeshDrawCommand& dc : drawCommands) {
addDraw(dc);
}
}
return view;
}
void IndirectDrawList::upload(GfxDevice& gfxDevice)
{
ZoneScopedN("IndirectDrawList::upload");
PerFrameBuffers& frame = frames[currentFrame];
// Growing is cheap thanks to the deletion queue: the old buffer may still be
// read by this slot's previous frame, so it is retired instead of destroyed.
if (instances.size() > frame.instanceCapacity || frame.instances.buffer == VK_NULL_HANDLE) {
gfxDevice.destroyBuffer(frame.instances);
frame.instanceCapacity = grownCapacity(frame.instanceCapacity, InitialInstanceCapacity, instances.size());
frame.instances = gfxDevice.createBuffer(
frame.instanceCapacity * sizeof(GPUInstanceData),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
vkutil::addDebugLabel(gfxDevice.getDevice(), frame.instances.buffer, "mesh instances");
}
if (commands.size() > frame.commandCapacity || frame.commands.buffer == VK_NULL_HANDLE) {
gfxDevice.destroyBuffer(frame.commands);
frame.commandCapacity = grownCapacity(frame.commandCapacity, InitialCommandCapacity, commands.size());
frame.commands = gfxDevice.createBuffer(
frame.commandCapacity * sizeof(VkDrawIndexedIndirectCommand),
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
vkutil::addDebugLabel(gfxDevice.getDevice(), frame.commands.buffer, "mesh indirect commands");
}
// Each frame in flight has its own buffers, so nothing on the GPU is reading
// these while we write. vkQueueSubmit makes the host writes visible to the GPU.
if (!instances.empty()) {
std::memcpy(frame.instances.info.pMappedData, instances.data(), instances.size() * sizeof(GPUInstanceData));
gfxDevice.getMemoryManager().flushAllocation(frame.instances, 0, instances.size() * sizeof(GPUInstanceData));
}
if (!commands.empty()) {
std::memcpy(frame.commands.info.pMappedData, commands.data(), commands.size() * sizeof(VkDrawIndexedIndirectCommand));
gfxDevice.getMemoryManager().flushAllocation(frame.commands, 0, commands.size() * sizeof(VkDrawIndexedIndirectCommand));
}
}
void IndirectDrawList::draw(VkCommandBuffer cmd, const GPUBuffer& indexBuffer, const View& view) const
{
if (view.commandCount == 0) {
return;
}
const PerFrameBuffers& frame = frames[currentFrame];
assert(frame.commands.buffer != VK_NULL_HANDLE && "IndirectDrawList::upload must run before draw");
vkCmdBindIndexBuffer(cmd, indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexedIndirect(
cmd,
frame.commands.buffer,
static_cast<VkDeviceSize>(view.firstCommand) * sizeof(VkDrawIndexedIndirectCommand),
view.commandCount,
sizeof(VkDrawIndexedIndirectCommand));
}
VkDeviceAddress IndirectDrawList::getInstanceBufferAddress() const
{
return frames[currentFrame].instances.address;
}
+79 -18
View File
@@ -49,15 +49,73 @@ MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh)
return id;
}
void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const
void MeshCache::ensureIndexCapacity(GfxDevice& gfxDevice, std::uint32_t requiredIndices)
{
if (requiredIndices <= indexCapacity) {
return;
}
std::uint32_t newCapacity = std::max(indexCapacity, InitialIndexCapacity);
while (newCapacity < requiredIndices) {
newCapacity *= 2;
}
GPUBuffer newBuffer = gfxDevice.createBuffer(
static_cast<std::size_t>(newCapacity) * sizeof(std::uint32_t),
VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE);
vkutil::addDebugLabel(gfxDevice.getDevice(), newBuffer.buffer, "mesh indices");
if (indexCount > 0) {
try {
gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) {
const VkBufferMemoryBarrier2 previousCopies{
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.buffer = indexBuffer.buffer,
.offset = 0,
.size = VK_WHOLE_SIZE,
};
const VkDependencyInfo dependency{
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
.bufferMemoryBarrierCount = 1,
.pBufferMemoryBarriers = &previousCopies,
};
vkCmdPipelineBarrier2(cmd, &dependency);
const VkBufferCopy copy{
.srcOffset = 0,
.dstOffset = 0,
.size = static_cast<VkDeviceSize>(indexCount) * sizeof(std::uint32_t),
};
vkCmdCopyBuffer(cmd, indexBuffer.buffer, newBuffer.buffer, 1, &copy);
});
} catch (...) {
gfxDevice.destroyBufferNow(newBuffer);
throw;
}
}
// frames in flight were recorded with the old buffer, the deletion queue keeps it alive for them
gfxDevice.destroyBuffer(indexBuffer);
indexBuffer = newBuffer;
indexCapacity = newCapacity;
}
void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh)
{
try {
// create index buffer
const auto numIndices = static_cast<std::uint32_t>(cpuMesh.indices.size());
ensureIndexCapacity(gfxDevice, indexCount + numIndices);
gpuMesh.firstIndex = indexCount;
const auto indexBufferSize = cpuMesh.indices.size() * sizeof(std::uint32_t);
gpuMesh.indexBuffer = gfxDevice.createBuffer(
indexBufferSize,
VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE);
const auto indexBufferOffset = static_cast<VkDeviceSize>(gpuMesh.firstIndex) * sizeof(std::uint32_t);
// create vertex buffer
const auto vertexBufferSize = cpuMesh.vertices.size() * sizeof(CPUMesh::Vertex);
@@ -81,6 +139,8 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
memcpy(static_cast<char*>(vertexIndexData) + vertexBufferSize, cpuMesh.indices.data(), indexBufferSize);
gfxDevice.getMemoryManager().flushAllocation(vertexIndexStaging);
// Writing into the shared index buffer is safe while frames are in flight:
// this range is new, they only read the ranges of meshes added before it.
gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) {
vkutil::bufferHostWriteToTransferReadBarrier(
cmd,
@@ -97,17 +157,17 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
const auto indexCopy = VkBufferCopy{
.srcOffset = vertexBufferSize,
.dstOffset = 0,
.dstOffset = indexBufferOffset,
.size = indexBufferSize,
};
vkCmdCopyBuffer(cmd, vertexIndexStaging.buffer, gpuMesh.indexBuffer.buffer, 1, &indexCopy);
vkCmdCopyBuffer(cmd, vertexIndexStaging.buffer, indexBuffer.buffer, 1, &indexCopy);
const std::array<VkBufferMemoryBarrier2, 2> destinationBarriers{{
{
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
@@ -119,13 +179,13 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT,
.dstAccessMask = VK_ACCESS_2_INDEX_READ_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_INDEX_INPUT_BIT | VK_PIPELINE_STAGE_2_COPY_BIT,
.dstAccessMask = VK_ACCESS_2_INDEX_READ_BIT | VK_ACCESS_2_TRANSFER_READ_BIT,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.buffer = gpuMesh.indexBuffer.buffer,
.offset = 0,
.size = VK_WHOLE_SIZE,
.buffer = indexBuffer.buffer,
.offset = indexBufferOffset,
.size = indexBufferSize,
}
}};
const VkDependencyInfo destinationDependency{
@@ -141,11 +201,10 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
}
gfxDevice.destroyBufferNow(vertexIndexStaging);
indexCount += numIndices;
const auto vtxBufferName = cpuMesh.name + " (vtx)";
const auto idxBufferName = cpuMesh.name + " (idx)";
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.vertexBuffer.buffer, vtxBufferName.c_str());
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.indexBuffer.buffer, idxBufferName.c_str());
if (gpuMesh.hasSkeleton) {
// create skinning data buffer
@@ -214,7 +273,6 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
} catch (...) {
gfxDevice.destroyBufferNow(gpuMesh.skinningDataBuffer);
gfxDevice.destroyBufferNow(gpuMesh.vertexBuffer);
gfxDevice.destroyBufferNow(gpuMesh.indexBuffer);
gpuMesh = {};
throw;
}
@@ -246,8 +304,11 @@ std::optional<MeshID> MeshCache::findMeshByKey(std::string_view key) const
void MeshCache::cleanup(GfxDevice& gfxDevice)
{
gfxDevice.destroyBuffer(indexBuffer);
indexCount = 0;
indexCapacity = 0;
for (auto& mesh : meshes) {
gfxDevice.destroyBuffer(mesh.indexBuffer);
gfxDevice.destroyBuffer(mesh.vertexBuffer);
gfxDevice.destroyBuffer(mesh.skinningDataBuffer);
}
+34 -96
View File
@@ -8,7 +8,6 @@
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Caches/MeshCache.h>
#include <destrum/Graphics/Frustum.h>
#include <destrum/Graphics/Util.h>
#include "volk.h"
@@ -119,10 +118,9 @@ void MeshPipeline::drawDepthPrepass(
VkCommandBuffer cmd,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands)
const IndirectDrawList& drawList,
const IndirectDrawList::View& view)
{
if (!m_depthPrepassPipeline)
{
@@ -134,20 +132,18 @@ void MeshPipeline::drawDepthPrepass(
*m_depthPrepassPipeline,
renderExtent,
resources,
camera,
sceneDataBuffer,
drawCommands,
sortedDrawCommands);
drawList,
view);
}
void MeshPipeline::draw(
VkCommandBuffer cmd,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands)
const IndirectDrawList& drawList,
const IndirectDrawList::View& view)
{
if (!m_pipeline)
{
@@ -159,10 +155,9 @@ void MeshPipeline::draw(
*m_pipeline,
renderExtent,
resources,
camera,
sceneDataBuffer,
drawCommands,
sortedDrawCommands);
drawList,
view);
}
void MeshPipeline::drawMeshes(
@@ -170,13 +165,10 @@ void MeshPipeline::drawMeshes(
const Pipeline& pipeline,
VkExtent2D renderExtent,
const RenderResources& resources,
const Camera& camera,
const GPUBuffer& sceneDataBuffer,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands)
const IndirectDrawList& drawList,
const IndirectDrawList::View& view)
{
const MeshCache& meshCache = resources.meshes();
pipeline.bind(cmd);
resources.bindBindlessDescSet(cmd, m_pipelineLayout);
@@ -204,86 +196,32 @@ void MeshPipeline::drawMeshes(
m_renderWireframe ? VK_POLYGON_MODE_LINE : VK_POLYGON_MODE_FILL
);
MeshID prevMeshId = NULL_MESH_ID;
int actualDrawCalls = 0;
const auto frustum = edge::createFrustumFromCamera(camera);
const auto drawOne = [&](const MeshDrawCommand& dc)
{
if (!edge::isInFrustum(frustum, dc.worldBoundingSphere))
{
return;
}
++actualDrawCalls;
const auto& mesh = meshCache.getMesh(dc.meshId);
const std::string& meshKey = meshCache.getMeshKey(dc.meshId);
vkutil::cmdBeginLabelFormatted(
cmd,
vkutil::labelColors::draw,
"%s [mesh %zu, material %u%s]",
meshKey.empty() ? "Mesh" : meshKey.c_str(),
dc.meshId,
dc.materialId,
dc.skinnedMesh != nullptr ? ", skinned" : "");
if (dc.meshId != prevMeshId)
{
prevMeshId = dc.meshId;
vkCmdBindIndexBuffer(
cmd,
mesh.indexBuffer.buffer,
0,
VK_INDEX_TYPE_UINT32
);
}
assert(dc.materialId != NULL_MATERIAL_ID);
const auto pushConstants = PushConstants{
.transform = dc.transformMatrix,
.sceneDataBuffer = sceneDataBuffer.address,
.vertexBuffer =
dc.skinnedMesh != nullptr
? dc.skinnedMesh->skinnedVertexBuffer.address
: mesh.vertexBuffer.address,
.materialId = dc.materialId,
.padding = 0,
};
vkCmdPushConstants(
cmd,
m_pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
0,
sizeof(PushConstants),
&pushConstants
);
vkCmdDrawIndexed(cmd, mesh.numIndices, 1, 0, 0, 0);
vkutil::cmdEndLabel(cmd);
// The same for every draw, per-draw data (transform, material, vertex buffer)
// lives in the instance buffer and is looked up with gl_InstanceIndex
const auto pushConstants = PushConstants{
.sceneDataBuffer = sceneDataBuffer.address,
.instanceBuffer = drawList.getInstanceBufferAddress(),
};
if (!sortedDrawCommands.empty())
{
for (std::size_t drawIndex : sortedDrawCommands)
{
assert(drawIndex < drawCommands.size());
drawOne(drawCommands[drawIndex]);
}
}
else
{
for (const MeshDrawCommand& dc : drawCommands)
{
drawOne(dc);
}
}
vkCmdPushConstants(
cmd,
m_pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
0,
sizeof(PushConstants),
&pushConstants
);
vkutil::cmdBeginLabelFormatted(
cmd,
vkutil::labelColors::draw,
"%u indirect draws, %u instances",
view.commandCount,
view.instanceCount);
drawList.draw(cmd, resources.meshes().getIndexBuffer(), view);
vkutil::cmdEndLabel(cmd);
}
void MeshPipeline::cleanup(VkDevice device)
@@ -12,7 +12,6 @@
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Caches/MeshCache.h>
#include <destrum/Graphics/Frustum.h>
#include <destrum/Graphics/Util.h>
#include "volk.h"
@@ -206,8 +205,8 @@ float ShadowMapPipeline::getNormalOffset() const
void ShadowMapPipeline::drawDirectionalShadow(
VkCommandBuffer cmd,
const RenderResources& resources,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const IndirectDrawList& drawList,
const IndirectDrawList::View& view,
const glm::mat4& lightViewProj)
{
if (!m_pipeline || shadowMapId == NULL_IMAGE_ID)
@@ -219,15 +218,15 @@ void ShadowMapPipeline::drawDirectionalShadow(
beginDepthRendering(cmd, resources, shadowMap);
setRenderRegion(cmd, VkRect2D{.offset = {}, .extent = shadowMap.getExtent2D()});
drawShadowCasters(cmd, resources, drawCommands, sortedDrawCommands, lightViewProj);
drawShadowCasters(cmd, resources, drawList, view, lightViewProj);
endDepthRendering(cmd, shadowMap);
}
void ShadowMapPipeline::drawSpotShadows(
VkCommandBuffer cmd,
const RenderResources& resources,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const IndirectDrawList& drawList,
const std::vector<IndirectDrawList::View>& views,
const std::vector<glm::mat4>& lightViewProjs)
{
if (!m_pipeline || spotShadowAtlasId == NULL_IMAGE_ID || lightViewProjs.empty())
@@ -236,6 +235,7 @@ void ShadowMapPipeline::drawSpotShadows(
}
assert(lightViewProjs.size() <= MAX_SPOT_SHADOWS);
assert(views.size() == lightViewProjs.size());
const auto& spotShadowAtlas = resources.getImage(spotShadowAtlasId);
beginDepthRendering(cmd, resources, spotShadowAtlas);
@@ -244,7 +244,7 @@ void ShadowMapPipeline::drawSpotShadows(
vkutil::cmdBeginLabelFormatted(cmd, vkutil::labelColors::shadow, "Spot shadow %u", shadowIndex);
setRenderRegion(cmd, spotShadowTileRegion(shadowIndex));
drawShadowCasters(cmd, resources, drawCommands, sortedDrawCommands, lightViewProjs[shadowIndex]);
drawShadowCasters(cmd, resources, drawList, views[shadowIndex], lightViewProjs[shadowIndex]);
vkutil::cmdEndLabel(cmd);
}
@@ -306,73 +306,31 @@ void ShadowMapPipeline::setRenderRegion(VkCommandBuffer cmd, const VkRect2D& reg
void ShadowMapPipeline::drawShadowCasters(
VkCommandBuffer cmd,
const RenderResources& resources,
const std::vector<MeshDrawCommand>& drawCommands,
const std::vector<std::size_t>& sortedDrawCommands,
const IndirectDrawList& drawList,
const IndirectDrawList::View& view,
const glm::mat4& lightViewProj)
{
const MeshCache& meshCache = resources.meshes();
const Frustum lightFrustum = edge::createFrustumFromViewProj(lightViewProj);
MeshID previousMeshId = NULL_MESH_ID;
const auto drawCaster = [&](const MeshDrawCommand& dc)
{
if (!dc.castShadow || !edge::isInFrustum(lightFrustum, dc.worldBoundingSphere))
{
return;
}
const auto& mesh = meshCache.getMesh(dc.meshId);
const std::string& meshKey = meshCache.getMeshKey(dc.meshId);
vkutil::cmdBeginLabelFormatted(
cmd,
vkutil::labelColors::draw,
"%s [mesh %zu, material %u%s]",
meshKey.empty() ? "Mesh" : meshKey.c_str(),
dc.meshId,
dc.materialId,
dc.skinnedMesh != nullptr ? ", skinned" : "");
if (dc.meshId != previousMeshId)
{
previousMeshId = dc.meshId;
vkCmdBindIndexBuffer(cmd, mesh.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
}
const auto pushConstants = PushConstants{
.modelToLightClip = lightViewProj * dc.transformMatrix,
.vertexBuffer =
dc.skinnedMesh != nullptr
? dc.skinnedMesh->skinnedVertexBuffer.address
: mesh.vertexBuffer.address,
};
vkCmdPushConstants(
cmd,
m_pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(PushConstants),
&pushConstants);
vkCmdDrawIndexed(cmd, mesh.numIndices, 1, 0, 0, 0);
vkutil::cmdEndLabel(cmd);
const auto pushConstants = PushConstants{
.lightViewProj = lightViewProj,
.instanceBuffer = drawList.getInstanceBufferAddress(),
};
if (!sortedDrawCommands.empty())
{
for (std::size_t drawIndex : sortedDrawCommands)
{
assert(drawIndex < drawCommands.size());
drawCaster(drawCommands[drawIndex]);
}
}
else
{
for (const MeshDrawCommand& dc : drawCommands)
{
drawCaster(dc);
}
}
vkCmdPushConstants(
cmd,
m_pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(PushConstants),
&pushConstants);
vkutil::cmdBeginLabelFormatted(
cmd,
vkutil::labelColors::draw,
"%u indirect draws, %u instances",
view.commandCount,
view.instanceCount);
drawList.draw(cmd, resources.meshes().getIndexBuffer(), view);
vkutil::cmdEndLabel(cmd);
}
+61 -21
View File
@@ -79,22 +79,14 @@ void GameRenderer::endDrawing()
sortedMeshDrawCommands.resize(meshDrawCommands.size());
std::iota(sortedMeshDrawCommands.begin(), sortedMeshDrawCommands.end(), 0);
// Grouping by mesh lets IndirectDrawList merge each run into one instanced draw.
// Material and vertex buffer are per instance, so they don't break a batch.
std::stable_sort(
sortedMeshDrawCommands.begin(),
sortedMeshDrawCommands.end(),
[this](std::size_t left, std::size_t right)
{
const auto& lhs = meshDrawCommands[left];
const auto& rhs = meshDrawCommands[right];
if (lhs.meshId != rhs.meshId)
{
return lhs.meshId < rhs.meshId;
}
if (lhs.materialId != rhs.materialId)
{
return lhs.materialId < rhs.materialId;
}
return (lhs.skinnedMesh != nullptr) < (rhs.skinnedMesh != nullptr);
return meshDrawCommands[left].meshId < meshDrawCommands[right].meshId;
});
}
@@ -147,6 +139,55 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
spotShadowViewProjs.push_back(spotShadow.lightViewProj);
}
// Culling stays on the CPU: every view (camera, directional light, each spot light)
// gets its own range of indirect commands in this frame's draw list.
IndirectDrawList::View mainView;
IndirectDrawList::View directionalShadowView;
std::vector<IndirectDrawList::View> spotShadowViews;
{
ZoneScopedN("Build Indirect Draws");
const auto& meshes = resources->meshes().getMeshes();
drawList.begin(gfxDevice.getCurrentFrameIndex());
mainView = drawList.addView(
meshes,
meshDrawCommands,
sortedMeshDrawCommands,
edge::createFrustumFromCamera(camera),
IndirectDrawList::Filter::All);
if (shadowLightDirection)
{
directionalShadowView = drawList.addView(
meshes,
meshDrawCommands,
sortedMeshDrawCommands,
edge::createFrustumFromViewProj(lightViewProj),
IndirectDrawList::Filter::ShadowCasters);
}
spotShadowViews.reserve(spotShadowViewProjs.size());
for (const glm::mat4& spotViewProj : spotShadowViewProjs)
{
spotShadowViews.push_back(drawList.addView(
meshes,
meshDrawCommands,
sortedMeshDrawCommands,
edge::createFrustumFromViewProj(spotViewProj),
IndirectDrawList::Filter::ShadowCasters));
}
drawList.upload(gfxDevice);
lastDrawStats = DrawStats{
.meshDraws = static_cast<std::uint32_t>(meshDrawCommands.size()),
.indirectCommands = static_cast<std::uint32_t>(drawList.getCommands().size()),
.instances = static_cast<std::uint32_t>(drawList.getInstances().size()),
.mainViewCommands = mainView.commandCount,
.mainViewInstances = mainView.instanceCount,
};
}
// TODO: there has to be a better way to do this
const auto gpuSceneData = GPUSceneData{
.view = sceneData.camera.GetViewMatrix(),
@@ -225,8 +266,8 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
shadowMapPipeline->drawDirectionalShadow(
cmd,
*resources,
meshDrawCommands,
sortedMeshDrawCommands,
drawList,
directionalShadowView,
lightViewProj);
}
@@ -238,8 +279,8 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
shadowMapPipeline->drawSpotShadows(
cmd,
*resources,
meshDrawCommands,
sortedMeshDrawCommands,
drawList,
spotShadowViews,
spotShadowViewProjs);
}
@@ -260,10 +301,9 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
cmd,
drawImage.getExtent2D(),
*resources,
camera,
sceneDataBuffer.getBuffer(),
meshDrawCommands,
sortedMeshDrawCommands);
drawList,
mainView);
vkCmdEndRendering(cmd);
}
@@ -319,10 +359,9 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
cmd,
drawImage.getExtent2D(),
*resources,
camera,
sceneDataBuffer.getBuffer(),
meshDrawCommands,
sortedMeshDrawCommands);
drawList,
mainView);
}
{
@@ -400,6 +439,7 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
meshPipeline->cleanup(device);
sceneDataBuffer.cleanup(gfxDevice);
drawList.cleanup(gfxDevice);
// if (drawImageId != NULL_IMAGE_ID)
// gfxDevice.destroyImage();