Understaing per-object resources

I am trying to render a scene with Vulkan consisting of different meshes with different transforms. The objects can be added and removed from scene. I am trying what vulkan objects must be used as “global”, and which must be created for rendering for each object. In OpenGL with prepared state I should perform simply bind vertex buffer + drawCall for each object, having the same shader, material bindings etc.
Lets assume each object uses the same shader with the same vbo layout. What it “top” of the objects hierarchy I should create/modify for each additional object?
Firstly, I thought it is also vertex buffer + per-object uniform buffers (like translation, color). But then I found, that pipeline (which I assumed was global) uses VkPipelineVertexInputStateCreateInfo, which is per-object. Does that mean, that I should create separate pipeline for each drawble object?

The vertex input state does not bind any data and as such is not per-object (as in “data”). It only describes the attribute binding points layout. So you can use a pipeline created with that vertex input state for all meshes that share the same attribute descriptions. If you have e.g. two different attribute layouts for a dozen of meshes you’d still only need two pipelines for a basic setup.

Pipelines are meant to be detached from your actual object, so when thinking about how to setup and create your pipelines don’t think about the actual data of the object you’re rendering.

This is similar to descriptor set layouts and actual descriptor sets, with the former describing a layout and the latter one pointing to actual data, giving you a 1…n relation of a number of descriptor sets that actually share the descriptor set layout (and pipeline layout).

1 Like

But then I found, that pipeline (which I assumed was global) uses VkPipelineVertexInputStateCreateInfo, which is per-object.

OK, since you’re familiar with OpenGL, I’ll explain this in OpenGL terminology.

VkPipelineVertexInputStateCreateInfo == Vertex Array Object. This object describes the state you would specify with glVertexAttrib[I/L]Format, glVertexAttribBinding, and glVertexBindingDivisor. It’s everything that goes into a VAO except glVertexBufferBinding.

It has nothing at all to do with uniforms. So stop thinking that it does.

Does that mean, that I should create separate pipeline for each drawble object?

Would you set up a different VAO for each object? Or would you force your objects to use the same vertex format, then bind different buffers based on what object you want to read?

The goal in Vulkan is to minimize state changes. To make you do that, Vulkan’s API gives you a single state object, which is monolithic. Changing it is big and painful, so you’ll want to avoid doing that as much as possible.

You’re supposed to design your system so that you change the pipeline relatively rarely. Ideally, the number of pipeline changes you make would not be based on how many objects you’re rendering. That is, you’d have a pipeline for skinned objects, one for unskinned objects, lighting passes, tone mapping, SSAO, and so forth.

1 Like

Thanks for the answers! I’ve created vk_pipeline_guard, containing VkPipelineVertexInputStateCreateInfo, and each mesh is hidden in vk_geometry_draw_task, which contains the vertex buffer with forced layout for generation (now it is simple pos + norm), and per-object uniform object buffer. My renderer has std::vector<vk_geometry_draw_task*>, which is iterated inside:

for (auto task : drawTasks)
{
vkCmdBindDescriptorSets(context->draw_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, context->pipeline_layout, 0, 1, &task->desc_set, 0, NULL);
vkCmdBindVertexBuffers(context->draw_cmd, VERTEX_BUFFER_BIND_ID, 1, &task->vertices.buf, offsets);
vkCmdDraw(context->draw_cmd, task->vertexCount, 1, 0, 0);
}

So I was able to render normals of my scene with all the transforms and geometry correct.

Now I am trying to perform a much further step and do it offscreen (preparation for deferred shading). So I am trying to understand offscreen rendering vulkan concept, basing on the sample https://github.com/SaschaWillems/Vulkan/blob/master/offscreen/offscreen.cpp .

Referring to openGL, it is very simple - create image, create framebuffer, attach images. Then render framebuffer - render whatever to currently bound framebuffer. First, I’ve already understood, that texture here consists of 3 subentities (image, imageview, sampler).

But why while initialising framebuffer we need render pass in the VkFramebufferCreateInfo::renderPass? Why should the framebuffer care what will I use it for?

The render pass (and sub passes) does not determine what the frame buffer is actually used for. It describes what render passes the frame buffer is actually compatible with. Rendering anything in Vulkan is always tied to a render pass.

You can find a detailed overview on render passes (and frame buffers) in chapter 7 of the Vulkan specification.

