1 #include <gtest/gtest.h>
2
3 #include "CompositorVk.h"
4
5 #include <algorithm>
6 #include <array>
7 #include <glm/gtx/matrix_transform_2d.hpp>
8 #include <optional>
9
10 #include "tests/VkTestUtils.h"
11 #include "vulkan/VulkanDispatch.h"
12 #include "vulkan/vk_util.h"
13
14 class CompositorVkTest : public ::testing::Test {
15 protected:
16 using RenderTarget =
17 emugl::RenderResourceVk<VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
18 VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT>;
19 using RenderTexture = emugl::RenderTextureVk;
20
SetUpTestCase()21 static void SetUpTestCase() { k_vk = emugl::vkDispatch(false); }
22
23 static constexpr uint32_t k_numOfRenderTargets = 10;
24 static constexpr uint32_t k_renderTargetWidth = 255;
25 static constexpr uint32_t k_renderTargetHeight = 255;
26 static constexpr uint32_t k_renderTargetNumOfPixels =
27 k_renderTargetWidth * k_renderTargetHeight;
28
SetUp()29 void SetUp() override {
30 ASSERT_NE(k_vk, nullptr);
31 createInstance();
32 pickPhysicalDevice();
33 createLogicalDevice();
34
35 VkFormatProperties formatProperties;
36 k_vk->vkGetPhysicalDeviceFormatProperties(
37 m_vkPhysicalDevice, RenderTarget::k_vkFormat, &formatProperties);
38 if (!(formatProperties.optimalTilingFeatures &
39 VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) {
40 GTEST_SKIP();
41 }
42 k_vk->vkGetPhysicalDeviceFormatProperties(
43 m_vkPhysicalDevice, RenderTexture::k_vkFormat, &formatProperties);
44 if (!(formatProperties.optimalTilingFeatures &
45 VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)) {
46 GTEST_SKIP();
47 }
48
49 VkCommandPoolCreateInfo commandPoolCi = {
50 .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
51 .queueFamilyIndex = m_compositorQueueFamilyIndex};
52 ASSERT_EQ(k_vk->vkCreateCommandPool(m_vkDevice, &commandPoolCi, nullptr,
53 &m_vkCommandPool),
54 VK_SUCCESS);
55 k_vk->vkGetDeviceQueue(m_vkDevice, m_compositorQueueFamilyIndex, 0,
56 &m_compositorVkQueue);
57 ASSERT_TRUE(m_compositorVkQueue != VK_NULL_HANDLE);
58
59 for (uint32_t i = 0; i < k_numOfRenderTargets; i++) {
60 auto renderTarget = RenderTarget::create(
61 *k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
62 m_vkCommandPool, k_renderTargetHeight, k_renderTargetWidth);
63 ASSERT_NE(renderTarget, nullptr);
64 m_renderTargets.emplace_back(std::move(renderTarget));
65 }
66
67 m_renderTargetImageViews.resize(m_renderTargets.size());
68 ASSERT_EQ(
69 std::transform(
70 m_renderTargets.begin(), m_renderTargets.end(),
71 m_renderTargetImageViews.begin(),
72 [](const std::unique_ptr<const RenderTarget> &renderTarget) {
73 return renderTarget->m_vkImageView;
74 }),
75 m_renderTargetImageViews.end());
76 }
77
TearDown()78 void TearDown() override {
79 m_renderTargets.clear();
80 k_vk->vkDestroyCommandPool(m_vkDevice, m_vkCommandPool, nullptr);
81 k_vk->vkDestroyDevice(m_vkDevice, nullptr);
82 m_vkDevice = VK_NULL_HANDLE;
83 k_vk->vkDestroyInstance(m_vkInstance, nullptr);
84 m_vkInstance = VK_NULL_HANDLE;
85 }
86
createCompositor()87 std::unique_ptr<CompositorVk> createCompositor() {
88 return CompositorVk::create(
89 *k_vk, m_vkDevice, m_vkPhysicalDevice, m_compositorVkQueue,
90 RenderTarget::k_vkFormat, RenderTarget::k_vkImageLayout,
91 RenderTarget::k_vkImageLayout, k_renderTargetWidth,
92 k_renderTargetHeight, m_renderTargetImageViews, m_vkCommandPool);
93 }
94
createSampler()95 VkSampler createSampler() {
96 VkSamplerCreateInfo samplerCi = {
97 .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
98 .magFilter = VK_FILTER_NEAREST,
99 .minFilter = VK_FILTER_NEAREST,
100 .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
101 .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
102 .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
103 .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
104 .mipLodBias = 0.0f,
105 .anisotropyEnable = VK_FALSE,
106 .maxAnisotropy = 1.0f,
107 .compareEnable = VK_FALSE,
108 .compareOp = VK_COMPARE_OP_ALWAYS,
109 .minLod = 0.0f,
110 .maxLod = 0.0f,
111 .borderColor = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK,
112 .unnormalizedCoordinates = VK_FALSE};
113 VkSampler res;
114 VK_CHECK(k_vk->vkCreateSampler(m_vkDevice, &samplerCi, nullptr, &res));
115 return res;
116 }
117
118 static const goldfish_vk::VulkanDispatch *k_vk;
119 VkInstance m_vkInstance = VK_NULL_HANDLE;
120 VkPhysicalDevice m_vkPhysicalDevice = VK_NULL_HANDLE;
121 uint32_t m_compositorQueueFamilyIndex = 0;
122 VkDevice m_vkDevice = VK_NULL_HANDLE;
123 std::vector<std::unique_ptr<const RenderTarget>> m_renderTargets;
124 std::vector<VkImageView> m_renderTargetImageViews;
125 VkCommandPool m_vkCommandPool = VK_NULL_HANDLE;
126 VkQueue m_compositorVkQueue = VK_NULL_HANDLE;
127
128 private:
createInstance()129 void createInstance() {
130 VkApplicationInfo appInfo = {
131 .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
132 .pNext = nullptr,
133 .pApplicationName = "emulator CompositorVk unittest",
134 .applicationVersion = VK_MAKE_VERSION(1, 0, 0),
135 .pEngineName = "No Engine",
136 .engineVersion = VK_MAKE_VERSION(1, 0, 0),
137 .apiVersion = VK_API_VERSION_1_1};
138 VkInstanceCreateInfo instanceCi = {
139 .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
140 .pApplicationInfo = &appInfo,
141 .enabledExtensionCount = 0,
142 .ppEnabledExtensionNames = nullptr};
143 ASSERT_EQ(k_vk->vkCreateInstance(&instanceCi, nullptr, &m_vkInstance),
144 VK_SUCCESS);
145 ASSERT_TRUE(m_vkInstance != VK_NULL_HANDLE);
146 }
147
pickPhysicalDevice()148 void pickPhysicalDevice() {
149 uint32_t physicalDeviceCount = 0;
150 ASSERT_EQ(k_vk->vkEnumeratePhysicalDevices(
151 m_vkInstance, &physicalDeviceCount, nullptr),
152 VK_SUCCESS);
153 ASSERT_GT(physicalDeviceCount, 0);
154 std::vector<VkPhysicalDevice> physicalDevices(physicalDeviceCount);
155 ASSERT_EQ(
156 k_vk->vkEnumeratePhysicalDevices(m_vkInstance, &physicalDeviceCount,
157 physicalDevices.data()),
158 VK_SUCCESS);
159 for (const auto &device : physicalDevices) {
160 VkPhysicalDeviceDescriptorIndexingFeaturesEXT descIndexingFeatures = {
161 .sType =
162 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT};
163 VkPhysicalDeviceFeatures2 features = {
164 .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
165 .pNext = &descIndexingFeatures};
166 k_vk->vkGetPhysicalDeviceFeatures2(device, &features);
167 if (!CompositorVk::validatePhysicalDeviceFeatures(features)) {
168 continue;
169 }
170 uint32_t queueFamilyCount = 0;
171 k_vk->vkGetPhysicalDeviceQueueFamilyProperties(
172 device, &queueFamilyCount, nullptr);
173 ASSERT_GT(queueFamilyCount, 0);
174 std::vector<VkQueueFamilyProperties> queueFamilyProperties(
175 queueFamilyCount);
176 k_vk->vkGetPhysicalDeviceQueueFamilyProperties(
177 device, &queueFamilyCount, queueFamilyProperties.data());
178 uint32_t queueFamilyIndex = 0;
179 for (; queueFamilyIndex < queueFamilyCount; queueFamilyIndex++) {
180 if (CompositorVk::validateQueueFamilyProperties(
181 queueFamilyProperties[queueFamilyIndex])) {
182 break;
183 }
184 }
185 if (queueFamilyIndex == queueFamilyCount) {
186 continue;
187 }
188
189 m_compositorQueueFamilyIndex = queueFamilyIndex;
190 m_vkPhysicalDevice = device;
191 return;
192 }
193 FAIL() << "Can't find a suitable VkPhysicalDevice.";
194 }
195
createLogicalDevice()196 void createLogicalDevice() {
197 const float queuePriority = 1.0f;
198 VkDeviceQueueCreateInfo queueCi = {
199 .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
200 .queueFamilyIndex = m_compositorQueueFamilyIndex,
201 .queueCount = 1,
202 .pQueuePriorities = &queuePriority};
203 VkPhysicalDeviceDescriptorIndexingFeaturesEXT descIndexingFeatures = {
204 .sType =
205 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT};
206 VkPhysicalDeviceFeatures2 features = {
207 .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
208 .pNext = &descIndexingFeatures};
209 ASSERT_TRUE(CompositorVk::enablePhysicalDeviceFeatures(features));
210 const std::vector<const char *> enabledDeviceExtensions =
211 CompositorVk::getRequiredDeviceExtensions();
212 VkDeviceCreateInfo deviceCi = {
213 .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
214 .pNext = &features,
215 .queueCreateInfoCount = 1,
216 .pQueueCreateInfos = &queueCi,
217 .enabledLayerCount = 0,
218 .enabledExtensionCount =
219 static_cast<uint32_t>(enabledDeviceExtensions.size()),
220 .ppEnabledExtensionNames = enabledDeviceExtensions.data()};
221 ASSERT_EQ(k_vk->vkCreateDevice(m_vkPhysicalDevice, &deviceCi, nullptr,
222 &m_vkDevice),
223 VK_SUCCESS);
224 ASSERT_TRUE(m_vkDevice != VK_NULL_HANDLE);
225 }
226 };
227
228 const goldfish_vk::VulkanDispatch *CompositorVkTest::k_vk = nullptr;
229
TEST_F(CompositorVkTest,Init)230 TEST_F(CompositorVkTest, Init) { ASSERT_NE(createCompositor(), nullptr); }
231
TEST_F(CompositorVkTest,ValidatePhysicalDeviceFeatures)232 TEST_F(CompositorVkTest, ValidatePhysicalDeviceFeatures) {
233 VkPhysicalDeviceFeatures2 features = {
234 .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
235 ASSERT_FALSE(CompositorVk::validatePhysicalDeviceFeatures(features));
236 VkPhysicalDeviceDescriptorIndexingFeaturesEXT descIndexingFeatures = {
237 .sType =
238 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT,
239 .pNext = &descIndexingFeatures};
240 ASSERT_FALSE(CompositorVk::validatePhysicalDeviceFeatures(features));
241 descIndexingFeatures.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE;
242 ASSERT_TRUE(CompositorVk::validatePhysicalDeviceFeatures(features));
243 }
244
TEST_F(CompositorVkTest,ValidateQueueFamilyProperties)245 TEST_F(CompositorVkTest, ValidateQueueFamilyProperties) {
246 VkQueueFamilyProperties properties = {};
247 properties.queueFlags &= ~VK_QUEUE_GRAPHICS_BIT;
248 ASSERT_FALSE(CompositorVk::validateQueueFamilyProperties(properties));
249 properties.queueFlags |= VK_QUEUE_GRAPHICS_BIT;
250 ASSERT_TRUE(CompositorVk::validateQueueFamilyProperties(properties));
251 }
252
TEST_F(CompositorVkTest,EnablePhysicalDeviceFeatures)253 TEST_F(CompositorVkTest, EnablePhysicalDeviceFeatures) {
254 VkPhysicalDeviceFeatures2 features = {
255 .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
256 ASSERT_FALSE(CompositorVk::enablePhysicalDeviceFeatures(features));
257 VkPhysicalDeviceDescriptorIndexingFeaturesEXT descIndexingFeatures = {
258 .sType =
259 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT,
260 .pNext = &descIndexingFeatures};
261 ASSERT_TRUE(CompositorVk::enablePhysicalDeviceFeatures(features));
262 ASSERT_EQ(descIndexingFeatures.descriptorBindingSampledImageUpdateAfterBind,
263 VK_TRUE);
264 }
265
TEST_F(CompositorVkTest,EmptyCompositionShouldDrawABlackFrame)266 TEST_F(CompositorVkTest, EmptyCompositionShouldDrawABlackFrame) {
267 std::vector<uint32_t> pixels(k_renderTargetNumOfPixels);
268 for (uint32_t i = 0; i < k_renderTargetNumOfPixels; i++) {
269 uint8_t v = static_cast<uint8_t>((i / 4) & 0xff);
270 uint8_t *pixel = reinterpret_cast<uint8_t *>(&pixels[i]);
271 pixel[0] = v;
272 pixel[1] = v;
273 pixel[2] = v;
274 pixel[3] = 0xff;
275 }
276 for (uint32_t i = 0; i < k_numOfRenderTargets; i++) {
277 ASSERT_TRUE(m_renderTargets[i]->write(pixels));
278 auto maybeImageBytes = m_renderTargets[i]->read();
279 ASSERT_TRUE(maybeImageBytes.has_value());
280 for (uint32_t i = 0; i < k_renderTargetNumOfPixels; i++) {
281 ASSERT_EQ(pixels[i], maybeImageBytes.value()[i]);
282 }
283 }
284
285 auto compositor = createCompositor();
286 ASSERT_NE(compositor, nullptr);
287
288 // render to render targets with event index
289 std::vector<VkCommandBuffer> cmdBuffs = {};
290 for (uint32_t i = 0; i < k_numOfRenderTargets; i++) {
291 if (i % 2 == 0) {
292 cmdBuffs.emplace_back(compositor->getCommandBuffer(i));
293 }
294 }
295 VkSubmitInfo submitInfo = {
296 .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
297 .commandBufferCount = static_cast<uint32_t>(cmdBuffs.size()),
298 .pCommandBuffers = cmdBuffs.data()};
299 ASSERT_EQ(k_vk->vkQueueSubmit(m_compositorVkQueue, 1, &submitInfo,
300 VK_NULL_HANDLE),
301 VK_SUCCESS);
302
303 ASSERT_EQ(k_vk->vkQueueWaitIdle(m_compositorVkQueue), VK_SUCCESS);
304 for (uint32_t i = 0; i < k_numOfRenderTargets; i++) {
305 auto maybeImagePixels = m_renderTargets[i]->read();
306 ASSERT_TRUE(maybeImagePixels.has_value());
307 const auto &imagePixels = maybeImagePixels.value();
308 for (uint32_t j = 0; j < k_renderTargetNumOfPixels; j++) {
309 const auto pixel =
310 reinterpret_cast<const uint8_t *>(&imagePixels[j]);
311 // should only render to render targets with even index
312 if (i % 2 == 0) {
313 ASSERT_EQ(pixel[0], 0);
314 ASSERT_EQ(pixel[1], 0);
315 ASSERT_EQ(pixel[2], 0);
316 ASSERT_EQ(pixel[3], 0xff);
317 } else {
318 ASSERT_EQ(pixels[j], imagePixels[j]);
319 }
320 }
321 }
322 }
323
TEST_F(CompositorVkTest,SimpleComposition)324 TEST_F(CompositorVkTest, SimpleComposition) {
325 constexpr uint32_t textureLeft = 30;
326 constexpr uint32_t textureRight = 50;
327 constexpr uint32_t textureTop = 10;
328 constexpr uint32_t textureBottom = 40;
329 constexpr uint32_t textureWidth = textureRight - textureLeft;
330 constexpr uint32_t textureHeight = textureBottom - textureTop;
331 auto sampler = createSampler();
332 auto texture = RenderTexture::create(*k_vk, m_vkDevice, m_vkPhysicalDevice,
333 m_compositorVkQueue, m_vkCommandPool,
334 textureWidth, textureHeight);
335 uint32_t textureColor;
336 uint8_t *textureColor_ = reinterpret_cast<uint8_t *>(&textureColor);
337 textureColor_[0] = 0xff;
338 textureColor_[1] = 0;
339 textureColor_[2] = 0;
340 textureColor_[3] = 0xff;
341 std::vector<uint32_t> pixels(textureWidth * textureHeight, textureColor);
342 ASSERT_TRUE(texture->write(pixels));
343 auto compositor = createCompositor();
344 ASSERT_NE(compositor, nullptr);
345 auto composition = std::make_unique<Composition>(
346 texture->m_vkImageView, sampler, textureWidth, textureHeight);
347 auto transform = glm::translate(glm::mat3(1.0f),
348 glm::vec2(static_cast<float>(textureLeft),
349 static_cast<float>(textureTop)));
350 composition->m_transform = transform;
351 compositor->setComposition(0, std::move(composition));
352 VkCommandBuffer cmdBuff = compositor->getCommandBuffer(0);
353 VkSubmitInfo submitInfo = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
354 .commandBufferCount = 1,
355 .pCommandBuffers = &cmdBuff};
356 ASSERT_EQ(k_vk->vkQueueSubmit(m_compositorVkQueue, 1, &submitInfo,
357 VK_NULL_HANDLE),
358 VK_SUCCESS);
359 ASSERT_EQ(k_vk->vkQueueWaitIdle(m_compositorVkQueue), VK_SUCCESS);
360
361 auto maybeImagePixels = m_renderTargets[0]->read();
362 ASSERT_TRUE(maybeImagePixels.has_value());
363 const auto &imagePixels = maybeImagePixels.value();
364
365 for (uint32_t i = 0; i < k_renderTargetHeight; i++) {
366 for (uint32_t j = 0; j < k_renderTargetWidth; j++) {
367 uint32_t offset = i * k_renderTargetWidth + j;
368 const uint8_t *pixel =
369 reinterpret_cast<const uint8_t *>(&imagePixels[offset]);
370 EXPECT_EQ(pixel[1], 0);
371 EXPECT_EQ(pixel[2], 0);
372 EXPECT_EQ(pixel[3], 0xff);
373 if (i >= textureTop && i < textureBottom && j >= textureLeft &&
374 j < textureRight) {
375 EXPECT_EQ(pixel[0], 0xff);
376 } else {
377 EXPECT_EQ(pixel[0], 0);
378 }
379 }
380 }
381 k_vk->vkDestroySampler(m_vkDevice, sampler, nullptr);
382 }
383
TEST_F(CompositorVkTest,CompositingWithDifferentCompositionOnMultipleTargets)384 TEST_F(CompositorVkTest, CompositingWithDifferentCompositionOnMultipleTargets) {
385 constexpr uint32_t textureWidth = 20;
386 constexpr uint32_t textureHeight = 30;
387 struct Rect {
388 uint32_t m_top;
389 uint32_t m_bottom;
390 uint32_t m_left;
391 uint32_t m_right;
392 };
393 std::vector<Rect> texturePositions(k_numOfRenderTargets);
394 for (int i = 0; i < k_numOfRenderTargets; i++) {
395 auto left = (i * 30) % (k_renderTargetWidth - textureWidth);
396 auto top = (i * 20) % (k_renderTargetHeight - textureHeight);
397 texturePositions[i].m_top = top;
398 texturePositions[i].m_bottom = top + textureHeight;
399 texturePositions[i].m_left = left;
400 texturePositions[i].m_right = left + textureWidth;
401 }
402 auto sampler = createSampler();
403 auto texture = RenderTexture::create(*k_vk, m_vkDevice, m_vkPhysicalDevice,
404 m_compositorVkQueue, m_vkCommandPool,
405 textureWidth, textureHeight);
406 uint32_t textureColor;
407 uint8_t *textureColor_ = reinterpret_cast<uint8_t *>(&textureColor);
408 textureColor_[0] = 0xff;
409 textureColor_[1] = 0;
410 textureColor_[2] = 0;
411 textureColor_[3] = 0xff;
412 std::vector<uint32_t> pixels(textureWidth * textureHeight, textureColor);
413 ASSERT_TRUE(texture->write(pixels));
414 auto compositor = createCompositor();
415 ASSERT_NE(compositor, nullptr);
416 for (int i = 0; i < k_numOfRenderTargets; i++) {
417 const auto &pos = texturePositions[i];
418 auto composition = std::make_unique<Composition>(
419 texture->m_vkImageView, sampler, textureWidth, textureHeight);
420 auto transform = glm::translate(
421 glm::mat3(1.0f), glm::vec2(static_cast<float>(pos.m_left),
422 static_cast<float>(pos.m_top)));
423 composition->m_transform = transform;
424 compositor->setComposition(i, std::move(composition));
425 VkCommandBuffer cmdBuff = compositor->getCommandBuffer(i);
426 VkSubmitInfo submitInfo = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
427 .commandBufferCount = 1,
428 .pCommandBuffers = &cmdBuff};
429 ASSERT_EQ(k_vk->vkQueueSubmit(m_compositorVkQueue, 1, &submitInfo,
430 VK_NULL_HANDLE),
431 VK_SUCCESS);
432 ASSERT_EQ(k_vk->vkQueueWaitIdle(m_compositorVkQueue), VK_SUCCESS);
433
434 auto maybeImagePixels = m_renderTargets[i]->read();
435 ASSERT_TRUE(maybeImagePixels.has_value());
436 const auto &imagePixels = maybeImagePixels.value();
437
438 for (uint32_t i = 0; i < k_renderTargetHeight; i++) {
439 for (uint32_t j = 0; j < k_renderTargetWidth; j++) {
440 uint32_t offset = i * k_renderTargetWidth + j;
441 const uint8_t *pixel =
442 reinterpret_cast<const uint8_t *>(&imagePixels[offset]);
443 EXPECT_EQ(pixel[1], 0);
444 EXPECT_EQ(pixel[2], 0);
445 EXPECT_EQ(pixel[3], 0xff);
446 if (i >= pos.m_top && i < pos.m_bottom && j >= pos.m_left &&
447 j < pos.m_right) {
448 EXPECT_EQ(pixel[0], 0xff);
449 } else {
450 EXPECT_EQ(pixel[0], 0);
451 }
452 }
453 }
454 }
455 k_vk->vkDestroySampler(m_vkDevice, sampler, nullptr);
456 }