aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/core/application.cpp3
-rw-r--r--src/platform/opengl/opengl_device.cpp14
-rw-r--r--src/platform/vulkan/vulkan_device.cpp186
-rw-r--r--src/rendering/black_hole_renderer.cpp8
-rw-r--r--src/rendering/rhi.h32
-rw-r--r--src/rendering/scene_renderer.cpp6
6 files changed, 160 insertions, 89 deletions
diff --git a/src/core/application.cpp b/src/core/application.cpp
index 6239c36..9cd19ec 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -69,7 +69,8 @@ namespace Donut
else
m_device = RHI::create_opengl_device();
- if (!m_device->init(m_window->get_native_window(), w, h))
+ RHI::NativeWindow native{ m_window->get_native_window(), w, h };
+ if (!m_device->init(native))
{
DONUT_ERROR("RHI device initialization failed");
return;
diff --git a/src/platform/opengl/opengl_device.cpp b/src/platform/opengl/opengl_device.cpp
index 3d45d24..c4332fa 100644
--- a/src/platform/opengl/opengl_device.cpp
+++ b/src/platform/opengl/opengl_device.cpp
@@ -205,17 +205,17 @@ namespace Donut::RHI
class GLDevice : public Device
{
public:
- auto init(void* window, int width, int height) -> bool override
+ auto init(const NativeWindow& window) -> bool override
{
- m_window = window;
- glfwMakeContextCurrent(static_cast<GLFWwindow*>(window));
+ m_window = window.glfw_handle;
+ glfwMakeContextCurrent(static_cast<GLFWwindow*>(m_window));
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { DONUT_ERROR("GL RHI: GLAD load failed"); return false; }
- m_width = width; m_height = height;
+ m_width = window.width; m_height = window.height;
const char* name = (const char*)glGetString(GL_RENDERER);
m_name = name ? name : "OpenGL";
glGenVertexArrays(1, &m_vao); glBindVertexArray(m_vao); // GL core needs one bound VAO
m_cmds = create_scope<GLCommandList>(m_vao);
- DONUT_INFO("GL RHI device: {} ({}x{})", m_name, width, height);
+ DONUT_INFO("GL RHI device: {} ({}x{})", m_name, m_width, m_height);
return true;
}
auto shutdown() -> void override
@@ -235,8 +235,8 @@ namespace Donut::RHI
HDRIManager::get().set_current_hdri(path);
return create_ref<GLTexture>(HDRIManager::get().get_current_hdri());
}
- auto create_render_target(int w, int h, Format color, bool depth, Filter fl, int /*mips*/) -> Ref<RenderTarget> override
- { return create_ref<GLRenderTarget>(w, h, color, depth, fl); }
+ auto create_render_target(int w, int h, Format color, Format depth, Filter fl, int /*mips*/) -> Ref<RenderTarget> override
+ { return create_ref<GLRenderTarget>(w, h, color, depth != Format::None, fl); }
auto create_pipeline(const PipelineDesc& d) -> Ref<Pipeline> override
{ return create_ref<GLPipeline>(d); }
diff --git a/src/platform/vulkan/vulkan_device.cpp b/src/platform/vulkan/vulkan_device.cpp
index 4311d0c..45e773e 100644
--- a/src/platform/vulkan/vulkan_device.cpp
+++ b/src/platform/vulkan/vulkan_device.cpp
@@ -19,6 +19,7 @@
#include <cstring>
#include <cstdlib>
#include <fstream>
+#include <unordered_map>
namespace Donut::RHI
{
@@ -109,11 +110,15 @@ namespace Donut::RHI
public:
~VkRenderTargetR() override
{
- if (m_fb) vkDestroyFramebuffer(m_device, m_fb, nullptr);
- if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr);
- if (m_view) vkDestroyImageView(m_device, m_view, nullptr);
- if (m_image) vkDestroyImage(m_device, m_image, nullptr);
- if (m_mem) vkFreeMemory(m_device, m_mem, nullptr);
+ if (m_fb) vkDestroyFramebuffer(m_device, m_fb, nullptr);
+ if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr);
+ if (m_view) vkDestroyImageView(m_device, m_view, nullptr);
+ if (m_image) vkDestroyImage(m_device, m_image, nullptr);
+ if (m_mem) vkFreeMemory(m_device, m_mem, nullptr);
+ if (m_depth_view) vkDestroyImageView(m_device, m_depth_view, nullptr);
+ if (m_depth_image) vkDestroyImage(m_device, m_depth_image, nullptr);
+ if (m_depth_mem) vkFreeMemory(m_device, m_depth_mem, nullptr);
+ // m_pass is owned by the device's render-pass cache, not by us.
}
auto width() const -> int override { return m_w; }
auto height() const -> int override { return m_h; }
@@ -126,6 +131,11 @@ namespace Donut::RHI
VkImageView m_view = VK_NULL_HANDLE;
VkSampler m_sampler = VK_NULL_HANDLE;
VkFramebuffer m_fb = VK_NULL_HANDLE;
+ VkRenderPass m_pass = VK_NULL_HANDLE; // borrowed (device pass cache)
+ bool m_has_depth = false;
+ VkImage m_depth_image = VK_NULL_HANDLE;
+ VkDeviceMemory m_depth_mem = VK_NULL_HANDLE;
+ VkImageView m_depth_view = VK_NULL_HANDLE;
VkTextureR m_color; // borrowed wrapper (view+sampler) for sampling
};
@@ -159,9 +169,9 @@ namespace Donut::RHI
if (target)
{
auto* rt = static_cast<VkRenderTargetR*>(target);
- rpbi.renderPass = m_offscreen_rp; rpbi.framebuffer = rt->m_fb;
+ rpbi.renderPass = rt->m_pass; rpbi.framebuffer = rt->m_fb;
rpbi.renderArea = { { 0, 0 }, { (uint32_t)rt->m_w, (uint32_t)rt->m_h } };
- rpbi.clearValueCount = 1; rpbi.pClearValues = cvs;
+ rpbi.clearValueCount = rt->m_has_depth ? 2 : 1; rpbi.pClearValues = cvs;
}
else
{
@@ -240,7 +250,6 @@ namespace Donut::RHI
VkDevice m_device = VK_NULL_HANDLE;
VkCommandBuffer m_cmd = VK_NULL_HANDLE;
VkRenderPass m_swapchain_rp = VK_NULL_HANDLE;
- VkRenderPass m_offscreen_rp = VK_NULL_HANDLE;
VkFramebuffer m_swapchain_fb = VK_NULL_HANDLE;
VkExtent2D m_extent{};
VkDescriptorPool m_frame_pool = VK_NULL_HANDLE;
@@ -254,7 +263,7 @@ namespace Donut::RHI
class VulkanDevice : public Device
{
public:
- auto init(void* glfwWindow, int width, int height) -> bool override;
+ auto init(const NativeWindow& window) -> bool override;
auto shutdown() -> void override;
auto resize(int width, int height) -> void override { m_framebuffer_resized = true; m_width = width; m_height = height; }
auto wait_idle() -> void override { if (m_device) vkDeviceWaitIdle(m_device); }
@@ -262,7 +271,7 @@ namespace Donut::RHI
auto create_buffer(BufferType type, size_t size, const void* data) -> Ref<Buffer> override;
auto create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref<Texture> override;
auto create_cubemap_from_hdri(const std::string& path) -> Ref<Texture> override;
- auto create_render_target(int w, int h, Format color, bool with_depth, Filter filter, int mips) -> Ref<RenderTarget> override;
+ auto create_render_target(int w, int h, Format color, Format depth, Filter filter, int mips) -> Ref<RenderTarget> override;
auto create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> override;
auto begin_frame(const glm::vec4& clear) -> CommandList* override;
@@ -284,8 +293,10 @@ namespace Donut::RHI
auto pick_physical_and_device() -> bool;
auto create_swapchain() -> bool;
auto create_image_views() -> bool;
- auto create_swapchain_render_pass() -> bool;
- auto create_offscreen_render_pass() -> bool;
+ // Render passes are format-driven and cached: a pipeline/target's
+ // attachment signature (colour + optional depth, present vs sampled)
+ // maps to one pass. `present` = presented swapchain image.
+ auto get_render_pass(VkFormat color, VkFormat depth, bool present) -> VkRenderPass;
auto create_depth_and_framebuffers() -> bool;
auto create_command_and_sync() -> bool;
auto recreate_swapchain() -> bool;
@@ -309,8 +320,8 @@ namespace Donut::RHI
VkExtent2D m_extent{};
std::vector<VkImage> m_images;
std::vector<VkImageView> m_image_views;
- VkRenderPass m_swapchain_rp = VK_NULL_HANDLE;
- VkRenderPass m_offscreen_rp = VK_NULL_HANDLE;
+ VkRenderPass m_swapchain_rp = VK_NULL_HANDLE; // also lives in m_pass_cache
+ std::unordered_map<uint64_t, VkRenderPass> m_pass_cache; // keyed by (color,depth,present)
std::vector<VkFramebuffer> m_framebuffers;
VkImage m_depth_image = VK_NULL_HANDLE; VkDeviceMemory m_depth_mem = VK_NULL_HANDLE; VkImageView m_depth_view = VK_NULL_HANDLE;
@@ -511,59 +522,66 @@ namespace Donut::RHI
return true;
}
- // Swapchain pass: colour + depth. Scene geometry uses the depth; the
- // black-hole present + ImGui simply don't test it.
- auto VulkanDevice::create_swapchain_render_pass() -> bool
+ // A render pass for one attachment signature, created once and cached.
+ // `present` targets (the swapchain) finish PRESENT_SRC and sync on the
+ // colour-output stage; `sampled` targets (off-screen) finish
+ // SHADER_READ_ONLY and round-trip through the fragment shader so the next
+ // pass can sample them. Depth (VK_FORMAT_UNDEFINED = none) is optional.
+ auto VulkanDevice::get_render_pass(VkFormat color, VkFormat depth, bool present) -> VkRenderPass
{
+ uint64_t key = (uint64_t)(uint32_t)color
+ | ((uint64_t)(uint32_t)depth << 24)
+ | ((uint64_t)(present ? 1 : 0) << 48);
+ auto it = m_pass_cache.find(key);
+ if (it != m_pass_cache.end()) return it->second;
+
+ const bool has_depth = depth != VK_FORMAT_UNDEFINED;
VkAttachmentDescription atts[2]{};
- atts[0].format = m_swapchain_format; atts[0].samples = VK_SAMPLE_COUNT_1_BIT;
+ atts[0].format = color; atts[0].samples = VK_SAMPLE_COUNT_1_BIT;
atts[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
atts[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- atts[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
- atts[1].format = VK_FORMAT_D32_SFLOAT; atts[1].samples = VK_SAMPLE_COUNT_1_BIT;
+ atts[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+ atts[0].finalLayout = present ? VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+ atts[1].format = depth; atts[1].samples = VK_SAMPLE_COUNT_1_BIT;
atts[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
atts[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
atts[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
VkAttachmentReference depth_ref{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL };
VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
- subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; subpass.pDepthStencilAttachment = &depth_ref;
- VkSubpassDependency dep{};
- dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0;
- dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
- dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
- dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
- VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
- rpci.attachmentCount = 2; rpci.pAttachments = atts; rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
- rpci.dependencyCount = 1; rpci.pDependencies = &dep;
- VKD_CHECK(vkCreateRenderPass(m_device, &rpci, nullptr, &m_swapchain_rp));
- return true;
- }
+ subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref;
+ if (has_depth) subpass.pDepthStencilAttachment = &depth_ref;
- // Off-screen colour pass (RGBA8), leaving the image SHADER_READ_ONLY so the
- // present pass can sample it. Shared by every render target.
- auto VulkanDevice::create_offscreen_render_pass() -> bool
- {
- VkAttachmentDescription color{};
- color.format = VK_FORMAT_R8G8B8A8_UNORM; color.samples = VK_SAMPLE_COUNT_1_BIT;
- color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
- color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
- VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
- VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
- subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref;
VkSubpassDependency deps[2]{};
- deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0;
- deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
- deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- deps[1].srcSubpass = 0; deps[1].dstSubpass = VK_SUBPASS_EXTERNAL;
- deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
+ uint32_t dep_count;
+ if (present)
+ {
+ deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0;
+ deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
+ deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
+ deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+ dep_count = 1;
+ }
+ else
+ {
+ deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0;
+ deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
+ deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+ deps[1].srcSubpass = 0; deps[1].dstSubpass = VK_SUBPASS_EXTERNAL;
+ deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+ deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
+ dep_count = 2;
+ }
+
VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
- rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
- rpci.dependencyCount = 2; rpci.pDependencies = deps;
- VKD_CHECK(vkCreateRenderPass(m_device, &rpci, nullptr, &m_offscreen_rp));
- return true;
+ rpci.attachmentCount = has_depth ? 2 : 1; rpci.pAttachments = atts;
+ rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
+ rpci.dependencyCount = dep_count; rpci.pDependencies = deps;
+ VkRenderPass rp = VK_NULL_HANDLE;
+ if (vkCreateRenderPass(m_device, &rpci, nullptr, &rp) != VK_SUCCESS)
+ { DONUT_ERROR("Vulkan RHI: render pass creation failed"); return VK_NULL_HANDLE; }
+ m_pass_cache[key] = rp;
+ return rp;
}
auto VulkanDevice::create_depth_and_framebuffers() -> bool
@@ -631,15 +649,16 @@ namespace Donut::RHI
return true;
}
- auto VulkanDevice::init(void* glfwWindow, int width, int height) -> bool
+ auto VulkanDevice::init(const NativeWindow& window) -> bool
{
- m_window = (GLFWwindow*)glfwWindow; m_width = width; m_height = height;
+ m_window = (GLFWwindow*)window.glfw_handle; m_width = window.width; m_height = window.height;
if (!create_instance()) return false;
if (!pick_physical_and_device()) return false;
if (!create_swapchain()) return false;
if (!create_image_views()) return false;
- if (!create_swapchain_render_pass()) return false;
- if (!create_offscreen_render_pass()) return false;
+ // The swapchain is a "present" target: its colour format + a D32 depth.
+ m_swapchain_rp = get_render_pass(m_swapchain_format, VK_FORMAT_D32_SFLOAT, true);
+ if (!m_swapchain_rp) return false;
if (!create_depth_and_framebuffers())return false;
if (!create_command_and_sync()) return false;
DONUT_INFO("Vulkan RHI device ready: {} swapchain images, {}x{}", (int)m_images.size(), m_extent.width, m_extent.height);
@@ -739,13 +758,13 @@ namespace Donut::RHI
return tex;
}
- auto VulkanDevice::create_render_target(int w, int h, Format color, bool /*with_depth*/, Filter filter, int /*mips*/) -> Ref<RenderTarget>
+ auto VulkanDevice::create_render_target(int w, int h, Format color, Format depth, Filter filter, int /*mips*/) -> Ref<RenderTarget>
{
auto rt = create_ref<VkRenderTargetR>();
rt->m_device = m_device; rt->m_w = w; rt->m_h = h;
- VkFormat fmt = vk_format(color);
+ VkFormat cfmt = (color == Format::Swapchain) ? m_swapchain_format : vk_format(color);
VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
- ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 };
+ ici.imageType = VK_IMAGE_TYPE_2D; ici.format = cfmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 };
ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT;
ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
vkCreateImage(m_device, &ici, nullptr, &rt->m_image);
@@ -755,15 +774,40 @@ namespace Donut::RHI
vkAllocateMemory(m_device, &ai, nullptr, &rt->m_mem);
vkBindImageMemory(m_device, rt->m_image, rt->m_mem, 0);
VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- vci.image = rt->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt;
+ vci.image = rt->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = cfmt;
vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
vkCreateImageView(m_device, &vci, nullptr, &rt->m_view);
VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
smci.magFilter = vk_filter(filter); smci.minFilter = vk_filter(filter);
smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
vkCreateSampler(m_device, &smci, nullptr, &rt->m_sampler);
+
+ // Optional depth attachment (for off-screen passes that need a depth test).
+ rt->m_has_depth = depth != Format::None;
+ VkFormat dfmt = VK_FORMAT_UNDEFINED;
+ if (rt->m_has_depth)
+ {
+ dfmt = vk_format(depth);
+ VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
+ dici.imageType = VK_IMAGE_TYPE_2D; dici.format = dfmt; dici.extent = { (uint32_t)w, (uint32_t)h, 1 };
+ dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT;
+ dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
+ vkCreateImage(m_device, &dici, nullptr, &rt->m_depth_image);
+ VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(m_device, rt->m_depth_image, &dreq);
+ VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
+ dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
+ vkAllocateMemory(m_device, &dai, nullptr, &rt->m_depth_mem);
+ vkBindImageMemory(m_device, rt->m_depth_image, rt->m_depth_mem, 0);
+ VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
+ dvci.image = rt->m_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = dfmt;
+ dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 };
+ vkCreateImageView(m_device, &dvci, nullptr, &rt->m_depth_view);
+ }
+
+ rt->m_pass = get_render_pass(cfmt, dfmt, false); // off-screen (sampled) target
+ VkImageView atts[2] = { rt->m_view, rt->m_depth_view };
VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
- fbci.renderPass = m_offscreen_rp; fbci.attachmentCount = 1; fbci.pAttachments = &rt->m_view;
+ fbci.renderPass = rt->m_pass; fbci.attachmentCount = rt->m_has_depth ? 2u : 1u; fbci.pAttachments = atts;
fbci.width = w; fbci.height = h; fbci.layers = 1;
vkCreateFramebuffer(m_device, &fbci, nullptr, &rt->m_fb);
@@ -1114,9 +1158,15 @@ namespace Donut::RHI
gpci.stageCount = 2; gpci.pStages = stages;
gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps;
gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.pDynamicState = &dsci;
- if (desc.has_depth) gpci.pDepthStencilState = &ds;
+ // Resolve the target's attachment signature to a (cached) render pass.
+ // Pipeline<->pass compatibility is by attachment format, so this is the
+ // same pass the matching swapchain / render target renders into.
+ VkFormat pcolor = (desc.target.color == Format::Swapchain) ? m_swapchain_format : vk_format(desc.target.color);
+ VkFormat pdepth = (desc.target.depth == Format::None) ? VK_FORMAT_UNDEFINED : vk_format(desc.target.depth);
+ bool present = desc.target.color == Format::Swapchain;
+ if (pdepth != VK_FORMAT_UNDEFINED) gpci.pDepthStencilState = &ds;
gpci.layout = p->m_layout;
- gpci.renderPass = desc.has_depth ? m_swapchain_rp : m_offscreen_rp;
+ gpci.renderPass = get_render_pass(pcolor, pdepth, present);
gpci.subpass = 0;
VkResult pr = vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpci, nullptr, &p->m_pipeline);
vkDestroyShaderModule(m_device, vmod, nullptr); vkDestroyShaderModule(m_device, fmod, nullptr);
@@ -1144,7 +1194,7 @@ namespace Donut::RHI
vkBeginCommandBuffer(cmd, &bi);
m_cmds.m_device = m_device; m_cmds.m_cmd = cmd;
- m_cmds.m_swapchain_rp = m_swapchain_rp; m_cmds.m_offscreen_rp = m_offscreen_rp;
+ m_cmds.m_swapchain_rp = m_swapchain_rp;
m_cmds.m_swapchain_fb = m_framebuffers[m_image_index]; m_cmds.m_extent = m_extent;
m_cmds.m_frame_pool = m_frame_pools[m_current_frame]; m_cmds.m_pipe = nullptr;
return &m_cmds;
@@ -1227,8 +1277,8 @@ namespace Donut::RHI
for (auto f : m_in_flight) vkDestroyFence(m_device, f, nullptr);
m_render_finished.clear(); m_image_available.clear(); m_in_flight.clear();
if (m_command_pool) vkDestroyCommandPool(m_device, m_command_pool, nullptr);
- if (m_offscreen_rp) vkDestroyRenderPass(m_device, m_offscreen_rp, nullptr);
- if (m_swapchain_rp) vkDestroyRenderPass(m_device, m_swapchain_rp, nullptr);
+ for (auto& [key, rp] : m_pass_cache) vkDestroyRenderPass(m_device, rp, nullptr);
+ m_pass_cache.clear(); m_swapchain_rp = VK_NULL_HANDLE;
cleanup_swapchain();
vkDestroyDevice(m_device, nullptr); m_device = VK_NULL_HANDLE;
if (m_surface) vkDestroySurfaceKHR(m_instance, m_surface, nullptr);
diff --git a/src/rendering/black_hole_renderer.cpp b/src/rendering/black_hole_renderer.cpp
index 46b002c..e4bc2a4 100644
--- a/src/rendering/black_hole_renderer.cpp
+++ b/src/rendering/black_hole_renderer.cpp
@@ -27,8 +27,8 @@ namespace Donut
{
m_device = &device;
- m_geo_lo = device.create_render_target(GEO_LO_W, GEO_LO_H, Format::RGBA8, false, Filter::Linear);
- m_geo_hi = device.create_render_target(GEO_HI_W, GEO_HI_H, Format::RGBA8, false, Filter::Linear);
+ m_geo_lo = device.create_render_target(GEO_LO_W, GEO_LO_H, Format::RGBA8, Format::None, Filter::Linear);
+ m_geo_hi = device.create_render_target(GEO_HI_W, GEO_HI_H, Format::RGBA8, Format::None, Filter::Linear);
// Fullscreen quad shared by the geodesic and present passes: pos.xy + uv.
const float quad[] = {
@@ -61,7 +61,7 @@ namespace Donut
{ ResourceKind::Texture, 4, "u_HDRIEnvironment" },
};
d.topology = Topology::Triangles;
- d.has_depth = false; // color-only off-screen target
+ d.target = { Format::RGBA8, Format::None }; // colour-only off-screen target
m_geo_pipeline = device.create_pipeline(d);
}
{
@@ -70,7 +70,7 @@ namespace Donut
d.vertex_layout = { 16, { { 0, 2, 0 }, { 1, 2, 8 } } };
d.resources = { { ResourceKind::Texture, 0, "u_ScreenTexture" } };
d.topology = Topology::Triangles;
- d.has_depth = true; // swapchain target carries depth (unused here)
+ d.target = { Format::Swapchain, Format::D32 }; // swapchain target (depth unused)
m_present_pipeline = device.create_pipeline(d);
}
diff --git a/src/rendering/rhi.h b/src/rendering/rhi.h
index 65f10b5..2110926 100644
--- a/src/rendering/rhi.h
+++ b/src/rendering/rhi.h
@@ -18,11 +18,33 @@ namespace Donut::RHI
{
enum class Format
{
- RGBA8, // 8-bit unorm colour (swapchain / LDR targets)
+ None, // no attachment (e.g. a target/pipeline with no depth)
+ Swapchain, // a pipeline's colour target = the presented image (resolved per backend)
+ RGBA8, // 8-bit unorm colour (LDR / off-screen)
RGBA16F, // half-float colour (HDR / cubemap)
D32, // 32-bit depth
};
+ // The attachment signature of a render pass / target: a colour format plus an
+ // optional depth format. A pipeline is compatible with any target sharing this
+ // signature, so this is what replaces "which render pass" as an explicit value.
+ struct RenderTargetFormat
+ {
+ Format color = Format::RGBA8;
+ Format depth = Format::None;
+ };
+
+ // The window the device presents into. GLFW is the windowing layer for every
+ // backend, so this carries the GLFWwindow*; each backend derives what it needs
+ // (a GL context, a Vulkan surface, later a CAMetalLayer / HWND) from it. The
+ // RHI names the concept "native window" rather than baking GLFW into its API.
+ struct NativeWindow
+ {
+ void* glfw_handle = nullptr;
+ int width = 0;
+ int height = 0;
+ };
+
enum class BufferType { Vertex, Index, Uniform };
enum class Topology { Triangles, Lines };
enum class CullMode { None, Back, Front };
@@ -68,8 +90,7 @@ namespace Donut::RHI
bool depth_test = false;
bool depth_write = false;
CompareOp depth_op = CompareOp::Less;
- Format color_format = Format::RGBA8; // format of the target it renders into
- bool has_depth = false; // target has a depth attachment
+ RenderTargetFormat target; // attachment signature of the pass it draws into
};
// Records draws for one frame. Obtained from Device::begin_frame (targets the
@@ -102,8 +123,7 @@ namespace Donut::RHI
public:
virtual ~Device() = default;
- // glfwWindow: opaque GLFW handle (GL context for OpenGL, NO_API otherwise).
- virtual auto init(void* glfwWindow, int width, int height) -> bool = 0;
+ virtual auto init(const NativeWindow& window) -> bool = 0;
virtual auto shutdown() -> void = 0;
virtual auto resize(int width, int height) -> void = 0;
virtual auto wait_idle() -> void = 0;
@@ -111,7 +131,7 @@ namespace Donut::RHI
virtual auto create_buffer(BufferType type, size_t size, const void* data = nullptr) -> Ref<Buffer> = 0;
virtual auto create_texture(int width, int height, Format format, Filter filter, const void* data = nullptr) -> Ref<Texture> = 0;
virtual auto create_cubemap_from_hdri(const std::string& equirect_path) -> Ref<Texture> = 0;
- virtual auto create_render_target(int width, int height, Format color, bool with_depth,
+ virtual auto create_render_target(int width, int height, Format color, Format depth = Format::None,
Filter sample_filter = Filter::Linear, int mip_levels = 1) -> Ref<RenderTarget> = 0;
virtual auto create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> = 0;
diff --git a/src/rendering/scene_renderer.cpp b/src/rendering/scene_renderer.cpp
index e276074..5ee7657 100644
--- a/src/rendering/scene_renderer.cpp
+++ b/src/rendering/scene_renderer.cpp
@@ -71,7 +71,7 @@ namespace Donut
d.topology = Topology::Lines;
d.blend = BlendMode::AlphaBlend;
d.depth_test = true; d.depth_write = false; d.depth_op = CompareOp::LessEqual;
- d.has_depth = true;
+ d.target = { Format::Swapchain, Format::D32 };
m_grid_pipeline = device.create_pipeline(d);
}
@@ -108,7 +108,7 @@ namespace Donut
{ ResourceKind::Texture, 1, "u_HDRIEnvironment" } };
d.topology = Topology::Triangles;
d.depth_test = true; d.depth_write = true; d.depth_op = CompareOp::Less;
- d.has_depth = true;
+ d.target = { Format::Swapchain, Format::D32 };
m_sphere_pipeline = device.create_pipeline(d);
}
@@ -133,7 +133,7 @@ namespace Donut
{ ResourceKind::Texture, 1, "u_Skybox" } };
d.topology = Topology::Triangles;
d.depth_test = false; d.depth_write = false;
- d.has_depth = true;
+ d.target = { Format::Swapchain, Format::D32 };
m_skybox_pipeline = device.create_pipeline(d);
}