OK, now I got it, VkRenderPassBeginInfo is referring to the renderPass and framebuffer, which this pass will be executed to. Now I’ve created offscreen framebuffer, and executed my renderPass into it instead of swapchain FB. By the way, Sascha Willems, I am using your repo with examples as a base.

Now I am trying to understand, is it really rendered to offscreen FB. In openGL I’ve created texture_guard, which has the method save_to_png - it read image memory into RAM, and then encoded it with libpng into file. I am trying to implement the same feature in vulkan, but facing difficulties.

  1. I am creating attachment image with this code with following allocating and binding memory:

VkFormat fbColorFormat = VK_FORMAT_B8G8R8A8_UNORM;

VkImageCreateInfo image = {};
image.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
image.pNext = NULL;
image.imageType = VK_IMAGE_TYPE_2D;
image.format = fbColorFormat;
image.extent.width = context->offScreenFrameBuf.width;
image.extent.height = context->offScreenFrameBuf.height;
image.extent.depth = 1;
image.mipLevels = 1;
image.arrayLayers = 1;
image.samples = VK_SAMPLE_COUNT_1_BIT;
image.tiling = VK_IMAGE_TILING_OPTIMAL;
image.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;

  1. I am rendering scene to the offscreen FB by changing renderPassBegin:

VkRenderPassBeginInfo rp_begin = {};
rp_begin.framebuffer = context->offScreenFrameBuf.frameBuffer;

  1. I am trying to map attachment memory and read it, assuming that its size is 4 * width * height (RGBA, 1byte/channel).

USImage bmp(context->width, context->height);
auto img = context->offScreenFrameBuf.color;

void *pData;
auto err = vkMapMemory(context->device, img.mem, 0, bmp.width() * bmp.height() * 4, 0, (void **)&pData);
assert(!err);
memcpy(pData, bmp.data(), bmp.width() * bmp.height() * 4);
vkUnmapMemory(context->device, img.mem);

bmp.saveToPNG(“tmp.png”);

  1. Assertion fails with error VK_ERROR_OUT_OF_DEVICE_MEMORY (-2). Its size is 1024*768. What am I doing wrong?

Your framebuffer image is located on the device at stored in an optimal tiled format that differs between the implementations. It’s not possible to directly copy from these images.

  1. I am trying to map attachment memory and read it, assuming that its size is 4 * width * height (RGBA, 1byte/channel).

That’s not correct for optimal tiled images. Optimal tiled means that the image is stored in a vendor specific (internal and opaque) format, so size, etc. vary depending on the GPU you’re running on. But you’re not gonna read back from an optimal tiled image anyway.

Doing stuff “like” in OpenGL is usually not going to work in Vulkan. You gotta think more about how a GPU actually works as you’re going to explicitly do all the stuff that OpenGL “magically” did for you.

