diff --git a/util/test/demos/CMakeLists.txt b/util/test/demos/CMakeLists.txt
index ed0818b0e..8df03e981 100644
--- a/util/test/demos/CMakeLists.txt
+++ b/util/test/demos/CMakeLists.txt
@@ -135,6 +135,7 @@ set(VULKAN_SRC
vk/vk_multi_entry.cpp
vk/vk_multi_present.cpp
vk/vk_multi_thread_windows.cpp
+ vk/vk_multi_view.cpp
vk/vk_overlay_test.cpp
vk/vk_parameter_zoo.cpp
vk/vk_pixel_history.cpp
diff --git a/util/test/demos/demos.vcxproj b/util/test/demos/demos.vcxproj
index e157c3731..300371308 100644
--- a/util/test/demos/demos.vcxproj
+++ b/util/test/demos/demos.vcxproj
@@ -319,6 +319,7 @@
+
diff --git a/util/test/demos/demos.vcxproj.filters b/util/test/demos/demos.vcxproj.filters
index 3ef4e7d1f..552682bbc 100644
--- a/util/test/demos/demos.vcxproj.filters
+++ b/util/test/demos/demos.vcxproj.filters
@@ -685,6 +685,9 @@
D3D12\demos
+
+ Vulkan\demos
+
diff --git a/util/test/demos/vk/vk_multi_view.cpp b/util/test/demos/vk/vk_multi_view.cpp
new file mode 100644
index 000000000..1c89e205f
--- /dev/null
+++ b/util/test/demos/vk/vk_multi_view.cpp
@@ -0,0 +1,304 @@
+/******************************************************************************
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2024 Baldur Karlsson
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ ******************************************************************************/
+
+#include "vk_test.h"
+
+RD_TEST(VK_Multi_View, VulkanGraphicsTest)
+{
+ static constexpr const char *Description =
+ "Basic multi-view test like VK_Simple_Triangle but for multi-view rendering";
+
+ const std::string common = R"EOSHADER(
+
+#version 460 core
+
+#extension GL_EXT_multiview : require
+
+#define v2f v2f_block \
+{ \
+ vec4 pos; \
+ vec4 col; \
+ vec4 uv; \
+}
+
+)EOSHADER";
+
+ const std::string multiviewVertex = common + R"EOSHADER(
+
+layout(location = 0) in vec3 Position;
+layout(location = 1) in vec4 Color;
+layout(location = 2) in vec2 UV;
+
+layout(location = 0) out v2f vertOut;
+
+void main()
+{
+ vertOut.pos = vec4(Position.xyz*vec3(1,-1,1), 1);
+ gl_Position = vertOut.pos;
+ vertOut.col = Color;
+ vertOut.uv = vec4(UV.xy, 0, 1);
+
+ if (gl_ViewIndex == 0)
+ vertOut.col = vec4(1, 0, 0, 1);
+ if (gl_ViewIndex == 1)
+ vertOut.col = vec4(0, 1, 0, 1);
+}
+
+)EOSHADER";
+
+ const std::string multiViewGeom = common + R"EOSHADER(
+
+layout(triangles) in;
+layout(triangle_strip, max_vertices = 3) out;
+
+layout(location = 0) in v2f_block
+{
+ vec4 pos;
+ vec4 col;
+ vec4 uv;
+} gin[3];
+
+layout(location = 0) out g2f_block
+{
+ vec4 pos;
+ vec4 col;
+ vec4 uv;
+} gout;
+
+void main()
+{
+ for(int i = 0; i < 3; i++)
+ {
+ gl_Position = gl_in[i].gl_Position;
+
+ gout.pos = gin[i].pos;
+ gout.col = gin[i].col;
+ gout.uv = gin[i].uv;
+
+ if (gl_ViewIndex == 0)
+ gout.col = vec4(1, 0, 0, 1);
+ if (gl_ViewIndex == 1)
+ gout.col = vec4(0, 1, 0, 1);
+ EmitVertex();
+ }
+ EndPrimitive();
+}
+
+)EOSHADER";
+ const std::string multiViewPixel = common + R"EOSHADER(
+
+layout(location = 0) in v2f vertIn;
+
+layout(location = 0, index = 0) out vec4 Color;
+
+void main()
+{
+ Color = vertIn.col;
+ if (gl_ViewIndex == 0)
+ Color = vec4(1, 0, 0, 1);
+ if (gl_ViewIndex == 1)
+ Color = vec4(0, 1, 0, 1);
+}
+
+)EOSHADER";
+ void Prepare(int argc, char **argv)
+ {
+ features.geometryShader = VK_TRUE;
+ devExts.push_back(VK_KHR_MULTIVIEW_EXTENSION_NAME);
+
+ VulkanGraphicsTest::Prepare(argc, argv);
+
+ static VkPhysicalDeviceMultiviewFeaturesKHR multiview = {
+ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTIVIEW_FEATURES_KHR,
+ };
+
+ getPhysFeatures2(&multiview);
+ if(!multiview.multiview)
+ Avail = "Multiview feature 'multiview' not available";
+
+ devInfoNext = &multiview;
+ }
+
+ int main()
+ {
+ // initialise, create window, create context, etc
+ if(!Init())
+ return 3;
+
+ vkh::RenderPassCreator renderPassCreateInfo;
+
+ renderPassCreateInfo.attachments.push_back(vkh::AttachmentDescription(
+ mainWindow->format, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+ VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE, VK_SAMPLE_COUNT_1_BIT,
+ VK_ATTACHMENT_LOAD_OP_DONT_CARE, VK_ATTACHMENT_STORE_OP_DONT_CARE));
+
+ renderPassCreateInfo.addSubpass(
+ {VkAttachmentReference({0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL})});
+
+ uint32_t countViews = 2;
+ uint32_t viewMask = (1 << countViews) - 1;
+ uint32_t correlationMask = viewMask;
+ uint32_t countLayers = countViews + 2;
+
+ VkRenderPassMultiviewCreateInfo rpMultiviewCreateInfo = {
+ VK_STRUCTURE_TYPE_RENDER_PASS_MULTIVIEW_CREATE_INFO};
+ rpMultiviewCreateInfo.subpassCount = 1;
+ rpMultiviewCreateInfo.pViewMasks = &viewMask;
+ rpMultiviewCreateInfo.correlationMaskCount = 1;
+ rpMultiviewCreateInfo.pCorrelationMasks = &correlationMask;
+
+ renderPassCreateInfo.next((void *)&rpMultiviewCreateInfo);
+
+ VkRenderPass renderPass = createRenderPass(renderPassCreateInfo);
+
+ AllocatedImage fbColourImage(
+ this,
+ vkh::ImageCreateInfo(mainWindow->scissor.extent.width, mainWindow->scissor.extent.height, 0,
+ mainWindow->format, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, 1, countLayers),
+ VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_GPU_ONLY}));
+
+ VkImageViewCreateInfo colourViewInfo = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
+ colourViewInfo.image = fbColourImage.image;
+ colourViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
+ colourViewInfo.format = mainWindow->format;
+ colourViewInfo.flags = 0;
+ colourViewInfo.subresourceRange = {};
+ colourViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+ colourViewInfo.subresourceRange.baseMipLevel = 0;
+ colourViewInfo.subresourceRange.levelCount = VK_REMAINING_MIP_LEVELS;
+ colourViewInfo.subresourceRange.baseArrayLayer = 1;
+ colourViewInfo.subresourceRange.layerCount = countViews;
+ VkImageView fbColourView = createImageView(&colourViewInfo);
+
+ VkFramebuffer framebuffer = createFramebuffer(
+ vkh::FramebufferCreateInfo(renderPass, {fbColourView}, mainWindow->scissor.extent));
+
+ VkPipelineLayout layout = createPipelineLayout(vkh::PipelineLayoutCreateInfo());
+
+ vkh::GraphicsPipelineCreateInfo pipeCreateInfo;
+
+ pipeCreateInfo.layout = layout;
+ pipeCreateInfo.renderPass = renderPass;
+
+ VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
+ 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;
+ pipeCreateInfo.colorBlendState.attachments = {colorBlendAttachment};
+ pipeCreateInfo.depthStencilState.depthTestEnable = VK_FALSE;
+ pipeCreateInfo.depthStencilState.stencilTestEnable = VK_FALSE;
+
+ pipeCreateInfo.vertexInputState.vertexBindingDescriptions = {vkh::vertexBind(0, DefaultA2V)};
+ pipeCreateInfo.vertexInputState.vertexAttributeDescriptions = {
+ vkh::vertexAttr(0, 0, DefaultA2V, pos),
+ vkh::vertexAttr(1, 0, DefaultA2V, col),
+ vkh::vertexAttr(2, 0, DefaultA2V, uv),
+ };
+
+ pipeCreateInfo.stages = {
+ CompileShaderModule(multiviewVertex, ShaderLang::glsl, ShaderStage::vert, "main"),
+ CompileShaderModule(VKDefaultPixel, ShaderLang::glsl, ShaderStage::frag, "main"),
+ };
+
+ std::vector testNames;
+ std::vector testPipes;
+ testPipes.push_back(createGraphicsPipeline(pipeCreateInfo));
+ testNames.push_back("Vertex: viewIndex");
+
+ pipeCreateInfo.stages = {
+ CompileShaderModule(VKDefaultVertex, ShaderLang::glsl, ShaderStage::vert, "main"),
+ CompileShaderModule(multiViewPixel, ShaderLang::glsl, ShaderStage::frag, "main"),
+ };
+ testPipes.push_back(createGraphicsPipeline(pipeCreateInfo));
+ testNames.push_back("Fragment: viewIndex");
+
+ pipeCreateInfo.stages = {
+ CompileShaderModule(VKDefaultVertex, ShaderLang::glsl, ShaderStage::vert, "main"),
+ CompileShaderModule(VKDefaultPixel, ShaderLang::glsl, ShaderStage::frag, "main"),
+ CompileShaderModule(multiViewGeom, ShaderLang::glsl, ShaderStage::geom, "main"),
+ };
+ testPipes.push_back(createGraphicsPipeline(pipeCreateInfo));
+ testNames.push_back("Geometry: viewIndex");
+
+ pipeCreateInfo.stages = {
+ CompileShaderModule(VKDefaultVertex, ShaderLang::glsl, ShaderStage::vert, "main"),
+ CompileShaderModule(VKDefaultPixel, ShaderLang::glsl, ShaderStage::frag, "main"),
+ };
+ testPipes.push_back(createGraphicsPipeline(pipeCreateInfo));
+ testNames.push_back("No viewIndex");
+
+ AllocatedBuffer vb(
+ this,
+ vkh::BufferCreateInfo(sizeof(DefaultTri),
+ VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT),
+ VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_CPU_TO_GPU}));
+
+ vb.upload(DefaultTri);
+
+ while(Running())
+ {
+ VkCommandBuffer cmd = GetCommandBuffer();
+
+ vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo());
+ VkImage swapimg =
+ StartUsingBackbuffer(cmd, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL);
+
+ vkCmdClearColorImage(cmd, swapimg, VK_IMAGE_LAYOUT_GENERAL,
+ vkh::ClearColorValue(0.2f, 0.2f, 0.2f, 1.0f), 1,
+ vkh::ImageSubresourceRange());
+
+ // Render multiview to its own framebuffer
+
+ vkCmdBeginRenderPass(cmd,
+ vkh::RenderPassBeginInfo(renderPass, framebuffer, mainWindow->scissor,
+ {vkh::ClearValue(0.2f, 0.3f, 0.4f, 1.0f)}),
+ VK_SUBPASS_CONTENTS_INLINE);
+
+ for(size_t i = 0; i < testPipes.size(); ++i)
+ {
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, testPipes[i]);
+ vkCmdSetViewport(cmd, 0, 1, &mainWindow->viewport);
+ vkCmdSetScissor(cmd, 0, 1, &mainWindow->scissor);
+ vkh::cmdBindVertexBuffers(cmd, 0, {vb.buffer}, {0});
+ setMarker(cmd, testNames[i]);
+ vkCmdDraw(cmd, 3, 1, 0, 0);
+ }
+
+ vkCmdEndRenderPass(cmd);
+
+ // TODO: in the future could copy the multiview renderpass to the framebuffer (left, right)
+ FinishUsingBackbuffer(cmd, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL);
+
+ vkEndCommandBuffer(cmd);
+
+ Submit(0, 1, {cmd});
+
+ Present();
+ }
+
+ return 0;
+ }
+};
+
+REGISTER_TEST();
diff --git a/util/test/tests/Vulkan/VK_Multi_View.py b/util/test/tests/Vulkan/VK_Multi_View.py
new file mode 100644
index 000000000..5a943a3f3
--- /dev/null
+++ b/util/test/tests/Vulkan/VK_Multi_View.py
@@ -0,0 +1,85 @@
+import renderdoc as rd
+import rdtest
+
+class VK_Multi_View(rdtest.TestCase):
+ demos_test_name = 'VK_Multi_View'
+
+ def check_capture(self):
+ if not self.controller.GetAPIProperties().shaderDebugging:
+ rdtest.log.success("Shader debugging not enabled, skipping test")
+ return
+
+ x = 200
+ y = 150
+
+ for test_name in ["Vertex: viewIndex", "Geometry: viewIndex", "Fragment: viewIndex", "No viewIndex"]:
+ rdtest.log.print("Test {}".format(test_name))
+ action: rd.ActionDescription = self.find_action(test_name).next
+ self.controller.SetFrameEvent(action.eventId, True)
+
+ pipe: rd.PipeState = self.controller.GetPipelineState()
+ if not pipe.GetShaderReflection(rd.ShaderStage.Pixel).debugInfo.debuggable:
+ raise rdtest.TestFailureException("Test {} shader can not be debugged".format(test_name))
+
+ for view in range(2):
+ # Debug the pixel shader
+ inputs = rd.DebugPixelInputs()
+ inputs.view = view
+ trace: rd.ShaderDebugTrace = self.controller.DebugPixel(x, y, inputs)
+ if trace.debugger is None:
+ self.controller.FreeTrace(trace)
+ raise rdtest.TestFailureException("Test {} view {} did not debug at all".format(test_name, view))
+
+ cycles, variables = self.process_trace(trace)
+ output: rd.SourceVariableMapping = self.find_output_source_var(trace, rd.ShaderBuiltin.ColorOutput, 0)
+ debugged = self.evaluate_source_var(output, variables)
+ slice = view + 1
+ sub = rd.Subresource(0, slice, 0)
+ self.check_pixel_value(pipe.GetOutputTargets()[0].resourceId, x, y, debugged.value.f32v[0:4], sub=sub)
+ self.controller.FreeTrace(trace)
+
+ inst = 0
+ postvs = self.get_postvs(action, rd.MeshDataStage.VSOut, instance=inst, view=view)
+ for vtx in range(action.numIndices):
+ idx = vtx
+ self.check_debug(vtx, idx, inst, view, postvs)
+ rdtest.log.print(f"View {view} Slice {slice} passed")
+
+ rdtest.log.success("All tests matched")
+
+
+ def check_debug(self, vtx, idx, inst, view, postvs):
+ trace: rd.ShaderDebugTrace = self.controller.DebugVertex(vtx, inst, idx, view)
+
+ if trace.debugger is None:
+ self.controller.FreeTrace(trace)
+
+ raise rdtest.TestFailureException("Couldn't debug vertex {} in instance {} for view {}".format(vtx, inst, view))
+
+ cycles, variables = self.process_trace(trace)
+
+ for var in trace.sourceVars:
+ var: rd.SourceVariableMapping
+ if var.variables[0].type == rd.DebugVariableType.Variable and var.signatureIndex >= 0:
+ name = var.name
+
+ if name not in postvs[vtx].keys():
+ raise rdtest.TestFailureException("Don't have expected output for {}".format(name))
+
+ expect = postvs[vtx][name]
+ value = self.evaluate_source_var(var, variables)
+
+ if len(expect) != value.columns:
+ raise rdtest.TestFailureException(
+ "Output {} at vert {} (idx {}) instance {} view {} has different size ({} values) to expectation ({} values)"
+ .format(name, vtx, idx, inst, view, value.columns, len(expect)))
+
+ debugged = value.value.f32v[0:value.columns]
+
+ if not rdtest.value_compare(expect, debugged):
+ raise rdtest.TestFailureException(
+ "Debugged value {} at vert {} (idx {}) instance {} view {}: {} doesn't exactly match postvs output {}".format(
+ name, vtx, idx, inst, view, debugged, expect))
+ rdtest.log.success('Successfully debugged vertex {} in instance {} for view {}'
+ .format(vtx, inst, view))
+