I am building a graphics renderer using Vulkan in C++. I am facing an issue where I have implemented subpass rendering, but nothing is being rendered to the screen. No validation errors are being recorded. Can you help diagnose the issue?

Here is some code to assist:

void Renderer::CreateRenderPass()
{
    vk::AttachmentDescription positionAttachment{};
    positionAttachment.format = vk::Format::eR16G16B16A16Sfloat; // High precision format
    positionAttachment.samples = vk::SampleCountFlagBits::e1;
    positionAttachment.loadOp = vk::AttachmentLoadOp::eClear;
    positionAttachment.storeOp = vk::AttachmentStoreOp::eStore;
    positionAttachment.stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
    positionAttachment.stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
    positionAttachment.initialLayout = vk::ImageLayout::eUndefined;
    positionAttachment.finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal;

    vk::AttachmentDescription normalAttachment = positionAttachment;
    normalAttachment.format = vk::Format::eR16G16B16A16Sfloat;

    vk::AttachmentDescription albedoAttachment = positionAttachment;
    albedoAttachment.format = swapChainImageFormat; // Assuming albedo might use swap chain format

    vk::AttachmentDescription depthAttachment = positionAttachment;
    depthAttachment.format = FindDepthFormat(*GetPhysicalDevice());
    depthAttachment.samples = msaaSamples;
    depthAttachment.finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal;

    vk::AttachmentDescription colorAttachmentResolve{};
    colorAttachmentResolve.format = swapChainImageFormat;
    colorAttachmentResolve.samples = vk::SampleCountFlagBits::e1;
    colorAttachmentResolve.loadOp = vk::AttachmentLoadOp::eDontCare; // Shouldn’t clear resolve attachment
    colorAttachmentResolve.storeOp = vk::AttachmentStoreOp::eStore;
    colorAttachmentResolve.stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
    colorAttachmentResolve.stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
    colorAttachmentResolve.initialLayout = vk::ImageLayout::eUndefined;
    colorAttachmentResolve.finalLayout = vk::ImageLayout::ePresentSrcKHR;

    std::array<vk::AttachmentDescription, 5> attachments = {
        positionAttachment, normalAttachment, albedoAttachment, depthAttachment, colorAttachmentResolve
    };

    vk::AttachmentReference positionAttachmentRef{};
    positionAttachmentRef.attachment = 0;
    positionAttachmentRef.layout = vk::ImageLayout::eColorAttachmentOptimal;

    vk::AttachmentReference normalAttachmentRef{};
    normalAttachmentRef.attachment = 1;
    normalAttachmentRef.layout = vk::ImageLayout::eColorAttachmentOptimal;

    vk::AttachmentReference albedoAttachmentRef{};
    albedoAttachmentRef.attachment = 2;
    albedoAttachmentRef.layout = vk::ImageLayout::eColorAttachmentOptimal;

    vk::AttachmentReference depthAttachmentRef{};
    depthAttachmentRef.attachment = 3;
    depthAttachmentRef.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal;

    vk::AttachmentReference colorAttachmentResolveRef{};
    colorAttachmentResolveRef.attachment = 4;
    colorAttachmentResolveRef.layout = vk::ImageLayout::eColorAttachmentOptimal;

    std::array<vk::AttachmentReference, 3> colorAttachments = {
        positionAttachmentRef, normalAttachmentRef, albedoAttachmentRef
    };

    // Input attachments for the lighting pass
    std::array<vk::AttachmentReference, 3> inputAttachments = {
        vk::AttachmentReference{0, vk::ImageLayout::eShaderReadOnlyOptimal},
        vk::AttachmentReference{1, vk::ImageLayout::eShaderReadOnlyOptimal},
        vk::AttachmentReference{2, vk::ImageLayout::eShaderReadOnlyOptimal}
    };

    // Geometry pass subpass: writes to color and depth attachments
    vk::SubpassDescription geometryPass{};
    geometryPass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
    geometryPass.colorAttachmentCount = static_cast<uint32_t>(colorAttachments.size());
    geometryPass.pColorAttachments = colorAttachments.data();
    geometryPass.pDepthStencilAttachment = &depthAttachmentRef;

    // Lighting pass subpass: reads from input attachments
    vk::AttachmentReference colorAttachmentFinalRef{};
    colorAttachmentFinalRef.attachment = 4;
    colorAttachmentFinalRef.layout = vk::ImageLayout::eColorAttachmentOptimal;

    vk::SubpassDescription lightingPass{};
    lightingPass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
    lightingPass.inputAttachmentCount = static_cast<uint32_t>(inputAttachments.size());
    lightingPass.pInputAttachments = inputAttachments.data();
    lightingPass.colorAttachmentCount = 1;
    lightingPass.pColorAttachments = &colorAttachmentFinalRef;

    std::array<vk::SubpassDescription, 2> subpasses = { geometryPass, lightingPass };

    // Define subpass dependencies
    vk::SubpassDependency externalToGeometryDependency{};
    externalToGeometryDependency.srcSubpass = VK_SUBPASS_EXTERNAL;
    externalToGeometryDependency.dstSubpass = 0;
    externalToGeometryDependency.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
    externalToGeometryDependency.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
    externalToGeometryDependency.srcAccessMask = {};
    externalToGeometryDependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;

    vk::SubpassDependency geometryToLightingDependency{};
    geometryToLightingDependency.srcSubpass = 0;
    geometryToLightingDependency.dstSubpass = 1;
    geometryToLightingDependency.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
    geometryToLightingDependency.dstStageMask = vk::PipelineStageFlagBits::eFragmentShader;
    geometryToLightingDependency.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
    geometryToLightingDependency.dstAccessMask = vk::AccessFlagBits::eShaderRead;
    geometryToLightingDependency.dependencyFlags = vk::DependencyFlagBits::eByRegion;

    vk::SubpassDependency geometryToLightingDepthDependency{};
    geometryToLightingDepthDependency.srcSubpass = 0;
    geometryToLightingDepthDependency.dstSubpass = 1;
    geometryToLightingDepthDependency.srcStageMask = vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests;
    geometryToLightingDepthDependency.dstStageMask = vk::PipelineStageFlagBits::eFragmentShader;
    geometryToLightingDepthDependency.srcAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentWrite;
    geometryToLightingDepthDependency.dstAccessMask = vk::AccessFlagBits::eShaderRead;
    geometryToLightingDepthDependency.dependencyFlags = vk::DependencyFlagBits::eByRegion;

    std::array<vk::SubpassDependency, 3> dependencies = {
        externalToGeometryDependency,
        geometryToLightingDependency,
        geometryToLightingDepthDependency
    };

    vk::RenderPassCreateInfo renderPassInfo{};
    renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
    renderPassInfo.pAttachments = attachments.data();
    renderPassInfo.subpassCount = static_cast<uint32_t>(subpasses.size());
    renderPassInfo.pSubpasses = subpasses.data();
    renderPassInfo.dependencyCount = static_cast<uint32_t>(dependencies.size());
    renderPassInfo.pDependencies = dependencies.data();

    if (device.createRenderPass(&renderPassInfo, nullptr, &renderPass) != vk::Result::eSuccess)
    {
        throw std::runtime_error("Failed to create render pass.");
    }
}