So if you want to read back your framebuffer you have to do “inverse” staging:

  • Set the correct usage flags to prepare your frame buffer for a transfer (TRANSFER_SRC)
  • Create a host visible staging buffer (or image, whatever you prefer, though image should be the easier way) to store your framebuffer data with TRANSFER_DST usage
  • Insert a proper barrier to ensure that all writes to the framebuffer color attachment have been finished before you start reading from it and transfers your image to the proper layout (VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
  • Use vkCmdCopyImageToBuffer to copy the framebuffer data to your local staging buffer (or vkCmdBlitImage if you use a linear image as the target)
  • Do a memcpy from that host visible buffer
  • Don’t forget to transform the layout back to COLOR_ATTACHMENT

That’s it in a nutshell. Thinking of it I should maybe add an example that does some kind of read back.

Am I correct, that I will need 2 images even for offscreen buffer: one is color attachment (it is optimally stored in GPU and we can do nothing with its layout) and the second is temporary with “readable” layout, where we will transfer the attachment data?

Only if you wan’t to copy to a linear image. IMO using a host visible buffer as a temporary target for the read back along with vkCmdCopyImageToBuffer is the better solution. And that’s pretty much just the inverted way of e.g. uploading texture data and as such easy to implement, so I’d go with that. But yes, either way you’ll always need something that is host visible if you want to read back the data from the device. This is something that was also done in OpenGL but hidden away from you by the API.

Also note that if you do lots of read backs (or async ones) you can go with a dedicated transfer queue.

Yes, I understand that this operation is not runtime-usable, but it is a simple debug/testing feature. Now I am going step-by-step with you algorithm, because I want to understand it, and has faced one more misunderstanding.
Setting image layout and copying requiers command buffer. So can I create command buffer in the beginning, write there all the operations: setting fb-attachment layout, setting destination linear image layout, copying, waiting barrier, restoring attachment layout, and then flush the buffer in the end? Or should I flush the buffer to the queue in the end of each step?

Pretty much yes. You can e.g. create a separate primary command once at the start of the application that only contains the transitions and copy commands. Then if you want to copy the framebuffer, just submit that command buffer to the queue after you’re finished with your offscreen rendering. And don’t forget to sync. If it’s for debugging only you may use a vkDeviceWaitIdle to make sure everything is finished, but it’s generally a better idea to sync the rendering and copying using sync objects like semaphores or some other barriers. Note that this is one way you could do it, though there are other alternatives. But if you’re starting this should be the easiest way.

It works! I’ve written some wrappers for command buffer and queue. Code is trivial for you, but maybe it will help somebody, here it is:

USImage bmp(context->width, context->height);

texture_object staging_texture;

VkImageCreateInfo image_create_info = {};
image_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
image_create_info.pNext = NULL;
image_create_info.imageType = VK_IMAGE_TYPE_2D;
image_create_info.format = VK_FORMAT_R8G8B8A8_UNORM;
image_create_info.extent = { context-&gt;width, context-&gt;height, 1 };
image_create_info.mipLevels = 1;
image_create_info.arrayLayers = 1;
image_create_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_create_info.tiling = VK_IMAGE_TILING_LINEAR;
image_create_info.usage = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
image_create_info.flags = 0;
image_create_info.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;

VkMemoryAllocateInfo mem_alloc = {};
mem_alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
mem_alloc.pNext = NULL;
mem_alloc.allocationSize = 0;
mem_alloc.memoryTypeIndex = 0;

VkMemoryRequirements mem_reqs;

auto err = vkCreateImage(context-&gt;device, &image_create_info, NULL, &staging_texture.image);
assert(!err);

vkGetImageMemoryRequirements(context-&gt;device, staging_texture.image, &mem_reqs);

mem_alloc.allocationSize = mem_reqs.size;
bool pass = memory_type_from_properties(context, mem_reqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, &mem_alloc.memoryTypeIndex);
assert(pass);

err = vkAllocateMemory(context-&gt;device, &mem_alloc, NULL, &staging_texture.mem);
assert(!err);

err = vkBindImageMemory(context-&gt;device, staging_texture.image, staging_texture.mem, 0);
assert(!err);

vk_command_buffer cbuf(context);
cbuf.begin();

auto att_img = context-&gt;offScreenFrameBuf.color;

//Make attachment readable
{
	VkImageMemoryBarrier image_memory_barrier = {};
	image_memory_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
	image_memory_barrier.pNext = NULL;
	image_memory_barrier.srcAccessMask = 0; //VK_ACCESS_INPUT_ATTACHMENT_READ_BIT?
	image_memory_barrier.dstAccessMask = 0;
	image_memory_barrier.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
	image_memory_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
	image_memory_barrier.image = att_img.image;
	image_memory_barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };

	vkCmdPipelineBarrier(cbuf.cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, 0, NULL, 0, NULL, 1, &image_memory_barrier);
}

VkImageCopy copy_region = {};
copy_region.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
copy_region.srcOffset = { 0, 0, 0 };
copy_region.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
copy_region.dstOffset = { 0, 0, 0 };
copy_region.extent = { context-&gt;width, context-&gt;height, 1 };

vkCmdCopyImage(cbuf.cmdBuf, att_img.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging_texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy_region);

//Restore attachment
{
	VkImageMemoryBarrier image_memory_barrier = {};
	image_memory_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
	image_memory_barrier.pNext = NULL;
	image_memory_barrier.srcAccessMask = 0;
	image_memory_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INPUT_ATTACHMENT_READ_BIT;
	image_memory_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
	image_memory_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
	image_memory_barrier.image = att_img.image;
	image_memory_barrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };

	vkCmdPipelineBarrier(cbuf.cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, 0, NULL, 0, NULL, 1, &image_memory_barrier);
}

cbuf.end(); 
context-&gt;queue-&gt;submit(&cbuf);
context-&gt;queue-&gt;wait();

unsigned char *data;
err = vkMapMemory(context-&gt;device, staging_texture.mem, 0, mem_alloc.allocationSize, 0, (void**)&data);
assert(!err);
//memset(data, 1, mem_alloc.allocationSize);

