blob: c39959a8c8d01aa1cc9240ca81ac1b7edabfd96a [file] [log] [blame]
/*------------------------------------------------------------------------
* Vulkan Conformance Tests
* ------------------------
*
* Copyright (c) 2021 The Khronos Group Inc.
* Copyright (c) 2021 Valve Corporation.
*
* 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.
*
*//*!
* \file
* \brief Mesh Shader Smoke Tests
*//*--------------------------------------------------------------------*/
#include "vktMeshShaderSmokeTests.hpp"
#include "vktTestCase.hpp"
#include "vkBuilderUtil.hpp"
#include "vkImageWithMemory.hpp"
#include "vkBufferWithMemory.hpp"
#include "vkObjUtil.hpp"
#include "vkTypeUtil.hpp"
#include "vkCmdUtil.hpp"
#include "vkImageUtil.hpp"
#include "tcuImageCompare.hpp"
#include <utility>
#include <vector>
#include <string>
#include <sstream>
namespace vkt
{
namespace MeshShader
{
namespace
{
using GroupPtr = de::MovePtr<tcu::TestCaseGroup>;
using namespace vk;
void checkTaskMeshShaderSupport (Context& context, bool requireTask, bool requireMesh)
{
context.requireDeviceFunctionality("VK_NV_mesh_shader");
DE_ASSERT(requireTask || requireMesh);
const auto& meshFeatures = context.getMeshShaderFeatures();
if (requireTask && !meshFeatures.taskShader)
TCU_THROW(NotSupportedError, "Task shader not supported");
if (requireMesh && !meshFeatures.meshShader)
TCU_THROW(NotSupportedError, "Mesh shader not supported");
}
std::string commonMeshFragShader ()
{
std::string frag =
"#version 450\n"
"#extension GL_NV_mesh_shader : enable\n"
"\n"
"layout (location=0) in perprimitiveNV vec4 triangleColor;\n"
"layout (location=0) out vec4 outColor;\n"
"\n"
"void main ()\n"
"{\n"
" outColor = triangleColor;\n"
"}\n"
;
return frag;
}
struct MeshTriangleRendererParams
{
std::vector<tcu::Vec4> vertexCoords;
std::vector<uint32_t> vertexIndices;
uint32_t taskCount;
tcu::Vec4 expectedColor;
MeshTriangleRendererParams (std::vector<tcu::Vec4> vertexCoords_, std::vector<uint32_t> vertexIndices_, uint32_t taskCount_, const tcu::Vec4& expectedColor_)
: vertexCoords (std::move(vertexCoords_))
, vertexIndices (std::move(vertexIndices_))
, taskCount (taskCount_)
, expectedColor (expectedColor_)
{}
MeshTriangleRendererParams (MeshTriangleRendererParams&& other)
: MeshTriangleRendererParams (std::move(other.vertexCoords), std::move(other.vertexIndices), other.taskCount, other.expectedColor)
{}
};
class MeshOnlyTriangleCase : public vkt::TestCase
{
public:
MeshOnlyTriangleCase (tcu::TestContext& testCtx, const std::string& name, const std::string& description) : vkt::TestCase (testCtx, name, description) {}
virtual ~MeshOnlyTriangleCase (void) {}
void initPrograms (vk::SourceCollections& programCollection) const override;
TestInstance* createInstance (Context& context) const override;
void checkSupport (Context& context) const override;
};
class MeshTaskTriangleCase : public vkt::TestCase
{
public:
MeshTaskTriangleCase (tcu::TestContext& testCtx, const std::string& name, const std::string& description) : vkt::TestCase (testCtx, name, description) {}
virtual ~MeshTaskTriangleCase (void) {}
void initPrograms (vk::SourceCollections& programCollection) const override;
TestInstance* createInstance (Context& context) const override;
void checkSupport (Context& context) const override;
};
// Note: not actually task-only. The task shader will not emit mesh shader work groups.
class TaskOnlyTriangleCase : public vkt::TestCase
{
public:
TaskOnlyTriangleCase (tcu::TestContext& testCtx, const std::string& name, const std::string& description) : vkt::TestCase (testCtx, name, description) {}
virtual ~TaskOnlyTriangleCase (void) {}
void initPrograms (vk::SourceCollections& programCollection) const override;
TestInstance* createInstance (Context& context) const override;
void checkSupport (Context& context) const override;
};
class MeshTriangleRenderer : public vkt::TestInstance
{
public:
MeshTriangleRenderer (Context& context, MeshTriangleRendererParams params) : vkt::TestInstance(context), m_params(std::move(params)) {}
virtual ~MeshTriangleRenderer (void) {}
tcu::TestStatus iterate (void) override;
protected:
MeshTriangleRendererParams m_params;
};
void MeshOnlyTriangleCase::checkSupport (Context& context) const
{
checkTaskMeshShaderSupport(context, false, true);
}
void MeshTaskTriangleCase::checkSupport (Context& context) const
{
checkTaskMeshShaderSupport(context, true, true);
}
void TaskOnlyTriangleCase::checkSupport (Context& context) const
{
checkTaskMeshShaderSupport(context, true, true);
}
void MeshOnlyTriangleCase::initPrograms (SourceCollections& dst) const
{
std::ostringstream mesh;
mesh
<< "#version 450\n"
<< "#extension GL_NV_mesh_shader : enable\n"
<< "\n"
// We will actually output a single triangle and most invocations will do no work.
<< "layout(local_size_x=32) in;\n"
<< "layout(triangles) out;\n"
<< "layout(max_vertices=256, max_primitives=256) out;\n"
<< "\n"
// Unique vertex coordinates.
<< "layout (set=0, binding=0) uniform CoordsBuffer {\n"
<< " vec4 coords[3];\n"
<< "} cb;\n"
// Unique vertex indices.
<< "layout (set=0, binding=1, std430) readonly buffer IndexBuffer {\n"
<< " uint indices[3];\n"
<< "} ib;\n"
<< "\n"
// Triangle color.
<< "layout (location=0) out perprimitiveNV vec4 triangleColor[];\n"
<< "\n"
<< "void main ()\n"
<< "{\n"
<< " gl_PrimitiveCountNV = 1u;\n"
<< " triangleColor[0] = vec4(0.0, 0.0, 1.0, 1.0);\n"
<< "\n"
<< " const uint vertex = gl_LocalInvocationIndex;\n"
<< " if (vertex < 3u)\n"
<< " {\n"
<< " const uint vertexIndex = ib.indices[vertex];\n"
<< " gl_PrimitiveIndicesNV[vertex] = vertexIndex;\n"
<< " gl_MeshVerticesNV[vertexIndex].gl_Position = cb.coords[vertexIndex];\n"
<< " }\n"
<< "}\n"
;
dst.glslSources.add("mesh") << glu::MeshSource(mesh.str());
dst.glslSources.add("frag") << glu::FragmentSource(commonMeshFragShader());
}
void MeshTaskTriangleCase::initPrograms (SourceCollections& dst) const
{
std::string taskDataDecl =
"taskNV TaskData {\n"
" uint triangleIndex;\n"
"} td;\n"
;
std::ostringstream task;
task
// Each work group spawns 1 task each (2 in total) and each task will draw 1 triangle.
<< "#version 450\n"
<< "#extension GL_NV_mesh_shader : enable\n"
<< "\n"
<< "layout(local_size_x=32) in;\n"
<< "\n"
<< "out " << taskDataDecl
<< "\n"
<< "void main ()\n"
<< "{\n"
<< " if (gl_LocalInvocationIndex == 0u)\n"
<< " {\n"
<< " gl_TaskCountNV = 1u;\n"
<< " td.triangleIndex = gl_WorkGroupID.x;\n"
<< " }\n"
<< "}\n"
;
dst.glslSources.add("task") << glu::TaskSource(task.str());
std::ostringstream mesh;
mesh
<< "#version 450\n"
<< "#extension GL_NV_mesh_shader : enable\n"
<< "\n"
// We will actually output a single triangle and most invocations will do no work.
<< "layout(local_size_x=32) in;\n"
<< "layout(triangles) out;\n"
<< "layout(max_vertices=256, max_primitives=256) out;\n"
<< "\n"
// Unique vertex coordinates.
<< "layout (set=0, binding=0) uniform CoordsBuffer {\n"
<< " vec4 coords[4];\n"
<< "} cb;\n"
// Unique vertex indices.
<< "layout (set=0, binding=1, std430) readonly buffer IndexBuffer {\n"
<< " uint indices[6];\n"
<< "} ib;\n"
<< "\n"
// Triangle color.
<< "layout (location=0) out perprimitiveNV vec4 triangleColor[];\n"
<< "\n"
<< "in " << taskDataDecl
<< "\n"
<< "void main ()\n"
<< "{\n"
<< " if (gl_LocalInvocationIndex == 0u)\n"
<< " {\n"
<< " gl_PrimitiveCountNV = 1u;\n"
<< " triangleColor[0] = vec4(0.0, 0.0, 1.0, 1.0);\n"
<< " }\n"
<< "\n"
// Each "active" invocation will copy one vertex.
<< " if (gl_LocalInvocationIndex < 3u)\n"
<< " {\n"
<< "\n"
<< " const uint triangleVertex = gl_LocalInvocationIndex;\n"
<< " const uint coordsIndex = ib.indices[td.triangleIndex * 3u + triangleVertex];\n"
<< "\n"
// Copy vertex coordinates.
<< " gl_MeshVerticesNV[triangleVertex].gl_Position = cb.coords[coordsIndex];\n"
// Index renumbering: final indices will always be 0, 1, 2.
<< " gl_PrimitiveIndicesNV[triangleVertex] = triangleVertex;\n"
<< " }\n"
<< "}\n"
;
dst.glslSources.add("mesh") << glu::MeshSource(mesh.str());
dst.glslSources.add("frag") << glu::FragmentSource(commonMeshFragShader());
}
void TaskOnlyTriangleCase::initPrograms (SourceCollections& dst) const
{
// The task shader does not spawn any mesh shader invocations.
std::ostringstream task;
task
<< "#version 450\n"
<< "#extension GL_NV_mesh_shader : enable\n"
<< "\n"
<< "layout(local_size_x=1) in;\n"
<< "\n"
<< "void main ()\n"
<< "{\n"
<< " gl_TaskCountNV = 0u;\n"
<< "}\n"
;
dst.glslSources.add("task") << glu::TaskSource(task.str());
// Same shader as the mesh only case, but it should not be launched.
std::ostringstream mesh;
mesh
<< "#version 450\n"
<< "#extension GL_NV_mesh_shader : enable\n"
<< "\n"
<< "layout(local_size_x=32) in;\n"
<< "layout(triangles) out;\n"
<< "layout(max_vertices=256, max_primitives=256) out;\n"
<< "\n"
<< "layout (set=0, binding=0) uniform CoordsBuffer {\n"
<< " vec4 coords[3];\n"
<< "} cb;\n"
<< "layout (set=0, binding=1, std430) readonly buffer IndexBuffer {\n"
<< " uint indices[3];\n"
<< "} ib;\n"
<< "\n"
<< "layout (location=0) out perprimitiveNV vec4 triangleColor[];\n"
<< "\n"
<< "void main ()\n"
<< "{\n"
<< " gl_PrimitiveCountNV = 1u;\n"
<< " triangleColor[0] = vec4(0.0, 0.0, 1.0, 1.0);\n"
<< "\n"
<< " const uint vertex = gl_LocalInvocationIndex;\n"
<< " if (vertex < 3u)\n"
<< " {\n"
<< " const uint vertexIndex = ib.indices[vertex];\n"
<< " gl_PrimitiveIndicesNV[vertex] = vertexIndex;\n"
<< " gl_MeshVerticesNV[vertexIndex].gl_Position = cb.coords[vertexIndex];\n"
<< " }\n"
<< "}\n"
;
dst.glslSources.add("mesh") << glu::MeshSource(mesh.str());
dst.glslSources.add("frag") << glu::FragmentSource(commonMeshFragShader());
}
TestInstance* MeshOnlyTriangleCase::createInstance (Context& context) const
{
const std::vector<tcu::Vec4> vertexCoords =
{
tcu::Vec4(-1.0f, -1.0f, 0.0f, 1.0f),
tcu::Vec4(-1.0f, 3.0f, 0.0f, 1.0f),
tcu::Vec4( 3.0f, -1.0f, 0.0f, 1.0f),
};
const std::vector<uint32_t> vertexIndices = { 0u, 1u, 2u };
MeshTriangleRendererParams params (std::move(vertexCoords), std::move(vertexIndices), 1u, tcu::Vec4(0.0f, 0.0f, 1.0f, 1.0f));
return new MeshTriangleRenderer(context, std::move(params));
}
TestInstance* MeshTaskTriangleCase::createInstance (Context& context) const
{
const std::vector<tcu::Vec4> vertexCoords =
{
tcu::Vec4(-1.0f, -1.0f, 0.0f, 1.0f),
tcu::Vec4(-1.0f, 1.0f, 0.0f, 1.0f),
tcu::Vec4( 1.0f, -1.0f, 0.0f, 1.0f),
tcu::Vec4( 1.0f, 1.0f, 0.0f, 1.0f),
};
const std::vector<uint32_t> vertexIndices = { 2u, 0u, 1u, 1u, 3u, 2u };
MeshTriangleRendererParams params (std::move(vertexCoords), std::move(vertexIndices), 2u, tcu::Vec4(0.0f, 0.0f, 1.0f, 1.0f));
return new MeshTriangleRenderer(context, std::move(params));
}
TestInstance* TaskOnlyTriangleCase::createInstance (Context& context) const
{
const std::vector<tcu::Vec4> vertexCoords =
{
tcu::Vec4(-1.0f, -1.0f, 0.0f, 1.0f),
tcu::Vec4(-1.0f, 3.0f, 0.0f, 1.0f),
tcu::Vec4( 3.0f, -1.0f, 0.0f, 1.0f),
};
const std::vector<uint32_t> vertexIndices = { 0u, 1u, 2u };
// Note we expect the clear color.
MeshTriangleRendererParams params (std::move(vertexCoords), std::move(vertexIndices), 1u, tcu::Vec4(0.0f, 0.0f, 0.0f, 1.0f));
return new MeshTriangleRenderer(context, std::move(params));
}
tcu::TestStatus MeshTriangleRenderer::iterate ()
{
const auto& vkd = m_context.getDeviceInterface();
const auto device = m_context.getDevice();
auto& alloc = m_context.getDefaultAllocator();
const auto qIndex = m_context.getUniversalQueueFamilyIndex();
const auto queue = m_context.getUniversalQueue();
const auto vertexBufferStages = VK_SHADER_STAGE_MESH_BIT_NV;
const auto vertexBufferSize = static_cast<VkDeviceSize>(de::dataSize(m_params.vertexCoords));
const auto vertexBufferUsage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
const auto vertexBufferLoc = DescriptorSetUpdateBuilder::Location::binding(0u);
const auto vertexBufferType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
const auto indexBufferStages = VK_SHADER_STAGE_MESH_BIT_NV;
const auto indexBufferSize = static_cast<VkDeviceSize>(de::dataSize(m_params.vertexIndices));
const auto indexBufferUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
const auto indexBufferLoc = DescriptorSetUpdateBuilder::Location::binding(1u);
const auto indexBufferType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
// Vertex buffer.
const auto vertexBufferInfo = makeBufferCreateInfo(vertexBufferSize, vertexBufferUsage);
BufferWithMemory vertexBuffer (vkd, device, alloc, vertexBufferInfo, MemoryRequirement::HostVisible);
auto& vertexBufferAlloc = vertexBuffer.getAllocation();
void* vertexBufferDataPtr = vertexBufferAlloc.getHostPtr();
deMemcpy(vertexBufferDataPtr, m_params.vertexCoords.data(), static_cast<size_t>(vertexBufferSize));
flushAlloc(vkd, device, vertexBufferAlloc);
// Index buffer.
const auto indexBufferInfo = makeBufferCreateInfo(indexBufferSize, indexBufferUsage);
BufferWithMemory indexBuffer (vkd, device, alloc, indexBufferInfo, MemoryRequirement::HostVisible);
auto& indexBufferAlloc = indexBuffer.getAllocation();
void* indexBufferDataPtr = indexBufferAlloc.getHostPtr();
deMemcpy(indexBufferDataPtr, m_params.vertexIndices.data(), static_cast<size_t>(indexBufferSize));
flushAlloc(vkd, device, indexBufferAlloc);
// Color buffer.
const auto colorBufferFormat = VK_FORMAT_R8G8B8A8_UNORM;
const auto colorBufferExtent = makeExtent3D(8u, 8u, 1u);
const auto colorBufferUsage = (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
const VkImageCreateInfo colorBufferInfo =
{
VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, // VkStructureType sType;
nullptr, // const void* pNext;
0u, // VkImageCreateFlags flags;
VK_IMAGE_TYPE_2D, // VkImageType imageType;
colorBufferFormat, // VkFormat format;
colorBufferExtent, // VkExtent3D extent;
1u, // uint32_t mipLevels;
1u, // uint32_t arrayLayers;
VK_SAMPLE_COUNT_1_BIT, // VkSampleCountFlagBits samples;
VK_IMAGE_TILING_OPTIMAL, // VkImageTiling tiling;
colorBufferUsage, // VkImageUsageFlags usage;
VK_SHARING_MODE_EXCLUSIVE, // VkSharingMode sharingMode;
0u, // uint32_t queueFamilyIndexCount;
nullptr, // const uint32_t* pQueueFamilyIndices;
VK_IMAGE_LAYOUT_UNDEFINED, // VkImageLayout initialLayout;
};
ImageWithMemory colorBuffer(vkd, device, alloc, colorBufferInfo, MemoryRequirement::Any);
const auto colorSRR = makeImageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT, 0u, 1u, 0u, 1u);
const auto colorBufferView = makeImageView(vkd, device, colorBuffer.get(), VK_IMAGE_VIEW_TYPE_2D, colorBufferFormat, colorSRR);
// Render pass.
const auto renderPass = makeRenderPass(vkd, device, colorBufferFormat);
// Framebuffer.
const auto framebuffer = makeFramebuffer(vkd, device, renderPass.get(), colorBufferView.get(), colorBufferExtent.width, colorBufferExtent.height);
// Set layout.
DescriptorSetLayoutBuilder layoutBuilder;
layoutBuilder.addSingleBinding(vertexBufferType, vertexBufferStages);
layoutBuilder.addSingleBinding(indexBufferType, indexBufferStages);
const auto setLayout = layoutBuilder.build(vkd, device);
// Descriptor pool.
DescriptorPoolBuilder poolBuilder;
poolBuilder.addType(vertexBufferType);
poolBuilder.addType(indexBufferType);
const auto descriptorPool = poolBuilder.build(vkd, device, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, 1u);
// Descriptor set.
const auto descriptorSet = makeDescriptorSet(vkd, device, descriptorPool.get(), setLayout.get());
// Update descriptor set.
DescriptorSetUpdateBuilder updateBuilder;
const auto vertexBufferDescInfo = makeDescriptorBufferInfo(vertexBuffer.get(), 0ull, vertexBufferSize);
const auto indexBufferDescInfo = makeDescriptorBufferInfo(indexBuffer.get(), 0ull, indexBufferSize);
updateBuilder.writeSingle(descriptorSet.get(), vertexBufferLoc, vertexBufferType, &vertexBufferDescInfo);
updateBuilder.writeSingle(descriptorSet.get(), indexBufferLoc, indexBufferType, &indexBufferDescInfo);
updateBuilder.update(vkd, device);
// Pipeline layout.
const auto pipelineLayout = makePipelineLayout(vkd, device, setLayout.get());
// Shader modules.
Move<VkShaderModule> taskModule;
const auto& binaries = m_context.getBinaryCollection();
if (binaries.contains("task"))
taskModule = createShaderModule(vkd, device, binaries.get("task"), 0u);
const auto meshModule = createShaderModule(vkd, device, binaries.get("mesh"), 0u);
const auto fragModule = createShaderModule(vkd, device, binaries.get("frag"), 0u);
// Graphics pipeline.
std::vector<VkViewport> viewports (1u, makeViewport(colorBufferExtent));
std::vector<VkRect2D> scissors (1u, makeRect2D(colorBufferExtent));
const auto pipeline = makeGraphicsPipeline(vkd, device, pipelineLayout.get(), taskModule.get(), meshModule.get(), fragModule.get(), renderPass.get(), viewports, scissors);
// Command pool and buffer.
const auto cmdPool = makeCommandPool(vkd, device, qIndex);
const auto cmdBufferPtr = allocateCommandBuffer(vkd, device, cmdPool.get(), VK_COMMAND_BUFFER_LEVEL_PRIMARY);
const auto cmdBuffer = cmdBufferPtr.get();
// Output buffer.
const auto tcuFormat = mapVkFormat(colorBufferFormat);
const auto outBufferSize = static_cast<VkDeviceSize>(static_cast<uint32_t>(tcu::getPixelSize(tcuFormat)) * colorBufferExtent.width * colorBufferExtent.height);
const auto outBufferUsage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
const auto outBufferInfo = makeBufferCreateInfo(outBufferSize, outBufferUsage);
BufferWithMemory outBuffer (vkd, device, alloc, outBufferInfo, MemoryRequirement::HostVisible);
auto& outBufferAlloc = outBuffer.getAllocation();
void* outBufferData = outBufferAlloc.getHostPtr();
// Draw triangle.
beginCommandBuffer(vkd, cmdBuffer);
beginRenderPass(vkd, cmdBuffer, renderPass.get(), framebuffer.get(), scissors.at(0), tcu::Vec4(0.0f, 0.0f, 0.0f, 1.0f)/*clear color*/);
vkd.cmdBindDescriptorSets(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout.get(), 0u, 1u, &descriptorSet.get(), 0u, nullptr);
vkd.cmdBindPipeline(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.get());
vkd.cmdDrawMeshTasksNV(cmdBuffer, m_params.taskCount, 0u);
endRenderPass(vkd, cmdBuffer);
// Copy color buffer to output buffer.
const tcu::IVec3 imageDim (static_cast<int>(colorBufferExtent.width), static_cast<int>(colorBufferExtent.height), static_cast<int>(colorBufferExtent.depth));
const tcu::IVec2 imageSize (imageDim.x(), imageDim.y());
copyImageToBuffer(vkd, cmdBuffer, colorBuffer.get(), outBuffer.get(), imageSize);
endCommandBuffer(vkd, cmdBuffer);
submitCommandsAndWait(vkd, device, queue, cmdBuffer);
// Invalidate alloc.
invalidateAlloc(vkd, device, outBufferAlloc);
tcu::ConstPixelBufferAccess outPixels(tcuFormat, imageDim, outBufferData);
auto& log = m_context.getTestContext().getLog();
const tcu::Vec4 threshold (0.0f); // The color can be represented exactly.
if (!tcu::floatThresholdCompare(log, "Result", "", m_params.expectedColor, outPixels, threshold, tcu::COMPARE_LOG_EVERYTHING))
return tcu::TestStatus::fail("Failed; check log for details");
return tcu::TestStatus::pass("Pass");
}
}
tcu::TestCaseGroup* createMeshShaderSmokeTests (tcu::TestContext& testCtx)
{
GroupPtr smokeTests (new tcu::TestCaseGroup(testCtx, "smoke", "Mesh Shader Smoke Tests"));
smokeTests->addChild(new MeshOnlyTriangleCase(testCtx, "mesh_shader_triangle", ""));
smokeTests->addChild(new MeshTaskTriangleCase(testCtx, "mesh_task_shader_triangle", ""));
smokeTests->addChild(new TaskOnlyTriangleCase(testCtx, "task_only_shader_triangle", ""));
return smokeTests.release();
}
} // MeshShader
} // vkt