1 /*------------------------------------------------------------------------
2 * Vulkan Conformance Tests
3 * ------------------------
4 *
5 * Copyright (c) 2019 The Khronos Group Inc.
6 * Copyright (c) 2019 The Android Open Source Project
7 *
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 *
20 *//*!
21 * \file
22 * \brief Cube image with misaligned baseArrayLayer tests
23 *//*--------------------------------------------------------------------*/
24
25 #include "vktImageMisalignedCubeTests.hpp"
26 #include "vktTestCaseUtil.hpp"
27 #include "vktImageTestsUtil.hpp"
28 #include "vktImageTexture.hpp"
29
30 #include "vkDefs.hpp"
31 #include "vkRef.hpp"
32 #include "vkRefUtil.hpp"
33 #include "vkPlatform.hpp"
34 #include "vkPrograms.hpp"
35 #include "vkMemUtil.hpp"
36 #include "vkBarrierUtil.hpp"
37 #include "vkBuilderUtil.hpp"
38 #include "vkImageUtil.hpp"
39 #include "vkCmdUtil.hpp"
40 #include "vkObjUtil.hpp"
41 #include "vkTypeUtil.hpp"
42 #include "vkBufferWithMemory.hpp"
43
44 #include "deUniquePtr.hpp"
45 #include "deStringUtil.hpp"
46 #include "deMath.h"
47
48 #include <string>
49
50 using namespace vk;
51
52 namespace vkt
53 {
54 namespace image
55 {
56 namespace
57 {
58
makeImageCreateInfo(const tcu::IVec3 & size,const VkFormat format)59 inline VkImageCreateInfo makeImageCreateInfo(const tcu::IVec3 &size, const VkFormat format)
60 {
61 const VkImageUsageFlags usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
62 VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
63 const VkImageCreateInfo imageParams = {
64 VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, // VkStructureType sType;
65 DE_NULL, // const void* pNext;
66 VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT, // VkImageCreateFlags flags;
67 VK_IMAGE_TYPE_2D, // VkImageType imageType;
68 format, // VkFormat format;
69 makeExtent3D(size.x(), size.y(), 1u), // VkExtent3D extent;
70 1u, // uint32_t mipLevels;
71 (uint32_t)size.z(), // uint32_t arrayLayers;
72 VK_SAMPLE_COUNT_1_BIT, // VkSampleCountFlagBits samples;
73 VK_IMAGE_TILING_OPTIMAL, // VkImageTiling tiling;
74 usage, // VkImageUsageFlags usage;
75 VK_SHARING_MODE_EXCLUSIVE, // VkSharingMode sharingMode;
76 0u, // uint32_t queueFamilyIndexCount;
77 DE_NULL, // const uint32_t* pQueueFamilyIndices;
78 VK_IMAGE_LAYOUT_UNDEFINED, // VkImageLayout initialLayout;
79 };
80
81 return imageParams;
82 }
83
fillBuffer(const DeviceInterface & vk,const VkDevice device,const Allocation & alloc,const VkDeviceSize offset,const VkDeviceSize size,const VkFormat format,const tcu::Vec4 & color)84 void fillBuffer(const DeviceInterface &vk, const VkDevice device, const Allocation &alloc, const VkDeviceSize offset,
85 const VkDeviceSize size, const VkFormat format, const tcu::Vec4 &color)
86 {
87 const tcu::TextureFormat textureFormat = mapVkFormat(format);
88 const uint32_t colorPixelSize = static_cast<uint32_t>(tcu::getPixelSize(textureFormat));
89 tcu::TextureLevel colorPixelBuffer(textureFormat, 1, 1);
90 tcu::PixelBufferAccess colorPixel(colorPixelBuffer);
91
92 colorPixel.setPixel(color, 0, 0);
93
94 const uint8_t *src = static_cast<uint8_t *>(colorPixel.getDataPtr());
95 uint8_t *dstBase = static_cast<uint8_t *>(alloc.getHostPtr());
96 uint8_t *dst = &dstBase[offset];
97
98 for (uint32_t pixelPos = 0; pixelPos < size; pixelPos += colorPixelSize)
99 deMemcpy(&dst[pixelPos], src, colorPixelSize);
100
101 flushMappedMemoryRange(vk, device, alloc.getMemory(), alloc.getOffset() + offset, size);
102 }
103
makeBufferImageCopy(const vk::VkDeviceSize & bufferOffset,const vk::VkImageSubresourceLayers & imageSubresource,const vk::VkOffset3D & imageOffset,const vk::VkExtent3D & imageExtent)104 VkBufferImageCopy makeBufferImageCopy(const vk::VkDeviceSize &bufferOffset,
105 const vk::VkImageSubresourceLayers &imageSubresource,
106 const vk::VkOffset3D &imageOffset, const vk::VkExtent3D &imageExtent)
107 {
108 const VkBufferImageCopy copyParams = {
109 bufferOffset, // VkDeviceSize bufferOffset;
110 0u, // uint32_t bufferRowLength;
111 0u, // uint32_t bufferImageHeight;
112 imageSubresource, // VkImageSubresourceLayers imageSubresource;
113 imageOffset, // VkOffset3D imageOffset;
114 imageExtent, // VkExtent3D imageExtent;
115 };
116 return copyParams;
117 }
118
119 //! Interpret the memory as IVec4
readVec4(const void * const data,const uint32_t ndx)120 inline tcu::Vec4 readVec4(const void *const data, const uint32_t ndx)
121 {
122 const float *const p = reinterpret_cast<const float *>(data);
123 const uint32_t ofs = 4 * ndx;
124
125 return tcu::Vec4(p[ofs + 0], p[ofs + 1], p[ofs + 2], p[ofs + 3]);
126 }
127
128 class MisalignedCubeTestInstance : public TestInstance
129 {
130 public:
131 MisalignedCubeTestInstance(Context &context, const tcu::IVec3 &size, const VkFormat format);
132 tcu::TestStatus iterate(void);
133
134 private:
135 const tcu::IVec3 &m_size;
136 const VkFormat m_format;
137 };
138
MisalignedCubeTestInstance(Context & context,const tcu::IVec3 & size,const VkFormat format)139 MisalignedCubeTestInstance::MisalignedCubeTestInstance(Context &context, const tcu::IVec3 &size, const VkFormat format)
140 : TestInstance(context)
141 , m_size(size)
142 , m_format(format)
143 {
144 }
145
iterate(void)146 tcu::TestStatus MisalignedCubeTestInstance::iterate(void)
147 {
148 DE_ASSERT(de::inRange(m_size.z(), 6, 16));
149 DE_ASSERT(m_format == VK_FORMAT_R8G8B8A8_UNORM);
150
151 const DeviceInterface &vk = m_context.getDeviceInterface();
152 const VkDevice device = m_context.getDevice();
153 Allocator &allocator = m_context.getDefaultAllocator();
154 const VkQueue queue = m_context.getUniversalQueue();
155 const uint32_t queueFamilyIndex = m_context.getUniversalQueueFamilyIndex();
156 const uint32_t numLayers = m_size.z();
157 const uint32_t cube0LayerStart = 0;
158 const uint32_t cube1LayerStart = numLayers - 6u;
159 const VkDeviceSize resultBufferSizeBytes = 2 * 6 * 4 * sizeof(float); // vec4[6] in shader
160 const VkExtent3D imageExtent = makeExtent3D(m_size.x(), m_size.y(), 1u);
161 const uint32_t pixelSize = static_cast<uint32_t>(tcu::getPixelSize(mapVkFormat(m_format)));
162 const uint32_t layerSize = imageExtent.width * imageExtent.height * pixelSize;
163 const float eps = 1.0f / float(2 * 256);
164
165 const VkBufferCreateInfo resultBufferCreateInfo =
166 makeBufferCreateInfo(resultBufferSizeBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
167 de::MovePtr<BufferWithMemory> resultBuffer = de::MovePtr<BufferWithMemory>(
168 new BufferWithMemory(vk, device, allocator, resultBufferCreateInfo, MemoryRequirement::HostVisible));
169 const Allocation &resultBufferAlloc = resultBuffer->getAllocation();
170 const VkImageCreateInfo imageCreateInfo = makeImageCreateInfo(m_size, m_format);
171 de::MovePtr<Image> image =
172 de::MovePtr<Image>(new Image(vk, device, allocator, imageCreateInfo, MemoryRequirement::Any));
173 const VkImageSubresourceRange imageSubresourceRange0 =
174 makeImageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT, 0u, 1u, cube0LayerStart, 6u);
175 Move<VkImageView> imageView0 =
176 makeImageView(vk, device, image->get(), VK_IMAGE_VIEW_TYPE_CUBE, m_format, imageSubresourceRange0);
177 const VkImageSubresourceRange imageSubresourceRange1 =
178 makeImageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT, 0u, 1u, cube1LayerStart, 6u);
179 Move<VkImageView> imageView1 =
180 makeImageView(vk, device, image->get(), VK_IMAGE_VIEW_TYPE_CUBE, m_format, imageSubresourceRange1);
181
182 Move<VkDescriptorSetLayout> descriptorSetLayout =
183 DescriptorSetLayoutBuilder()
184 .addSingleBinding(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_SHADER_STAGE_COMPUTE_BIT)
185 .addSingleBinding(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_SHADER_STAGE_COMPUTE_BIT)
186 .addSingleBinding(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_COMPUTE_BIT)
187 .build(vk, device);
188 Move<VkDescriptorPool> descriptorPool =
189 DescriptorPoolBuilder()
190 .addType(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE)
191 .addType(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE)
192 .addType(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
193 .build(vk, device, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, 1u);
194 Move<VkDescriptorSet> descriptorSet = makeDescriptorSet(vk, device, *descriptorPool, *descriptorSetLayout);
195 const VkDescriptorImageInfo descriptorImageInfo0 =
196 makeDescriptorImageInfo(DE_NULL, *imageView0, VK_IMAGE_LAYOUT_GENERAL);
197 const VkDescriptorImageInfo descriptorImageInfo1 =
198 makeDescriptorImageInfo(DE_NULL, *imageView1, VK_IMAGE_LAYOUT_GENERAL);
199 const VkDescriptorBufferInfo descriptorBufferInfo =
200 makeDescriptorBufferInfo(resultBuffer->get(), 0ull, resultBufferSizeBytes);
201
202 const Move<VkShaderModule> shaderModule =
203 createShaderModule(vk, device, m_context.getBinaryCollection().get("comp"), 0);
204 const Move<VkPipelineLayout> pipelineLayout = makePipelineLayout(vk, device, *descriptorSetLayout);
205 const Move<VkPipeline> pipeline = makeComputePipeline(vk, device, *pipelineLayout, *shaderModule);
206 const Move<VkCommandPool> cmdPool =
207 createCommandPool(vk, device, VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, queueFamilyIndex);
208 const Move<VkCommandBuffer> cmdBuffer =
209 allocateCommandBuffer(vk, device, *cmdPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY);
210
211 const VkDeviceSize clearBufferSize = layerSize * numLayers;
212 const Move<VkBuffer> clearBuffer = makeBuffer(vk, device, clearBufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
213 const de::MovePtr<Allocation> clearBufferAlloc =
214 bindBuffer(vk, device, allocator, *clearBuffer, MemoryRequirement::HostVisible);
215 const VkImageSubresourceRange clearSubresRange =
216 makeImageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT, 0u, 1u, 0u, numLayers);
217 const VkImageMemoryBarrier clearBarrier =
218 makeImageMemoryBarrier(0u, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED,
219 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, image->get(), clearSubresRange);
220 const VkImageMemoryBarrier preShaderImageBarrier = makeImageMemoryBarrier(
221 VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
222 VK_IMAGE_LAYOUT_GENERAL, image->get(), clearSubresRange);
223 const VkBufferMemoryBarrier postShaderBarrier = makeBufferMemoryBarrier(
224 VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT, resultBuffer->get(), 0ull, VK_WHOLE_SIZE);
225 bool result = true;
226
227 DescriptorSetUpdateBuilder()
228 .writeSingle(*descriptorSet, DescriptorSetUpdateBuilder::Location::binding(0u),
229 VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &descriptorImageInfo0)
230 .writeSingle(*descriptorSet, DescriptorSetUpdateBuilder::Location::binding(1u),
231 VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &descriptorImageInfo1)
232 .writeSingle(*descriptorSet, DescriptorSetUpdateBuilder::Location::binding(2u),
233 VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, &descriptorBufferInfo)
234 .update(vk, device);
235
236 beginCommandBuffer(vk, *cmdBuffer);
237
238 vk.cmdBindPipeline(*cmdBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline);
239 vk.cmdBindDescriptorSets(*cmdBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, *pipelineLayout, 0u, 1u, &*descriptorSet, 0u,
240 DE_NULL);
241
242 vk.cmdPipelineBarrier(*cmdBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0u, 0u,
243 DE_NULL, 0u, DE_NULL, 1u, &clearBarrier);
244
245 // Clear layers with predefined values
246 for (uint32_t layerNdx = 0; layerNdx < numLayers; ++layerNdx)
247 {
248 const float componentValue = float(16 * layerNdx) / 255.0f;
249 const tcu::Vec4 clearColor = tcu::Vec4(componentValue, componentValue, componentValue, 1.0f);
250 const VkDeviceSize bufferOffset = layerNdx * layerSize;
251 const VkImageSubresourceLayers imageSubresource =
252 makeImageSubresourceLayers(VK_IMAGE_ASPECT_COLOR_BIT, 0u, layerNdx, 1u);
253 const VkBufferImageCopy bufferImageCopyRegion =
254 makeBufferImageCopy(bufferOffset, imageSubresource, makeOffset3D(0u, 0u, 0u), imageExtent);
255
256 fillBuffer(vk, device, *clearBufferAlloc, bufferOffset, layerSize, m_format, clearColor);
257
258 vk.cmdCopyBufferToImage(*cmdBuffer, *clearBuffer, image->get(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1u,
259 &bufferImageCopyRegion);
260 }
261
262 vk.cmdPipelineBarrier(*cmdBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0u, 0u,
263 DE_NULL, 0u, DE_NULL, 1u, &preShaderImageBarrier);
264
265 vk.cmdDispatch(*cmdBuffer, 1, 1, 1);
266
267 vk.cmdPipelineBarrier(*cmdBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, DE_NULL,
268 1, &postShaderBarrier, 0, DE_NULL);
269
270 endCommandBuffer(vk, *cmdBuffer);
271
272 submitCommandsAndWait(vk, device, queue, *cmdBuffer);
273
274 invalidateAlloc(vk, device, resultBufferAlloc);
275
276 // Check cube 0
277 for (uint32_t layerNdx = 0; layerNdx < 6; ++layerNdx)
278 {
279 const uint32_t layerUsed = cube0LayerStart + layerNdx;
280 const float componentValue = float(16 * layerUsed) / 255.0f;
281 const tcu::Vec4 expectedColor = tcu::Vec4(componentValue, componentValue, componentValue, 1.0f);
282 const tcu::Vec4 resultColor = readVec4(resultBufferAlloc.getHostPtr(), layerNdx);
283 const tcu::Vec4 delta = expectedColor - resultColor;
284
285 if (deFloatAbs(delta.x()) > eps || deFloatAbs(delta.y()) > eps || deFloatAbs(delta.z()) > eps ||
286 deFloatAbs(delta.w()) > eps)
287 result = false;
288 }
289
290 // Check cube 1
291 for (uint32_t layerNdx = 0; layerNdx < 6; ++layerNdx)
292 {
293 const uint32_t layerUsed = cube1LayerStart + layerNdx;
294 const float componentValue = float(16 * layerUsed) / 255.0f;
295 const tcu::Vec4 expectedColor = tcu::Vec4(componentValue, componentValue, componentValue, 1.0f);
296 const tcu::Vec4 resultColor = readVec4(resultBufferAlloc.getHostPtr(), layerNdx + 6u);
297 const tcu::Vec4 delta = expectedColor - resultColor;
298
299 if (deFloatAbs(delta.x()) > eps || deFloatAbs(delta.y()) > eps || deFloatAbs(delta.z()) > eps ||
300 deFloatAbs(delta.w()) > eps)
301 result = false;
302 }
303
304 if (result)
305 return tcu::TestStatus::pass("pass");
306 else
307 return tcu::TestStatus::fail("fail");
308 }
309
310 class MisalignedCubeTest : public TestCase
311 {
312 public:
313 MisalignedCubeTest(tcu::TestContext &testCtx, const std::string &name, const tcu::IVec3 &size,
314 const VkFormat format);
315
316 void initPrograms(SourceCollections &programCollection) const;
317 TestInstance *createInstance(Context &context) const;
318
319 private:
320 const tcu::IVec3 m_size;
321 const VkFormat m_format;
322 };
323
MisalignedCubeTest(tcu::TestContext & testCtx,const std::string & name,const tcu::IVec3 & size,const VkFormat format)324 MisalignedCubeTest::MisalignedCubeTest(tcu::TestContext &testCtx, const std::string &name, const tcu::IVec3 &size,
325 const VkFormat format)
326 : TestCase(testCtx, name)
327 , m_size(size)
328 , m_format(format)
329 {
330 }
331
initPrograms(SourceCollections & programCollection) const332 void MisalignedCubeTest::initPrograms(SourceCollections &programCollection) const
333 {
334 const std::string formatQualifierStr = getShaderImageFormatQualifier(mapVkFormat(m_format));
335
336 std::ostringstream src;
337 src << glu::getGLSLVersionDeclaration(glu::GLSL_VERSION_440) << "\n"
338 << "\n"
339 << "layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;\n"
340 << "layout (binding = 0, " << formatQualifierStr << ") "
341 << "readonly uniform highp imageCube u_cubeImage0;\n"
342 << "layout (binding = 1, " << formatQualifierStr << ") "
343 << "readonly uniform highp imageCube u_cubeImage1;\n"
344 << "layout (binding = 2) writeonly buffer Output\n"
345 << "{\n"
346 << " vec4 cube0_color0;\n"
347 << " vec4 cube0_color1;\n"
348 << " vec4 cube0_color2;\n"
349 << " vec4 cube0_color3;\n"
350 << " vec4 cube0_color4;\n"
351 << " vec4 cube0_color5;\n"
352 << " vec4 cube1_color0;\n"
353 << " vec4 cube1_color1;\n"
354 << " vec4 cube1_color2;\n"
355 << " vec4 cube1_color3;\n"
356 << " vec4 cube1_color4;\n"
357 << " vec4 cube1_color5;\n"
358 << "} sb_out;\n"
359 << "\n"
360 << "void main (void)\n"
361 << "{\n"
362 << " sb_out.cube0_color0 = imageLoad(u_cubeImage0, ivec3(1, 1, 0));\n"
363 << " sb_out.cube0_color1 = imageLoad(u_cubeImage0, ivec3(1, 1, 1));\n"
364 << " sb_out.cube0_color2 = imageLoad(u_cubeImage0, ivec3(1, 1, 2));\n"
365 << " sb_out.cube0_color3 = imageLoad(u_cubeImage0, ivec3(1, 1, 3));\n"
366 << " sb_out.cube0_color4 = imageLoad(u_cubeImage0, ivec3(1, 1, 4));\n"
367 << " sb_out.cube0_color5 = imageLoad(u_cubeImage0, ivec3(1, 1, 5));\n"
368 << " sb_out.cube1_color0 = imageLoad(u_cubeImage1, ivec3(1, 1, 0));\n"
369 << " sb_out.cube1_color1 = imageLoad(u_cubeImage1, ivec3(1, 1, 1));\n"
370 << " sb_out.cube1_color2 = imageLoad(u_cubeImage1, ivec3(1, 1, 2));\n"
371 << " sb_out.cube1_color3 = imageLoad(u_cubeImage1, ivec3(1, 1, 3));\n"
372 << " sb_out.cube1_color4 = imageLoad(u_cubeImage1, ivec3(1, 1, 4));\n"
373 << " sb_out.cube1_color5 = imageLoad(u_cubeImage1, ivec3(1, 1, 5));\n"
374 << "}\n";
375
376 programCollection.glslSources.add("comp") << glu::ComputeSource(src.str());
377 }
378
createInstance(Context & context) const379 TestInstance *MisalignedCubeTest::createInstance(Context &context) const
380 {
381 return new MisalignedCubeTestInstance(context, m_size, m_format);
382 }
383
384 //! Base sizes used to generate actual imager sizes in the test.
385 static const tcu::IVec3 s_baseImageSizes[] = {
386 tcu::IVec3(16, 16, 7), tcu::IVec3(16, 16, 8), tcu::IVec3(16, 16, 9), tcu::IVec3(16, 16, 10), tcu::IVec3(16, 16, 11),
387 };
388
389 } // namespace
390
createMisalignedCubeTests(tcu::TestContext & testCtx)391 tcu::TestCaseGroup *createMisalignedCubeTests(tcu::TestContext &testCtx)
392 {
393 // Cube image with misaligned baseArrayLayer test cases
394 de::MovePtr<tcu::TestCaseGroup> testGroup(new tcu::TestCaseGroup(testCtx, "misaligned_cube"));
395
396 const VkFormat format = VK_FORMAT_R8G8B8A8_UNORM;
397
398 for (int imageSizeNdx = 0; imageSizeNdx < DE_LENGTH_OF_ARRAY(s_baseImageSizes); ++imageSizeNdx)
399 {
400 const tcu::IVec3 size = s_baseImageSizes[imageSizeNdx];
401
402 testGroup->addChild(new MisalignedCubeTest(testCtx, de::toString(size.z()), size, format));
403 }
404
405 return testGroup.release();
406 }
407
408 } // namespace image
409 } // namespace vkt
410