memcpy(bmp.data(), data, bmp.width() * bmp.height() * 4);
vkUnmapMemory(context-&gt;device, staging_texture.mem);

bmp.saveToPNG("tmp.png");

vkDestroyImage(context-&gt;device, staging_texture.image, NULL);
vkFreeMemory(context-&gt;device, staging_texture.mem, NULL);

the usage member is set incorrectly. It’s a flag field of VkImageUsageFlagBits while you set it to a layout. You want VK_IMAGE_USAGE_TRANSFER_DST_BIT for that. (layout TRANSFER_DST is 7 so it included the correct bit by chance)

The barrier is also incorrect, srcAccessMask should be COLOR_ATTACHMENT_WRITE and old layout should be the finalLayout as defined in the renderpass (probably COLOR_ATTACHMENT_OPTIMAL)

The stage mask in that first barrier should be from COLOR_ATTACHMENT_OUTPUT to TRANSFER.

After the copy the destination image should also get a image barrier, with srcAccessMask of TRANSFER_WRITE and srcLayout of TRANSFER_DST and a dstAccessMask of HOST_READ dstLayout of GENERAL with stage flags from TRANSFER to HOST

Ok… carefully fixed your remarks, it still works :slight_smile: Also changed barrier namings and added one more barrier.

//Make attachment readable
VkImageMemoryBarrier fb_set_readable_barrier = {};
fb_set_readable_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
fb_set_readable_barrier.pNext = NULL;
fb_set_readable_barrier.srcAccessMask = 0;
fb_set_readable_barrier.dstAccessMask = 0;
fb_set_readable_barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
fb_set_readable_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
fb_set_readable_barrier.image = att_img.image;
fb_set_readable_barrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };

vkCmdPipelineBarrier(cbuf.cmdBuf, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 1, &fb_set_readable_barrier);

//Copy framebuffer-&gt;staging image
VkImageCopy copy_region = {};
copy_region.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
copy_region.srcOffset = { 0, 0, 0 };
copy_region.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
copy_region.dstOffset = { 0, 0, 0 };
copy_region.extent = { context-&gt;width, context-&gt;height, 1 };

vkCmdCopyImage(cbuf.cmdBuf, att_img.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging_texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy_region);

//Wait copy to be finished
VkImageMemoryBarrier copy_finished_barrier = {};
copy_finished_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
copy_finished_barrier.pNext = NULL;
copy_finished_barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
copy_finished_barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
copy_finished_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
copy_finished_barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
copy_finished_barrier.image = staging_texture.image;
copy_finished_barrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };

vkCmdPipelineBarrier(cbuf.cmdBuf, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, NULL, 0, NULL, 1, &copy_finished_barrier);

//Restore attachment
VkImageMemoryBarrier fb_restore_barrier = {};
fb_restore_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
fb_restore_barrier.pNext = NULL;
fb_restore_barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
fb_restore_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INPUT_ATTACHMENT_READ_BIT;
fb_restore_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
fb_restore_barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
fb_restore_barrier.image = att_img.image;
fb_restore_barrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };

vkCmdPipelineBarrier(cbuf.cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, 0, NULL, 0, NULL, 1, &fb_restore_barrier);

The accessmasks in the fb_set_readable_barrier are still wrong.

As are the stage masks for the fb_restore_barrier (should be STAGE_TRANSFER to COLOR_ATTACHMENT_OUTPUT)

That’s right?

fb_set_readable_barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INPUT_ATTACHMENT_READ_BIT;
fb_set_readable_barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

vkCmdPipelineBarrier(cbuf.cmdBuf, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, NULL, 0, NULL, 1, &fb_restore_barrier);

I am trying to perform next step - render to MRT, is performed in https://github.com/SaschaWillems/Vulkan/tree/master/deferred and facing another difficulties.
I have a framebuffer with 1 color and 1 depth attachment. I am trying to add one more color attachment.

  1. Firstly I modify frag shader:

layout (location = 0) out vec4 uWorldPos;
layout (location = 1) out vec4 uDiffColor;

  1. Next, while forming framebuffer, I insert to 0 new element (it has also VK_FORMAT_B8G8R8A8_UNORM, and with simple replacement everything works). This also borns a subquestion - why I can not make depth attachment with 0-index?

VkImageView attachments[3];
attachments[0] = position.view;
attachments[1] = diffuse.view;
attachments[2] = depth.view;

