initial commit-moved from vulkan_guide

This commit is contained in:
2025-10-10 22:53:54 +09:00
commit 8853429937
2484 changed files with 973414 additions and 0 deletions

82
src/render/primitives.h Normal file
View File

@@ -0,0 +1,82 @@
#pragma once
#include <vector>
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
#include "core/vk_types.h"
namespace primitives {
inline void buildCube(std::vector<Vertex>& vertices, std::vector<uint32_t>& indices) {
vertices.clear();
indices.clear();
struct Face {
glm::vec3 normal;
glm::vec3 v0, v1, v2, v3;
} faces[6] = {
{ {0,0,1}, { -0.5f,-0.5f, 0.5f}, { 0.5f,-0.5f, 0.5f}, { -0.5f, 0.5f, 0.5f}, { 0.5f, 0.5f, 0.5f} },
{ {0,0,-1},{ -0.5f,-0.5f,-0.5f}, { -0.5f, 0.5f,-0.5f}, { 0.5f,-0.5f,-0.5f}, { 0.5f, 0.5f,-0.5f} },
{ {0,1,0}, { -0.5f, 0.5f, 0.5f}, { 0.5f, 0.5f, 0.5f}, { -0.5f, 0.5f,-0.5f}, { 0.5f, 0.5f,-0.5f} },
{ {0,-1,0},{ -0.5f,-0.5f, 0.5f}, { -0.5f,-0.5f,-0.5f}, { 0.5f,-0.5f, 0.5f}, { 0.5f,-0.5f,-0.5f} },
{ {1,0,0}, { 0.5f,-0.5f, 0.5f}, { 0.5f,-0.5f,-0.5f}, { 0.5f, 0.5f, 0.5f}, { 0.5f, 0.5f,-0.5f} },
{ {-1,0,0},{ -0.5f,-0.5f, 0.5f}, { -0.5f, 0.5f, 0.5f}, { -0.5f,-0.5f,-0.5f}, { -0.5f, 0.5f,-0.5f} }
};
for (auto& f : faces) {
uint32_t start = (uint32_t)vertices.size();
Vertex v0{f.v0, 0, f.normal, 0, glm::vec4(1.0f)};
Vertex v1{f.v1, 1, f.normal, 0, glm::vec4(1.0f)};
Vertex v2{f.v2, 0, f.normal, 1, glm::vec4(1.0f)};
Vertex v3{f.v3, 1, f.normal, 1, glm::vec4(1.0f)};
vertices.push_back(v0);
vertices.push_back(v1);
vertices.push_back(v2);
vertices.push_back(v3);
indices.push_back(start + 0);
indices.push_back(start + 1);
indices.push_back(start + 2);
indices.push_back(start + 2);
indices.push_back(start + 1);
indices.push_back(start + 3);
}
}
inline void buildSphere(std::vector<Vertex>& vertices, std::vector<uint32_t>& indices, int sectors = 16, int stacks = 16) {
vertices.clear();
indices.clear();
float radius = 0.5f;
for (int i = 0; i <= stacks; ++i) {
float v = (float)i / stacks;
const float phi = v * glm::pi<float>();
float y = cos(phi);
float r = sin(phi);
for (int j = 0; j <= sectors; ++j) {
float u = (float)j / sectors;
float theta = u * glm::two_pi<float>();
float x = r * cos(theta);
float z = r * sin(theta);
Vertex vert;
vert.position = glm::vec3(x, y, z) * radius;
vert.normal = glm::normalize(glm::vec3(x, y, z));
vert.uv_x = u;
vert.uv_y = 1.0f - v;
vert.color = glm::vec4(1.0f);
vertices.push_back(vert);
}
}
for (int i = 0; i < stacks; ++i) {
for (int j = 0; j < sectors; ++j) {
uint32_t first = i * (sectors + 1) + j;
uint32_t second = first + sectors + 1;
indices.push_back(first);
indices.push_back(second);
indices.push_back(first + 1);
indices.push_back(first + 1);
indices.push_back(second);
indices.push_back(second + 1);
}
}
}
} // namespace primitives

98
src/render/rg_builder.cpp Normal file
View File

@@ -0,0 +1,98 @@
#include <render/rg_builder.h>
#include <render/rg_resources.h>
// ---- RGPassResources ----
VkImage RGPassResources::image(RGImageHandle h) const
{
const RGImageRecord *rec = _registry ? _registry->get_image(h) : nullptr;
return rec ? rec->image : VK_NULL_HANDLE;
}
VkImageView RGPassResources::image_view(RGImageHandle h) const
{
const RGImageRecord *rec = _registry ? _registry->get_image(h) : nullptr;
return rec ? rec->imageView : VK_NULL_HANDLE;
}
VkBuffer RGPassResources::buffer(RGBufferHandle h) const
{
const RGBufferRecord *rec = _registry ? _registry->get_buffer(h) : nullptr;
return rec ? rec->buffer : VK_NULL_HANDLE;
}
// ---- RGPassBuilder ----
void RGPassBuilder::read(RGImageHandle h, RGImageUsage usage)
{
_imageReads.push_back({h, usage});
}
void RGPassBuilder::write(RGImageHandle h, RGImageUsage usage)
{
_imageWrites.push_back({h, usage});
}
void RGPassBuilder::read_buffer(RGBufferHandle h, RGBufferUsage usage)
{
_bufferReads.push_back({h, usage});
}
void RGPassBuilder::write_buffer(RGBufferHandle h, RGBufferUsage usage)
{
_bufferWrites.push_back({h, usage});
}
void RGPassBuilder::read_buffer(VkBuffer buffer, RGBufferUsage usage, VkDeviceSize size, const char* name)
{
if (!_registry || buffer == VK_NULL_HANDLE) return;
// Dedup/import
RGBufferHandle h = _registry->find_buffer(buffer);
if (!h.valid())
{
RGImportedBufferDesc d{};
d.name = name ? name : "external.buffer";
d.buffer = buffer;
d.size = size;
d.currentStage = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT;
d.currentAccess = 0;
h = _registry->add_imported(d);
}
read_buffer(h, usage);
}
void RGPassBuilder::write_buffer(VkBuffer buffer, RGBufferUsage usage, VkDeviceSize size, const char* name)
{
if (!_registry || buffer == VK_NULL_HANDLE) return;
RGBufferHandle h = _registry->find_buffer(buffer);
if (!h.valid())
{
RGImportedBufferDesc d{};
d.name = name ? name : "external.buffer";
d.buffer = buffer;
d.size = size;
d.currentStage = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT;
d.currentAccess = 0;
h = _registry->add_imported(d);
}
write_buffer(h, usage);
}
void RGPassBuilder::write_color(RGImageHandle h, bool clearOnLoad, VkClearValue clear)
{
RGAttachmentInfo a{};
a.image = h;
a.clearOnLoad = clearOnLoad;
a.clear = clear;
a.store = true;
_colors.push_back(a);
write(h, RGImageUsage::ColorAttachment);
}
void RGPassBuilder::write_depth(RGImageHandle h, bool clearOnLoad, VkClearValue clear)
{
if (_depthRef == nullptr) _depthRef = &_depthTemp;
_depthRef->image = h;
_depthRef->clearOnLoad = clearOnLoad;
_depthRef->clear = clear;
_depthRef->store = true;
write(h, RGImageUsage::DepthAttachment);
}

90
src/render/rg_builder.h Normal file
View File

@@ -0,0 +1,90 @@
#pragma once
#include <render/rg_types.h>
#include <vector>
class RGResourceRegistry;
class EngineContext;
struct RGPassImageAccess
{
RGImageHandle image;
RGImageUsage usage;
};
struct RGPassBufferAccess
{
RGBufferHandle buffer;
RGBufferUsage usage;
};
// Read-only interface for pass record callbacks to fetch resolved resources
class RGPassResources
{
public:
RGPassResources(const RGResourceRegistry *registry) : _registry(registry)
{
}
VkImage image(RGImageHandle h) const;
VkImageView image_view(RGImageHandle h) const;
VkBuffer buffer(RGBufferHandle h) const;
private:
const RGResourceRegistry *_registry;
};
// Builder used inside add_*_pass setup lambda to declare reads/writes/attachments
class RGPassBuilder
{
public:
RGPassBuilder(RGResourceRegistry *registry,
std::vector<RGPassImageAccess> &reads,
std::vector<RGPassImageAccess> &writes,
std::vector<RGPassBufferAccess> &bufferReads,
std::vector<RGPassBufferAccess> &bufferWrites,
std::vector<RGAttachmentInfo> &colorAttachments,
RGAttachmentInfo *&depthAttachmentRef)
: _registry(registry)
, _imageReads(reads)
, _imageWrites(writes)
, _bufferReads(bufferReads)
, _bufferWrites(bufferWrites)
, _colors(colorAttachments)
, _depthRef(depthAttachmentRef)
{
}
// Declare that the pass will sample/read an image
void read(RGImageHandle h, RGImageUsage usage);
// Declare that the pass will write to an image
void write(RGImageHandle h, RGImageUsage usage);
// Declare buffer accesses
void read_buffer(RGBufferHandle h, RGBufferUsage usage);
void write_buffer(RGBufferHandle h, RGBufferUsage usage);
// Convenience: declare access to external VkBuffer. Will import/dedup and
// register the access for this pass.
void read_buffer(VkBuffer buffer, RGBufferUsage usage, VkDeviceSize size = 0, const char* name = nullptr);
void write_buffer(VkBuffer buffer, RGBufferUsage usage, VkDeviceSize size = 0, const char* name = nullptr);
// Graphics attachments
void write_color(RGImageHandle h, bool clearOnLoad = false, VkClearValue clear = {});
void write_depth(RGImageHandle h, bool clearOnLoad = false, VkClearValue clear = {});
private:
RGResourceRegistry *_registry;
std::vector<RGPassImageAccess> &_imageReads;
std::vector<RGPassImageAccess> &_imageWrites;
std::vector<RGPassBufferAccess> &_bufferReads;
std::vector<RGPassBufferAccess> &_bufferWrites;
std::vector<RGAttachmentInfo> &_colors;
RGAttachmentInfo *&_depthRef;
RGAttachmentInfo _depthTemp{}; // temporary storage used during build
};

887
src/render/rg_graph.cpp Normal file
View File

