VulkanBenchmarks: split out benchmark classes into separate files

* Move out VulkanBenchmark and DrawBenchmark classes to their own
  hpp/cpps.
* Move out ClearImageBenchmark and TriangleBenchmark classes and tests
  to their own cpps.
* Add non-template DrawBenchmark::addVertexBuffer that takes the size of
  the vertex type. This allows for the implementation to be moved to the
  cpp file, keeping the header light and easy to understand.

Bug: b/176981107
Change-Id: I657bd112c5523aa56dc5617082d6744b9edac03a
Reviewed-on: https://swiftshader-review.googlesource.com/c/SwiftShader/+/52130
Kokoro-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Nicolas Capens <nicolascapens@google.com>
Tested-by: Antonio Maiorano <amaiorano@google.com>
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
diff --git a/tests/VulkanBenchmarks/CMakeLists.txt b/tests/VulkanBenchmarks/CMakeLists.txt
index 9186bd6..cbcdaed 100644
--- a/tests/VulkanBenchmarks/CMakeLists.txt
+++ b/tests/VulkanBenchmarks/CMakeLists.txt
@@ -22,8 +22,13 @@
 )
 
 set(VULKAN_BENCHMARKS_SRC_FILES
+    ClearImageBenchmarks.cpp
+    DrawBenchmark.cpp
+    DrawBenchmark.hpp
     main.cpp
-    VulkanBenchmarks.cpp
+    TriangleBenchmarks.cpp
+    VulkanBenchmark.cpp
+    VulkanBenchmark.hpp
 )
 
 add_executable(VulkanBenchmarks
diff --git a/tests/VulkanBenchmarks/ClearImageBenchmarks.cpp b/tests/VulkanBenchmarks/ClearImageBenchmarks.cpp
new file mode 100644
index 0000000..0cc64d5
--- /dev/null
+++ b/tests/VulkanBenchmarks/ClearImageBenchmarks.cpp
@@ -0,0 +1,138 @@
+// Copyright 2021 The SwiftShader Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "VulkanBenchmark.hpp"
+#include "benchmark/benchmark.h"
+
+#include <cassert>
+
+class ClearImageBenchmark : public VulkanBenchmark
+{
+public:
+	void initialize(vk::Format clearFormat, vk::ImageAspectFlagBits clearAspect)
+	{
+		VulkanBenchmark::initialize();
+
+		vk::ImageCreateInfo imageInfo;
+		imageInfo.imageType = vk::ImageType::e2D;
+		imageInfo.format = clearFormat;
+		imageInfo.tiling = vk::ImageTiling::eOptimal;
+		imageInfo.initialLayout = vk::ImageLayout::eGeneral;
+		imageInfo.usage = vk::ImageUsageFlagBits::eTransferDst;
+		imageInfo.samples = vk::SampleCountFlagBits::e4;
+		imageInfo.extent = vk::Extent3D(1024, 1024, 1);
+		imageInfo.mipLevels = 1;
+		imageInfo.arrayLayers = 1;
+
+		image = device.createImage(imageInfo);
+
+		vk::MemoryRequirements memoryRequirements = device.getImageMemoryRequirements(image);
+
+		vk::MemoryAllocateInfo allocateInfo;
+		allocateInfo.allocationSize = memoryRequirements.size;
+		allocateInfo.memoryTypeIndex = 0;
+
+		memory = device.allocateMemory(allocateInfo);
+
+		device.bindImageMemory(image, memory, 0);
+
+		vk::CommandPoolCreateInfo commandPoolCreateInfo;
+		commandPoolCreateInfo.queueFamilyIndex = queueFamilyIndex;
+
+		commandPool = device.createCommandPool(commandPoolCreateInfo);
+
+		vk::CommandBufferAllocateInfo commandBufferAllocateInfo;
+		commandBufferAllocateInfo.commandPool = commandPool;
+		commandBufferAllocateInfo.commandBufferCount = 1;
+
+		commandBuffer = device.allocateCommandBuffers(commandBufferAllocateInfo)[0];
+
+		vk::CommandBufferBeginInfo commandBufferBeginInfo;
+		commandBufferBeginInfo.flags = {};
+
+		commandBuffer.begin(commandBufferBeginInfo);
+
+		vk::ImageSubresourceRange range;
+		range.aspectMask = clearAspect;
+		range.baseMipLevel = 0;
+		range.levelCount = 1;
+		range.baseArrayLayer = 0;
+		range.layerCount = 1;
+
+		if(clearAspect == vk::ImageAspectFlagBits::eColor)
+		{
+			vk::ClearColorValue clearColorValue;
+			clearColorValue.float32[0] = 0.0f;
+			clearColorValue.float32[1] = 1.0f;
+			clearColorValue.float32[2] = 0.0f;
+			clearColorValue.float32[3] = 1.0f;
+
+			commandBuffer.clearColorImage(image, vk::ImageLayout::eGeneral, &clearColorValue, 1, &range);
+		}
+		else if(clearAspect == vk::ImageAspectFlagBits::eDepth)
+		{
+			vk::ClearDepthStencilValue clearDepthStencilValue;
+			clearDepthStencilValue.depth = 1.0f;
+			clearDepthStencilValue.stencil = 0xFF;
+
+			commandBuffer.clearDepthStencilImage(image, vk::ImageLayout::eGeneral, &clearDepthStencilValue, 1, &range);
+		}
+		else
+			assert(false);
+
+		commandBuffer.end();
+	}
+
+	~ClearImageBenchmark()
+	{
+		device.freeCommandBuffers(commandPool, 1, &commandBuffer);
+		device.destroyCommandPool(commandPool, nullptr);
+		device.freeMemory(memory, nullptr);
+		device.destroyImage(image, nullptr);
+	}
+
+	void clear()
+	{
+		vk::SubmitInfo submitInfo;
+		submitInfo.commandBufferCount = 1;
+		submitInfo.pCommandBuffers = &commandBuffer;
+
+		queue.submit(1, &submitInfo, nullptr);
+		queue.waitIdle();
+	}
+
+private:
+	vk::Image image;                  // Owning handle
+	vk::DeviceMemory memory;          // Owning handle
+	vk::CommandPool commandPool;      // Owning handle
+	vk::CommandBuffer commandBuffer;  // Owning handle
+};
+
+static void ClearImage(benchmark::State &state, vk::Format clearFormat, vk::ImageAspectFlagBits clearAspect)
+{
+	ClearImageBenchmark benchmark;
+	benchmark.initialize(clearFormat, clearAspect);
+
+	// Execute once to have the Reactor routine generated.
+	benchmark.clear();
+
+	for(auto _ : state)
+	{
+		benchmark.clear();
+	}
+}
+
+BENCHMARK_CAPTURE(ClearImage, VK_FORMAT_R8G8B8A8_UNORM, vk::Format::eR8G8B8A8Unorm, vk::ImageAspectFlagBits::eColor)->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(ClearImage, VK_FORMAT_R32_SFLOAT, vk::Format::eR32Sfloat, vk::ImageAspectFlagBits::eColor)->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(ClearImage, VK_FORMAT_D32_SFLOAT, vk::Format::eD32Sfloat, vk::ImageAspectFlagBits::eDepth)->Unit(benchmark::kMillisecond);
diff --git a/tests/VulkanBenchmarks/DrawBenchmark.cpp b/tests/VulkanBenchmarks/DrawBenchmark.cpp
new file mode 100644
index 0000000..567e542
--- /dev/null
+++ b/tests/VulkanBenchmarks/DrawBenchmark.cpp
@@ -0,0 +1,449 @@
+// Copyright 2021 The SwiftShader Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "DrawBenchmark.hpp"
+
+#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))
+
+DrawBenchmark::DrawBenchmark(Multisample multisample)
+    : multisample(multisample == Multisample::True)
+{
+}
+
+DrawBenchmark::~DrawBenchmark()
+{
+	device.freeCommandBuffers(commandPool, commandBuffers);
+
+	device.destroyDescriptorPool(descriptorPool);
+	for(auto &sampler : samplers)
+	{
+		device.destroySampler(sampler, nullptr);
+	}
+	images.clear();
+	device.destroyCommandPool(commandPool, nullptr);
+
+	for(auto &fence : waitFences)
+	{
+		device.destroyFence(fence, nullptr);
+	}
+
+	device.destroySemaphore(renderCompleteSemaphore, nullptr);
+	device.destroySemaphore(presentCompleteSemaphore, nullptr);
+
+	device.destroyPipeline(pipeline);
+	device.destroyPipelineLayout(pipelineLayout, nullptr);
+	device.destroyDescriptorSetLayout(descriptorSetLayout);
+
+	device.freeMemory(vertices.memory, nullptr);
+	device.destroyBuffer(vertices.buffer, nullptr);
+
+	for(auto &framebuffer : framebuffers)
+	{
+		framebuffer.reset();
+	}
+
+	device.destroyRenderPass(renderPass, nullptr);
+
+	swapchain.reset();
+	window.reset();
+}
+
+void DrawBenchmark::initialize()
+{
+	VulkanBenchmark::initialize();
+
+	window.reset(new Window(instance, windowSize));
+	swapchain.reset(new Swapchain(physicalDevice, device, *window));
+
+	renderPass = createRenderPass(swapchain->colorFormat);
+	createFramebuffers(renderPass);
+
+	prepareVertices();
+
+	pipeline = createGraphicsPipeline(renderPass);
+
+	createSynchronizationPrimitives();
+
+	createCommandBuffers(renderPass);
+}
+
+void DrawBenchmark::renderFrame()
+{
+	swapchain->acquireNextImage(presentCompleteSemaphore, currentFrameBuffer);
+
+	device.waitForFences(1, &waitFences[currentFrameBuffer], VK_TRUE, UINT64_MAX);
+	device.resetFences(1, &waitFences[currentFrameBuffer]);
+
+	vk::PipelineStageFlags waitStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
+
+	vk::SubmitInfo submitInfo;
+	submitInfo.pWaitDstStageMask = &waitStageMask;
+	submitInfo.pWaitSemaphores = &presentCompleteSemaphore;
+	submitInfo.waitSemaphoreCount = 1;
+	submitInfo.pSignalSemaphores = &renderCompleteSemaphore;
+	submitInfo.signalSemaphoreCount = 1;
+	submitInfo.pCommandBuffers = &commandBuffers[currentFrameBuffer];
+	submitInfo.commandBufferCount = 1;
+
+	queue.submit(1, &submitInfo, waitFences[currentFrameBuffer]);
+
+	swapchain->queuePresent(queue, currentFrameBuffer, renderCompleteSemaphore);
+}
+
+void DrawBenchmark::show()
+{
+	window->show();
+}
+
+vk::RenderPass DrawBenchmark::createRenderPass(vk::Format colorFormat)
+{
+	std::vector<vk::AttachmentDescription> attachments(multisample ? 2 : 1);
+
+	if(multisample)
+	{
+		// Color attachment
+		attachments[0].format = colorFormat;
+		attachments[0].samples = vk::SampleCountFlagBits::e4;
+		attachments[0].loadOp = vk::AttachmentLoadOp::eClear;
+		attachments[0].storeOp = vk::AttachmentStoreOp::eStore;
+		attachments[0].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
+		attachments[0].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
+		attachments[0].initialLayout = vk::ImageLayout::eUndefined;
+		attachments[0].finalLayout = vk::ImageLayout::eColorAttachmentOptimal;
+
+		// Resolve attachment
+		attachments[1].format = colorFormat;
+		attachments[1].samples = vk::SampleCountFlagBits::e1;
+		attachments[1].loadOp = vk::AttachmentLoadOp::eDontCare;
+		attachments[1].storeOp = vk::AttachmentStoreOp::eStore;
+		attachments[1].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
+		attachments[1].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
+		attachments[1].initialLayout = vk::ImageLayout::eUndefined;
+		attachments[1].finalLayout = vk::ImageLayout::ePresentSrcKHR;
+	}
+	else
+	{
+		attachments[0].format = colorFormat;
+		attachments[0].samples = vk::SampleCountFlagBits::e1;
+		attachments[0].loadOp = vk::AttachmentLoadOp::eDontCare;
+		attachments[0].storeOp = vk::AttachmentStoreOp::eStore;
+		attachments[0].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
+		attachments[0].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
+		attachments[0].initialLayout = vk::ImageLayout::eUndefined;
+		attachments[0].finalLayout = vk::ImageLayout::ePresentSrcKHR;
+	}
+
+	vk::AttachmentReference attachment0;
+	attachment0.attachment = 0;
+	attachment0.layout = vk::ImageLayout::eColorAttachmentOptimal;
+
+	vk::AttachmentReference attachment1;
+	attachment1.attachment = 1;
+	attachment1.layout = vk::ImageLayout::eColorAttachmentOptimal;
+
+	vk::SubpassDescription subpassDescription;
+	subpassDescription.pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
+	subpassDescription.colorAttachmentCount = 1;
+	subpassDescription.pResolveAttachments = multisample ? &attachment1 : nullptr;
+	subpassDescription.pColorAttachments = &attachment0;
+
+	std::array<vk::SubpassDependency, 2> dependencies;
+
+	dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
+	dependencies[0].dstSubpass = 0;
+	dependencies[0].srcStageMask = vk::PipelineStageFlagBits::eBottomOfPipe;
+	dependencies[0].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
+	dependencies[0].srcAccessMask = vk::AccessFlagBits::eMemoryRead;
+	dependencies[0].dstAccessMask = vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite;
+	dependencies[0].dependencyFlags = vk::DependencyFlagBits::eByRegion;
+
+	dependencies[1].srcSubpass = 0;
+	dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
+	dependencies[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
+	dependencies[1].dstStageMask = vk::PipelineStageFlagBits::eBottomOfPipe;
+	dependencies[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite;
+	dependencies[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead;
+	dependencies[1].dependencyFlags = vk::DependencyFlagBits::eByRegion;
+
+	vk::RenderPassCreateInfo renderPassInfo;
+	renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
+	renderPassInfo.pAttachments = attachments.data();
+	renderPassInfo.subpassCount = 1;
+	renderPassInfo.pSubpasses = &subpassDescription;
+	renderPassInfo.dependencyCount = static_cast<uint32_t>(dependencies.size());
+	renderPassInfo.pDependencies = dependencies.data();
+
+	return device.createRenderPass(renderPassInfo);
+}
+
+void DrawBenchmark::createFramebuffers(vk::RenderPass renderPass)
+{
+	framebuffers.resize(swapchain->imageCount());
+
+	for(size_t i = 0; i < framebuffers.size(); i++)
+	{
+		framebuffers[i].reset(new Framebuffer(device, swapchain->getImageView(i), swapchain->colorFormat, renderPass, swapchain->getExtent(), multisample));
+	}
+}
+
+void DrawBenchmark::prepareVertices()
+{
+	doCreateVertexBuffers();
+}
+
+vk::Pipeline DrawBenchmark::createGraphicsPipeline(vk::RenderPass renderPass)
+{
+	auto setLayoutBindings = doCreateDescriptorSetLayouts();
+
+	std::vector<vk::DescriptorSetLayout> setLayouts;
+	if(!setLayoutBindings.empty())
+	{
+		vk::DescriptorSetLayoutCreateInfo layoutInfo;
+		layoutInfo.bindingCount = static_cast<uint32_t>(setLayoutBindings.size());
+		layoutInfo.pBindings = setLayoutBindings.data();
+		descriptorSetLayout = device.createDescriptorSetLayout(layoutInfo);
+
+		setLayouts.push_back(descriptorSetLayout);
+	}
+
+	vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo;
+	pipelineLayoutCreateInfo.setLayoutCount = static_cast<uint32_t>(setLayouts.size());
+	pipelineLayoutCreateInfo.pSetLayouts = setLayouts.data();
+	pipelineLayout = device.createPipelineLayout(pipelineLayoutCreateInfo);
+
+	vk::GraphicsPipelineCreateInfo pipelineCreateInfo;
+	pipelineCreateInfo.layout = pipelineLayout;
+	pipelineCreateInfo.renderPass = renderPass;
+
+	vk::PipelineInputAssemblyStateCreateInfo inputAssemblyState;
+	inputAssemblyState.topology = vk::PrimitiveTopology::eTriangleList;
+
+	vk::PipelineRasterizationStateCreateInfo rasterizationState;
+	rasterizationState.depthClampEnable = VK_FALSE;
+	rasterizationState.rasterizerDiscardEnable = VK_FALSE;
+	rasterizationState.polygonMode = vk::PolygonMode::eFill;
+	rasterizationState.cullMode = vk::CullModeFlagBits::eNone;
+	rasterizationState.frontFace = vk::FrontFace::eCounterClockwise;
+	rasterizationState.depthBiasEnable = VK_FALSE;
+	rasterizationState.lineWidth = 1.0f;
+
+	vk::PipelineColorBlendAttachmentState blendAttachmentState;
+	blendAttachmentState.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA;
+	blendAttachmentState.blendEnable = VK_FALSE;
+	vk::PipelineColorBlendStateCreateInfo colorBlendState;
+	colorBlendState.attachmentCount = 1;
+	colorBlendState.pAttachments = &blendAttachmentState;
+
+	vk::PipelineViewportStateCreateInfo viewportState;
+	viewportState.viewportCount = 1;
+	viewportState.scissorCount = 1;
+
+	std::vector<vk::DynamicState> dynamicStateEnables;
+	dynamicStateEnables.push_back(vk::DynamicState::eViewport);
+	dynamicStateEnables.push_back(vk::DynamicState::eScissor);
+	vk::PipelineDynamicStateCreateInfo dynamicState = {};
+	dynamicState.pDynamicStates = dynamicStateEnables.data();
+	dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStateEnables.size());
+
+	vk::PipelineDepthStencilStateCreateInfo depthStencilState;
+	depthStencilState.depthTestEnable = VK_FALSE;
+	depthStencilState.depthWriteEnable = VK_FALSE;
+	depthStencilState.depthCompareOp = vk::CompareOp::eLessOrEqual;
+	depthStencilState.depthBoundsTestEnable = VK_FALSE;
+	depthStencilState.back.failOp = vk::StencilOp::eKeep;
+	depthStencilState.back.passOp = vk::StencilOp::eKeep;
+	depthStencilState.back.compareOp = vk::CompareOp::eAlways;
+	depthStencilState.stencilTestEnable = VK_FALSE;
+	depthStencilState.front = depthStencilState.back;
+
+	vk::PipelineMultisampleStateCreateInfo multisampleState;
+	multisampleState.rasterizationSamples = multisample ? vk::SampleCountFlagBits::e4 : vk::SampleCountFlagBits::e1;
+	multisampleState.pSampleMask = nullptr;
+
+	vk::ShaderModule vertexModule = doCreateVertexShader();
+	vk::ShaderModule fragmentModule = doCreateFragmentShader();
+
+	assert(vertexModule);    // TODO: if nullptr, use a default
+	assert(fragmentModule);  // TODO: if nullptr, use a default
+
+	std::array<vk::PipelineShaderStageCreateInfo, 2> shaderStages;
+
+	shaderStages[0].module = vertexModule;
+	shaderStages[0].stage = vk::ShaderStageFlagBits::eVertex;
+	shaderStages[0].pName = "main";
+
+	shaderStages[1].module = fragmentModule;
+	shaderStages[1].stage = vk::ShaderStageFlagBits::eFragment;
+	shaderStages[1].pName = "main";
+
+	pipelineCreateInfo.stageCount = static_cast<uint32_t>(shaderStages.size());
+	pipelineCreateInfo.pStages = shaderStages.data();
+	pipelineCreateInfo.pVertexInputState = &vertices.inputState;
+	pipelineCreateInfo.pInputAssemblyState = &inputAssemblyState;
+	pipelineCreateInfo.pRasterizationState = &rasterizationState;
+	pipelineCreateInfo.pColorBlendState = &colorBlendState;
+	pipelineCreateInfo.pMultisampleState = &multisampleState;
+	pipelineCreateInfo.pViewportState = &viewportState;
+	pipelineCreateInfo.pDepthStencilState = &depthStencilState;
+	pipelineCreateInfo.renderPass = renderPass;
+	pipelineCreateInfo.pDynamicState = &dynamicState;
+
+	auto pipeline = device.createGraphicsPipeline(nullptr, pipelineCreateInfo).value;
+
+	device.destroyShaderModule(fragmentModule);
+	device.destroyShaderModule(vertexModule);
+
+	return pipeline;
+}
+
+void DrawBenchmark::createSynchronizationPrimitives()
+{
+	vk::SemaphoreCreateInfo semaphoreCreateInfo;
+	presentCompleteSemaphore = device.createSemaphore(semaphoreCreateInfo);
+	renderCompleteSemaphore = device.createSemaphore(semaphoreCreateInfo);
+
+	vk::FenceCreateInfo fenceCreateInfo;
+	fenceCreateInfo.flags = vk::FenceCreateFlagBits::eSignaled;
+	waitFences.resize(swapchain->imageCount());
+	for(auto &fence : waitFences)
+	{
+		fence = device.createFence(fenceCreateInfo);
+	}
+}
+
+void DrawBenchmark::createCommandBuffers(vk::RenderPass renderPass)
+{
+	vk::CommandPoolCreateInfo commandPoolCreateInfo;
+	commandPoolCreateInfo.queueFamilyIndex = queueFamilyIndex;
+	commandPoolCreateInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;
+	commandPool = device.createCommandPool(commandPoolCreateInfo);
+
+	std::vector<vk::DescriptorSet> descriptorSets;
+	if(descriptorSetLayout)
+	{
+		std::array<vk::DescriptorPoolSize, 1> poolSizes = {};
+		poolSizes[0].type = vk::DescriptorType::eCombinedImageSampler;
+		poolSizes[0].descriptorCount = 1;
+
+		vk::DescriptorPoolCreateInfo poolInfo;
+		poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
+		poolInfo.pPoolSizes = poolSizes.data();
+		poolInfo.maxSets = 1;
+
+		descriptorPool = device.createDescriptorPool(poolInfo);
+
+		std::vector<vk::DescriptorSetLayout> layouts(1, descriptorSetLayout);
+		vk::DescriptorSetAllocateInfo allocInfo;
+		allocInfo.descriptorPool = descriptorPool;
+		allocInfo.descriptorSetCount = 1;
+		allocInfo.pSetLayouts = layouts.data();
+
+		descriptorSets = device.allocateDescriptorSets(allocInfo);
+
+		doUpdateDescriptorSet(commandPool, descriptorSets[0]);
+	}
+
+	vk::CommandBufferAllocateInfo commandBufferAllocateInfo;
+	commandBufferAllocateInfo.commandPool = commandPool;
+	commandBufferAllocateInfo.commandBufferCount = static_cast<uint32_t>(swapchain->imageCount());
+	commandBufferAllocateInfo.level = vk::CommandBufferLevel::ePrimary;
+
+	commandBuffers = device.allocateCommandBuffers(commandBufferAllocateInfo);
+
+	for(size_t i = 0; i < commandBuffers.size(); i++)
+	{
+		vk::CommandBufferBeginInfo commandBufferBeginInfo;
+		commandBuffers[i].begin(commandBufferBeginInfo);
+
+		vk::ClearValue clearValues[1];
+		clearValues[0].color = vk::ClearColorValue(std::array<float, 4>{ 0.5f, 0.5f, 0.5f, 1.0f });
+
+		vk::RenderPassBeginInfo renderPassBeginInfo;
+		renderPassBeginInfo.framebuffer = framebuffers[i]->getFramebuffer();
+		renderPassBeginInfo.renderPass = renderPass;
+		renderPassBeginInfo.renderArea.offset.x = 0;
+		renderPassBeginInfo.renderArea.offset.y = 0;
+		renderPassBeginInfo.renderArea.extent = windowSize;
+		renderPassBeginInfo.clearValueCount = ARRAY_SIZE(clearValues);
+		renderPassBeginInfo.pClearValues = clearValues;
+		commandBuffers[i].beginRenderPass(renderPassBeginInfo, vk::SubpassContents::eInline);
+
+		// Set dynamic state
+		vk::Viewport viewport(0.0f, 0.0f, static_cast<float>(windowSize.width), static_cast<float>(windowSize.height), 0.0f, 1.0f);
+		commandBuffers[i].setViewport(0, 1, &viewport);
+
+		vk::Rect2D scissor(vk::Offset2D(0, 0), windowSize);
+		commandBuffers[i].setScissor(0, 1, &scissor);
+
+		if(!descriptorSets.empty())
+		{
+			commandBuffers[i].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout, 0, 1, &descriptorSets[0], 0, nullptr);
+		}
+
+		// Draw
+		commandBuffers[i].bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
+		VULKAN_HPP_NAMESPACE::DeviceSize offset = 0;
+		commandBuffers[i].bindVertexBuffers(0, 1, &vertices.buffer, &offset);
+		commandBuffers[i].draw(vertices.numVertices, 1, 0, 0);
+
+		commandBuffers[i].endRenderPass();
+		commandBuffers[i].end();
+	}
+}
+
+void DrawBenchmark::addVertexBuffer(void *vertexBufferData, size_t vertexBufferDataSize, size_t vertexSize, std::vector<vk::VertexInputAttributeDescription> inputAttributes)
+{
+	assert(!vertices.buffer);  // For now, only support adding once
+
+	vk::BufferCreateInfo vertexBufferInfo;
+	vertexBufferInfo.size = vertexBufferDataSize;
+	vertexBufferInfo.usage = vk::BufferUsageFlagBits::eVertexBuffer;
+	vertices.buffer = device.createBuffer(vertexBufferInfo);
+
+	vk::MemoryAllocateInfo memoryAllocateInfo;
+	vk::MemoryRequirements memoryRequirements = device.getBufferMemoryRequirements(vertices.buffer);
+	memoryAllocateInfo.allocationSize = memoryRequirements.size;
+	memoryAllocateInfo.memoryTypeIndex = Util::getMemoryTypeIndex(physicalDevice, memoryRequirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
+	vertices.memory = device.allocateMemory(memoryAllocateInfo);
+
+	void *data = device.mapMemory(vertices.memory, 0, VK_WHOLE_SIZE);
+	memcpy(data, vertexBufferData, vertexBufferDataSize);
+	device.unmapMemory(vertices.memory);
+	device.bindBufferMemory(vertices.buffer, vertices.memory, 0);
+
+	vertices.inputBinding.binding = 0;
+	vertices.inputBinding.stride = static_cast<uint32_t>(vertexSize);
+	vertices.inputBinding.inputRate = vk::VertexInputRate::eVertex;
+
+	vertices.inputAttributes = std::move(inputAttributes);
+
+	vertices.inputState.vertexBindingDescriptionCount = 1;
+	vertices.inputState.pVertexBindingDescriptions = &vertices.inputBinding;
+	vertices.inputState.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertices.inputAttributes.size());
+	vertices.inputState.pVertexAttributeDescriptions = vertices.inputAttributes.data();
+
+	// Note that we assume data is tightly packed
+	vertices.numVertices = static_cast<uint32_t>(vertexBufferDataSize / vertexSize);
+}
+
+vk::ShaderModule DrawBenchmark::createShaderModule(const char *glslSource, EShLanguage glslLanguage)
+{
+	auto spirv = Util::compileGLSLtoSPIRV(glslSource, glslLanguage);
+
+	vk::ShaderModuleCreateInfo moduleCreateInfo;
+	moduleCreateInfo.codeSize = spirv.size() * sizeof(uint32_t);
+	moduleCreateInfo.pCode = (uint32_t *)spirv.data();
+
+	return device.createShaderModule(moduleCreateInfo);
+}
diff --git a/tests/VulkanBenchmarks/DrawBenchmark.hpp b/tests/VulkanBenchmarks/DrawBenchmark.hpp
new file mode 100644
index 0000000..2c30609
--- /dev/null
+++ b/tests/VulkanBenchmarks/DrawBenchmark.hpp
@@ -0,0 +1,174 @@
+// Copyright 2021 The SwiftShader Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef DRAW_BENCHMARK_HPP_
+#define DRAW_BENCHMARK_HPP_
+
+#include "Framebuffer.hpp"
+#include "Image.hpp"
+#include "Swapchain.hpp"
+#include "Util.hpp"
+#include "VulkanBenchmark.hpp"
+#include "Window.hpp"
+
+enum class Multisample
+{
+	False,
+	True
+};
+
+class DrawBenchmark : public VulkanBenchmark
+{
+public:
+	DrawBenchmark(Multisample multisample);
+	~DrawBenchmark();
+
+	void initialize();
+	void renderFrame();
+	void show();
+
+private:
+	void createSynchronizationPrimitives();
+	void createCommandBuffers(vk::RenderPass renderPass);
+	void prepareVertices();
+	void createFramebuffers(vk::RenderPass renderPass);
+	vk::RenderPass createRenderPass(vk::Format colorFormat);
+	vk::Pipeline createGraphicsPipeline(vk::RenderPass renderPass);
+	void addVertexBuffer(void *vertexBufferData, size_t vertexBufferDataSize, size_t vertexSize, std::vector<vk::VertexInputAttributeDescription> inputAttributes);
+
+protected:
+	/////////////////////////
+	// Hooks
+	/////////////////////////
+
+	// Called from prepareVertices.
+	// Child type may call addVertexBuffer() from this function.
+	virtual void doCreateVertexBuffers() {}
+
+	// Called from createGraphicsPipeline.
+	// Child type may return vector of DescriptorSetLayoutBindings for which a DescriptorSetLayout
+	// will be created and stored in this->descriptorSetLayout.
+	virtual std::vector<vk::DescriptorSetLayoutBinding> doCreateDescriptorSetLayouts()
+	{
+		return {};
+	}
+
+	// Called from createGraphicsPipeline.
+	// Child type may call createShaderModule() and return the result.
+	virtual vk::ShaderModule doCreateVertexShader()
+	{
+		return nullptr;
+	}
+
+	// Called from createGraphicsPipeline.
+	// Child type may call createShaderModule() and return the result.
+	virtual vk::ShaderModule doCreateFragmentShader()
+	{
+		return nullptr;
+	}
+
+	// Called from createCommandBuffers.
+	// Child type may create resources (addImage, addSampler, etc.), and make sure to
+	// call device.updateDescriptorSets.
+	virtual void doUpdateDescriptorSet(vk::CommandPool &commandPool, vk::DescriptorSet &descriptorSet)
+	{
+	}
+
+	/////////////////////////
+	// Resource Management
+	/////////////////////////
+
+	// Call from doCreateFragmentShader()
+	vk::ShaderModule createShaderModule(const char *glslSource, EShLanguage glslLanguage);
+
+	// Call from doCreateVertexBuffers()
+	template<typename VertexType>
+	void addVertexBuffer(VertexType *vertexBufferData, size_t vertexBufferDataSize, std::vector<vk::VertexInputAttributeDescription> inputAttributes)
+	{
+		addVertexBuffer(vertexBufferData, vertexBufferDataSize, sizeof(VertexType), std::move(inputAttributes));
+	}
+
+	template<typename T>
+	struct Resource
+	{
+		size_t id;
+		T &obj;
+	};
+
+	template<typename... Args>
+	Resource<Image> addImage(Args &&... args)
+	{
+		images.emplace_back(std::make_unique<Image>(std::forward<Args>(args)...));
+		return { images.size() - 1, *images.back() };
+	}
+
+	Image &getImageById(size_t id)
+	{
+		return *images[id].get();
+	}
+
+	Resource<vk::Sampler> addSampler(const vk::SamplerCreateInfo &samplerCreateInfo)
+	{
+		auto sampler = device.createSampler(samplerCreateInfo);
+		samplers.push_back(sampler);
+		return { samplers.size() - 1, sampler };
+	}
+
+	vk::Sampler &getSamplerById(size_t id)
+	{
+		return samplers[id];
+	}
+
+private:
+	const vk::Extent2D windowSize = { 1280, 720 };
+	const bool multisample;
+
+	std::unique_ptr<Window> window;
+	std::unique_ptr<Swapchain> swapchain;
+
+	vk::RenderPass renderPass;  // Owning handle
+	std::vector<std::unique_ptr<Framebuffer>> framebuffers;
+	uint32_t currentFrameBuffer = 0;
+
+	struct VertexBuffer
+	{
+		vk::Buffer buffer;        // Owning handle
+		vk::DeviceMemory memory;  // Owning handle
+
+		vk::VertexInputBindingDescription inputBinding;
+		std::vector<vk::VertexInputAttributeDescription> inputAttributes;
+		vk::PipelineVertexInputStateCreateInfo inputState;
+
+		uint32_t numVertices;
+	} vertices;
+
+	vk::DescriptorSetLayout descriptorSetLayout;  // Owning handle
+	vk::PipelineLayout pipelineLayout;            // Owning handle
+	vk::Pipeline pipeline;                        // Owning handle
+
+	vk::Semaphore presentCompleteSemaphore;  // Owning handle
+	vk::Semaphore renderCompleteSemaphore;   // Owning handle
+	std::vector<vk::Fence> waitFences;       // Owning handles
+
+	vk::CommandPool commandPool;        // Owning handle
+	vk::DescriptorPool descriptorPool;  // Owning handle
+
+	// Resources
+	std::vector<std::unique_ptr<Image>> images;
+	std::vector<vk::Sampler> samplers;  // Owning handles
+
+	std::vector<vk::CommandBuffer> commandBuffers;  // Owning handles
+};
+
+#endif  // DRAW_BENCHMARK_HPP_
diff --git a/tests/VulkanBenchmarks/TriangleBenchmarks.cpp b/tests/VulkanBenchmarks/TriangleBenchmarks.cpp
new file mode 100644
index 0000000..832aa69
--- /dev/null
+++ b/tests/VulkanBenchmarks/TriangleBenchmarks.cpp
@@ -0,0 +1,311 @@
+// Copyright 2021 The SwiftShader Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "Buffer.hpp"
+#include "DrawBenchmark.hpp"
+#include "benchmark/benchmark.h"
+
+#include <cassert>
+#include <vector>
+
+class TriangleSolidColorBenchmark : public DrawBenchmark
+{
+public:
+	TriangleSolidColorBenchmark(Multisample multisample)
+	    : DrawBenchmark(multisample)
+	{}
+
+protected:
+	void doCreateVertexBuffers() override
+	{
+		struct Vertex
+		{
+			float position[3];
+		};
+
+		Vertex vertexBufferData[] = {
+			{ { 1.0f, 1.0f, 0.5f } },
+			{ { -1.0f, 1.0f, 0.5f } },
+			{ { 0.0f, -1.0f, 0.5f } }
+		};
+
+		std::vector<vk::VertexInputAttributeDescription> inputAttributes;
+		inputAttributes.push_back(vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, position)));
+
+		addVertexBuffer(vertexBufferData, sizeof(vertexBufferData), std::move(inputAttributes));
+	}
+
+	vk::ShaderModule doCreateVertexShader() override
+	{
+		const char *vertexShader = R"(#version 310 es
+			layout(location = 0) in vec3 inPos;
+
+			void main()
+			{
+				gl_Position = vec4(inPos.xyz, 1.0);
+			})";
+
+		return createShaderModule(vertexShader, EShLanguage::EShLangVertex);
+	}
+
+	vk::ShaderModule doCreateFragmentShader() override
+	{
+		const char *fragmentShader = R"(#version 310 es
+			precision highp float;
+
+			layout(location = 0) out vec4 outColor;
+
+			void main()
+			{
+				outColor = vec4(1.0, 1.0, 1.0, 1.0);
+			})";
+
+		return createShaderModule(fragmentShader, EShLanguage::EShLangFragment);
+	}
+};
+
+class TriangleInterpolateColorBenchmark : public DrawBenchmark
+{
+public:
+	TriangleInterpolateColorBenchmark(Multisample multisample)
+	    : DrawBenchmark(multisample)
+	{}
+
+protected:
+	void doCreateVertexBuffers() override
+	{
+		struct Vertex
+		{
+			float position[3];
+			float color[3];
+		};
+
+		Vertex vertexBufferData[] = {
+			{ { 1.0f, 1.0f, 0.05f }, { 1.0f, 0.0f, 0.0f } },
+			{ { -1.0f, 1.0f, 0.5f }, { 0.0f, 1.0f, 0.0f } },
+			{ { 0.0f, -1.0f, 0.5f }, { 0.0f, 0.0f, 1.0f } }
+		};
+
+		std::vector<vk::VertexInputAttributeDescription> inputAttributes;
+		inputAttributes.push_back(vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, position)));
+		inputAttributes.push_back(vk::VertexInputAttributeDescription(1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)));
+
+		addVertexBuffer(vertexBufferData, sizeof(vertexBufferData), std::move(inputAttributes));
+	}
+
+	vk::ShaderModule doCreateVertexShader() override
+	{
+		const char *vertexShader = R"(#version 310 es
+			layout(location = 0) in vec3 inPos;
+			layout(location = 1) in vec3 inColor;
+
+			layout(location = 0) out vec3 outColor;
+
+			void main()
+			{
+				outColor = inColor;
+				gl_Position = vec4(inPos.xyz, 1.0);
+			})";
+
+		return createShaderModule(vertexShader, EShLanguage::EShLangVertex);
+	}
+
+	vk::ShaderModule doCreateFragmentShader() override
+	{
+		const char *fragmentShader = R"(#version 310 es
+			precision highp float;
+
+			layout(location = 0) in vec3 inColor;
+
+			layout(location = 0) out vec4 outColor;
+
+			void main()
+			{
+				outColor = vec4(inColor, 1.0);
+			})";
+
+		return createShaderModule(fragmentShader, EShLanguage::EShLangFragment);
+	}
+};
+
+class TriangleSampleTextureBenchmark : public DrawBenchmark
+{
+public:
+	TriangleSampleTextureBenchmark(Multisample multisample)
+	    : DrawBenchmark(multisample)
+	{}
+
+protected:
+	void doCreateVertexBuffers() override
+	{
+		struct Vertex
+		{
+			float position[3];
+			float color[3];
+			float texCoord[2];
+		};
+
+		Vertex vertexBufferData[] = {
+			{ { 1.0f, 1.0f, 0.5f }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f } },
+			{ { -1.0f, 1.0f, 0.5f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f } },
+			{ { 0.0f, -1.0f, 0.5f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f } }
+		};
+
+		std::vector<vk::VertexInputAttributeDescription> inputAttributes;
+		inputAttributes.push_back(vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, position)));
+		inputAttributes.push_back(vk::VertexInputAttributeDescription(1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)));
+		inputAttributes.push_back(vk::VertexInputAttributeDescription(2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, texCoord)));
+
+		addVertexBuffer(vertexBufferData, sizeof(vertexBufferData), std::move(inputAttributes));
+	}
+
+	vk::ShaderModule doCreateVertexShader() override
+	{
+		const char *vertexShader = R"(#version 310 es
+			layout(location = 0) in vec3 inPos;
+			layout(location = 1) in vec3 inColor;
+
+			layout(location = 0) out vec3 outColor;
+			layout(location = 1) out vec2 fragTexCoord;
+
+			void main()
+			{
+				outColor = inColor;
+				gl_Position = vec4(inPos.xyz, 1.0);
+				fragTexCoord = inPos.xy;
+			})";
+
+		return createShaderModule(vertexShader, EShLanguage::EShLangVertex);
+	}
+
+	vk::ShaderModule doCreateFragmentShader() override
+	{
+		const char *fragmentShader = R"(#version 310 es
+			precision highp float;
+
+			layout(location = 0) in vec3 inColor;
+			layout(location = 1) in vec2 fragTexCoord;
+
+			layout(location = 0) out vec4 outColor;
+
+			layout(binding = 0) uniform sampler2D texSampler;
+
+			void main()
+			{
+				outColor = texture(texSampler, fragTexCoord) * vec4(inColor, 1.0);
+			})";
+
+		return createShaderModule(fragmentShader, EShLanguage::EShLangFragment);
+	}
+
+	std::vector<vk::DescriptorSetLayoutBinding> doCreateDescriptorSetLayouts() override
+	{
+		vk::DescriptorSetLayoutBinding samplerLayoutBinding;
+		samplerLayoutBinding.binding = 1;
+		samplerLayoutBinding.descriptorCount = 1;
+		samplerLayoutBinding.descriptorType = vk::DescriptorType::eCombinedImageSampler;
+		samplerLayoutBinding.pImmutableSamplers = nullptr;
+		samplerLayoutBinding.stageFlags = vk::ShaderStageFlagBits::eFragment;
+
+		return { samplerLayoutBinding };
+	}
+
+	void doUpdateDescriptorSet(vk::CommandPool &commandPool, vk::DescriptorSet &descriptorSet) override
+	{
+		auto &texture = addImage(device, 16, 16, vk::Format::eR8G8B8A8Unorm).obj;
+
+		// Fill texture with white
+		vk::DeviceSize bufferSize = 16 * 16 * 4;
+		Buffer buffer(device, bufferSize, vk::BufferUsageFlagBits::eTransferSrc);
+		void *data = buffer.mapMemory();
+		memset(data, 255, bufferSize);
+		buffer.unmapMemory();
+
+		Util::transitionImageLayout(device, commandPool, queue, texture.getImage(), vk::Format::eR8G8B8A8Unorm, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal);
+		Util::copyBufferToImage(device, commandPool, queue, buffer.getBuffer(), texture.getImage(), 16, 16);
+		Util::transitionImageLayout(device, commandPool, queue, texture.getImage(), vk::Format::eR8G8B8A8Unorm, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);
+
+		vk::SamplerCreateInfo samplerInfo;
+		samplerInfo.magFilter = vk::Filter::eLinear;
+		samplerInfo.minFilter = vk::Filter::eLinear;
+		samplerInfo.addressModeU = vk::SamplerAddressMode::eRepeat;
+		samplerInfo.addressModeV = vk::SamplerAddressMode::eRepeat;
+		samplerInfo.addressModeW = vk::SamplerAddressMode::eRepeat;
+		samplerInfo.anisotropyEnable = VK_FALSE;
+		samplerInfo.unnormalizedCoordinates = VK_FALSE;
+		samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear;
+		samplerInfo.mipLodBias = 0.0f;
+		samplerInfo.minLod = 0.0f;
+		samplerInfo.maxLod = 0.0f;
+
+		auto sampler = addSampler(samplerInfo);
+
+		vk::DescriptorImageInfo imageInfo;
+		imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
+		imageInfo.imageView = texture.getImageView();
+		imageInfo.sampler = sampler.obj;
+
+		std::array<vk::WriteDescriptorSet, 1> descriptorWrites = {};
+
+		descriptorWrites[0].dstSet = descriptorSet;
+		descriptorWrites[0].dstBinding = 1;
+		descriptorWrites[0].dstArrayElement = 0;
+		descriptorWrites[0].descriptorType = vk::DescriptorType::eCombinedImageSampler;
+		descriptorWrites[0].descriptorCount = 1;
+		descriptorWrites[0].pImageInfo = &imageInfo;
+
+		device.updateDescriptorSets(static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
+	}
+};
+
+template<typename T>
+static void RunBenchmark(benchmark::State &state, T &benchmark)
+{
+	benchmark.initialize();
+
+	if(false) benchmark.show();  // Enable for visual verification.
+
+	// Warmup
+	benchmark.renderFrame();
+
+	for(auto _ : state)
+	{
+		benchmark.renderFrame();
+	}
+}
+
+static void TriangleSolidColor(benchmark::State &state, Multisample multisample)
+{
+	TriangleSolidColorBenchmark benchmark(multisample);
+	RunBenchmark(state, benchmark);
+}
+
+static void TriangleInterpolateColor(benchmark::State &state, Multisample multisample)
+{
+	TriangleInterpolateColorBenchmark benchmark(multisample);
+	RunBenchmark(state, benchmark);
+}
+
+static void TriangleSampleTexture(benchmark::State &state, Multisample multisample)
+{
+	TriangleSampleTextureBenchmark benchmark(multisample);
+	RunBenchmark(state, benchmark);
+}
+
+BENCHMARK_CAPTURE(TriangleSolidColor, TriangleSolidColor, Multisample::False)->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(TriangleInterpolateColor, TriangleInterpolateColor, Multisample::False)->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(TriangleSampleTexture, TriangleSampleTexture, Multisample::False)->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(TriangleSolidColor, TriangleSolidColor_Multisample, Multisample::True)->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(TriangleInterpolateColor, TriangleInterpolateColor_Multisample, Multisample::True)->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(TriangleSampleTexture, TriangleSampleTexture_Multisample, Multisample::True)->Unit(benchmark::kMillisecond);
diff --git a/tests/VulkanBenchmarks/VulkanBenchmark.cpp b/tests/VulkanBenchmarks/VulkanBenchmark.cpp
new file mode 100644
index 0000000..41dda63
--- /dev/null
+++ b/tests/VulkanBenchmarks/VulkanBenchmark.cpp
@@ -0,0 +1,59 @@
+// Copyright 2021 The SwiftShader Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "VulkanBenchmark.hpp"
+
+VulkanBenchmark::~VulkanBenchmark()
+{
+	device.waitIdle();
+	device.destroy(nullptr);
+	instance.destroy(nullptr);
+}
+
+void VulkanBenchmark::initialize()
+{
+	// TODO(b/158231104): Other platforms
+#if defined(_WIN32)
+	dl = std::make_unique<vk::DynamicLoader>("./vk_swiftshader.dll");
+#elif defined(__linux__)
+	dl = std::make_unique<vk::DynamicLoader>("./libvk_swiftshader.so");
+#else
+#	error Unimplemented platform
+#endif
+	assert(dl->success());
+
+	PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = dl->getProcAddress<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr");
+	VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr);
+
+	instance = vk::createInstance({}, nullptr);
+	VULKAN_HPP_DEFAULT_DISPATCHER.init(instance);
+
+	std::vector<vk::PhysicalDevice> physicalDevices = instance.enumeratePhysicalDevices();
+	assert(!physicalDevices.empty());
+	physicalDevice = physicalDevices[0];
+
+	const float defaultQueuePriority = 0.0f;
+	vk::DeviceQueueCreateInfo queueCreatInfo;
+	queueCreatInfo.queueFamilyIndex = queueFamilyIndex;
+	queueCreatInfo.queueCount = 1;
+	queueCreatInfo.pQueuePriorities = &defaultQueuePriority;
+
+	vk::DeviceCreateInfo deviceCreateInfo;
+	deviceCreateInfo.queueCreateInfoCount = 1;
+	deviceCreateInfo.pQueueCreateInfos = &queueCreatInfo;
+
+	device = physicalDevice.createDevice(deviceCreateInfo, nullptr);
+
+	queue = device.getQueue(queueFamilyIndex, 0);
+}
diff --git a/tests/VulkanBenchmarks/VulkanBenchmark.hpp b/tests/VulkanBenchmarks/VulkanBenchmark.hpp
new file mode 100644
index 0000000..f724bcc
--- /dev/null
+++ b/tests/VulkanBenchmarks/VulkanBenchmark.hpp
@@ -0,0 +1,41 @@
+// Copyright 2021 The SwiftShader Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef VULKAN_BENCHMARK_HPP_
+#define VULKAN_BENCHMARK_HPP_
+
+#include "VulkanHeaders.hpp"
+
+class VulkanBenchmark
+{
+public:
+	VulkanBenchmark() = default;
+	virtual ~VulkanBenchmark();
+
+	// Call once after construction so that virtual functions may be called during init
+	void initialize();
+
+private:
+	std::unique_ptr<vk::DynamicLoader> dl;
+
+protected:
+	const uint32_t queueFamilyIndex = 0;
+
+	vk::Instance instance;  // Owning handle
+	vk::PhysicalDevice physicalDevice;
+	vk::Device device;  // Owning handle
+	vk::Queue queue;
+};
+
+#endif  // VULKAN_BENCHMARK_HPP_
diff --git a/tests/VulkanBenchmarks/VulkanBenchmarks.cpp b/tests/VulkanBenchmarks/VulkanBenchmarks.cpp
deleted file mode 100644
index 2647640..0000000
--- a/tests/VulkanBenchmarks/VulkanBenchmarks.cpp
+++ /dev/null
@@ -1,1062 +0,0 @@
-// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "benchmark/benchmark.h"
-
-#include "Buffer.hpp"
-#include "Framebuffer.hpp"
-#include "Image.hpp"
-#include "Swapchain.hpp"
-#include "Util.hpp"
-#include "VulkanHeaders.hpp"
-#include "Window.hpp"
-
-#include <cassert>
-#include <vector>
-
-#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))
-
-class VulkanBenchmark
-{
-public:
-	VulkanBenchmark()
-	{
-	}
-
-	virtual ~VulkanBenchmark()
-	{
-		device.waitIdle();
-		device.destroy(nullptr);
-		instance.destroy(nullptr);
-	}
-
-	// Call once after construction so that virtual functions may be called during init
-	void initialize()
-	{
-		// TODO(b/158231104): Other platforms
-#if defined(_WIN32)
-		dl = std::make_unique<vk::DynamicLoader>("./vk_swiftshader.dll");
-#elif defined(__linux__)
-		dl = std::make_unique<vk::DynamicLoader>("./libvk_swiftshader.so");
-#else
-#	error Unimplemented platform
-#endif
-		assert(dl->success());
-
-		PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = dl->getProcAddress<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr");
-		VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr);
-
-		instance = vk::createInstance({}, nullptr);
-		VULKAN_HPP_DEFAULT_DISPATCHER.init(instance);
-
-		std::vector<vk::PhysicalDevice> physicalDevices = instance.enumeratePhysicalDevices();
-		assert(!physicalDevices.empty());
-		physicalDevice = physicalDevices[0];
-
-		const float defaultQueuePriority = 0.0f;
-		vk::DeviceQueueCreateInfo queueCreatInfo;
-		queueCreatInfo.queueFamilyIndex = queueFamilyIndex;
-		queueCreatInfo.queueCount = 1;
-		queueCreatInfo.pQueuePriorities = &defaultQueuePriority;
-
-		vk::DeviceCreateInfo deviceCreateInfo;
-		deviceCreateInfo.queueCreateInfoCount = 1;
-		deviceCreateInfo.pQueueCreateInfos = &queueCreatInfo;
-
-		device = physicalDevice.createDevice(deviceCreateInfo, nullptr);
-
-		queue = device.getQueue(queueFamilyIndex, 0);
-	}
-
-private:
-	std::unique_ptr<vk::DynamicLoader> dl;
-
-protected:
-	const uint32_t queueFamilyIndex = 0;
-
-	vk::Instance instance;  // Owning handle
-	vk::PhysicalDevice physicalDevice;
-	vk::Device device;  // Owning handle
-	vk::Queue queue;
-};
-
-class ClearImageBenchmark : public VulkanBenchmark
-{
-public:
-	void initialize(vk::Format clearFormat, vk::ImageAspectFlagBits clearAspect)
-	{
-		VulkanBenchmark::initialize();
-
-		vk::ImageCreateInfo imageInfo;
-		imageInfo.imageType = vk::ImageType::e2D;
-		imageInfo.format = clearFormat;
-		imageInfo.tiling = vk::ImageTiling::eOptimal;
-		imageInfo.initialLayout = vk::ImageLayout::eGeneral;
-		imageInfo.usage = vk::ImageUsageFlagBits::eTransferDst;
-		imageInfo.samples = vk::SampleCountFlagBits::e4;
-		imageInfo.extent = vk::Extent3D(1024, 1024, 1);
-		imageInfo.mipLevels = 1;
-		imageInfo.arrayLayers = 1;
-
-		image = device.createImage(imageInfo);
-
-		vk::MemoryRequirements memoryRequirements = device.getImageMemoryRequirements(image);
-
-		vk::MemoryAllocateInfo allocateInfo;
-		allocateInfo.allocationSize = memoryRequirements.size;
-		allocateInfo.memoryTypeIndex = 0;
-
-		memory = device.allocateMemory(allocateInfo);
-
-		device.bindImageMemory(image, memory, 0);
-
-		vk::CommandPoolCreateInfo commandPoolCreateInfo;
-		commandPoolCreateInfo.queueFamilyIndex = queueFamilyIndex;
-
-		commandPool = device.createCommandPool(commandPoolCreateInfo);
-
-		vk::CommandBufferAllocateInfo commandBufferAllocateInfo;
-		commandBufferAllocateInfo.commandPool = commandPool;
-		commandBufferAllocateInfo.commandBufferCount = 1;
-
-		commandBuffer = device.allocateCommandBuffers(commandBufferAllocateInfo)[0];
-
-		vk::CommandBufferBeginInfo commandBufferBeginInfo;
-		commandBufferBeginInfo.flags = {};
-
-		commandBuffer.begin(commandBufferBeginInfo);
-
-		vk::ImageSubresourceRange range;
-		range.aspectMask = clearAspect;
-		range.baseMipLevel = 0;
-		range.levelCount = 1;
-		range.baseArrayLayer = 0;
-		range.layerCount = 1;
-
-		if(clearAspect == vk::ImageAspectFlagBits::eColor)
-		{
-			vk::ClearColorValue clearColorValue;
-			clearColorValue.float32[0] = 0.0f;
-			clearColorValue.float32[1] = 1.0f;
-			clearColorValue.float32[2] = 0.0f;
-			clearColorValue.float32[3] = 1.0f;
-
-			commandBuffer.clearColorImage(image, vk::ImageLayout::eGeneral, &clearColorValue, 1, &range);
-		}
-		else if(clearAspect == vk::ImageAspectFlagBits::eDepth)
-		{
-			vk::ClearDepthStencilValue clearDepthStencilValue;
-			clearDepthStencilValue.depth = 1.0f;
-			clearDepthStencilValue.stencil = 0xFF;
-
-			commandBuffer.clearDepthStencilImage(image, vk::ImageLayout::eGeneral, &clearDepthStencilValue, 1, &range);
-		}
-		else
-			assert(false);
-
-		commandBuffer.end();
-	}
-
-	~ClearImageBenchmark()
-	{
-		device.freeCommandBuffers(commandPool, 1, &commandBuffer);
-		device.destroyCommandPool(commandPool, nullptr);
-		device.freeMemory(memory, nullptr);
-		device.destroyImage(image, nullptr);
-	}
-
-	void clear()
-	{
-		vk::SubmitInfo submitInfo;
-		submitInfo.commandBufferCount = 1;
-		submitInfo.pCommandBuffers = &commandBuffer;
-
-		queue.submit(1, &submitInfo, nullptr);
-		queue.waitIdle();
-	}
-
-private:
-	vk::Image image;                  // Owning handle
-	vk::DeviceMemory memory;          // Owning handle
-	vk::CommandPool commandPool;      // Owning handle
-	vk::CommandBuffer commandBuffer;  // Owning handle
-};
-
-static void ClearImage(benchmark::State &state, vk::Format clearFormat, vk::ImageAspectFlagBits clearAspect)
-{
-	ClearImageBenchmark benchmark;
-	benchmark.initialize(clearFormat, clearAspect);
-
-	// Execute once to have the Reactor routine generated.
-	benchmark.clear();
-
-	for(auto _ : state)
-	{
-		benchmark.clear();
-	}
-}
-
-enum class Multisample
-{
-	False,
-	True
-};
-
-class DrawBenchmark : public VulkanBenchmark
-{
-public:
-	DrawBenchmark(Multisample multisample)
-	    : multisample(multisample == Multisample::True)
-	{
-	}
-
-	void initialize()
-	{
-		VulkanBenchmark::initialize();
-
-		window.reset(new Window(instance, windowSize));
-		swapchain.reset(new Swapchain(physicalDevice, device, *window));
-
-		renderPass = createRenderPass(swapchain->colorFormat);
-		createFramebuffers(renderPass);
-
-		prepareVertices();
-
-		pipeline = createGraphicsPipeline(renderPass);
-
-		createSynchronizationPrimitives();
-
-		createCommandBuffers(renderPass);
-	}
-
-	~DrawBenchmark()
-	{
-		device.freeCommandBuffers(commandPool, commandBuffers);
-
-		device.destroyDescriptorPool(descriptorPool);
-		for(auto &sampler : samplers)
-		{
-			device.destroySampler(sampler, nullptr);
-		}
-		images.clear();
-		device.destroyCommandPool(commandPool, nullptr);
-
-		for(auto &fence : waitFences)
-		{
-			device.destroyFence(fence, nullptr);
-		}
-
-		device.destroySemaphore(renderCompleteSemaphore, nullptr);
-		device.destroySemaphore(presentCompleteSemaphore, nullptr);
-
-		device.destroyPipeline(pipeline);
-		device.destroyPipelineLayout(pipelineLayout, nullptr);
-		device.destroyDescriptorSetLayout(descriptorSetLayout);
-
-		device.freeMemory(vertices.memory, nullptr);
-		device.destroyBuffer(vertices.buffer, nullptr);
-
-		for(auto &framebuffer : framebuffers)
-		{
-			framebuffer.reset();
-		}
-
-		device.destroyRenderPass(renderPass, nullptr);
-
-		swapchain.reset();
-		window.reset();
-	}
-
-	void renderFrame()
-	{
-		swapchain->acquireNextImage(presentCompleteSemaphore, currentFrameBuffer);
-
-		device.waitForFences(1, &waitFences[currentFrameBuffer], VK_TRUE, UINT64_MAX);
-		device.resetFences(1, &waitFences[currentFrameBuffer]);
-
-		vk::PipelineStageFlags waitStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
-
-		vk::SubmitInfo submitInfo;
-		submitInfo.pWaitDstStageMask = &waitStageMask;
-		submitInfo.pWaitSemaphores = &presentCompleteSemaphore;
-		submitInfo.waitSemaphoreCount = 1;
-		submitInfo.pSignalSemaphores = &renderCompleteSemaphore;
-		submitInfo.signalSemaphoreCount = 1;
-		submitInfo.pCommandBuffers = &commandBuffers[currentFrameBuffer];
-		submitInfo.commandBufferCount = 1;
-
-		queue.submit(1, &submitInfo, waitFences[currentFrameBuffer]);
-
-		swapchain->queuePresent(queue, currentFrameBuffer, renderCompleteSemaphore);
-	}
-
-	void show()
-	{
-		window->show();
-	}
-
-private:
-	void createSynchronizationPrimitives()
-	{
-		vk::SemaphoreCreateInfo semaphoreCreateInfo;
-		presentCompleteSemaphore = device.createSemaphore(semaphoreCreateInfo);
-		renderCompleteSemaphore = device.createSemaphore(semaphoreCreateInfo);
-
-		vk::FenceCreateInfo fenceCreateInfo;
-		fenceCreateInfo.flags = vk::FenceCreateFlagBits::eSignaled;
-		waitFences.resize(swapchain->imageCount());
-		for(auto &fence : waitFences)
-		{
-			fence = device.createFence(fenceCreateInfo);
-		}
-	}
-
-	void createCommandBuffers(vk::RenderPass renderPass)
-	{
-		vk::CommandPoolCreateInfo commandPoolCreateInfo;
-		commandPoolCreateInfo.queueFamilyIndex = queueFamilyIndex;
-		commandPoolCreateInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;
-		commandPool = device.createCommandPool(commandPoolCreateInfo);
-
-		std::vector<vk::DescriptorSet> descriptorSets;
-		if(descriptorSetLayout)
-		{
-			std::array<vk::DescriptorPoolSize, 1> poolSizes = {};
-			poolSizes[0].type = vk::DescriptorType::eCombinedImageSampler;
-			poolSizes[0].descriptorCount = 1;
-
-			vk::DescriptorPoolCreateInfo poolInfo;
-			poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
-			poolInfo.pPoolSizes = poolSizes.data();
-			poolInfo.maxSets = 1;
-
-			descriptorPool = device.createDescriptorPool(poolInfo);
-
-			std::vector<vk::DescriptorSetLayout> layouts(1, descriptorSetLayout);
-			vk::DescriptorSetAllocateInfo allocInfo;
-			allocInfo.descriptorPool = descriptorPool;
-			allocInfo.descriptorSetCount = 1;
-			allocInfo.pSetLayouts = layouts.data();
-
-			descriptorSets = device.allocateDescriptorSets(allocInfo);
-
-			doUpdateDescriptorSet(commandPool, descriptorSets[0]);
-		}
-
-		vk::CommandBufferAllocateInfo commandBufferAllocateInfo;
-		commandBufferAllocateInfo.commandPool = commandPool;
-		commandBufferAllocateInfo.commandBufferCount = static_cast<uint32_t>(swapchain->imageCount());
-		commandBufferAllocateInfo.level = vk::CommandBufferLevel::ePrimary;
-
-		commandBuffers = device.allocateCommandBuffers(commandBufferAllocateInfo);
-
-		for(size_t i = 0; i < commandBuffers.size(); i++)
-		{
-			vk::CommandBufferBeginInfo commandBufferBeginInfo;
-			commandBuffers[i].begin(commandBufferBeginInfo);
-
-			vk::ClearValue clearValues[1];
-			clearValues[0].color = vk::ClearColorValue(std::array<float, 4>{ 0.5f, 0.5f, 0.5f, 1.0f });
-
-			vk::RenderPassBeginInfo renderPassBeginInfo;
-			renderPassBeginInfo.framebuffer = framebuffers[i]->getFramebuffer();
-			renderPassBeginInfo.renderPass = renderPass;
-			renderPassBeginInfo.renderArea.offset.x = 0;
-			renderPassBeginInfo.renderArea.offset.y = 0;
-			renderPassBeginInfo.renderArea.extent = windowSize;
-			renderPassBeginInfo.clearValueCount = ARRAY_SIZE(clearValues);
-			renderPassBeginInfo.pClearValues = clearValues;
-			commandBuffers[i].beginRenderPass(renderPassBeginInfo, vk::SubpassContents::eInline);
-
-			// Set dynamic state
-			vk::Viewport viewport(0.0f, 0.0f, static_cast<float>(windowSize.width), static_cast<float>(windowSize.height), 0.0f, 1.0f);
-			commandBuffers[i].setViewport(0, 1, &viewport);
-
-			vk::Rect2D scissor(vk::Offset2D(0, 0), windowSize);
-			commandBuffers[i].setScissor(0, 1, &scissor);
-
-			if(!descriptorSets.empty())
-			{
-				commandBuffers[i].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout, 0, 1, &descriptorSets[0], 0, nullptr);
-			}
-
-			// Draw
-			commandBuffers[i].bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
-			VULKAN_HPP_NAMESPACE::DeviceSize offset = 0;
-			commandBuffers[i].bindVertexBuffers(0, 1, &vertices.buffer, &offset);
-			commandBuffers[i].draw(vertices.numVertices, 1, 0, 0);
-
-			commandBuffers[i].endRenderPass();
-			commandBuffers[i].end();
-		}
-	}
-
-	void prepareVertices()
-	{
-		doCreateVertexBuffers();
-	}
-
-	void createFramebuffers(vk::RenderPass renderPass)
-	{
-		framebuffers.resize(swapchain->imageCount());
-
-		for(size_t i = 0; i < framebuffers.size(); i++)
-		{
-			framebuffers[i].reset(new Framebuffer(device, swapchain->getImageView(i), swapchain->colorFormat, renderPass, swapchain->getExtent(), multisample));
-		}
-	}
-
-	vk::RenderPass createRenderPass(vk::Format colorFormat)
-	{
-		std::vector<vk::AttachmentDescription> attachments(multisample ? 2 : 1);
-
-		if(multisample)
-		{
-			// Color attachment
-			attachments[0].format = colorFormat;
-			attachments[0].samples = vk::SampleCountFlagBits::e4;
-			attachments[0].loadOp = vk::AttachmentLoadOp::eClear;
-			attachments[0].storeOp = vk::AttachmentStoreOp::eStore;
-			attachments[0].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
-			attachments[0].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
-			attachments[0].initialLayout = vk::ImageLayout::eUndefined;
-			attachments[0].finalLayout = vk::ImageLayout::eColorAttachmentOptimal;
-
-			// Resolve attachment
-			attachments[1].format = colorFormat;
-			attachments[1].samples = vk::SampleCountFlagBits::e1;
-			attachments[1].loadOp = vk::AttachmentLoadOp::eDontCare;
-			attachments[1].storeOp = vk::AttachmentStoreOp::eStore;
-			attachments[1].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
-			attachments[1].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
-			attachments[1].initialLayout = vk::ImageLayout::eUndefined;
-			attachments[1].finalLayout = vk::ImageLayout::ePresentSrcKHR;
-		}
-		else
-		{
-			attachments[0].format = colorFormat;
-			attachments[0].samples = vk::SampleCountFlagBits::e1;
-			attachments[0].loadOp = vk::AttachmentLoadOp::eDontCare;
-			attachments[0].storeOp = vk::AttachmentStoreOp::eStore;
-			attachments[0].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
-			attachments[0].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
-			attachments[0].initialLayout = vk::ImageLayout::eUndefined;
-			attachments[0].finalLayout = vk::ImageLayout::ePresentSrcKHR;
-		}
-
-		vk::AttachmentReference attachment0;
-		attachment0.attachment = 0;
-		attachment0.layout = vk::ImageLayout::eColorAttachmentOptimal;
-
-		vk::AttachmentReference attachment1;
-		attachment1.attachment = 1;
-		attachment1.layout = vk::ImageLayout::eColorAttachmentOptimal;
-
-		vk::SubpassDescription subpassDescription;
-		subpassDescription.pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
-		subpassDescription.colorAttachmentCount = 1;
-		subpassDescription.pResolveAttachments = multisample ? &attachment1 : nullptr;
-		subpassDescription.pColorAttachments = &attachment0;
-
-		std::array<vk::SubpassDependency, 2> dependencies;
-
-		dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
-		dependencies[0].dstSubpass = 0;
-		dependencies[0].srcStageMask = vk::PipelineStageFlagBits::eBottomOfPipe;
-		dependencies[0].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
-		dependencies[0].srcAccessMask = vk::AccessFlagBits::eMemoryRead;
-		dependencies[0].dstAccessMask = vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite;
-		dependencies[0].dependencyFlags = vk::DependencyFlagBits::eByRegion;
-
-		dependencies[1].srcSubpass = 0;
-		dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
-		dependencies[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
-		dependencies[1].dstStageMask = vk::PipelineStageFlagBits::eBottomOfPipe;
-		dependencies[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite;
-		dependencies[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead;
-		dependencies[1].dependencyFlags = vk::DependencyFlagBits::eByRegion;
-
-		vk::RenderPassCreateInfo renderPassInfo;
-		renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
-		renderPassInfo.pAttachments = attachments.data();
-		renderPassInfo.subpassCount = 1;
-		renderPassInfo.pSubpasses = &subpassDescription;
-		renderPassInfo.dependencyCount = static_cast<uint32_t>(dependencies.size());
-		renderPassInfo.pDependencies = dependencies.data();
-
-		return device.createRenderPass(renderPassInfo);
-	}
-
-	vk::Pipeline createGraphicsPipeline(vk::RenderPass renderPass)
-	{
-		auto setLayoutBindings = doCreateDescriptorSetLayouts();
-
-		std::vector<vk::DescriptorSetLayout> setLayouts;
-		if(!setLayoutBindings.empty())
-		{
-			vk::DescriptorSetLayoutCreateInfo layoutInfo;
-			layoutInfo.bindingCount = static_cast<uint32_t>(setLayoutBindings.size());
-			layoutInfo.pBindings = setLayoutBindings.data();
-			descriptorSetLayout = device.createDescriptorSetLayout(layoutInfo);
-
-			setLayouts.push_back(descriptorSetLayout);
-		}
-
-		vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo;
-		pipelineLayoutCreateInfo.setLayoutCount = static_cast<uint32_t>(setLayouts.size());
-		pipelineLayoutCreateInfo.pSetLayouts = setLayouts.data();
-		pipelineLayout = device.createPipelineLayout(pipelineLayoutCreateInfo);
-
-		vk::GraphicsPipelineCreateInfo pipelineCreateInfo;
-		pipelineCreateInfo.layout = pipelineLayout;
-		pipelineCreateInfo.renderPass = renderPass;
-
-		vk::PipelineInputAssemblyStateCreateInfo inputAssemblyState;
-		inputAssemblyState.topology = vk::PrimitiveTopology::eTriangleList;
-
-		vk::PipelineRasterizationStateCreateInfo rasterizationState;
-		rasterizationState.depthClampEnable = VK_FALSE;
-		rasterizationState.rasterizerDiscardEnable = VK_FALSE;
-		rasterizationState.polygonMode = vk::PolygonMode::eFill;
-		rasterizationState.cullMode = vk::CullModeFlagBits::eNone;
-		rasterizationState.frontFace = vk::FrontFace::eCounterClockwise;
-		rasterizationState.depthBiasEnable = VK_FALSE;
-		rasterizationState.lineWidth = 1.0f;
-
-		vk::PipelineColorBlendAttachmentState blendAttachmentState;
-		blendAttachmentState.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA;
-		blendAttachmentState.blendEnable = VK_FALSE;
-		vk::PipelineColorBlendStateCreateInfo colorBlendState;
-		colorBlendState.attachmentCount = 1;
-		colorBlendState.pAttachments = &blendAttachmentState;
-
-		vk::PipelineViewportStateCreateInfo viewportState;
-		viewportState.viewportCount = 1;
-		viewportState.scissorCount = 1;
-
-		std::vector<vk::DynamicState> dynamicStateEnables;
-		dynamicStateEnables.push_back(vk::DynamicState::eViewport);
-		dynamicStateEnables.push_back(vk::DynamicState::eScissor);
-		vk::PipelineDynamicStateCreateInfo dynamicState = {};
-		dynamicState.pDynamicStates = dynamicStateEnables.data();
-		dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStateEnables.size());
-
-		vk::PipelineDepthStencilStateCreateInfo depthStencilState;
-		depthStencilState.depthTestEnable = VK_FALSE;
-		depthStencilState.depthWriteEnable = VK_FALSE;
-		depthStencilState.depthCompareOp = vk::CompareOp::eLessOrEqual;
-		depthStencilState.depthBoundsTestEnable = VK_FALSE;
-		depthStencilState.back.failOp = vk::StencilOp::eKeep;
-		depthStencilState.back.passOp = vk::StencilOp::eKeep;
-		depthStencilState.back.compareOp = vk::CompareOp::eAlways;
-		depthStencilState.stencilTestEnable = VK_FALSE;
-		depthStencilState.front = depthStencilState.back;
-
-		vk::PipelineMultisampleStateCreateInfo multisampleState;
-		multisampleState.rasterizationSamples = multisample ? vk::SampleCountFlagBits::e4 : vk::SampleCountFlagBits::e1;
-		multisampleState.pSampleMask = nullptr;
-
-		vk::ShaderModule vertexModule = doCreateVertexShader();
-		vk::ShaderModule fragmentModule = doCreateFragmentShader();
-
-		assert(vertexModule);    // TODO: if nullptr, use a default
-		assert(fragmentModule);  // TODO: if nullptr, use a default
-
-		std::array<vk::PipelineShaderStageCreateInfo, 2> shaderStages;
-
-		shaderStages[0].module = vertexModule;
-		shaderStages[0].stage = vk::ShaderStageFlagBits::eVertex;
-		shaderStages[0].pName = "main";
-
-		shaderStages[1].module = fragmentModule;
-		shaderStages[1].stage = vk::ShaderStageFlagBits::eFragment;
-		shaderStages[1].pName = "main";
-
-		pipelineCreateInfo.stageCount = static_cast<uint32_t>(shaderStages.size());
-		pipelineCreateInfo.pStages = shaderStages.data();
-		pipelineCreateInfo.pVertexInputState = &vertices.inputState;
-		pipelineCreateInfo.pInputAssemblyState = &inputAssemblyState;
-		pipelineCreateInfo.pRasterizationState = &rasterizationState;
-		pipelineCreateInfo.pColorBlendState = &colorBlendState;
-		pipelineCreateInfo.pMultisampleState = &multisampleState;
-		pipelineCreateInfo.pViewportState = &viewportState;
-		pipelineCreateInfo.pDepthStencilState = &depthStencilState;
-		pipelineCreateInfo.renderPass = renderPass;
-		pipelineCreateInfo.pDynamicState = &dynamicState;
-
-		auto pipeline = device.createGraphicsPipeline(nullptr, pipelineCreateInfo).value;
-
-		device.destroyShaderModule(fragmentModule);
-		device.destroyShaderModule(vertexModule);
-
-		return pipeline;
-	}
-
-protected:
-	/////////////////////////
-	// Hooks
-	/////////////////////////
-
-	// Called from prepareVertices.
-	// Child type may call addVertexBuffer() from this function.
-	virtual void doCreateVertexBuffers() {}
-
-	// Called from createGraphicsPipeline.
-	// Child type may return vector of DescriptorSetLayoutBindings for which a DescriptorSetLayout
-	// will be created and stored in this->descriptorSetLayout.
-	virtual std::vector<vk::DescriptorSetLayoutBinding> doCreateDescriptorSetLayouts()
-	{
-		return {};
-	}
-
-	// Called from createGraphicsPipeline.
-	// Child type may call createShaderModule() and return the result.
-	virtual vk::ShaderModule doCreateVertexShader()
-	{
-		return nullptr;
-	}
-
-	// Called from createGraphicsPipeline.
-	// Child type may call createShaderModule() and return the result.
-	virtual vk::ShaderModule doCreateFragmentShader()
-	{
-		return nullptr;
-	}
-
-	// Called from createCommandBuffers.
-	// Child type may create resources (addImage, addSampler, etc.), and make sure to
-	// call device.updateDescriptorSets.
-	virtual void doUpdateDescriptorSet(vk::CommandPool &commandPool, vk::DescriptorSet &descriptorSet)
-	{
-	}
-
-	/////////////////////////
-	// Resource Management
-	/////////////////////////
-
-	// Call from doCreateFragmentShader()
-	vk::ShaderModule createShaderModule(const char *glslSource, EShLanguage glslLanguage)
-	{
-		auto spirv = Util::compileGLSLtoSPIRV(glslSource, glslLanguage);
-
-		vk::ShaderModuleCreateInfo moduleCreateInfo;
-		moduleCreateInfo.codeSize = spirv.size() * sizeof(uint32_t);
-		moduleCreateInfo.pCode = (uint32_t *)spirv.data();
-
-		return device.createShaderModule(moduleCreateInfo);
-	}
-
-	// Call from doCreateVertexBuffers()
-	template<typename VertexType>
-	void addVertexBuffer(VertexType *vertexBufferData, size_t vertexBufferDataSize, std::vector<vk::VertexInputAttributeDescription> inputAttributes)
-	{
-		assert(!vertices.buffer);  // For now, only support adding once
-
-		vk::BufferCreateInfo vertexBufferInfo;
-		vertexBufferInfo.size = vertexBufferDataSize;
-		vertexBufferInfo.usage = vk::BufferUsageFlagBits::eVertexBuffer;
-		vertices.buffer = device.createBuffer(vertexBufferInfo);
-
-		vk::MemoryAllocateInfo memoryAllocateInfo;
-		vk::MemoryRequirements memoryRequirements = device.getBufferMemoryRequirements(vertices.buffer);
-		memoryAllocateInfo.allocationSize = memoryRequirements.size;
-		memoryAllocateInfo.memoryTypeIndex = Util::getMemoryTypeIndex(physicalDevice, memoryRequirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
-		vertices.memory = device.allocateMemory(memoryAllocateInfo);
-
-		void *data = device.mapMemory(vertices.memory, 0, VK_WHOLE_SIZE);
-		memcpy(data, vertexBufferData, vertexBufferDataSize);
-		device.unmapMemory(vertices.memory);
-		device.bindBufferMemory(vertices.buffer, vertices.memory, 0);
-
-		vertices.inputBinding.binding = 0;
-		vertices.inputBinding.stride = sizeof(VertexType);
-		vertices.inputBinding.inputRate = vk::VertexInputRate::eVertex;
-
-		vertices.inputAttributes = std::move(inputAttributes);
-
-		vertices.inputState.vertexBindingDescriptionCount = 1;
-		vertices.inputState.pVertexBindingDescriptions = &vertices.inputBinding;
-		vertices.inputState.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertices.inputAttributes.size());
-		vertices.inputState.pVertexAttributeDescriptions = vertices.inputAttributes.data();
-
-		// Note that we assume data is tightly packed
-		vertices.numVertices = static_cast<uint32_t>(vertexBufferDataSize / sizeof(VertexType));
-	}
-
-	template<typename T>
-	struct Resource
-	{
-		size_t id;
-		T &obj;
-	};
-
-	template<typename... Args>
-	Resource<Image> addImage(Args &&... args)
-	{
-		images.emplace_back(std::make_unique<Image>(std::forward<Args>(args)...));
-		return { images.size() - 1, *images.back() };
-	}
-
-	Image &getImageById(size_t id)
-	{
-		return *images[id].get();
-	}
-
-	Resource<vk::Sampler> addSampler(const vk::SamplerCreateInfo &samplerCreateInfo)
-	{
-		auto sampler = device.createSampler(samplerCreateInfo);
-		samplers.push_back(sampler);
-		return { samplers.size() - 1, sampler };
-	}
-
-	vk::Sampler &getSamplerById(size_t id)
-	{
-		return samplers[id];
-	}
-
-private:
-	const vk::Extent2D windowSize = { 1280, 720 };
-	const bool multisample;
-
-	std::unique_ptr<Window> window;
-	std::unique_ptr<Swapchain> swapchain;
-
-	vk::RenderPass renderPass;  // Owning handle
-	std::vector<std::unique_ptr<Framebuffer>> framebuffers;
-	uint32_t currentFrameBuffer = 0;
-
-	struct VertexBuffer
-	{
-		vk::Buffer buffer;        // Owning handle
-		vk::DeviceMemory memory;  // Owning handle
-
-		vk::VertexInputBindingDescription inputBinding;
-		std::vector<vk::VertexInputAttributeDescription> inputAttributes;
-		vk::PipelineVertexInputStateCreateInfo inputState;
-
-		uint32_t numVertices;
-	} vertices;
-
-	vk::DescriptorSetLayout descriptorSetLayout;  // Owning handle
-	vk::PipelineLayout pipelineLayout;            // Owning handle
-	vk::Pipeline pipeline;                        // Owning handle
-
-	vk::Semaphore presentCompleteSemaphore;  // Owning handle
-	vk::Semaphore renderCompleteSemaphore;   // Owning handle
-	std::vector<vk::Fence> waitFences;       // Owning handles
-
-	vk::CommandPool commandPool;        // Owning handle
-	vk::DescriptorPool descriptorPool;  // Owning handle
-
-	// Resources
-	std::vector<std::unique_ptr<Image>> images;
-	std::vector<vk::Sampler> samplers;  // Owning handles
-
-	std::vector<vk::CommandBuffer> commandBuffers;  // Owning handles
-};
-
-class TriangleSolidColorBenchmark : public DrawBenchmark
-{
-public:
-	TriangleSolidColorBenchmark(Multisample multisample)
-	    : DrawBenchmark(multisample)
-	{}
-
-protected:
-	void doCreateVertexBuffers() override
-	{
-		struct Vertex
-		{
-			float position[3];
-		};
-
-		Vertex vertexBufferData[] = {
-			{ { 1.0f, 1.0f, 0.5f } },
-			{ { -1.0f, 1.0f, 0.5f } },
-			{ { 0.0f, -1.0f, 0.5f } }
-		};
-
-		std::vector<vk::VertexInputAttributeDescription> inputAttributes;
-		inputAttributes.push_back(vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, position)));
-
-		addVertexBuffer(vertexBufferData, sizeof(vertexBufferData), std::move(inputAttributes));
-	}
-
-	vk::ShaderModule doCreateVertexShader() override
-	{
-		const char *vertexShader = R"(#version 310 es
-			layout(location = 0) in vec3 inPos;
-
-			void main()
-			{
-				gl_Position = vec4(inPos.xyz, 1.0);
-			})";
-
-		return createShaderModule(vertexShader, EShLanguage::EShLangVertex);
-	}
-
-	vk::ShaderModule doCreateFragmentShader() override
-	{
-		const char *fragmentShader = R"(#version 310 es
-			precision highp float;
-
-			layout(location = 0) out vec4 outColor;
-
-			void main()
-			{
-				outColor = vec4(1.0, 1.0, 1.0, 1.0);
-			})";
-
-		return createShaderModule(fragmentShader, EShLanguage::EShLangFragment);
-	}
-};
-
-class TriangleInterpolateColorBenchmark : public DrawBenchmark
-{
-public:
-	TriangleInterpolateColorBenchmark(Multisample multisample)
-	    : DrawBenchmark(multisample)
-	{}
-
-protected:
-	void doCreateVertexBuffers() override
-	{
-		struct Vertex
-		{
-			float position[3];
-			float color[3];
-		};
-
-		Vertex vertexBufferData[] = {
-			{ { 1.0f, 1.0f, 0.05f }, { 1.0f, 0.0f, 0.0f } },
-			{ { -1.0f, 1.0f, 0.5f }, { 0.0f, 1.0f, 0.0f } },
-			{ { 0.0f, -1.0f, 0.5f }, { 0.0f, 0.0f, 1.0f } }
-		};
-
-		std::vector<vk::VertexInputAttributeDescription> inputAttributes;
-		inputAttributes.push_back(vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, position)));
-		inputAttributes.push_back(vk::VertexInputAttributeDescription(1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)));
-
-		addVertexBuffer(vertexBufferData, sizeof(vertexBufferData), std::move(inputAttributes));
-	}
-
-	vk::ShaderModule doCreateVertexShader() override
-	{
-		const char *vertexShader = R"(#version 310 es
-			layout(location = 0) in vec3 inPos;
-			layout(location = 1) in vec3 inColor;
-
-			layout(location = 0) out vec3 outColor;
-
-			void main()
-			{
-				outColor = inColor;
-				gl_Position = vec4(inPos.xyz, 1.0);
-			})";
-
-		return createShaderModule(vertexShader, EShLanguage::EShLangVertex);
-	}
-
-	vk::ShaderModule doCreateFragmentShader() override
-	{
-		const char *fragmentShader = R"(#version 310 es
-			precision highp float;
-
-			layout(location = 0) in vec3 inColor;
-
-			layout(location = 0) out vec4 outColor;
-
-			void main()
-			{
-				outColor = vec4(inColor, 1.0);
-			})";
-
-		return createShaderModule(fragmentShader, EShLanguage::EShLangFragment);
-	}
-};
-
-class TriangleSampleTextureBenchmark : public DrawBenchmark
-{
-public:
-	TriangleSampleTextureBenchmark(Multisample multisample)
-	    : DrawBenchmark(multisample)
-	{}
-
-protected:
-	void doCreateVertexBuffers() override
-	{
-		struct Vertex
-		{
-			float position[3];
-			float color[3];
-			float texCoord[2];
-		};
-
-		Vertex vertexBufferData[] = {
-			{ { 1.0f, 1.0f, 0.5f }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f } },
-			{ { -1.0f, 1.0f, 0.5f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f } },
-			{ { 0.0f, -1.0f, 0.5f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f } }
-		};
-
-		std::vector<vk::VertexInputAttributeDescription> inputAttributes;
-		inputAttributes.push_back(vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, position)));
-		inputAttributes.push_back(vk::VertexInputAttributeDescription(1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)));
-		inputAttributes.push_back(vk::VertexInputAttributeDescription(2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, texCoord)));
-
-		addVertexBuffer(vertexBufferData, sizeof(vertexBufferData), std::move(inputAttributes));
-	}
-
-	vk::ShaderModule doCreateVertexShader() override
-	{
-		const char *vertexShader = R"(#version 310 es
-			layout(location = 0) in vec3 inPos;
-			layout(location = 1) in vec3 inColor;
-
-			layout(location = 0) out vec3 outColor;
-			layout(location = 1) out vec2 fragTexCoord;
-
-			void main()
-			{
-				outColor = inColor;
-				gl_Position = vec4(inPos.xyz, 1.0);
-				fragTexCoord = inPos.xy;
-			})";
-
-		return createShaderModule(vertexShader, EShLanguage::EShLangVertex);
-	}
-
-	vk::ShaderModule doCreateFragmentShader() override
-	{
-		const char *fragmentShader = R"(#version 310 es
-			precision highp float;
-
-			layout(location = 0) in vec3 inColor;
-			layout(location = 1) in vec2 fragTexCoord;
-
-			layout(location = 0) out vec4 outColor;
-
-			layout(binding = 0) uniform sampler2D texSampler;
-
-			void main()
-			{
-				outColor = texture(texSampler, fragTexCoord) * vec4(inColor, 1.0);
-			})";
-
-		return createShaderModule(fragmentShader, EShLanguage::EShLangFragment);
-	}
-
-	std::vector<vk::DescriptorSetLayoutBinding> doCreateDescriptorSetLayouts() override
-	{
-		vk::DescriptorSetLayoutBinding samplerLayoutBinding;
-		samplerLayoutBinding.binding = 1;
-		samplerLayoutBinding.descriptorCount = 1;
-		samplerLayoutBinding.descriptorType = vk::DescriptorType::eCombinedImageSampler;
-		samplerLayoutBinding.pImmutableSamplers = nullptr;
-		samplerLayoutBinding.stageFlags = vk::ShaderStageFlagBits::eFragment;
-
-		return { samplerLayoutBinding };
-	}
-
-	void doUpdateDescriptorSet(vk::CommandPool &commandPool, vk::DescriptorSet &descriptorSet) override
-	{
-		auto &texture = addImage(device, 16, 16, vk::Format::eR8G8B8A8Unorm).obj;
-
-		// Fill texture with white
-		vk::DeviceSize bufferSize = 16 * 16 * 4;
-		Buffer buffer(device, bufferSize, vk::BufferUsageFlagBits::eTransferSrc);
-		void *data = buffer.mapMemory();
-		memset(data, 255, bufferSize);
-		buffer.unmapMemory();
-
-		Util::transitionImageLayout(device, commandPool, queue, texture.getImage(), vk::Format::eR8G8B8A8Unorm, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal);
-		Util::copyBufferToImage(device, commandPool, queue, buffer.getBuffer(), texture.getImage(), 16, 16);
-		Util::transitionImageLayout(device, commandPool, queue, texture.getImage(), vk::Format::eR8G8B8A8Unorm, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);
-
-		vk::SamplerCreateInfo samplerInfo;
-		samplerInfo.magFilter = vk::Filter::eLinear;
-		samplerInfo.minFilter = vk::Filter::eLinear;
-		samplerInfo.addressModeU = vk::SamplerAddressMode::eRepeat;
-		samplerInfo.addressModeV = vk::SamplerAddressMode::eRepeat;
-		samplerInfo.addressModeW = vk::SamplerAddressMode::eRepeat;
-		samplerInfo.anisotropyEnable = VK_FALSE;
-		samplerInfo.unnormalizedCoordinates = VK_FALSE;
-		samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear;
-		samplerInfo.mipLodBias = 0.0f;
-		samplerInfo.minLod = 0.0f;
-		samplerInfo.maxLod = 0.0f;
-
-		auto sampler = addSampler(samplerInfo);
-
-		vk::DescriptorImageInfo imageInfo;
-		imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
-		imageInfo.imageView = texture.getImageView();
-		imageInfo.sampler = sampler.obj;
-
-		std::array<vk::WriteDescriptorSet, 1> descriptorWrites = {};
-
-		descriptorWrites[0].dstSet = descriptorSet;
-		descriptorWrites[0].dstBinding = 1;
-		descriptorWrites[0].dstArrayElement = 0;
-		descriptorWrites[0].descriptorType = vk::DescriptorType::eCombinedImageSampler;
-		descriptorWrites[0].descriptorCount = 1;
-		descriptorWrites[0].pImageInfo = &imageInfo;
-
-		device.updateDescriptorSets(static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
-	}
-};
-
-template<typename T>
-static void RunBenchmark(benchmark::State &state, T &benchmark)
-{
-	benchmark.initialize();
-
-	if(false) benchmark.show();  // Enable for visual verification.
-
-	// Warmup
-	benchmark.renderFrame();
-
-	for(auto _ : state)
-	{
-		benchmark.renderFrame();
-	}
-}
-
-static void TriangleSolidColor(benchmark::State &state, Multisample multisample)
-{
-	TriangleSolidColorBenchmark benchmark(multisample);
-	RunBenchmark(state, benchmark);
-}
-
-static void TriangleInterpolateColor(benchmark::State &state, Multisample multisample)
-{
-	TriangleInterpolateColorBenchmark benchmark(multisample);
-	RunBenchmark(state, benchmark);
-}
-
-static void TriangleSampleTexture(benchmark::State &state, Multisample multisample)
-{
-	TriangleSampleTextureBenchmark benchmark(multisample);
-	RunBenchmark(state, benchmark);
-}
-
-BENCHMARK_CAPTURE(ClearImage, VK_FORMAT_R8G8B8A8_UNORM, vk::Format::eR8G8B8A8Unorm, vk::ImageAspectFlagBits::eColor)->Unit(benchmark::kMillisecond);
-BENCHMARK_CAPTURE(ClearImage, VK_FORMAT_R32_SFLOAT, vk::Format::eR32Sfloat, vk::ImageAspectFlagBits::eColor)->Unit(benchmark::kMillisecond);
-BENCHMARK_CAPTURE(ClearImage, VK_FORMAT_D32_SFLOAT, vk::Format::eD32Sfloat, vk::ImageAspectFlagBits::eDepth)->Unit(benchmark::kMillisecond);
-
-BENCHMARK_CAPTURE(TriangleSolidColor, TriangleSolidColor, Multisample::False)->Unit(benchmark::kMillisecond);
-BENCHMARK_CAPTURE(TriangleInterpolateColor, TriangleInterpolateColor, Multisample::False)->Unit(benchmark::kMillisecond);
-BENCHMARK_CAPTURE(TriangleSampleTexture, TriangleSampleTexture, Multisample::False)->Unit(benchmark::kMillisecond);
-BENCHMARK_CAPTURE(TriangleSolidColor, TriangleSolidColor_Multisample, Multisample::True)->Unit(benchmark::kMillisecond);
-BENCHMARK_CAPTURE(TriangleInterpolateColor, TriangleInterpolateColor_Multisample, Multisample::True)->Unit(benchmark::kMillisecond);
-BENCHMARK_CAPTURE(TriangleSampleTexture, TriangleSampleTexture_Multisample, Multisample::True)->Unit(benchmark::kMillisecond);