fbufCreateInfo.attachmentCount = 3;

  1. In the renderPass creation, I also insert one element, creating 2 color-references:

VkAttachmentDescription attachments[3];
attachments[0].format = VK_FORMAT_B8G8R8A8_UNORM;
attachments[0].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

attachments[1].format = VK_FORMAT_B8G8R8A8_UNORM;
attachments[1].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

attachments[2].format = VK_FORMAT_D16_UNORM;
attachments[2].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[2].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[2].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[2].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[2].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[2].initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
attachments[2].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;


VkAttachmentReference color_references[2];
memset(color_references, 0, sizeof(color_references));

color_references[0].attachment = 0;
color_references[0].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

color_references[1].attachment = 1;
color_references[1].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

VkAttachmentReference depth_reference = {};
depth_reference.attachment = 2;
depth_reference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.flags = 0;
subpass.inputAttachmentCount = 0;
subpass.pInputAttachments = NULL;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = color_references;
subpass.pResolveAttachments = NULL;
subpass.pDepthStencilAttachment = &depth_reference;
subpass.preserveAttachmentCount = 0;
subpass.pPreserveAttachments = NULL;

VkRenderPassCreateInfo rp_info = {};
rp_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rp_info.pNext = NULL;
rp_info.attachmentCount = 3;
rp_info.pAttachments = attachments;
rp_info.subpassCount = 1;
rp_info.pSubpasses = &subpass;
rp_info.dependencyCount = 0;
rp_info.pDependencies = NULL;
  1. Now, when I am trying to read position texture (0 index), I am getting gray image (214, 214, 214). When diffuse - it is complete empty. Maybe there is one more place, where I should modify something?

It’s very hard to judge what’s wrong with just a code excerpt, but this one at least is wrong:

VkSubpassDescription subpass = {};
...
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = color_references;

You have two color_references, but set count of the subpass to one so the (only) sub pass you have never actually writes to the second color attachment.

If that doesn’t help, please check with validation layers enabled and upload working sources somewhere.

I’ve made some progress.

  1. I was able to make depth attachment first - I forgot to add one more clear value and change its position.
  2. Now attachments are 0-depth/stencil, 1-positions, 2-diffuse
  3. positions renders fine, I read correct image, magic 214 was clear value.
  4. When I read diffuse, it is black (empty, all the image (0, 0, 0, 0))
  5. And what means check with validation layers? I’ve seen them in the code, but still do not understand their meaning and usage.

Publishing working project is problematic, because I am trying to make a vulkan renderer as a part of my big project with lots of local references, it is normally build only with one of my computers.

So, now I have 2 same color images as attachments:

attachments[1].format = VK_FORMAT_R8G8B8A8_UNORM;
attachments[2].format = VK_FORMAT_R8G8B8A8_UNORM;

All are registered in subpass and renderpass:

std::array<VkAttachmentReference, 2> color_references = {};
color_references[0].attachment = 1;
color_references[0].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
color_references[1].attachment = 2;
color_references[1].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

VkSubpassDescription subpass = {};
subpass.colorAttachmentCount = 2;
subpass.pColorAttachments = color_references.data();

VkRenderPassCreateInfo rp_info = {};
rp_info.attachmentCount = 3;

And my framebuffer has 3 attachments:

VkImageView attachments[3];
attachments[0] = depth.view;
attachments[1] = position.view;
attachments[2] = diffuse.view;

VkFramebufferCreateInfo fbufCreateInfo = {};
fbufCreateInfo.pAttachments = attachments;
fbufCreateInfo.attachmentCount = 3;

And first 2 of 3 attachments work. Maybe there is someplace else, where I must add some output binding for shader for example?

Do you have proper image layout transitions in place so that all of your attachments are transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL before you actually try to sample from them in the final compositing?

Do you actually write to the output position of that attachment in your shader?

The validation layers are the single most important error checking (amongst other stuff) that Vulkan has to offer. They check the areas of the API usage you specify against the specs and, if you enable it, against device limits and features. So before starting to post requests to get help it’s a good idea to get used to the validation layers as they will help you track down many errors and mistakes you can make while using Vulkan.

So please enable them (using the validation meta layers like in my vulkandebug.h) and check for any errors they report when running your application. These error messages should help you locating your problem. Once you enable them you can post the validation layer errors along with your code to make it easier for people (that don’t have access to your code) to help you.