void Engine::Render()
{
	renderer.BeginFrame();
	RenderTest();
	renderer.EndFrame();
}

void Engine::RenderTest() {

	VkDevice device = *renderer.GetDevice();

	//glm::mat4 modelMatrix;
	glm::vec3 position = glm::vec3(0.0f, 0.0f, 10.0f);
	glm::vec3 rotation = glm::vec3(0.0f, 0.0f, 0.0f);
	glm::vec3 scale = glm::vec3(3.0f);

	glm::vec3 position2 = glm::vec3(0.0f, 5.0f, 0.0f);

	// Bind mesh vertex and index buffers
	VkBuffer vertexBuffers[] = { meshes[0].get()->GetVertexBuffer() };
	VkDeviceSize offsets[] = { 0 };
	vkCmdBindVertexBuffers(*renderer.GetCurrentCommandBuffer(), 0, 1, vertexBuffers, offsets);
	vkCmdBindIndexBuffer(*renderer.GetCurrentCommandBuffer(), meshes[0].get()->GetIndexBuffer(), 0, VK_INDEX_TYPE_UINT32);


	glm::mat4 modelMatrix[] = { glm::mat4(1.0f),glm::mat4(1.0f) };
	modelMatrix[0] = glm::translate(modelMatrix[0], position2);



	modelcount = 2;

	AmbientLight ambientUBO = {};

	ambientUBO.color = { 1.0, 1.0, 1.0 };
	ambientUBO.intensity = 0.1;

	DirectionalLight directionalUBO = {};

	directionalUBO.position = { 1.0, 1.0, 1.0 };
	directionalUBO.color = { 1.0, 1.0, 1.0 };
	directionalUBO.intensity = 1.0;

	BufferUtils::UpdateBuffer(device, modelMatrixMemory, sizeof(glm::mat4) * modelcount, modelMatrix);
	BufferUtils::UpdateBuffer(device, viewMatrixMemory, sizeof(glm::mat4), &camera.GetViewMatrix());
	BufferUtils::UpdateBuffer(device, projMatrixMemory, sizeof(glm::mat4), &camera.GetProjectionMatrix());
	BufferUtils::UpdateBuffer(device, modelCountMemory, sizeof(int), &modelcount);
	BufferUtils::UpdateBuffer(device, ambientLightMemory, sizeof(AmbientLight), &ambientUBO);
	BufferUtils::UpdateBuffer(device, directionalLightMemory, sizeof(DirectionalLight), &directionalUBO);


	vkDeviceWaitIdle(device);

	// Geometry Pass

	VkDescriptorSet geomSet = this->geomdescriptorSet.get();
	vkCmdBindPipeline(*renderer.GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.GetPipeline(0).get()->GetPipeline());
	vkCmdBindDescriptorSets(*renderer.GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, geompipelineLayout, 0, 1, &geomSet, 0, nullptr);

	uint32_t numIndices4 = static_cast<uint32_t>(meshes[0].get()->GetIndices().size());
	vkCmdDrawIndexed(*renderer.GetCurrentCommandBuffer(), numIndices4, modelcount, 0, 0, 0);

	//// Transition to Lighting Pass
	 
	renderer.GetCurrentCommandBuffer()->nextSubpass(vk::SubpassContents::eInline);

	//// Lighting Pass

	VkDescriptorSet lightSet = this->lightdescriptorSet.get();
	vkCmdBindPipeline(*renderer.GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.GetPipeline(1).get()->GetPipeline());
	vkCmdBindDescriptorSets(*renderer.GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, lightpipelineLayout, 0, 1, &lightSet, 0, nullptr);

	// Draw fullscreen quad
	renderer.GetCurrentCommandBuffer()->draw(3, 1, 0, 0);

}