@@ -0,0 +1,887 @@
#include <render/rg_graph.h>
#include <core/engine_context.h>
#include <core/vk_images.h>
#include <core/vk_initializers.h>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>
#include <cstdio>
#include <core/vk_swapchain.h>
#include <core/vk_initializers.h>
#include <core/vk_debug.h>
#include <fmt/core.h>
#include "vk_device.h"
void RenderGraph::init(EngineContext *ctx)
{
_context = ctx;
_resources.init(ctx);
}
void RenderGraph::clear()
{
_passes.clear();
_resources.reset();
}
RGImageHandle RenderGraph::import_image(const RGImportedImageDesc &desc)
{
return _resources.add_imported(desc);
}
RGBufferHandle RenderGraph::import_buffer(const RGImportedBufferDesc &desc)
{
return _resources.add_imported(desc);
}
RGImageHandle RenderGraph::create_image(const RGImageDesc &desc)
{
return _resources.add_transient(desc);
}
RGImageHandle RenderGraph::create_depth_image(const char* name, VkExtent2D extent, VkFormat format)
{
RGImageDesc d{};
d.name = name ? name : "depth.transient";
d.format = format;
d.extent = extent;
d.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
return create_image(d);
}
RGBufferHandle RenderGraph::create_buffer(const RGBufferDesc &desc)
{
return _resources.add_transient(desc);
}
void RenderGraph::add_pass(const char *name, RGPassType type, BuildCallback build, RecordCallback record)
{
Pass p{};
p.name = name;
p.type = type;
p.record = std::move(record);
// Build declarations via builder
RGAttachmentInfo *depthRef = nullptr;
RGPassBuilder builder(&_resources,
p.imageReads,
p.imageWrites,
p.bufferReads,
p.bufferWrites,
p.colorAttachments,
depthRef);
if (build) build(builder, _context);
if (depthRef)
{
p.hasDepth = true;
p.depthAttachment = *depthRef; // copy declared depth attachment
}
_passes.push_back(std::move(p));
}
void RenderGraph::add_pass(const char *name, RGPassType type, RecordCallback record)
{
// No declarations
add_pass(name, type, nullptr, std::move(record));
}
bool RenderGraph::compile()
{
if (!_context) return false;
// --- Build dependency graph (topological sort) from declared reads/writes ---
const int n = static_cast<int>(_passes.size());
if (n <= 1)
{
// trivial order; still compute barriers below
}
else
{
std::vector<std::unordered_set<int> > adjSet(n);
std::vector<int> indeg(n, 0);
auto add_edge = [&](int u, int v) {
if (u == v) return;
if (u < 0 || v < 0 || u >= n || v >= n) return;
if (adjSet[u].insert(v).second) indeg[v]++;
};
std::unordered_map<uint32_t, int> lastWriterImage;
std::unordered_map<uint32_t, std::vector<int> > lastReadersImage;
std::unordered_map<uint32_t, int> lastWriterBuffer;
std::unordered_map<uint32_t, std::vector<int> > lastReadersBuffer;
for (int i = 0; i < n; ++i)
{
const auto &p = _passes[i];
if (!p.enabled) continue;
// Image reads
for (const auto &r: p.imageReads)
{
if (!r.image.valid()) continue;
auto it = lastWriterImage.find(r.image.id);
if (it != lastWriterImage.end()) add_edge(it->second, i);
lastReadersImage[r.image.id].push_back(i);
}
// Image writes
for (const auto &w: p.imageWrites)
{
if (!w.image.valid()) continue;
auto itW = lastWriterImage.find(w.image.id);
if (itW != lastWriterImage.end()) add_edge(itW->second, i); // WAW
auto itR = lastReadersImage.find(w.image.id);
if (itR != lastReadersImage.end())
{
for (int rIdx: itR->second) add_edge(rIdx, i); // WAR
itR->second.clear();
}
lastWriterImage[w.image.id] = i;
}
// Buffer reads
for (const auto &r: p.bufferReads)
{
if (!r.buffer.valid()) continue;
auto it = lastWriterBuffer.find(r.buffer.id);
if (it != lastWriterBuffer.end()) add_edge(it->second, i);
lastReadersBuffer[r.buffer.id].push_back(i);
}
// Buffer writes
for (const auto &w: p.bufferWrites)
{
if (!w.buffer.valid()) continue;
auto itW = lastWriterBuffer.find(w.buffer.id);
if (itW != lastWriterBuffer.end()) add_edge(itW->second, i); // WAW
auto itR = lastReadersBuffer.find(w.buffer.id);
if (itR != lastReadersBuffer.end())
{
for (int rIdx: itR->second) add_edge(rIdx, i); // WAR
itR->second.clear();
}
lastWriterBuffer[w.buffer.id] = i;
}
}
// Kahn's algorithm
std::queue<int> q;
for (int i = 0; i < n; ++i) if (indeg[i] == 0) q.push(i);
std::vector<int> order;
order.reserve(n);
while (!q.empty())
{
int u = q.front();
q.pop();
order.push_back(u);
for (int v: adjSet[u])
{
if (--indeg[v] == 0) q.push(v);
}
}
if (static_cast<int>(order.size()) == n)
{
// Reorder passes by topological order
std::vector<Pass> sorted;
sorted.reserve(n);
for (int idx: order) sorted.push_back(std::move(_passes[idx]));
_passes = std::move(sorted);
}
else
{
// Cycle detected; keep insertion order but still compute barriers
}
}
struct ImageState
{
bool initialized = false;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkPipelineStageFlags2 stage = VK_PIPELINE_STAGE_2_NONE;
VkAccessFlags2 access = 0;
};
struct BufferState
{
bool initialized = false;
VkPipelineStageFlags2 stage = VK_PIPELINE_STAGE_2_NONE;
VkAccessFlags2 access = 0;
};
auto is_depth_format = [](VkFormat format) {
switch (format)
{
case VK_FORMAT_D16_UNORM:
case VK_FORMAT_D16_UNORM_S8_UINT:
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
return true;
default:
return false;
}
};
auto usage_requires_flag = [](RGImageUsage usage) -> VkImageUsageFlags {
switch (usage)
{
case RGImageUsage::SampledFragment:
case RGImageUsage::SampledCompute:
return VK_IMAGE_USAGE_SAMPLED_BIT;
case RGImageUsage::TransferSrc:
return VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
case RGImageUsage::TransferDst:
return VK_IMAGE_USAGE_TRANSFER_DST_BIT;
case RGImageUsage::ColorAttachment:
return VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
case RGImageUsage::DepthAttachment:
return VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
case RGImageUsage::ComputeWrite:
return VK_IMAGE_USAGE_STORAGE_BIT;
case RGImageUsage::Present:
return 0; // swapchain image
default:
return 0;
}
};
struct ImageUsageInfo
{
VkPipelineStageFlags2 stage;
VkAccessFlags2 access;
VkImageLayout layout;
};
struct BufferUsageInfo
{
VkPipelineStageFlags2 stage;
VkAccessFlags2 access;
};
auto usage_info_image = [](RGImageUsage usage) {
ImageUsageInfo info{};
switch (usage)
{
case RGImageUsage::SampledFragment:
info.stage = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT;
info.access = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
info.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
break;
case RGImageUsage::SampledCompute:
info.stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
info.access = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT;
info.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
break;
case RGImageUsage::TransferSrc:
info.stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
info.access = VK_ACCESS_2_TRANSFER_READ_BIT;
info.layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
break;
case RGImageUsage::TransferDst:
info.stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
info.access = VK_ACCESS_2_TRANSFER_WRITE_BIT;
info.layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
break;
case RGImageUsage::ColorAttachment:
info.stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
info.access = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT;
info.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
break;
case RGImageUsage::DepthAttachment:
info.stage = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT;
info.access = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
info.layout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
break;
case RGImageUsage::ComputeWrite:
info.stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
info.access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT;
info.layout = VK_IMAGE_LAYOUT_GENERAL;
break;
case RGImageUsage::Present:
info.stage = VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT;
info.access = VK_ACCESS_2_MEMORY_READ_BIT;
info.layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
break;
default:
info.stage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
info.access = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT;
info.layout = VK_IMAGE_LAYOUT_GENERAL;
break;
}
return info;
};
auto usage_info_buffer = [](RGBufferUsage usage) {
BufferUsageInfo info{};
switch (usage)
{
case RGBufferUsage::TransferSrc:
info.stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
info.access = VK_ACCESS_2_TRANSFER_READ_BIT;
break;
case RGBufferUsage::TransferDst:
info.stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
info.access = VK_ACCESS_2_TRANSFER_WRITE_BIT;
break;
case RGBufferUsage::VertexRead:
info.stage = VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT;
info.access = VK_ACCESS_2_VERTEX_ATTRIBUTE_READ_BIT;
break;
case RGBufferUsage::IndexRead:
info.stage = VK_PIPELINE_STAGE_2_INDEX_INPUT_BIT;
info.access = VK_ACCESS_2_INDEX_READ_BIT;
break;
case RGBufferUsage::UniformRead:
info.stage = VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
info.access = VK_ACCESS_2_UNIFORM_READ_BIT;
break;
case RGBufferUsage::StorageRead:
info.stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT;
info.access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT;
break;
case RGBufferUsage::StorageReadWrite:
info.stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT;
info.access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT;
break;
case RGBufferUsage::IndirectArgs:
info.stage = VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT;
info.access = VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT;
break;
default:
info.stage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
info.access = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT;
break;
}
return info;
};
auto buffer_usage_requires_flag = [](RGBufferUsage usage) -> VkBufferUsageFlags {
switch (usage)
{
case RGBufferUsage::TransferSrc: return VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
case RGBufferUsage::TransferDst: return VK_BUFFER_USAGE_TRANSFER_DST_BIT;
case RGBufferUsage::VertexRead: return VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
case RGBufferUsage::IndexRead: return VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
case RGBufferUsage::UniformRead: return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
case RGBufferUsage::StorageRead:
case RGBufferUsage::StorageReadWrite: return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
case RGBufferUsage::IndirectArgs: return VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
default: return 0;
}
};
const size_t imageCount = _resources.image_count();
const size_t bufferCount = _resources.buffer_count();
std::vector<ImageState> imageStates(imageCount);
std::vector<BufferState> bufferStates(bufferCount);
// Track first/last use for lifetime diagnostics and future aliasing
std::vector<int> imageFirst(imageCount, -1), imageLast(imageCount, -1);
std::vector<int> bufferFirst(bufferCount, -1), bufferLast(bufferCount, -1);
for (auto &pass: _passes)
{
pass.preImageBarriers.clear();
pass.preBufferBarriers.clear();
if (!pass.enabled) { continue; }
std::unordered_map<uint32_t, RGImageUsage> desiredImageUsages;
desiredImageUsages.reserve(pass.imageReads.size() + pass.imageWrites.size());
for (const auto &access: pass.imageReads)
{
if (!access.image.valid()) continue;
desiredImageUsages.emplace(access.image.id, access.usage);
if (access.image.id < imageCount)
{
if (imageFirst[access.image.id] == -1) imageFirst[access.image.id] = (int)(&pass - _passes.data());
imageLast[access.image.id] = (int)(&pass - _passes.data());
}
}
for (const auto &access: pass.imageWrites)
{
if (!access.image.valid()) continue;
desiredImageUsages[access.image.id] = access.usage;
if (access.image.id < imageCount)
{
if (imageFirst[access.image.id] == -1) imageFirst[access.image.id] = (int)(&pass - _passes.data());
imageLast[access.image.id] = (int)(&pass - _passes.data());
}
}
// Validation: basic layout/format/usage checks for images used by this pass
// Also build barriers
for (const auto &[id, usage]: desiredImageUsages)
{
if (id >= imageCount) continue;
ImageUsageInfo desired = usage_info_image(usage);
ImageState prev = imageStates[id];
VkImageLayout prevLayout = prev.initialized ? prev.layout : _resources.initial_layout(RGImageHandle{id});
VkPipelineStageFlags2 srcStage = prev.initialized
? prev.stage
: (prevLayout == VK_IMAGE_LAYOUT_UNDEFINED
? VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT
: VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT);
VkAccessFlags2 srcAccess = prev.initialized
? prev.access
: (prevLayout == VK_IMAGE_LAYOUT_UNDEFINED
? VkAccessFlags2{0}
: (VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT));
bool needBarrier = !prev.initialized
|| prevLayout != desired.layout
|| prev.stage != desired.stage
|| prev.access != desired.access;
if (needBarrier)
{
VkImageMemoryBarrier2 barrier{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2};
barrier.srcStageMask = srcStage;
barrier.srcAccessMask = srcAccess;
barrier.dstStageMask = desired.stage;
barrier.dstAccessMask = desired.access;
barrier.oldLayout = prevLayout;
barrier.newLayout = desired.layout;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
const RGImageRecord *rec = _resources.get_image(RGImageHandle{id});
barrier.image = rec ? rec->image : VK_NULL_HANDLE;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
if (usage == RGImageUsage::DepthAttachment || (rec && is_depth_format(rec->format)))
{
aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
}
barrier.subresourceRange = vkinit::image_subresource_range(aspect);
pass.preImageBarriers.push_back(barrier);
// Validation messages (debug-only style):
if (rec)
{
// Color attachments should not be depth formats and vice versa
if (usage == RGImageUsage::ColorAttachment && is_depth_format(rec->format))
{
fmt::println("[RG][Warn] Pass '{}' binds depth-format image '{}' as color attachment.",
pass.name, rec->name);
}
if (usage == RGImageUsage::DepthAttachment && !is_depth_format(rec->format))
{
fmt::println("[RG][Warn] Pass '{}' binds non-depth image '{}' as depth attachment.",
pass.name, rec->name);
}
// Usage flag sanity for transients we created
if (!rec->imported)
{
VkImageUsageFlags need = usage_requires_flag(usage);
if ((need & rec->creationUsage) != need)
{
fmt::println("[RG][Warn] Image '{}' used as '{}' but created without needed usage flags (0x{:x}).",
rec->name, (int)usage, (unsigned)need);
}
}
}
}
imageStates[id].initialized = true;
imageStates[id].layout = desired.layout;
imageStates[id].stage = desired.stage;
imageStates[id].access = desired.access;
}
if (bufferCount == 0) continue;
std::unordered_map<uint32_t, RGBufferUsage> desiredBufferUsages;
desiredBufferUsages.reserve(pass.bufferReads.size() + pass.bufferWrites.size());
for (const auto &access: pass.bufferReads)
{
if (!access.buffer.valid()) continue;
desiredBufferUsages.emplace(access.buffer.id, access.usage);
if (access.buffer.id < bufferCount)
{
if (bufferFirst[access.buffer.id] == -1) bufferFirst[access.buffer.id] = (int)(&pass - _passes.data());
bufferLast[access.buffer.id] = (int)(&pass - _passes.data());
}
}
for (const auto &access: pass.bufferWrites)
{
if (!access.buffer.valid()) continue;
desiredBufferUsages[access.buffer.id] = access.usage;
if (access.buffer.id < bufferCount)
{
if (bufferFirst[access.buffer.id] == -1) bufferFirst[access.buffer.id] = (int)(&pass - _passes.data());
bufferLast[access.buffer.id] = (int)(&pass - _passes.data());
}
}
for (const auto &[id, usage]: desiredBufferUsages)
{
if (id >= bufferCount) continue;
BufferUsageInfo desired = usage_info_buffer(usage);
BufferState prev = bufferStates[id];
VkPipelineStageFlags2 srcStage = prev.initialized
? prev.stage
: _resources.initial_stage(RGBufferHandle{id});
if (srcStage == VK_PIPELINE_STAGE_2_NONE)
{
srcStage = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT;
}
VkAccessFlags2 srcAccess = prev.initialized
? prev.access
: _resources.initial_access(RGBufferHandle{id});
bool needBarrier = !prev.initialized
|| prev.stage != desired.stage
|| prev.access != desired.access;
if (needBarrier)
{
VkBufferMemoryBarrier2 barrier{.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2};
barrier.srcStageMask = srcStage;
barrier.srcAccessMask = srcAccess;
barrier.dstStageMask = desired.stage;
barrier.dstAccessMask = desired.access;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
const RGBufferRecord *rec = _resources.get_buffer(RGBufferHandle{id});
barrier.buffer = rec ? rec->buffer : VK_NULL_HANDLE;
barrier.offset = 0;
barrier.size = rec ? rec->size : VK_WHOLE_SIZE;
pass.preBufferBarriers.push_back(barrier);
if (rec && !rec->imported)
{
VkBufferUsageFlags need = buffer_usage_requires_flag(usage);
if ((need & rec->usage) != need)
{
fmt::println("[RG][Warn] Buffer '{}' used as '{}' but created without needed usage flags (0x{:x}).",
rec->name, (int)usage, (unsigned)need);
}
}
}
bufferStates[id].initialized = true;
bufferStates[id].stage = desired.stage;
bufferStates[id].access = desired.access;
}
}
// Store lifetimes into records for diagnostics/aliasing
for (size_t i = 0; i < imageCount; ++i)
{
if (auto *rec = _resources.get_image(RGImageHandle{static_cast<uint32_t>(i)}))
{
rec->firstUse = imageFirst[i];
rec->lastUse = imageLast[i];
}
}
for (size_t i = 0; i < bufferCount; ++i)
{
if (auto *rec = _resources.get_buffer(RGBufferHandle{static_cast<uint32_t>(i)}))
{
rec->firstUse = bufferFirst[i];
rec->lastUse = bufferLast[i];
}
}
return true;
}
void RenderGraph::execute(VkCommandBuffer cmd)
{
for (size_t passIndex = 0; passIndex < _passes.size(); ++passIndex)
{
auto &p = _passes[passIndex];
if (!p.enabled) continue;
// Debug label per pass
if (_context && _context->getDevice())
{
char labelName[128];
std::snprintf(labelName, sizeof(labelName), "RG: %s", p.name.c_str());
vkdebug::cmd_begin_label(_context->getDevice()->device(), cmd, labelName);
}
if (!p.preImageBarriers.empty() || !p.preBufferBarriers.empty())
{
VkDependencyInfo dep{.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO};
dep.imageMemoryBarrierCount = static_cast<uint32_t>(p.preImageBarriers.size());
dep.pImageMemoryBarriers = p.preImageBarriers.empty() ? nullptr : p.preImageBarriers.data();
dep.bufferMemoryBarrierCount = static_cast<uint32_t>(p.preBufferBarriers.size());
dep.pBufferMemoryBarriers = p.preBufferBarriers.empty() ? nullptr : p.preBufferBarriers.data();
vkCmdPipelineBarrier2(cmd, &dep);
}
// Begin dynamic rendering if the pass declared attachments
bool doRendering = (!p.colorAttachments.empty() || p.hasDepth);
if (doRendering)
{
std::vector<VkRenderingAttachmentInfo> colorInfos;
colorInfos.reserve(p.colorAttachments.size());
VkRenderingAttachmentInfo depthInfo{};
bool hasDepth = false;
// Choose renderArea as the min of all attachment extents and the desired draw extent
VkExtent2D chosenExtent{_context->getDrawExtent()};
auto clamp_min = [](VkExtent2D a, VkExtent2D b) {
return VkExtent2D{std::min(a.width, b.width), std::min(a.height, b.height)};
};
// Resolve color attachments
VkExtent2D firstColorExtent{0,0};
bool warnedExtentMismatch = false;
for (const auto &a: p.colorAttachments)
{
const RGImageRecord *rec = _resources.get_image(a.image);
if (!rec || rec->imageView == VK_NULL_HANDLE) continue;
VkClearValue *pClear = nullptr;
VkClearValue clear = a.clear;
if (a.clearOnLoad) pClear = &clear;
VkRenderingAttachmentInfo info = vkinit::attachment_info(rec->imageView, pClear,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
if (!a.store) info.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorInfos.push_back(info);
if (rec->extent.width && rec->extent.height) chosenExtent = clamp_min(chosenExtent, rec->extent);
if (firstColorExtent.width == 0 && firstColorExtent.height == 0)
{
firstColorExtent = rec->extent;
}
else if (!warnedExtentMismatch && (rec->extent.width != firstColorExtent.width || rec->extent.height != firstColorExtent.height))
{
fmt::println("[RG][Warn] Pass '{}' has color attachments with mismatched extents ({}x{} vs {}x{}). Using min().",
p.name,
firstColorExtent.width, firstColorExtent.height,
rec->extent.width, rec->extent.height);
warnedExtentMismatch = true;
}
}
if (p.hasDepth)
{
const RGImageRecord *rec = _resources.get_image(p.depthAttachment.image);
if (rec && rec->imageView != VK_NULL_HANDLE)
{
depthInfo = vkinit::depth_attachment_info(rec->imageView, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
if (p.depthAttachment.clearOnLoad) depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
else depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
if (!p.depthAttachment.store) depthInfo.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
hasDepth = true;
if (rec->extent.width && rec->extent.height) chosenExtent = clamp_min(chosenExtent, rec->extent);
}
}
VkRenderingInfo ri{};
ri.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
ri.renderArea = VkRect2D{VkOffset2D{0, 0}, chosenExtent};
ri.layerCount = 1;
ri.colorAttachmentCount = static_cast<uint32_t>(colorInfos.size());
ri.pColorAttachments = colorInfos.empty() ? nullptr : colorInfos.data();
ri.pDepthAttachment = hasDepth ? &depthInfo : nullptr;
ri.pStencilAttachment = nullptr;
vkCmdBeginRendering(cmd, &ri);
}
if (p.record)
{
RGPassResources res(&_resources);
p.record(cmd, res, _context);
}
if (doRendering)
{
vkCmdEndRendering(cmd);
}
if (_context && _context->getDevice())
{
vkdebug::cmd_end_label(_context->getDevice()->device(), cmd);
}
}
}
// --- Import helpers ---
void RenderGraph::add_present_chain(RGImageHandle sourceDraw,
RGImageHandle targetSwapchain,
std::function<void(RenderGraph &)> appendExtra)
{
if (!sourceDraw.valid() || !targetSwapchain.valid()) return;
add_pass(
"CopyToSwapchain",
RGPassType::Transfer,
[sourceDraw, targetSwapchain](RGPassBuilder &builder, EngineContext *) {
builder.read(sourceDraw, RGImageUsage::TransferSrc);
builder.write(targetSwapchain, RGImageUsage::TransferDst);
},
[sourceDraw, targetSwapchain](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) {
VkImage src = res.image(sourceDraw);
VkImage dst = res.image(targetSwapchain);
if (src == VK_NULL_HANDLE || dst == VK_NULL_HANDLE) return;
vkutil::copy_image_to_image(cmd, src, dst, ctx->getDrawExtent(), ctx->getSwapchain()->swapchainExtent());
});
if (appendExtra)
{
appendExtra(*this);
}
add_pass(
"PreparePresent",
RGPassType::Transfer,
[targetSwapchain](RGPassBuilder &builder, EngineContext *) {
builder.write(targetSwapchain, RGImageUsage::Present);
},
[](VkCommandBuffer, const RGPassResources &, EngineContext *) {
});
}
RGImageHandle RenderGraph::import_draw_image()
{
RGImportedImageDesc d{};
d.name = "drawImage";
d.image = _context->getSwapchain()->drawImage().image;
d.imageView = _context->getSwapchain()->drawImage().imageView;
d.format = _context->getSwapchain()->drawImage().imageFormat;
d.extent = _context->getDrawExtent();
d.currentLayout = VK_IMAGE_LAYOUT_GENERAL;
return import_image(d);
}
// --- Debug helpers ---
void RenderGraph::debug_get_passes(std::vector<RGDebugPassInfo> &out) const
{
out.clear();
out.reserve(_passes.size());
for (const auto &p : _passes)
{
RGDebugPassInfo info{};
info.name = p.name;
info.type = p.type;
info.enabled = p.enabled;
info.imageReads = static_cast<uint32_t>(p.imageReads.size());
info.imageWrites = static_cast<uint32_t>(p.imageWrites.size());
info.bufferReads = static_cast<uint32_t>(p.bufferReads.size());
info.bufferWrites = static_cast<uint32_t>(p.bufferWrites.size());
info.colorAttachmentCount = static_cast<uint32_t>(p.colorAttachments.size());
info.hasDepth = p.hasDepth;
out.push_back(std::move(info));
}
}
void RenderGraph::debug_get_images(std::vector<RGDebugImageInfo> &out) const
{
out.clear();
out.reserve(_resources.image_count());
for (uint32_t i = 0; i < _resources.image_count(); ++i)
{
const RGImageRecord *rec = _resources.get_image(RGImageHandle{i});
if (!rec) continue;
RGDebugImageInfo info{};
info.id = i;
info.name = rec->name;
info.imported = rec->imported;
info.format = rec->format;
info.extent = rec->extent;
info.creationUsage = rec->creationUsage;
info.firstUse = rec->firstUse;
info.lastUse = rec->lastUse;
out.push_back(std::move(info));
}
}
void RenderGraph::debug_get_buffers(std::vector<RGDebugBufferInfo> &out) const
{
out.clear();
out.reserve(_resources.buffer_count());
for (uint32_t i = 0; i < _resources.buffer_count(); ++i)
{
const RGBufferRecord *rec = _resources.get_buffer(RGBufferHandle{i});
if (!rec) continue;
RGDebugBufferInfo info{};
info.id = i;
info.name = rec->name;
info.imported = rec->imported;
info.size = rec->size;
info.usage = rec->usage;
info.firstUse = rec->firstUse;
info.lastUse = rec->lastUse;
out.push_back(std::move(info));
}
}
RGImageHandle RenderGraph::import_depth_image()
{
RGImportedImageDesc d{};
d.name = "depthImage";
d.image = _context->getSwapchain()->depthImage().image;
d.imageView = _context->getSwapchain()->depthImage().imageView;
d.format = _context->getSwapchain()->depthImage().imageFormat;
d.extent = _context->getDrawExtent();
d.currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
return import_image(d);
}
RGImageHandle RenderGraph::import_gbuffer_position()
{
RGImportedImageDesc d{};
d.name = "gBuffer.position";
d.image = _context->getSwapchain()->gBufferPosition().image;
d.imageView = _context->getSwapchain()->gBufferPosition().imageView;
d.format = _context->getSwapchain()->gBufferPosition().imageFormat;
d.extent = _context->getDrawExtent();
d.currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
return import_image(d);
}
RGImageHandle RenderGraph::import_gbuffer_normal()
{
RGImportedImageDesc d{};
d.name = "gBuffer.normal";
d.image = _context->getSwapchain()->gBufferNormal().image;
d.imageView = _context->getSwapchain()->gBufferNormal().imageView;
d.format = _context->getSwapchain()->gBufferNormal().imageFormat;
d.extent = _context->getDrawExtent();
d.currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
return import_image(d);
}
RGImageHandle RenderGraph::import_gbuffer_albedo()
{
RGImportedImageDesc d{};
d.name = "gBuffer.albedo";
d.image = _context->getSwapchain()->gBufferAlbedo().image;
d.imageView = _context->getSwapchain()->gBufferAlbedo().imageView;
d.format = _context->getSwapchain()->gBufferAlbedo().imageFormat;
d.extent = _context->getDrawExtent();
d.currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
return import_image(d);
}
RGImageHandle RenderGraph::import_swapchain_image(uint32_t index)
{
RGImportedImageDesc d{};
d.name = "swapchain.image";
const auto &views = _context->getSwapchain()->swapchainImageViews();
const auto &imgs = _context->getSwapchain()->swapchainImages();
d.image = imgs[index];
d.imageView = views[index];
d.format = _context->getSwapchain()->swapchainImageFormat();
d.extent = _context->getSwapchain()->swapchainExtent();
d.currentLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
return import_image(d);
}

142
src/render/rg_graph.h Normal file
View File

@@ -0,0 +1,142 @@
#pragma once
#include <core/vk_types.h>
#include <render/rg_types.h>
#include <render/rg_resources.h>
#include <render/rg_builder.h>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
class EngineContext;
class RenderGraph
{
public:
void init(EngineContext* ctx);
void clear();
// Import externally owned images (swapchain, drawImage, g-buffers)
RGImageHandle import_image(const RGImportedImageDesc& desc);
// Create transient images (not used in v1 skeleton; stubbed for future)
RGImageHandle create_image(const RGImageDesc& desc);
// Convenience: create a transient depth image suitable for shadow mapping or depth-only passes
// Format defaults to D32_SFLOAT; usage is depth attachment + sampled so it can be read later.
RGImageHandle create_depth_image(const char* name, VkExtent2D extent, VkFormat format = VK_FORMAT_D32_SFLOAT);
// Buffer import/create helpers
RGBufferHandle import_buffer(const RGImportedBufferDesc& desc);
RGBufferHandle create_buffer(const RGBufferDesc& desc);
// Pass builder API
struct Pass; // fwd
using RecordCallback = std::function<void(VkCommandBuffer cmd, const class RGPassResources& res, EngineContext* ctx)>;
using BuildCallback = std::function<void(class RGPassBuilder& b, EngineContext* ctx)>;
void add_pass(const char* name, RGPassType type, BuildCallback build, RecordCallback record);
// Legacy simple add
void add_pass(const char* name, RGPassType type, RecordCallback record);
// Build internal state for this frame (no-op in v1)
bool compile();
// Execute in insertion order (skips disabled passes)
void execute(VkCommandBuffer cmd);
// Convenience import helpers (read from EngineContext::swapchain)
RGImageHandle import_draw_image();
RGImageHandle import_depth_image();
RGImageHandle import_gbuffer_position();
RGImageHandle import_gbuffer_normal();
RGImageHandle import_gbuffer_albedo();
RGImageHandle import_swapchain_image(uint32_t index);
void add_present_chain(RGImageHandle sourceDraw,
RGImageHandle targetSwapchain,
std::function<void(RenderGraph&)> appendExtra = {});
// --- Debug helpers ---
struct RGDebugPassInfo
{
std::string name;
RGPassType type{};
bool enabled = true;
uint32_t imageReads = 0;
uint32_t imageWrites = 0;
uint32_t bufferReads = 0;
uint32_t bufferWrites = 0;
uint32_t colorAttachmentCount = 0;
bool hasDepth = false;
};
struct RGDebugImageInfo
{
uint32_t id{};
std::string name;
bool imported = true;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent{0,0};
VkImageUsageFlags creationUsage = 0;
int firstUse = -1;
int lastUse = -1;
};
struct RGDebugBufferInfo
{
uint32_t id{};
std::string name;
bool imported = true;
VkDeviceSize size = 0;
VkBufferUsageFlags usage = 0;
int firstUse = -1;
int lastUse = -1;
};
size_t pass_count() const { return _passes.size(); }
const char* pass_name(size_t i) const { return i < _passes.size() ? _passes[i].name.c_str() : ""; }
bool pass_enabled(size_t i) const { return i < _passes.size() ? _passes[i].enabled : false; }
void set_pass_enabled(size_t i, bool e) { if (i < _passes.size()) _passes[i].enabled = e; }
void debug_get_passes(std::vector<RGDebugPassInfo>& out) const;
void debug_get_images(std::vector<RGDebugImageInfo>& out) const;
void debug_get_buffers(std::vector<RGDebugBufferInfo>& out) const;
private:
struct ImportedImage
{
RGImportedImageDesc desc;
RGImageHandle handle;
};
struct Pass
{
std::string name;
RGPassType type{};
RecordCallback record;
// Declarations
std::vector<RGPassImageAccess> imageReads;
std::vector<RGPassImageAccess> imageWrites;
std::vector<RGPassBufferAccess> bufferReads;
std::vector<RGPassBufferAccess> bufferWrites;
std::vector<RGAttachmentInfo> colorAttachments;
bool hasDepth = false;
RGAttachmentInfo depthAttachment{};
std::vector<VkImageMemoryBarrier2> preImageBarriers;
std::vector<VkBufferMemoryBarrier2> preBufferBarriers;
// Cached rendering info derived from declared attachments (filled at execute)
bool hasRendering = false;
VkExtent2D renderExtent{};
bool enabled = true;
};
EngineContext* _context = nullptr;
RGResourceRegistry _resources;
std::vector<Pass> _passes;
};

189
src/render/rg_resources.cpp Normal file
View File

@@ -0,0 +1,189 @@
#include <render/rg_resources.h>
#include <core/engine_context.h>
#include <core/vk_resource.h>
#include "frame_resources.h"
void RGResourceRegistry::reset()
{
_images.clear();
_buffers.clear();
_imageLookup.clear();
_bufferLookup.clear();
}
RGImageHandle RGResourceRegistry::add_imported(const RGImportedImageDesc& d)
{
// Deduplicate by VkImage
auto it = _imageLookup.find(d.image);
if (it != _imageLookup.end())
{
auto& rec = _images[it->second];
rec.name = d.name;
rec.image = d.image;
rec.imageView = d.imageView;
rec.format = d.format;
rec.extent = d.extent;
rec.initialLayout = d.currentLayout;
return RGImageHandle{it->second};
}
RGImageRecord rec{};
rec.name = d.name;
rec.imported = true;
rec.image = d.image;
rec.imageView = d.imageView;
rec.format = d.format;
rec.extent = d.extent;
rec.initialLayout = d.currentLayout;
_images.push_back(rec);
uint32_t id = static_cast<uint32_t>(_images.size() - 1);
if (d.image != VK_NULL_HANDLE) _imageLookup[d.image] = id;
return RGImageHandle{ id };
}
RGImageHandle RGResourceRegistry::add_transient(const RGImageDesc& d)
{
RGImageRecord rec{};
rec.name = d.name;
rec.imported = false;
rec.format = d.format;
rec.extent = d.extent;
rec.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
rec.creationUsage = d.usage;
VkExtent3D size{ d.extent.width, d.extent.height, 1 };
rec.allocation = _ctx->getResources()->create_image(size, d.format, d.usage);
rec.image = rec.allocation.image;
rec.imageView = rec.allocation.imageView;
// Cleanup at end of frame
if (_ctx && _ctx->currentFrame)
{
auto img = rec.allocation;
_ctx->currentFrame->_deletionQueue.push_function([ctx=_ctx, img]() {
ctx->getResources()->destroy_image(img);
});
}
_images.push_back(rec);
return RGImageHandle{ static_cast<uint32_t>(_images.size() - 1) };
}
RGBufferHandle RGResourceRegistry::add_imported(const RGImportedBufferDesc& d)
{
// Deduplicate by VkBuffer
auto it = _bufferLookup.find(d.buffer);
if (it != _bufferLookup.end())
{
auto& rec = _buffers[it->second];
rec.name = d.name;
rec.buffer = d.buffer;
rec.size = d.size;
// Keep the earliest known stage/access if set; otherwise record provided
if (rec.initialStage == VK_PIPELINE_STAGE_2_NONE) rec.initialStage = d.currentStage;
if (rec.initialAccess == 0) rec.initialAccess = d.currentAccess;
return RGBufferHandle{it->second};
}
RGBufferRecord rec{};
rec.name = d.name;
rec.imported = true;
rec.buffer = d.buffer;
rec.size = d.size;
rec.initialStage = d.currentStage;
rec.initialAccess = d.currentAccess;
_buffers.push_back(rec);
uint32_t id = static_cast<uint32_t>(_buffers.size() - 1);
if (d.buffer != VK_NULL_HANDLE) _bufferLookup[d.buffer] = id;
return RGBufferHandle{ id };
}
RGBufferHandle RGResourceRegistry::add_transient(const RGBufferDesc& d)
{
RGBufferRecord rec{};
rec.name = d.name;
rec.imported = false;
rec.size = d.size;
rec.usage = d.usage;
rec.initialStage = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT;
rec.initialAccess = 0;
rec.allocation = _ctx->getResources()->create_buffer(d.size, d.usage, d.memoryUsage);
rec.buffer = rec.allocation.buffer;
if (_ctx && _ctx->currentFrame)
{
auto buf = rec.allocation;
_ctx->currentFrame->_deletionQueue.push_function([ctx=_ctx, buf]() {
ctx->getResources()->destroy_buffer(buf);
});
}
_buffers.push_back(rec);
uint32_t id = static_cast<uint32_t>(_buffers.size() - 1);
if (rec.buffer != VK_NULL_HANDLE) _bufferLookup[rec.buffer] = id;
return RGBufferHandle{ id };
}
const RGImageRecord* RGResourceRegistry::get_image(RGImageHandle h) const
{
if (!h.valid() || h.id >= _images.size()) return nullptr;
return &_images[h.id];
}
RGImageRecord* RGResourceRegistry::get_image(RGImageHandle h)
{
if (!h.valid() || h.id >= _images.size()) return nullptr;
return &_images[h.id];
}
const RGBufferRecord* RGResourceRegistry::get_buffer(RGBufferHandle h) const
{
if (!h.valid() || h.id >= _buffers.size()) return nullptr;
return &_buffers[h.id];
}
RGBufferRecord* RGResourceRegistry::get_buffer(RGBufferHandle h)
{
if (!h.valid() || h.id >= _buffers.size()) return nullptr;
return &_buffers[h.id];
}
VkImageLayout RGResourceRegistry::initial_layout(RGImageHandle h) const
{
const RGImageRecord* rec = get_image(h);
return rec ? rec->initialLayout : VK_IMAGE_LAYOUT_UNDEFINED;
}
VkFormat RGResourceRegistry::image_format(RGImageHandle h) const
{
const RGImageRecord* rec = get_image(h);
return rec ? rec->format : VK_FORMAT_UNDEFINED;
}
VkPipelineStageFlags2 RGResourceRegistry::initial_stage(RGBufferHandle h) const
{
const RGBufferRecord* rec = get_buffer(h);
return rec ? rec->initialStage : VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT;
}
VkAccessFlags2 RGResourceRegistry::initial_access(RGBufferHandle h) const
{
const RGBufferRecord* rec = get_buffer(h);
return rec ? rec->initialAccess : VkAccessFlags2{0};
}
RGBufferHandle RGResourceRegistry::find_buffer(VkBuffer buffer) const
{
auto it = _bufferLookup.find(buffer);
if (it == _bufferLookup.end()) return RGBufferHandle{};
return RGBufferHandle{it->second};
}
RGImageHandle RGResourceRegistry::find_image(VkImage image) const
{
auto it = _imageLookup.find(image);
if (it == _imageLookup.end()) return RGImageHandle{};
return RGImageHandle{it->second};
}

90
src/render/rg_resources.h Normal file
View File

@@ -0,0 +1,90 @@
#pragma once
#include <core/vk_types.h>
#include <render/rg_types.h>
#include <string>
#include <vector>
#include <unordered_map>
class EngineContext;
struct RGImageRecord
{
std::string name;
bool imported = true;
// Unified view for either imported or transient
VkImage image = VK_NULL_HANDLE;
VkImageView imageView = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent{0, 0};
VkImageLayout initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VkImageUsageFlags creationUsage = 0; // if transient; 0 for imported
// If transient, keep allocation owner for cleanup
AllocatedImage allocation{};
// Lifetime indices within the compiled pass list (for aliasing/debug)
int firstUse = -1;
int lastUse = -1;
};
struct RGBufferRecord
{
std::string name;
bool imported = true;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize size = 0;
VkBufferUsageFlags usage = 0;
VkPipelineStageFlags2 initialStage = VK_PIPELINE_STAGE_2_NONE;
VkAccessFlags2 initialAccess = 0;
AllocatedBuffer allocation{};
// Lifetime indices (for aliasing/debug)
int firstUse = -1;
int lastUse = -1;
};
class RGResourceRegistry
{
public:
void init(EngineContext* ctx) { _ctx = ctx; }
void reset();
RGImageHandle add_imported(const RGImportedImageDesc& d);
RGImageHandle add_transient(const RGImageDesc& d);
RGBufferHandle add_imported(const RGImportedBufferDesc& d);
RGBufferHandle add_transient(const RGBufferDesc& d);
// Lookup existing handles by raw Vulkan objects (deduplicates imports)
RGBufferHandle find_buffer(VkBuffer buffer) const;
RGImageHandle find_image(VkImage image) const;
const RGImageRecord* get_image(RGImageHandle h) const;
RGImageRecord* get_image(RGImageHandle h);
const RGBufferRecord* get_buffer(RGBufferHandle h) const;
RGBufferRecord* get_buffer(RGBufferHandle h);
size_t image_count() const { return _images.size(); }
size_t buffer_count() const { return _buffers.size(); }
VkImageLayout initial_layout(RGImageHandle h) const;
VkFormat image_format(RGImageHandle h) const;
VkPipelineStageFlags2 initial_stage(RGBufferHandle h) const;
VkAccessFlags2 initial_access(RGBufferHandle h) const;
private:
EngineContext* _ctx = nullptr;
std::vector<RGImageRecord> _images;
std::vector<RGBufferRecord> _buffers;
// Reverse lookup to avoid duplicate imports of the same VkBuffer/VkImage
std::unordered_map<VkImage, uint32_t> _imageLookup;
std::unordered_map<VkBuffer, uint32_t> _bufferLookup;
};

101
src/render/rg_types.h Normal file
View File

@@ -0,0 +1,101 @@
#pragma once
#include <core/vk_types.h>
#include <string>
#include <vector>
// Lightweight, initial Render Graph types. These will expand as we migrate passes.
enum class RGPassType
{
Graphics,
Compute,
Transfer
};
enum class RGImageUsage
{
// Read usages
SampledFragment,
SampledCompute,
TransferSrc,
// Write usages
ColorAttachment,
DepthAttachment,
ComputeWrite,
TransferDst,
// Terminal
Present
};
enum class RGBufferUsage
{
TransferSrc,
TransferDst,
VertexRead,
IndexRead,
UniformRead,
StorageRead,
StorageReadWrite,
IndirectArgs
};
struct RGImageHandle
{
uint32_t id = 0xFFFFFFFFu;
bool valid() const { return id != 0xFFFFFFFFu; }
explicit operator bool() const { return valid(); }
};
struct RGBufferHandle
{
uint32_t id = 0xFFFFFFFFu;
bool valid() const { return id != 0xFFFFFFFFu; }
explicit operator bool() const { return valid(); }
};
struct RGImportedImageDesc
{
std::string name;
VkImage image = VK_NULL_HANDLE;
VkImageView imageView = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent{0, 0};
VkImageLayout currentLayout = VK_IMAGE_LAYOUT_UNDEFINED; // layout at graph begin
};
struct RGImportedBufferDesc
{
std::string name;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize size = 0;
VkPipelineStageFlags2 currentStage = VK_PIPELINE_STAGE_2_NONE;
VkAccessFlags2 currentAccess = 0;
};
struct RGImageDesc
{
std::string name;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent{0, 0};
VkImageUsageFlags usage = 0; // creation usage mask; graph sets layouts per-pass
};
struct RGBufferDesc
{
std::string name;
VkDeviceSize size = 0;
VkBufferUsageFlags usage = 0;
VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
};
// Simple attachment info for dynamic rendering; expanded later for load/store.
struct RGAttachmentInfo
{
RGImageHandle image;
VkClearValue clear{}; // default 0
bool clearOnLoad = false; // if true, use clear; else load
bool store = true; // store results
};

126
src/render/vk_materials.cpp Normal file
View File

@@ -0,0 +1,126 @@
#include "vk_materials.h"
#include "core/vk_engine.h"
#include "render/vk_pipelines.h"
#include "core/vk_initializers.h"
#include "core/vk_pipeline_manager.h"
#include "core/asset_manager.h"
namespace vkutil { bool load_shader_module(const char*, VkDevice, VkShaderModule*); }
void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine)
{
VkPushConstantRange matrixRange{};
matrixRange.offset = 0;
matrixRange.size = sizeof(GPUDrawPushConstants);
matrixRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
DescriptorLayoutBuilder layoutBuilder;
layoutBuilder.add_binding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
layoutBuilder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
layoutBuilder.add_binding(2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
materialLayout = layoutBuilder.build(engine->_deviceManager->device(),
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
VkDescriptorSetLayout layouts[] = {
engine->_descriptorManager->gpuSceneDataLayout(),
materialLayout
};
// Register pipelines with the central PipelineManager
GraphicsPipelineCreateInfo opaqueInfo{};
opaqueInfo.vertexShaderPath = engine->_context->getAssets()->shaderPath("mesh.vert.spv");
opaqueInfo.fragmentShaderPath = engine->_context->getAssets()->shaderPath("mesh.frag.spv");
opaqueInfo.setLayouts.assign(std::begin(layouts), std::end(layouts));
opaqueInfo.pushConstants = {matrixRange};
opaqueInfo.configure = [engine](PipelineBuilder &b) {
b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
b.set_polygon_mode(VK_POLYGON_MODE_FILL);
b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE);
b.set_multisampling_none();
b.disable_blending();
// Reverse-Z depth test configuration
b.enable_depthtest(true, VK_COMPARE_OP_GREATER_OR_EQUAL);
b.set_color_attachment_format(engine->_swapchainManager->drawImage().imageFormat);
b.set_depth_format(engine->_swapchainManager->depthImage().imageFormat);
};
engine->_pipelineManager->registerGraphics("mesh.opaque", opaqueInfo);
GraphicsPipelineCreateInfo transparentInfo = opaqueInfo;
transparentInfo.configure = [engine](PipelineBuilder &b) {
b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
b.set_polygon_mode(VK_POLYGON_MODE_FILL);
b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE);
b.set_multisampling_none();
// Physically-based transparency uses standard alpha blending
b.enable_blending_alphablend();
// Transparent pass: keep reverse-Z test (no writes)
b.enable_depthtest(false, VK_COMPARE_OP_GREATER_OR_EQUAL);
b.set_color_attachment_format(engine->_swapchainManager->drawImage().imageFormat);
b.set_depth_format(engine->_swapchainManager->depthImage().imageFormat);
};
engine->_pipelineManager->registerGraphics("mesh.transparent", transparentInfo);
GraphicsPipelineCreateInfo gbufferInfo{};
gbufferInfo.vertexShaderPath = engine->_context->getAssets()->shaderPath("mesh.vert.spv");
gbufferInfo.fragmentShaderPath = engine->_context->getAssets()->shaderPath("gbuffer.frag.spv");
gbufferInfo.setLayouts.assign(std::begin(layouts), std::end(layouts));
gbufferInfo.pushConstants = {matrixRange};
gbufferInfo.configure = [engine](PipelineBuilder &b) {
b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
b.set_polygon_mode(VK_POLYGON_MODE_FILL);
b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE);
b.set_multisampling_none();
b.disable_blending();
// GBuffer uses reverse-Z depth
b.enable_depthtest(true, VK_COMPARE_OP_GREATER_OR_EQUAL);
VkFormat gFormats[] = {
engine->_swapchainManager->gBufferPosition().imageFormat,
engine->_swapchainManager->gBufferNormal().imageFormat,
engine->_swapchainManager->gBufferAlbedo().imageFormat
};
b.set_color_attachment_formats(std::span<VkFormat>(gFormats, 3));
b.set_depth_format(engine->_swapchainManager->depthImage().imageFormat);
};
engine->_pipelineManager->registerGraphics("mesh.gbuffer", gbufferInfo);
engine->_pipelineManager->getMaterialPipeline("mesh.opaque", opaquePipeline);
engine->_pipelineManager->getMaterialPipeline("mesh.transparent", transparentPipeline);
engine->_pipelineManager->getMaterialPipeline("mesh.gbuffer", gBufferPipeline);
}
void GLTFMetallic_Roughness::clear_resources(VkDevice device) const
{
vkDestroyDescriptorSetLayout(device, materialLayout, nullptr);
}
MaterialInstance GLTFMetallic_Roughness::write_material(VkDevice device, MaterialPass pass,
const MaterialResources &resources,
DescriptorAllocatorGrowable &descriptorAllocator)
{
MaterialInstance matData{};
matData.passType = pass;
if (pass == MaterialPass::Transparent)
{
matData.pipeline = &transparentPipeline;
}
else
{
matData.pipeline = &gBufferPipeline;
}
matData.materialSet = descriptorAllocator.allocate(device, materialLayout);
writer.clear();
writer.write_buffer(0, resources.dataBuffer, sizeof(MaterialConstants), resources.dataBufferOffset,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
writer.write_image(1, resources.colorImage.imageView, resources.colorSampler,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.write_image(2, resources.metalRoughImage.imageView, resources.metalRoughSampler,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.update_set(device, matData.materialSet);
return matData;
}

42
src/render/vk_materials.h Normal file
View File

@@ -0,0 +1,42 @@
#pragma once
#include <core/vk_types.h>
#include <core/vk_descriptors.h>
class VulkanEngine;
struct GLTFMetallic_Roughness
{
MaterialPipeline opaquePipeline;
MaterialPipeline transparentPipeline;
MaterialPipeline gBufferPipeline;
VkDescriptorSetLayout materialLayout;
struct MaterialConstants
{
glm::vec4 colorFactors;
glm::vec4 metal_rough_factors;
glm::vec4 extra[14];
};
struct MaterialResources
{
AllocatedImage colorImage;
VkSampler colorSampler;
AllocatedImage metalRoughImage;
VkSampler metalRoughSampler;
VkBuffer dataBuffer;
uint32_t dataBufferOffset;
};
DescriptorWriter writer;
void build_pipelines(VulkanEngine *engine);
void clear_resources(VkDevice device) const;
MaterialInstance write_material(VkDevice device, MaterialPass pass, const MaterialResources &resources,
DescriptorAllocatorGrowable &descriptorAllocator);
};

244
src/render/vk_pipelines.cpp Normal file
View File

@@ -0,0 +1,244 @@
#include <render/vk_pipelines.h>
#include <fstream>
#include <core/vk_initializers.h>
bool vkutil::load_shader_module(const char *filePath, VkDevice device, VkShaderModule *outShaderModule)
{
std::ifstream file(filePath, std::ios::ate | std::ios::binary);
if (!file.is_open())
{
return false;
}
size_t fileSize = (size_t) file.tellg();
std::vector<uint32_t> buffer(fileSize / sizeof(uint32_t));
file.seekg(0);
file.read((char *) buffer.data(), fileSize);
file.close();
VkShaderModuleCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.pNext = nullptr;
createInfo.codeSize = buffer.size() * sizeof(uint32_t);
createInfo.pCode = buffer.data();
VkShaderModule shaderModule;
if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS)
{
return false;
}
*outShaderModule = shaderModule;
return true;
}
void PipelineBuilder::clear()
{
_inputAssembly = {.sType=VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
_rasterizer = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
_colorBlendAttachment = {};
_multisampling = {.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
_pipelineLayout = {};
_depthStencil = {.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
_renderInfo = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO};
_shaderStages.clear();
_colorAttachmentFormats.clear();
}
VkPipeline PipelineBuilder::build_pipeline(VkDevice device)
{
VkPipelineViewportStateCreateInfo viewportState = {};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.pNext = nullptr;
viewportState.viewportCount = 1;
viewportState.scissorCount = 1;
VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.pNext = nullptr;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY;
// For multiple color attachments (e.g., G-Buffer), we must provide one blend state per attachment.
// Depth-only pipelines are allowed (0 color attachments).
std::vector<VkPipelineColorBlendAttachmentState> blendAttachments;
uint32_t colorAttachmentCount = (uint32_t)_colorAttachmentFormats.size();
if (colorAttachmentCount > 0)
{
blendAttachments.assign(colorAttachmentCount, _colorBlendAttachment);
colorBlending.attachmentCount = colorAttachmentCount;
colorBlending.pAttachments = blendAttachments.data();
}
else
{
colorBlending.attachmentCount = 0;
colorBlending.pAttachments = nullptr;
}
VkPipelineVertexInputStateCreateInfo _vertexInputInfo = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
VkGraphicsPipelineCreateInfo pipelineInfo = {.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
pipelineInfo.pNext = &_renderInfo;
pipelineInfo.stageCount = (uint32_t) _shaderStages.size();
pipelineInfo.pStages = _shaderStages.data();
pipelineInfo.pVertexInputState = &_vertexInputInfo;
pipelineInfo.pInputAssemblyState = &_inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &_rasterizer;
pipelineInfo.pMultisampleState = &_multisampling;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDepthStencilState = &_depthStencil;
pipelineInfo.layout = _pipelineLayout;
VkDynamicState state[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamicInfo = {.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
dynamicInfo.pDynamicStates = &state[0];
dynamicInfo.dynamicStateCount = 2;
pipelineInfo.pDynamicState = &dynamicInfo;
VkPipeline newPipeline;
if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo,
nullptr, &newPipeline)
!= VK_SUCCESS)
{
fmt::println("failed to create pipeline");
return VK_NULL_HANDLE;
}
else
{
return newPipeline;
}
}
void PipelineBuilder::set_shaders(VkShaderModule vertexShader, VkShaderModule fragmentShader)
{
_shaderStages.clear();
_shaderStages.push_back(
vkinit::pipeline_shader_stage_create_info(VK_SHADER_STAGE_VERTEX_BIT, vertexShader));
_shaderStages.push_back(
vkinit::pipeline_shader_stage_create_info(VK_SHADER_STAGE_FRAGMENT_BIT, fragmentShader));
}
void PipelineBuilder::set_input_topology(VkPrimitiveTopology topology)
{
_inputAssembly.topology = topology;
_inputAssembly.primitiveRestartEnable = VK_FALSE;
}
void PipelineBuilder::set_polygon_mode(VkPolygonMode mode)
{
_rasterizer.polygonMode = mode;
_rasterizer.lineWidth = 1.f;
}
void PipelineBuilder::set_cull_mode(VkCullModeFlags cullMode, VkFrontFace frontFace)
{
_rasterizer.cullMode = cullMode;
_rasterizer.frontFace = frontFace;
}
void PipelineBuilder::set_multisampling_none()
{
_multisampling.sampleShadingEnable = VK_FALSE;
_multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
_multisampling.minSampleShading = 1.0f;
_multisampling.pSampleMask = nullptr;
_multisampling.alphaToCoverageEnable = VK_FALSE;
_multisampling.alphaToOneEnable = VK_FALSE;
}
void PipelineBuilder::disable_blending()
{
_colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
_colorBlendAttachment.blendEnable = VK_FALSE;
}
void PipelineBuilder::enable_blending_additive()
{
_colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
_colorBlendAttachment.blendEnable = VK_TRUE;
_colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
_colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
_colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
_colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
_colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
_colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
}
void PipelineBuilder::enable_blending_alphablend()
{
_colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
_colorBlendAttachment.blendEnable = VK_TRUE;
_colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
_colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
_colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
_colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
_colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
_colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
}
void PipelineBuilder::set_color_attachment_format(VkFormat format)
{
_colorAttachmentFormats.clear();
_colorAttachmentFormats.push_back(format);
_renderInfo.colorAttachmentCount = 1;
_renderInfo.pColorAttachmentFormats = _colorAttachmentFormats.data();
}
void PipelineBuilder::set_color_attachment_formats(std::span<VkFormat> formats)
{
_colorAttachmentFormats.assign(formats.begin(), formats.end());
_renderInfo.colorAttachmentCount = (uint32_t)_colorAttachmentFormats.size();
_renderInfo.pColorAttachmentFormats = _colorAttachmentFormats.data();
}
void PipelineBuilder::set_depth_format(VkFormat format)
{
_renderInfo.depthAttachmentFormat = format;
}
void PipelineBuilder::disable_depthtest()
{
_depthStencil.depthTestEnable = VK_FALSE;
_depthStencil.depthWriteEnable = VK_FALSE;
_depthStencil.depthCompareOp = VK_COMPARE_OP_NEVER;
_depthStencil.depthBoundsTestEnable = VK_FALSE;
_depthStencil.stencilTestEnable = VK_FALSE;
_depthStencil.front = {};
_depthStencil.back = {};
_depthStencil.minDepthBounds = 0.f;
_depthStencil.maxDepthBounds = 1.f;
}
void PipelineBuilder::enable_depthtest(bool depthWriteEnable, VkCompareOp op)
{
_depthStencil.depthTestEnable = VK_TRUE;
_depthStencil.depthWriteEnable = depthWriteEnable;
_depthStencil.depthCompareOp = op;
_depthStencil.depthBoundsTestEnable = VK_FALSE;
_depthStencil.stencilTestEnable = VK_FALSE;
_depthStencil.front = {};
_depthStencil.back = {};
_depthStencil.minDepthBounds = 0.f;
_depthStencil.maxDepthBounds = 1.f;
}

59
src/render/vk_pipelines.h Normal file
View File

@@ -0,0 +1,59 @@
#pragma once
#include <core/vk_types.h>
#include <fstream>
#include <core/vk_initializers.h>
namespace vkutil
{
bool load_shader_module(const char *filePath, VkDevice device, VkShaderModule *outShaderModule);
};
class PipelineBuilder
{
public:
std::vector<VkPipelineShaderStageCreateInfo> _shaderStages;
VkPipelineInputAssemblyStateCreateInfo _inputAssembly;
VkPipelineRasterizationStateCreateInfo _rasterizer;
VkPipelineColorBlendAttachmentState _colorBlendAttachment;
VkPipelineMultisampleStateCreateInfo _multisampling;
VkPipelineLayout _pipelineLayout;
VkPipelineDepthStencilStateCreateInfo _depthStencil;
VkPipelineRenderingCreateInfo _renderInfo;
VkFormat _colorAttachmentformat;
std::vector<VkFormat> _colorAttachmentFormats;
PipelineBuilder()
{ clear(); }
void clear();
VkPipeline build_pipeline(VkDevice device);
void set_shaders(VkShaderModule vertexShader, VkShaderModule fragmentShader);
void set_input_topology(VkPrimitiveTopology topology);
void set_polygon_mode(VkPolygonMode mode);
void set_cull_mode(VkCullModeFlags cullMode, VkFrontFace frontFace);
void set_multisampling_none();
void disable_blending();
void enable_blending_additive();
void enable_blending_alphablend();
void set_color_attachment_format(VkFormat format);
void set_color_attachment_formats(std::span<VkFormat> formats);
void set_depth_format(VkFormat format);
void enable_depthtest(bool depthWriteEnable,VkCompareOp op);
void disable_depthtest();
};

View File

@@ -0,0 +1,74 @@
#include "vk_renderpass.h"
#include "vk_renderpass_background.h"
#include "vk_renderpass_geometry.h"
#include "vk_renderpass_imgui.h"
#include "vk_renderpass_lighting.h"
#include "vk_renderpass_transparent.h"
#include "vk_renderpass_tonemap.h"
#include "vk_renderpass_shadow.h"
void RenderPassManager::init(EngineContext *context)
{
_context = context;
auto backgroundPass = std::make_unique<BackgroundPass>();
backgroundPass->init(context);
addPass(std::move(backgroundPass));
// Shadow map pass comes early in the frame
auto shadowPass = std::make_unique<ShadowPass>();
shadowPass->init(context);
addPass(std::move(shadowPass));
auto geometryPass = std::make_unique<GeometryPass>();
geometryPass->init(context);
addPass(std::move(geometryPass));
auto lightingPass = std::make_unique<LightingPass>();
lightingPass->init(context);
addPass(std::move(lightingPass));
auto transparentPass = std::make_unique<TransparentPass>();
transparentPass->init(context);
addPass(std::move(transparentPass));
auto tonemapPass = std::make_unique<TonemapPass>();
tonemapPass->init(context);
addPass(std::move(tonemapPass));
}
void RenderPassManager::cleanup()
{
for (auto &pass: _passes)
{
pass->cleanup();
}
if (_imguiPass)
{
_imguiPass->cleanup();
}
fmt::print("RenderPassManager::cleanup()\n");
_passes.clear();
_imguiPass.reset();
}
void RenderPassManager::addPass(std::unique_ptr<IRenderPass> pass)
{
_passes.push_back(std::move(pass));
}
void RenderPassManager::setImGuiPass(std::unique_ptr<IRenderPass> imguiPass)
{
_imguiPass = std::move(imguiPass);
if (_imguiPass)
{
_imguiPass->init(_context);
}
}
ImGuiPass *RenderPassManager::getImGuiPass()
{
if (!_imguiPass) return nullptr;
return dynamic_cast<ImGuiPass *>(_imguiPass.get());
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include <core/vk_types.h>
#include <vector>
#include <memory>
#include <functional>
class EngineContext;
class ImGuiPass;
class IRenderPass
{
public:
virtual ~IRenderPass() = default;
virtual void init(EngineContext *context) = 0;
virtual void cleanup() = 0;
virtual void execute(VkCommandBuffer cmd) = 0;
virtual const char *getName() const = 0;
};
class RenderPassManager
{
public:
void init(EngineContext *context);
void cleanup();
void addPass(std::unique_ptr<IRenderPass> pass);
void setImGuiPass(std::unique_ptr<IRenderPass> imguiPass);
ImGuiPass *getImGuiPass();
template<typename T>
T *getPass()
{
for (auto &pass: _passes)
{
if (T *typedPass = dynamic_cast<T *>(pass.get()))
{
return typedPass;
}
}
return nullptr;
}
private:
EngineContext *_context = nullptr;
std::vector<std::unique_ptr<IRenderPass> > _passes;
std::unique_ptr<IRenderPass> _imguiPass = nullptr;
};

View File

@@ -0,0 +1,99 @@
#include "vk_renderpass_background.h"
#include <string_view>
#include "vk_swapchain.h"
#include "core/engine_context.h"
#include "core/vk_resource.h"
#include "core/vk_pipeline_manager.h"
#include "core/asset_manager.h"
#include "render/rg_graph.h"
void BackgroundPass::init(EngineContext *context)
{
_context = context;
init_background_pipelines();
}
void BackgroundPass::init_background_pipelines()
{
ComputePipelineCreateInfo createInfo{};
createInfo.shaderPath = _context->getAssets()->shaderPath("gradient_color.comp.spv");
createInfo.descriptorTypes = {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE};
createInfo.pushConstantSize = sizeof(ComputePushConstants);
_context->pipelines->createComputePipeline("gradient", createInfo);
createInfo.shaderPath = _context->getAssets()->shaderPath("sky.comp.spv");
_context->pipelines->createComputePipeline("sky", createInfo);
_context->pipelines->createComputeInstance("background.gradient", "gradient");
_context->pipelines->createComputeInstance("background.sky", "sky");
_context->pipelines->setComputeInstanceStorageImage("background.gradient", 0,
_context->getSwapchain()->drawImage().imageView);
_context->pipelines->setComputeInstanceStorageImage("background.sky", 0,
_context->getSwapchain()->drawImage().imageView);
ComputeEffect gradient{};
gradient.name = "gradient";
gradient.data.data1 = glm::vec4(1, 0, 0, 1);
gradient.data.data2 = glm::vec4(0, 0, 1, 1);
ComputeEffect sky{};
sky.name = "sky";
sky.data.data1 = glm::vec4(0.1, 0.2, 0.4, 0.97);
_backgroundEffects.push_back(gradient);
_backgroundEffects.push_back(sky);
}
void BackgroundPass::execute(VkCommandBuffer)
{
// Background is executed via the render graph now.
}
void BackgroundPass::register_graph(RenderGraph *graph, RGImageHandle drawHandle, RGImageHandle depthHandle)
{
(void) depthHandle; // Reserved for future depth transitions.
if (!graph || !drawHandle.valid() || !_context) return;
if (_backgroundEffects.empty()) return;
graph->add_pass(
"Background",
RGPassType::Compute,
[drawHandle](RGPassBuilder &builder, EngineContext *) {
builder.write(drawHandle, RGImageUsage::ComputeWrite);
},
[this, drawHandle](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) {
VkImageView drawView = res.image_view(drawHandle);
if (drawView != VK_NULL_HANDLE)
{
_context->pipelines->setComputeInstanceStorageImage("background.gradient", 0, drawView);
_context->pipelines->setComputeInstanceStorageImage("background.sky", 0, drawView);
}
ComputeEffect &effect = _backgroundEffects[_currentEffect];
ComputeDispatchInfo dispatchInfo = ComputeManager::createDispatch2D(
ctx->getDrawExtent().width, ctx->getDrawExtent().height);
dispatchInfo.pushConstants = &effect.data;
dispatchInfo.pushConstantSize = sizeof(ComputePushConstants);
const char *instanceName = (std::string_view(effect.name) == std::string_view("gradient"))
? "background.gradient"
: "background.sky";
ctx->pipelines->dispatchComputeInstance(cmd, instanceName, dispatchInfo);
}
);
}
void BackgroundPass::cleanup()
{
if (_context && _context->pipelines)
{
_context->pipelines->destroyComputeInstance("background.gradient");
_context->pipelines->destroyComputeInstance("background.sky");
_context->pipelines->destroyComputePipeline("gradient");
_context->pipelines->destroyComputePipeline("sky");
}
fmt::print("RenderPassManager::cleanup()\n");
_backgroundEffects.clear();
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include "vk_renderpass.h"
#include "compute/vk_compute.h"
#include "render/rg_types.h"
class RenderGraph;
class BackgroundPass : public IRenderPass
{
public:
void init(EngineContext *context) override;
void cleanup() override;
void execute(VkCommandBuffer cmd) override;
const char *getName() const override { return "Background"; }
void register_graph(RenderGraph *graph, RGImageHandle drawHandle, RGImageHandle depthHandle);
void setCurrentEffect(int index) { _currentEffect = index; }
std::vector<ComputeEffect> &getEffects() { return _backgroundEffects; }
std::vector<ComputeEffect> _backgroundEffects;
int _currentEffect = 0;
private:
EngineContext *_context = nullptr;
void init_background_pipelines();
};

View File

@@ -0,0 +1,290 @@
#include "vk_renderpass_geometry.h"
#include <chrono>
#include <unordered_set>
#include "frame_resources.h"
#include "vk_descriptor_manager.h"
#include "vk_device.h"
#include "core/engine_context.h"
#include "core/vk_initializers.h"
#include "core/vk_resource.h"
#include "vk_mem_alloc.h"
#include "vk_scene.h"
#include "vk_swapchain.h"
#include "render/rg_graph.h"
bool is_visible(const RenderObject &obj, const glm::mat4 &viewproj)
{
const std::array<glm::vec3, 8> corners{
glm::vec3{+1, +1, +1}, glm::vec3{+1, +1, -1}, glm::vec3{+1, -1, +1}, glm::vec3{+1, -1, -1},
glm::vec3{-1, +1, +1}, glm::vec3{-1, +1, -1}, glm::vec3{-1, -1, +1}, glm::vec3{-1, -1, -1},
};
const glm::vec3 o = obj.bounds.origin;
const glm::vec3 e = obj.bounds.extents;
const glm::mat4 m = viewproj * obj.transform; // world -> clip
glm::vec4 clip[8];
for (int i = 0; i < 8; ++i)
{
const glm::vec3 p = o + corners[i] * e;
clip[i] = m * glm::vec4(p, 1.f);
}
auto all_out = [&](auto pred) {
for (int i = 0; i < 8; ++i)
{
if (!pred(clip[i])) return false;
}
return true;
};
// Clip volume in Vulkan (ZO): -w<=x<=w, -w<=y<=w, 0<=z<=w
if (all_out([](const glm::vec4 &v) { return v.x < -v.w; })) return false; // left
if (all_out([](const glm::vec4 &v) { return v.x > v.w; })) return false; // right
if (all_out([](const glm::vec4 &v) { return v.y < -v.w; })) return false; // bottom
if (all_out([](const glm::vec4 &v) { return v.y > v.w; })) return false; // top
if (all_out([](const glm::vec4 &v) { return v.z < 0.0f; })) return false; // near (ZO)
if (all_out([](const glm::vec4 &v) { return v.z > v.w; })) return false; // far
return true; // intersects or is fully inside
}
void GeometryPass::init(EngineContext *context)
{
_context = context;
}
void GeometryPass::execute(VkCommandBuffer)
{
// Geometry is executed via the render graph now.
}
void GeometryPass::register_graph(RenderGraph *graph,
RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo,
RGImageHandle depthHandle)
{
if (!graph || !gbufferPosition.valid() || !gbufferNormal.valid() || !gbufferAlbedo.valid() || !depthHandle.valid())
{
return;
}
graph->add_pass(
"Geometry",
RGPassType::Graphics,
[gbufferPosition, gbufferNormal, gbufferAlbedo, depthHandle](RGPassBuilder &builder, EngineContext *ctx)
{
VkClearValue clear{};
clear.color = {{0.f, 0.f, 0.f, 0.f}};
builder.write_color(gbufferPosition, true, clear);
builder.write_color(gbufferNormal, true, clear);
builder.write_color(gbufferAlbedo, true, clear);
// Reverse-Z: clear depth to 0.0
VkClearValue depthClear{};
depthClear.depthStencil = {0.f, 0};
builder.write_depth(depthHandle, true, depthClear);
// Register read buffers used by all draw calls (index + vertex SSBO)
if (ctx)
{
const DrawContext &dc = ctx->getMainDrawContext();
// Collect unique buffers to avoid duplicates
std::unordered_set<VkBuffer> indexSet;
std::unordered_set<VkBuffer> vertexSet;
indexSet.reserve(dc.OpaqueSurfaces.size() + dc.TransparentSurfaces.size());
vertexSet.reserve(dc.OpaqueSurfaces.size() + dc.TransparentSurfaces.size());
auto collect = [&](const std::vector<RenderObject>& v){
for (const auto &r : v)
{
if (r.indexBuffer) indexSet.insert(r.indexBuffer);
if (r.vertexBuffer) vertexSet.insert(r.vertexBuffer);
}
};
collect(dc.OpaqueSurfaces);
collect(dc.TransparentSurfaces);
for (VkBuffer b : indexSet)
builder.read_buffer(b, RGBufferUsage::IndexRead, 0, "geom.index");
for (VkBuffer b : vertexSet)
builder.read_buffer(b, RGBufferUsage::StorageRead, 0, "geom.vertex");
}
},
[this, gbufferPosition, gbufferNormal, gbufferAlbedo, depthHandle](VkCommandBuffer cmd,
const RGPassResources &res,
EngineContext *ctx)
{
draw_geometry(cmd, ctx, res, gbufferPosition, gbufferNormal, gbufferAlbedo, depthHandle);
});
}
void GeometryPass::draw_geometry(VkCommandBuffer cmd,
EngineContext *context,
const RGPassResources &resources,
RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo,
RGImageHandle depthHandle) const
{
EngineContext *ctxLocal = context ? context : _context;
if (!ctxLocal || !ctxLocal->currentFrame) return;
ResourceManager *resourceManager = ctxLocal->getResources();
DeviceManager *deviceManager = ctxLocal->getDevice();
DescriptorManager *descriptorLayouts = ctxLocal->getDescriptorLayouts();
if (!resourceManager || !deviceManager || !descriptorLayouts) return;
VkImageView positionView = resources.image_view(gbufferPosition);
VkImageView normalView = resources.image_view(gbufferNormal);
VkImageView albedoView = resources.image_view(gbufferAlbedo);
VkImageView depthView = resources.image_view(depthHandle);
if (positionView == VK_NULL_HANDLE || normalView == VK_NULL_HANDLE ||
albedoView == VK_NULL_HANDLE || depthView == VK_NULL_HANDLE)
{
return;
}
const auto& mainDrawContext = ctxLocal->getMainDrawContext();
const auto& sceneData = ctxLocal->getSceneData();
VkExtent2D drawExtent = ctxLocal->getDrawExtent();
auto start = std::chrono::system_clock::now();
std::vector<uint32_t> opaque_draws;
opaque_draws.reserve(mainDrawContext.OpaqueSurfaces.size());
for (int i = 0; i < mainDrawContext.OpaqueSurfaces.size(); i++)
{
if (is_visible(mainDrawContext.OpaqueSurfaces[i], sceneData.viewproj))
{
opaque_draws.push_back(i);
}
}
std::sort(opaque_draws.begin(), opaque_draws.end(), [&](const auto &iA, const auto &iB)
{
const RenderObject &A = mainDrawContext.OpaqueSurfaces[iA];
const RenderObject &B = mainDrawContext.OpaqueSurfaces[iB];
if (A.material == B.material)
{
return A.indexBuffer < B.indexBuffer;
}
return A.material < B.material;
});
// Dynamic rendering is now begun by the RenderGraph using the declared attachments.
AllocatedBuffer gpuSceneDataBuffer = resourceManager->create_buffer(sizeof(GPUSceneData),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
ctxLocal->currentFrame->_deletionQueue.push_function([resourceManager, gpuSceneDataBuffer]()
{
resourceManager->destroy_buffer(gpuSceneDataBuffer);
});
VmaAllocationInfo allocInfo{};
vmaGetAllocationInfo(deviceManager->allocator(), gpuSceneDataBuffer.allocation, &allocInfo);
auto *sceneUniformData = static_cast<GPUSceneData *>(allocInfo.pMappedData);
*sceneUniformData = sceneData;
vmaFlushAllocation(deviceManager->allocator(), gpuSceneDataBuffer.allocation, 0, sizeof(GPUSceneData));
VkDescriptorSet globalDescriptor = ctxLocal->currentFrame->_frameDescriptors.allocate(
deviceManager->device(), descriptorLayouts->gpuSceneDataLayout());
DescriptorWriter writer;
writer.write_buffer(0, gpuSceneDataBuffer.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
writer.update_set(deviceManager->device(), globalDescriptor);
MaterialPipeline *lastPipeline = nullptr;
MaterialInstance *lastMaterial = nullptr;
VkBuffer lastIndexBuffer = VK_NULL_HANDLE;
auto draw = [&](const RenderObject &r)
{
if (r.material != lastMaterial)
{
lastMaterial = r.material;
if (r.material->pipeline != lastPipeline)
{
lastPipeline = r.material->pipeline;
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 0, 1,
&globalDescriptor, 0, nullptr);
VkViewport viewport{};
viewport.x = 0;
viewport.y = 0;
viewport.width = static_cast<float>(drawExtent.width);
viewport.height = static_cast<float>(drawExtent.height);
viewport.minDepth = 0.f;
viewport.maxDepth = 1.f;
vkCmdSetViewport(cmd, 0, 1, &viewport);
VkRect2D scissor{};
scissor.offset.x = 0;
scissor.offset.y = 0;
scissor.extent.width = drawExtent.width;
scissor.extent.height = drawExtent.height;
vkCmdSetScissor(cmd, 0, 1, &scissor);
}
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 1, 1,
&r.material->materialSet, 0, nullptr);
}
if (r.indexBuffer != lastIndexBuffer)
{
lastIndexBuffer = r.indexBuffer;
vkCmdBindIndexBuffer(cmd, r.indexBuffer, 0, VK_INDEX_TYPE_UINT32);
}
GPUDrawPushConstants push_constants{};
push_constants.worldMatrix = r.transform;
push_constants.vertexBuffer = r.vertexBufferAddress;
vkCmdPushConstants(cmd, r.material->pipeline->layout, VK_SHADER_STAGE_VERTEX_BIT, 0,
sizeof(GPUDrawPushConstants), &push_constants);
vkCmdDrawIndexed(cmd, r.indexCount, 1, r.firstIndex, 0, 0);
if (ctxLocal->stats)
{
ctxLocal->stats->drawcall_count++;
ctxLocal->stats->triangle_count += r.indexCount / 3;
}
};
if (ctxLocal->stats)
{
ctxLocal->stats->drawcall_count = 0;
ctxLocal->stats->triangle_count = 0;
}
for (auto &r: opaque_draws)
{
draw(mainDrawContext.OpaqueSurfaces[r]);
}
// Transparent surfaces are rendered in a separate Transparent pass after lighting.
// RenderGraph will end dynamic rendering for this pass.
auto end = std::chrono::system_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
if (ctxLocal->stats)
{
ctxLocal->stats->mesh_draw_time = elapsed.count() / 1000.f;
}
}
void GeometryPass::cleanup()
{
fmt::print("GeometryPass::cleanup()\n");
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include "vk_renderpass.h"
#include <render/rg_types.h>
class SwapchainManager;
class RenderGraph;
class GeometryPass : public IRenderPass
{
public:
void init(EngineContext *context) override;
void cleanup() override;
void execute(VkCommandBuffer cmd) override;
const char *getName() const override { return "Geometry"; }
void register_graph(RenderGraph *graph,
RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo,
RGImageHandle depthHandle);
private:
EngineContext *_context = nullptr;
void draw_geometry(VkCommandBuffer cmd,
EngineContext *context,
const class RGPassResources &resources,
RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo,
RGImageHandle depthHandle) const;
};

View File

@@ -0,0 +1,113 @@
#include "vk_renderpass_imgui.h"
#include "imgui.h"
#include "imgui_impl_sdl2.h"
#include "imgui_impl_vulkan.h"
#include "vk_device.h"
#include "vk_swapchain.h"
#include "core/vk_initializers.h"
#include "core/engine_context.h"
#include "render/rg_graph.h"
void ImGuiPass::init(EngineContext *context)
{
_context = context;
VkDescriptorPoolSize pool_sizes[] = {
{VK_DESCRIPTOR_TYPE_SAMPLER, 1000},
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000},
{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000},
{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000},
{VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000},
{VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000},
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000},
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000},
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000},
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000},
{VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000}
};
VkDescriptorPoolCreateInfo pool_info = {};
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
pool_info.maxSets = 1000;
pool_info.poolSizeCount = (uint32_t) std::size(pool_sizes);
pool_info.pPoolSizes = pool_sizes;
VkDescriptorPool imguiPool;
VK_CHECK(vkCreateDescriptorPool(_context->device->device(), &pool_info, nullptr, &imguiPool));
ImGui::CreateContext();
ImGui_ImplSDL2_InitForVulkan(_context->window);
ImGui_ImplVulkan_InitInfo init_info = {};
init_info.Instance = _context->getDevice()->instance();
init_info.PhysicalDevice = _context->getDevice()->physicalDevice();
init_info.Device = _context->getDevice()->device();
init_info.Queue = _context->getDevice()->graphicsQueue();
init_info.DescriptorPool = imguiPool;
init_info.MinImageCount = 3;
init_info.ImageCount = 3;
init_info.UseDynamicRendering = true;
init_info.PipelineRenderingCreateInfo = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO};
init_info.PipelineRenderingCreateInfo.colorAttachmentCount = 1;
auto _swapchainImageFormat = _context->getSwapchain()->swapchainImageFormat();
init_info.PipelineRenderingCreateInfo.pColorAttachmentFormats = &_swapchainImageFormat;
init_info.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
ImGui_ImplVulkan_Init(&init_info);
ImGui_ImplVulkan_CreateFontsTexture();
// add the destroy the imgui created structures
_deletionQueue.push_function([=]() {
ImGui_ImplVulkan_Shutdown();
vkDestroyDescriptorPool(_context->getDevice()->device(), imguiPool, nullptr);
});
}
void ImGuiPass::cleanup()
{
fmt::print("ImGuiPass::cleanup()\n");
_deletionQueue.flush();
}
void ImGuiPass::execute(VkCommandBuffer)
{
// ImGui is executed via the render graph now.
}
void ImGuiPass::register_graph(RenderGraph *graph, RGImageHandle swapchainHandle)
{
if (!graph || !swapchainHandle.valid()) return;
graph->add_pass(
"ImGui",
RGPassType::Graphics,
[swapchainHandle](RGPassBuilder &builder, EngineContext *)
{
builder.write_color(swapchainHandle, false, {});
},
[this, swapchainHandle](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx)
{
draw_imgui(cmd, ctx, res, swapchainHandle);
});
}
void ImGuiPass::draw_imgui(VkCommandBuffer cmd,
EngineContext *context,
const RGPassResources &resources,
RGImageHandle targetHandle) const
{
EngineContext *ctxLocal = context ? context : _context;
if (!ctxLocal) return;
VkImageView targetImageView = resources.image_view(targetHandle);
if (targetImageView == VK_NULL_HANDLE) return;
// Dynamic rendering is handled by the RenderGraph; just render draw data.
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cmd);
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include "vk_renderpass.h"
#include "core/vk_types.h"
#include <render/rg_types.h>
class ImGuiPass : public IRenderPass
{
public:
void init(EngineContext *context) override;
void cleanup() override;
void execute(VkCommandBuffer cmd) override;
const char *getName() const override { return "ImGui"; }
void register_graph(class RenderGraph *graph,
RGImageHandle swapchainHandle);
private:
EngineContext *_context = nullptr;
void draw_imgui(VkCommandBuffer cmd,
EngineContext *context,
const class RGPassResources &resources,
RGImageHandle targetHandle) const;
DeletionQueue _deletionQueue;
};

View File

@@ -0,0 +1,208 @@
#include "vk_renderpass_lighting.h"
#include "frame_resources.h"
#include "vk_descriptor_manager.h"
#include "vk_device.h"
#include "core/engine_context.h"
#include "core/vk_initializers.h"
#include "core/vk_resource.h"
#include "render/vk_pipelines.h"
#include "core/vk_pipeline_manager.h"
#include "core/asset_manager.h"
#include "core/vk_descriptors.h"
#include "vk_mem_alloc.h"
#include "vk_sampler_manager.h"
#include "vk_swapchain.h"
#include "render/rg_graph.h"
void LightingPass::init(EngineContext *context)
{
_context = context;
// Build descriptor layout for GBuffer inputs
{
DescriptorLayoutBuilder builder;
builder.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
builder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
builder.add_binding(2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
_gBufferInputDescriptorLayout = builder.build(_context->getDevice()->device(), VK_SHADER_STAGE_FRAGMENT_BIT);
}
// Allocate and write GBuffer descriptor set
_gBufferInputDescriptorSet = _context->getDescriptors()->allocate(
_context->getDevice()->device(), _gBufferInputDescriptorLayout);
{
DescriptorWriter writer;
writer.write_image(0, _context->getSwapchain()->gBufferPosition().imageView, _context->getSamplers()->defaultLinear(),
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.write_image(1, _context->getSwapchain()->gBufferNormal().imageView, _context->getSamplers()->defaultLinear(),
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.write_image(2, _context->getSwapchain()->gBufferAlbedo().imageView, _context->getSamplers()->defaultLinear(),
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.update_set(_context->getDevice()->device(), _gBufferInputDescriptorSet);
}
// Shadow map descriptor layout (set = 2, updated per-frame)
{
DescriptorLayoutBuilder builder;
builder.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
_shadowDescriptorLayout = builder.build(_context->getDevice()->device(), VK_SHADER_STAGE_FRAGMENT_BIT);
}
// Build lighting pipeline through PipelineManager
VkDescriptorSetLayout layouts[] = {
_context->getDescriptorLayouts()->gpuSceneDataLayout(),
_gBufferInputDescriptorLayout,
_shadowDescriptorLayout
};
GraphicsPipelineCreateInfo info{};
info.vertexShaderPath = _context->getAssets()->shaderPath("fullscreen.vert.spv");
info.fragmentShaderPath = _context->getAssets()->shaderPath("deferred_lighting.frag.spv");
info.setLayouts.assign(std::begin(layouts), std::end(layouts));
info.configure = [this](PipelineBuilder &b) {
b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
b.set_polygon_mode(VK_POLYGON_MODE_FILL);
b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE);
b.set_multisampling_none();
b.enable_blending_alphablend();
b.disable_depthtest();
b.set_color_attachment_format(_context->getSwapchain()->drawImage().imageFormat);
};
_context->pipelines->createGraphicsPipeline("deferred_lighting", info);
// fetch the handles so current frame uses latest versions
MaterialPipeline mp{};
_context->pipelines->getMaterialPipeline("deferred_lighting", mp);
_pipeline = mp.pipeline;
_pipelineLayout = mp.layout;
_deletionQueue.push_function([&]() {
// Pipelines are owned by PipelineManager; only destroy our local descriptor set layout
vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _gBufferInputDescriptorLayout, nullptr);
vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _shadowDescriptorLayout, nullptr);
});
}
void LightingPass::execute(VkCommandBuffer)
{
// Lighting is executed via the render graph now.
}
void LightingPass::register_graph(RenderGraph *graph,
RGImageHandle drawHandle,
RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo,
RGImageHandle shadowDepth)
{
if (!graph || !drawHandle.valid() || !gbufferPosition.valid() || !gbufferNormal.valid() || !gbufferAlbedo.valid() || !shadowDepth.valid())
{
return;
}
graph->add_pass(
"Lighting",
RGPassType::Graphics,
[drawHandle, gbufferPosition, gbufferNormal, gbufferAlbedo, shadowDepth](RGPassBuilder &builder, EngineContext *)
{
builder.read(gbufferPosition, RGImageUsage::SampledFragment);
builder.read(gbufferNormal, RGImageUsage::SampledFragment);
builder.read(gbufferAlbedo, RGImageUsage::SampledFragment);
builder.read(shadowDepth, RGImageUsage::SampledFragment);
builder.write_color(drawHandle);
},
[this, drawHandle, shadowDepth](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx)
{
draw_lighting(cmd, ctx, res, drawHandle, shadowDepth);
});
}
void LightingPass::draw_lighting(VkCommandBuffer cmd,
EngineContext *context,
const RGPassResources &resources,
RGImageHandle drawHandle,
RGImageHandle shadowDepth)
{
EngineContext *ctxLocal = context ? context : _context;
if (!ctxLocal || !ctxLocal->currentFrame) return;
ResourceManager *resourceManager = ctxLocal->getResources();
DeviceManager *deviceManager = ctxLocal->getDevice();
DescriptorManager *descriptorLayouts = ctxLocal->getDescriptorLayouts();
PipelineManager *pipelineManager = ctxLocal->pipelines;
if (!resourceManager || !deviceManager || !descriptorLayouts || !pipelineManager) return;
VkImageView drawView = resources.image_view(drawHandle);
if (drawView == VK_NULL_HANDLE) return;
// Re-fetch pipeline in case it was hot-reloaded
pipelineManager->getGraphics("deferred_lighting", _pipeline, _pipelineLayout);
// Dynamic rendering is handled by the RenderGraph using the declared draw attachment.
AllocatedBuffer gpuSceneDataBuffer = resourceManager->create_buffer(
sizeof(GPUSceneData), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
ctxLocal->currentFrame->_deletionQueue.push_function([resourceManager, gpuSceneDataBuffer]()
{
resourceManager->destroy_buffer(gpuSceneDataBuffer);
});
VmaAllocationInfo allocInfo{};
vmaGetAllocationInfo(deviceManager->allocator(), gpuSceneDataBuffer.allocation, &allocInfo);
auto *sceneUniformData = static_cast<GPUSceneData *>(allocInfo.pMappedData);
*sceneUniformData = ctxLocal->getSceneData();
vmaFlushAllocation(deviceManager->allocator(), gpuSceneDataBuffer.allocation, 0, sizeof(GPUSceneData));
VkDescriptorSet globalDescriptor = ctxLocal->currentFrame->_frameDescriptors.allocate(
deviceManager->device(), descriptorLayouts->gpuSceneDataLayout());
DescriptorWriter writer;
writer.write_buffer(0, gpuSceneDataBuffer.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
writer.update_set(deviceManager->device(), globalDescriptor);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 0, 1, &globalDescriptor, 0,
nullptr);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 1, 1,
&_gBufferInputDescriptorSet, 0, nullptr);
// Allocate and write shadow descriptor set for this frame (set = 2)
VkDescriptorSet shadowSet = ctxLocal->currentFrame->_frameDescriptors.allocate(
deviceManager->device(), _shadowDescriptorLayout);
{
VkImageView shadowView = resources.image_view(shadowDepth);
DescriptorWriter writer2;
writer2.write_image(0, shadowView, ctxLocal->getSamplers()->defaultLinear(),
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer2.update_set(deviceManager->device(), shadowSet);
}
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 2, 1, &shadowSet, 0, nullptr);
VkViewport viewport{};
viewport.x = 0;
viewport.y = 0;
viewport.width = static_cast<float>(ctxLocal->getDrawExtent().width);
viewport.height = static_cast<float>(ctxLocal->getDrawExtent().height);
viewport.minDepth = 0.f;
viewport.maxDepth = 1.f;
vkCmdSetViewport(cmd, 0, 1, &viewport);
VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = {ctxLocal->getDrawExtent().width, ctxLocal->getDrawExtent().height};
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdDraw(cmd, 3, 1, 0, 0);
// RenderGraph ends rendering.
}
void LightingPass::cleanup()
{
_deletionQueue.flush();
fmt::print("LightingPass::cleanup()\n");
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include "vk_renderpass.h"
#include <render/rg_types.h>
class LightingPass : public IRenderPass
{
public:
void init(EngineContext *context) override;
void cleanup() override;
void execute(VkCommandBuffer cmd) override;
const char *getName() const override { return "Lighting"; }
void register_graph(class RenderGraph *graph,
RGImageHandle drawHandle,
RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo,
RGImageHandle shadowDepth);
private:
EngineContext *_context = nullptr;
VkDescriptorSetLayout _gBufferInputDescriptorLayout = VK_NULL_HANDLE;
VkDescriptorSet _gBufferInputDescriptorSet = VK_NULL_HANDLE;
VkDescriptorSetLayout _shadowDescriptorLayout = VK_NULL_HANDLE; // set=2
VkPipelineLayout _pipelineLayout = VK_NULL_HANDLE;
VkPipeline _pipeline = VK_NULL_HANDLE;
void draw_lighting(VkCommandBuffer cmd,
EngineContext *context,
const class RGPassResources &resources,
RGImageHandle drawHandle,
RGImageHandle shadowDepth);
DeletionQueue _deletionQueue;
};

View File

@@ -0,0 +1,184 @@
#include "vk_renderpass_shadow.h"
#include <unordered_set>
#include "core/engine_context.h"
#include "render/rg_graph.h"
#include "render/rg_builder.h"
#include "vk_swapchain.h"
#include "vk_scene.h"
#include "frame_resources.h"
#include "vk_descriptor_manager.h"
#include "vk_device.h"
#include "vk_resource.h"
#include "core/vk_initializers.h"
#include "core/vk_pipeline_manager.h"
#include "core/asset_manager.h"
#include "render/vk_pipelines.h"
#include "core/vk_types.h"
void ShadowPass::init(EngineContext *context)
{
_context = context;
if (!_context || !_context->pipelines) return;
// Build a depth-only graphics pipeline for shadow map rendering
VkPushConstantRange pc{};
pc.offset = 0;
pc.size = sizeof(GPUDrawPushConstants);
pc.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
GraphicsPipelineCreateInfo info{};
info.vertexShaderPath = _context->getAssets()->shaderPath("shadow.vert.spv");
info.fragmentShaderPath = _context->getAssets()->shaderPath("shadow.frag.spv");
info.setLayouts = { _context->getDescriptorLayouts()->gpuSceneDataLayout() };
info.pushConstants = { pc };
info.configure = [this](PipelineBuilder &b) {
b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
b.set_polygon_mode(VK_POLYGON_MODE_FILL);
b.set_cull_mode(VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE);
b.set_multisampling_none();
b.disable_blending();
// Reverse-Z depth test & depth-only pipeline
b.enable_depthtest(true, VK_COMPARE_OP_GREATER_OR_EQUAL);
b.set_depth_format(VK_FORMAT_D32_SFLOAT);
// Static depth bias to help with surface acne (will tune later)
b._rasterizer.depthBiasEnable = VK_TRUE;
b._rasterizer.depthBiasConstantFactor = 2.0f;
b._rasterizer.depthBiasSlopeFactor = 2.0f;
b._rasterizer.depthBiasClamp = 0.0f;
};
_context->pipelines->createGraphicsPipeline("mesh.shadow", info);
}
void ShadowPass::cleanup()
{
// Nothing yet; pipelines and descriptors will be added later
fmt::print("ShadowPass::cleanup()\n");
}
void ShadowPass::execute(VkCommandBuffer)
{
// Shadow rendering is done via the RenderGraph registration.
}
void ShadowPass::register_graph(RenderGraph *graph, RGImageHandle shadowDepth, VkExtent2D extent)
{
if (!graph || !shadowDepth.valid()) return;
graph->add_pass(
"ShadowMap",
RGPassType::Graphics,
[shadowDepth](RGPassBuilder &builder, EngineContext *ctx)
{
// Reverse-Z depth clear to 0.0
VkClearValue clear{}; clear.depthStencil = {0.f, 0};
builder.write_depth(shadowDepth, true, clear);
// Ensure index/vertex buffers are tracked as reads (like Geometry)
if (ctx)
{
const DrawContext &dc = ctx->getMainDrawContext();
std::unordered_set<VkBuffer> indexSet;
std::unordered_set<VkBuffer> vertexSet;
auto collect = [&](const std::vector<RenderObject> &v)
{
for (const auto &r : v)
{
if (r.indexBuffer) indexSet.insert(r.indexBuffer);
if (r.vertexBuffer) vertexSet.insert(r.vertexBuffer);
}
};
collect(dc.OpaqueSurfaces);
// Transparent surfaces are ignored for shadow map in this simple pass
for (VkBuffer b : indexSet)
builder.read_buffer(b, RGBufferUsage::IndexRead, 0, "shadow.index");
for (VkBuffer b : vertexSet)
builder.read_buffer(b, RGBufferUsage::StorageRead, 0, "shadow.vertex");
}
},
[this, shadowDepth, extent](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx)
{
draw_shadow(cmd, ctx, res, shadowDepth, extent);
});
}
void ShadowPass::draw_shadow(VkCommandBuffer cmd,
EngineContext *context,
const RGPassResources &/*resources*/,
RGImageHandle /*shadowDepth*/,
VkExtent2D extent) const
{
EngineContext *ctxLocal = context ? context : _context;
if (!ctxLocal || !ctxLocal->currentFrame) return;
ResourceManager *resourceManager = ctxLocal->getResources();
DeviceManager *deviceManager = ctxLocal->getDevice();
DescriptorManager *descriptorLayouts = ctxLocal->getDescriptorLayouts();
PipelineManager *pipelineManager = ctxLocal->pipelines;
if (!resourceManager || !deviceManager || !descriptorLayouts || !pipelineManager) return;
VkPipeline pipeline{}; VkPipelineLayout layout{};
if (!pipelineManager->getGraphics("mesh.shadow", pipeline, layout)) return;
// Create and upload per-pass scene UBO
AllocatedBuffer gpuSceneDataBuffer = resourceManager->create_buffer(
sizeof(GPUSceneData), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
ctxLocal->currentFrame->_deletionQueue.push_function([resourceManager, gpuSceneDataBuffer]()
{
resourceManager->destroy_buffer(gpuSceneDataBuffer);
});
VmaAllocationInfo allocInfo{};
vmaGetAllocationInfo(deviceManager->allocator(), gpuSceneDataBuffer.allocation, &allocInfo);
auto *sceneUniformData = static_cast<GPUSceneData *>(allocInfo.pMappedData);
*sceneUniformData = ctxLocal->getSceneData();
vmaFlushAllocation(deviceManager->allocator(), gpuSceneDataBuffer.allocation, 0, sizeof(GPUSceneData));
VkDescriptorSet globalDescriptor = ctxLocal->currentFrame->_frameDescriptors.allocate(
deviceManager->device(), descriptorLayouts->gpuSceneDataLayout());
DescriptorWriter writer;
writer.write_buffer(0, gpuSceneDataBuffer.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
writer.update_set(deviceManager->device(), globalDescriptor);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &globalDescriptor, 0, nullptr);
VkViewport viewport{};
viewport.x = 0;
viewport.y = 0;
viewport.width = static_cast<float>(extent.width);
viewport.height = static_cast<float>(extent.height);
viewport.minDepth = 0.f;
viewport.maxDepth = 1.f;
vkCmdSetViewport(cmd, 0, 1, &viewport);
VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = extent;
vkCmdSetScissor(cmd, 0, 1, &scissor);
const DrawContext &dc = ctxLocal->getMainDrawContext();
VkBuffer lastIndexBuffer = VK_NULL_HANDLE;
for (const auto &r : dc.OpaqueSurfaces)
{
if (r.indexBuffer != lastIndexBuffer)
{
lastIndexBuffer = r.indexBuffer;
vkCmdBindIndexBuffer(cmd, r.indexBuffer, 0, VK_INDEX_TYPE_UINT32);
}
GPUDrawPushConstants pc{};
pc.worldMatrix = r.transform;
pc.vertexBuffer = r.vertexBufferAddress;
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUDrawPushConstants), &pc);
vkCmdDrawIndexed(cmd, r.indexCount, 1, r.firstIndex, 0, 0);
}
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include "vk_renderpass.h"
#include <render/rg_types.h>
class RenderGraph;
class EngineContext;
class RGPassResources;
// Depth-only directional shadow map pass (skeleton)
// - Writes a depth image using reversed-Z (clear=0)
// - Draw function will be filled in a later step
class ShadowPass : public IRenderPass
{
public:
void init(EngineContext *context) override;
void cleanup() override;
void execute(VkCommandBuffer cmd) override;
const char *getName() const override { return "ShadowMap"; }
// Register the depth-only pass into the render graph
void register_graph(RenderGraph *graph, RGImageHandle shadowDepth, VkExtent2D extent);
private:
EngineContext *_context = nullptr;
void draw_shadow(VkCommandBuffer cmd,
EngineContext *context,
const RGPassResources &resources,
RGImageHandle shadowDepth,
VkExtent2D extent) const;
};

View File

@@ -0,0 +1,122 @@
#include "vk_renderpass_tonemap.h"
#include <core/engine_context.h>
#include <core/vk_descriptors.h>
#include <core/vk_descriptor_manager.h>
#include <core/vk_pipeline_manager.h>
#include <core/asset_manager.h>
#include <core/vk_device.h>
#include <core/vk_resource.h>
#include <vk_sampler_manager.h>
#include <render/rg_graph.h>
#include <render/rg_resources.h>
#include "frame_resources.h"
struct TonemapPush
{
float exposure;
int mode;
};
void TonemapPass::init(EngineContext *context)
{
_context = context;
_inputSetLayout = _context->getDescriptorLayouts()->singleImageLayout();
GraphicsPipelineCreateInfo info{};
info.vertexShaderPath = _context->getAssets()->shaderPath("fullscreen.vert.spv");
info.fragmentShaderPath = _context->getAssets()->shaderPath("tonemap.frag.spv");
info.setLayouts = { _inputSetLayout };
VkPushConstantRange pcr{};
pcr.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
pcr.offset = 0;
pcr.size = sizeof(TonemapPush);
info.pushConstants = { pcr };
info.configure = [this](PipelineBuilder &b) {
b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
b.set_polygon_mode(VK_POLYGON_MODE_FILL);
b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE);
b.set_multisampling_none();
b.disable_depthtest();
b.disable_blending();
b.set_color_attachment_format(VK_FORMAT_R8G8B8A8_UNORM);
};
_context->pipelines->createGraphicsPipeline("tonemap", info);
MaterialPipeline mp{};
_context->pipelines->getMaterialPipeline("tonemap", mp);
_pipeline = mp.pipeline;
_pipelineLayout = mp.layout;
}
void TonemapPass::cleanup()
{
_deletionQueue.flush();
}
void TonemapPass::execute(VkCommandBuffer)
{
// Executed via render graph.
}
RGImageHandle TonemapPass::register_graph(RenderGraph *graph, RGImageHandle hdrInput)
{
if (!graph || !hdrInput.valid()) return {};
RGImageDesc desc{};
desc.name = "ldr.tonemap";
desc.format = VK_FORMAT_R8G8B8A8_UNORM;
desc.extent = _context->getDrawExtent();
desc.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
RGImageHandle ldr = graph->create_image(desc);
graph->add_pass(
"Tonemap",
RGPassType::Graphics,
[hdrInput, ldr](RGPassBuilder &builder, EngineContext *) {
builder.read(hdrInput, RGImageUsage::SampledFragment);
builder.write_color(ldr, true /*clear*/);
},
[this, hdrInput](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) {
draw_tonemap(cmd, ctx, res, hdrInput);
}
);
return ldr;
}
void TonemapPass::draw_tonemap(VkCommandBuffer cmd, EngineContext *ctx, const RGPassResources &res,
RGImageHandle hdrInput)
{
if (!ctx || !ctx->currentFrame) return;
VkDevice device = ctx->getDevice()->device();
VkImageView hdrView = res.image_view(hdrInput);
if (hdrView == VK_NULL_HANDLE) return;
VkDescriptorSet set = ctx->currentFrame->_frameDescriptors.allocate(device, _inputSetLayout);
DescriptorWriter writer;
writer.write_image(0, hdrView, ctx->getSamplers()->defaultLinear(),
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.update_set(device, set);
ctx->pipelines->getGraphics("tonemap", _pipeline, _pipelineLayout);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 0, 1, &set, 0, nullptr);
TonemapPush push{_exposure, _mode};
vkCmdPushConstants(cmd, _pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(TonemapPush), &push);
VkExtent2D extent = ctx->getDrawExtent();
VkViewport vp{0.f, 0.f, (float)extent.width, (float)extent.height, 0.f, 1.f};
VkRect2D sc{{0,0}, extent};
vkCmdSetViewport(cmd, 0, 1, &vp);
vkCmdSetScissor(cmd, 0, 1, &sc);
vkCmdDraw(cmd, 3, 1, 0, 0);
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include <core/vk_types.h>
#include <render/vk_renderpass.h>
#include <render/rg_types.h>
class EngineContext;
class RenderGraph;
class RGPassResources;
class TonemapPass final : public IRenderPass
{
public:
void init(EngineContext *context) override;
void cleanup() override;
void execute(VkCommandBuffer) override; // Not used directly; executed via render graph
const char *getName() const override { return "Tonemap"; }
// Register pass in the render graph. Returns the LDR output image handle.
RGImageHandle register_graph(RenderGraph *graph, RGImageHandle hdrInput);
// Runtime parameters
void setExposure(float e) { _exposure = e; }
float exposure() const { return _exposure; }
void setMode(int m) { _mode = m; }
int mode() const { return _mode; }
private:
void draw_tonemap(VkCommandBuffer cmd, EngineContext *ctx, const RGPassResources &res,
RGImageHandle hdrInput);
EngineContext *_context = nullptr;
VkPipeline _pipeline = VK_NULL_HANDLE;
VkPipelineLayout _pipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout _inputSetLayout = VK_NULL_HANDLE;
float _exposure = 1.0f;
int _mode = 1; // default to ACES
DeletionQueue _deletionQueue;
};

View File

@@ -0,0 +1,159 @@
#include "vk_renderpass_transparent.h"
#include <algorithm>
#include <unordered_set>
#include "vk_scene.h"
#include "vk_swapchain.h"
#include "core/engine_context.h"
#include "core/vk_resource.h"
#include "core/vk_device.h"
#include "core/vk_descriptor_manager.h"
#include "core/frame_resources.h"
#include "render/rg_graph.h"
void TransparentPass::init(EngineContext *context)
{
_context = context;
}
void TransparentPass::execute(VkCommandBuffer)
{
// Executed through render graph.
}
void TransparentPass::register_graph(RenderGraph *graph, RGImageHandle drawHandle, RGImageHandle depthHandle)
{
if (!graph || !drawHandle.valid() || !depthHandle.valid()) return;
graph->add_pass(
"Transparent",
RGPassType::Graphics,
[drawHandle, depthHandle](RGPassBuilder &builder, EngineContext *ctx) {
// Draw transparent to the HDR target with depth testing against the existing depth buffer.
builder.write_color(drawHandle);
builder.write_depth(depthHandle, false /*load existing depth*/);
// Register external buffers used by draws
if (ctx)
{
const DrawContext &dc = ctx->getMainDrawContext();
std::unordered_set<VkBuffer> indexSet;
std::unordered_set<VkBuffer> vertexSet;
auto collect = [&](const std::vector<RenderObject> &v) {
for (const auto &r: v)
{
if (r.indexBuffer) indexSet.insert(r.indexBuffer);
if (r.vertexBuffer) vertexSet.insert(r.vertexBuffer);
}
};
collect(dc.TransparentSurfaces);
for (VkBuffer b: indexSet) builder.read_buffer(b, RGBufferUsage::IndexRead, 0, "trans.index");
for (VkBuffer b: vertexSet) builder.read_buffer(b, RGBufferUsage::StorageRead, 0, "trans.vertex");
}
},
[this, drawHandle, depthHandle](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) {
draw_transparent(cmd, ctx, res, drawHandle, depthHandle);
}
);
}
void TransparentPass::draw_transparent(VkCommandBuffer cmd,
EngineContext *context,
const RGPassResources &resources,
RGImageHandle /*drawHandle*/,
RGImageHandle /*depthHandle*/) const
{
EngineContext *ctxLocal = context ? context : _context;
if (!ctxLocal || !ctxLocal->currentFrame) return;
ResourceManager *resourceManager = ctxLocal->getResources();
DeviceManager *deviceManager = ctxLocal->getDevice();
DescriptorManager *descriptorLayouts = ctxLocal->getDescriptorLayouts();
if (!resourceManager || !deviceManager || !descriptorLayouts) return;
const auto &dc = ctxLocal->getMainDrawContext();
const auto &sceneData = ctxLocal->getSceneData();
// Prepare per-frame scene UBO
AllocatedBuffer gpuSceneDataBuffer = resourceManager->create_buffer(
sizeof(GPUSceneData), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
ctxLocal->currentFrame->_deletionQueue.push_function([resourceManager, gpuSceneDataBuffer]() {
resourceManager->destroy_buffer(gpuSceneDataBuffer);
});
VmaAllocationInfo allocInfo{};
vmaGetAllocationInfo(deviceManager->allocator(), gpuSceneDataBuffer.allocation, &allocInfo);
auto *sceneUniformData = static_cast<GPUSceneData *>(allocInfo.pMappedData);
*sceneUniformData = sceneData;
vmaFlushAllocation(deviceManager->allocator(), gpuSceneDataBuffer.allocation, 0, sizeof(GPUSceneData));
VkDescriptorSet globalDescriptor = ctxLocal->currentFrame->_frameDescriptors.allocate(
deviceManager->device(), descriptorLayouts->gpuSceneDataLayout());
DescriptorWriter writer;
writer.write_buffer(0, gpuSceneDataBuffer.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
writer.update_set(deviceManager->device(), globalDescriptor);
// Sort transparent back-to-front using camera-space depth
std::vector<const RenderObject *> draws;
draws.reserve(dc.TransparentSurfaces.size());
for (const auto &r: dc.TransparentSurfaces) draws.push_back(&r);
auto view = sceneData.view; // world -> view
auto depthOf = [&](const RenderObject *r) {
glm::vec4 c = r->transform * glm::vec4(r->bounds.origin, 1.f);
float z = (view * c).z;
return -z; // positive depth; larger = further
};
std::sort(draws.begin(), draws.end(), [&](const RenderObject *A, const RenderObject *B) {
return depthOf(A) > depthOf(B); // far to near
});
VkExtent2D extent = ctxLocal->getDrawExtent();
VkViewport viewport{0.f, 0.f, (float) extent.width, (float) extent.height, 0.f, 1.f};
vkCmdSetViewport(cmd, 0, 1, &viewport);
VkRect2D scissor{{0, 0}, extent};
vkCmdSetScissor(cmd, 0, 1, &scissor);
MaterialPipeline *lastPipeline = nullptr;
MaterialInstance *lastMaterial = nullptr;
VkBuffer lastIndexBuffer = VK_NULL_HANDLE;
auto draw = [&](const RenderObject &r) {
if (r.material != lastMaterial)
{
lastMaterial = r.material;
if (r.material->pipeline != lastPipeline)
{
lastPipeline = r.material->pipeline;
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 0, 1,
&globalDescriptor, 0, nullptr);
}
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 1, 1,
&r.material->materialSet, 0, nullptr);
}
if (r.indexBuffer != lastIndexBuffer)
{
lastIndexBuffer = r.indexBuffer;
vkCmdBindIndexBuffer(cmd, r.indexBuffer, 0, VK_INDEX_TYPE_UINT32);
}
GPUDrawPushConstants push{};
push.worldMatrix = r.transform;
push.vertexBuffer = r.vertexBufferAddress;
vkCmdPushConstants(cmd, r.material->pipeline->layout, VK_SHADER_STAGE_VERTEX_BIT, 0,
sizeof(GPUDrawPushConstants), &push);
vkCmdDrawIndexed(cmd, r.indexCount, 1, r.firstIndex, 0, 0);
if (ctxLocal->stats)
{
ctxLocal->stats->drawcall_count++;
ctxLocal->stats->triangle_count += r.indexCount / 3;
}
};
for (auto *pObj: draws) draw(*pObj);
}
void TransparentPass::cleanup()
{
fmt::print("TransparentPass::cleanup()\n");
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include "vk_renderpass.h"
#include "render/rg_types.h"
class TransparentPass : public IRenderPass
{
public:
void init(EngineContext *context) override;
void execute(VkCommandBuffer cmd) override;
void cleanup() override;
const char *getName() const override { return "Transparent"; }
// RenderGraph wiring
void register_graph(class RenderGraph *graph,
RGImageHandle drawHandle,
RGImageHandle depthHandle);
private:
void draw_transparent(VkCommandBuffer cmd,
EngineContext *context,
const class RGPassResources &resources,
RGImageHandle drawHandle,
RGImageHandle depthHandle) const;
EngineContext *_context{};
};