void Renderer::BeginFrame()
{
    // Wait for any previous work on this swapchain image to complete
    device.waitForFences(1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
    device.resetFences(1, &inFlightFences[currentFrame]);

    // Acquire an image from the swapchain, then begin recording commands for the current frame.
    vk::Result acquireResult = device.acquireNextImageKHR(swapchain, UINT64_MAX, imageAvailableSemaphores[currentFrame], vk::Fence(), &imageIndex);

    if (acquireResult != vk::Result::eSuccess && acquireResult != vk::Result::eSuboptimalKHR) {
        throw std::runtime_error("Failed to acquire next swapchain image.");
    }

    vk::CommandBufferBeginInfo beginInfo{};
    beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;

    currentCmdBufferIndex = (currentCmdBufferIndex + 1) % 2;
    currentCommandBuffer = commandBuffers[currentFrame * 2 + currentCmdBufferIndex];

    currentCommandBuffer.begin(beginInfo);

    vk::RenderPassBeginInfo renderPassInfo{};
    renderPassInfo.renderPass = renderPass;
    renderPassInfo.framebuffer = framebuffers[imageIndex];
    renderPassInfo.renderArea.offset = vk::Offset2D{ 0, 0 };
    renderPassInfo.renderArea.extent = swapChainExtent;

    // Set the clear color to black
    vk::ClearValue clearColor = { vk::ClearColorValue(std::array<float, 4>{0.0f, 0.0f, 0.0f, 1.0f}) };
    vk::ClearValue clearDepth = { vk::ClearDepthStencilValue(1.0f, 0) }; // Specify the depth clear value here

    //std::array<vk::ClearValue, 3> clearValues = { clearColor, clearDepth, clearColor }; // Add the clearDepth value to the array
    std::array<vk::ClearValue, 5> clearValues = { clearColor, clearColor, clearColor, clearDepth, clearColor }; // Add the clearDepth value to the array

    renderPassInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
    renderPassInfo.pClearValues = clearValues.data(); // Pass the clearValues array to pClearValues

    currentCommandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline);
}

void Renderer::EndFrame()
{
    currentCommandBuffer.endRenderPass();

    vk::SubmitInfo submitInfo{};

    vk::PipelineStageFlags waitStages[] = { vk::PipelineStageFlagBits::eColorAttachmentOutput };
    submitInfo.waitSemaphoreCount = 1;
    submitInfo.pWaitSemaphores = &imageAvailableSemaphores[currentFrame];
    submitInfo.pWaitDstStageMask = waitStages;

    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &currentCommandBuffer;

    submitInfo.signalSemaphoreCount = 1;
    submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentFrame];

    currentCommandBuffer.end();
    graphicsQueue.submit(1, &submitInfo, inFlightFences[currentFrame]);

    vk::PresentInfoKHR presentInfo{};
    presentInfo.waitSemaphoreCount = 1;
    presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentFrame];

    vk::SwapchainKHR swapChains[] = { swapchain };
    presentInfo.swapchainCount = 1;
    presentInfo.pSwapchains = swapChains;
    presentInfo.pImageIndices = &imageIndex;

    vk::Result queuePresentResult = presentQueue.presentKHR(presentInfo);

    if (queuePresentResult == vk::Result::eErrorOutOfDateKHR || queuePresentResult == vk::Result::eSuboptimalKHR) {
        // Handle swapchain recreation if needed, e.g., due to resizing the window or other swapchain properties changes
    }
    else if (queuePresentResult != vk::Result::eSuccess) {
        throw std::runtime_error("Failed to present the swapchain image.");
    }

    currentFrame = (currentFrame + 1) % kMaxFramesInFlight;
}