blob: c4bedd5f6c9977eacda397632005fb7b5df0993a [file]
/*
* Copyright (c) 2015-2026 The Khronos Group Inc.
* Copyright (c) 2015-2026 Valve Corporation
* Copyright (c) 2015-2026 LunarG, Inc.
* Copyright (c) 2015-2026 Google, Inc.
*
* 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
*/
#include "../framework/layer_validation_tests.h"
#include "../framework/pipeline_helper.h"
#include "../framework/descriptor_helper.h"
void DescriptorIndexingTest::ComputePipelineShaderTest(const char* shader, std::vector<VkDescriptorSetLayoutBinding>& bindings) {
AddRequiredExtensions(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
RETURN_IF_SKIP(Init());
InitRenderTarget();
CreateComputePipelineHelper pipe(*this);
pipe.dsl_bindings_.resize(bindings.size());
memcpy(pipe.dsl_bindings_.data(), bindings.data(), bindings.size() * sizeof(VkDescriptorSetLayoutBinding));
pipe.cs_ = VkShaderObj(*m_device, shader, VK_SHADER_STAGE_COMPUTE_BIT);
pipe.CreateComputePipeline();
}
class PositiveDescriptorIndexing : public DescriptorIndexingTest {};
TEST_F(PositiveDescriptorIndexing, BindingPartiallyBound) {
TEST_DESCRIPTION("Ensure that no validation errors for invalid descriptors if binding is PARTIALLY_BOUND");
SetTargetApiVersion(VK_API_VERSION_1_1);
AddRequiredExtensions(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
AddRequiredFeature(vkt::Feature::descriptorBindingPartiallyBound);
RETURN_IF_SKIP(Init());
InitRenderTarget();
VkDescriptorBindingFlags ds_binding_flags[2] = {};
VkDescriptorSetLayoutBindingFlagsCreateInfo layout_createinfo_binding_flags = vku::InitStructHelper();
ds_binding_flags[0] = 0;
// No Error
ds_binding_flags[1] = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT;
// Uncomment for Error
// ds_binding_flags[1] = 0;
layout_createinfo_binding_flags.bindingCount = 2;
layout_createinfo_binding_flags.pBindingFlags = ds_binding_flags;
OneOffDescriptorSet descriptor_set(m_device,
{
{0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr},
{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr},
},
0, &layout_createinfo_binding_flags, 0);
const vkt::PipelineLayout pipeline_layout(*m_device, {&descriptor_set.layout_});
vkt::Buffer buffer(*m_device, 32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT);
vkt::Buffer index_buffer(*m_device, sizeof(uint32_t), VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
descriptor_set.WriteDescriptorBufferInfo(0, buffer, 0, sizeof(uint32_t));
descriptor_set.UpdateDescriptorSets();
const char* shader_source = R"glsl(
#version 450
layout(set = 0, binding = 0) uniform foo_0 { int val; } doit;
layout(set = 0, binding = 1) uniform foo_1 { int val; } readit;
void main() {
if (doit.val == 0)
gl_Position = vec4(0.0);
else
gl_Position = vec4(readit.val);
}
)glsl";
VkShaderObj vs(*m_device, shader_source, VK_SHADER_STAGE_VERTEX_BIT);
CreatePipelineHelper pipe(*this);
pipe.shader_stages_[0] = vs.GetStageCreateInfo();
pipe.gp_ci_.layout = pipeline_layout;
pipe.CreateGraphicsPipeline();
VkCommandBufferBeginInfo begin_info = vku::InitStructHelper();
m_command_buffer.Begin(&begin_info);
vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe);
m_command_buffer.BeginRenderPass(m_renderPassBeginInfo);
vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set.set_, 0,
nullptr);
vk::CmdBindIndexBuffer(m_command_buffer, index_buffer, 0, VK_INDEX_TYPE_UINT32);
vk::CmdDrawIndexed(m_command_buffer, 1, 1, 0, 0, 0);
m_command_buffer.EndRenderPass();
m_command_buffer.End();
m_default_queue->SubmitAndWait(m_command_buffer);
}
TEST_F(PositiveDescriptorIndexing, UpdateAfterBind) {
TEST_DESCRIPTION("Test UPDATE_AFTER_BIND does not reset command buffers.");
SetTargetApiVersion(VK_API_VERSION_1_1);
AddRequiredExtensions(VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME);
AddRequiredExtensions(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
AddRequiredFeature(vkt::Feature::descriptorBindingStorageBufferUpdateAfterBind);
AddRequiredFeature(vkt::Feature::fragmentStoresAndAtomics);
AddRequiredFeature(vkt::Feature::synchronization2);
RETURN_IF_SKIP(Init());
InitRenderTarget();
vkt::Buffer buffer1(*m_device, 4096, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
vkt::Buffer buffer2(*m_device, 4096, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
vkt::Buffer buffer3(*m_device, 4096, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
OneOffDescriptorIndexingSet descriptor_set(
m_device,
{{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT},
{1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr, 0}});
const vkt::PipelineLayout pipeline_layout(*m_device, {&descriptor_set.layout_});
descriptor_set.WriteDescriptorBufferInfo(0, buffer1, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
descriptor_set.WriteDescriptorBufferInfo(1, buffer3, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
descriptor_set.UpdateDescriptorSets();
descriptor_set.Clear();
const char fsSource[] = R"glsl(
#version 450
layout (set = 0, binding = 0) buffer buf1 {
float a;
} ubuf1;
layout (set = 0, binding = 1) buffer buf2 {
float a;
} ubuf2;
void main() {
float f = ubuf1.a * ubuf2.a;
}
)glsl";
VkShaderObj fs(*m_device, fsSource, VK_SHADER_STAGE_FRAGMENT_BIT);
CreatePipelineHelper pipe(*this);
pipe.shader_stages_ = {pipe.vs_->GetStageCreateInfo(), fs.GetStageCreateInfo()};
pipe.pipeline_layout_ = vkt::PipelineLayout(*m_device, {&descriptor_set.layout_});
pipe.CreateGraphicsPipeline();
m_command_buffer.Begin();
m_command_buffer.BeginRenderPass(m_renderPassBeginInfo);
vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe);
vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set.set_, 0,
nullptr);
vk::CmdDraw(m_command_buffer, 3, 1, 0, 0);
m_command_buffer.EndRenderPass();
m_command_buffer.End();
buffer1.Destroy();
descriptor_set.WriteDescriptorBufferInfo(0, buffer2, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
descriptor_set.UpdateDescriptorSets();
VkCommandBufferSubmitInfo cb_info = vku::InitStructHelper();
cb_info.commandBuffer = m_command_buffer;
VkSubmitInfo2 submit_info = vku::InitStructHelper();
submit_info.commandBufferInfoCount = 1;
submit_info.pCommandBufferInfos = &cb_info;
vk::QueueSubmit2KHR(m_default_queue->handle(), 1, &submit_info, VK_NULL_HANDLE);
m_default_queue->Wait();
}
TEST_F(PositiveDescriptorIndexing, PartiallyBoundDescriptors) {
TEST_DESCRIPTION("Test partially bound descriptors do not reset command buffers.");
SetTargetApiVersion(VK_API_VERSION_1_1);
AddRequiredExtensions(VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME);
AddRequiredExtensions(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
AddRequiredFeature(vkt::Feature::descriptorBindingStorageBufferUpdateAfterBind);
AddRequiredFeature(vkt::Feature::descriptorBindingPartiallyBound);
AddRequiredFeature(vkt::Feature::fragmentStoresAndAtomics);
AddRequiredFeature(vkt::Feature::synchronization2);
RETURN_IF_SKIP(Init());
InitRenderTarget();
vkt::Buffer buffer1(*m_device, 4096, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
vkt::Buffer buffer3(*m_device, 4096, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
OneOffDescriptorIndexingSet descriptor_set(
m_device,
{{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT},
{1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr, 0}});
const vkt::PipelineLayout pipeline_layout(*m_device, {&descriptor_set.layout_});
descriptor_set.WriteDescriptorBufferInfo(0, buffer1, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
descriptor_set.WriteDescriptorBufferInfo(1, buffer3, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
descriptor_set.UpdateDescriptorSets();
const char fsSource[] = R"glsl(
#version 450
layout (set = 0, binding = 0) buffer buf1 {
float a;
} ubuf1;
layout (set = 0, binding = 1) buffer buf2 {
float a;
} ubuf2;
void main() {
float f = ubuf2.a;
}
)glsl";
VkShaderObj fs(*m_device, fsSource, VK_SHADER_STAGE_FRAGMENT_BIT);
CreatePipelineHelper pipe(*this);
pipe.shader_stages_ = {pipe.vs_->GetStageCreateInfo(), fs.GetStageCreateInfo()};
pipe.pipeline_layout_ = vkt::PipelineLayout(*m_device, {&descriptor_set.layout_});
pipe.CreateGraphicsPipeline();
m_command_buffer.Begin();
m_command_buffer.BeginRenderPass(m_renderPassBeginInfo);
vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe);
vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set.set_, 0,
nullptr);
vk::CmdDraw(m_command_buffer, 3, 1, 0, 0);
m_command_buffer.EndRenderPass();
m_command_buffer.End();
buffer1.Destroy();
VkCommandBufferSubmitInfo cb_info = vku::InitStructHelper();
cb_info.commandBuffer = m_command_buffer;
VkSubmitInfo2 submit_info = vku::InitStructHelper();
submit_info.commandBufferInfoCount = 1;
submit_info.pCommandBufferInfos = &cb_info;
vk::QueueSubmit2KHR(m_default_queue->handle(), 1, &submit_info, VK_NULL_HANDLE);
m_default_queue->Wait();
}
TEST_F(PositiveDescriptorIndexing, PipelineShaderBasic) {
TEST_DESCRIPTION("Test basic usage of GL_EXT_nonuniform_qualifier.");
std::vector<VkDescriptorSetLayoutBinding> bindings = {
{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
};
const char* csSource = R"glsl(
#version 450
#extension GL_EXT_nonuniform_qualifier : enable
layout(set=0, binding=0) buffer block { int x; };
void main() {
nonuniformEXT int data;
int table[5];
data = table[nonuniformEXT(x)];
}
)glsl";
ComputePipelineShaderTest(csSource, bindings);
}
TEST_F(PositiveDescriptorIndexing, PipelineShaderSampler2D) {
TEST_DESCRIPTION("Indexing into a Sampler2D (combined image sampler).");
std::vector<VkDescriptorSetLayoutBinding> bindings = {
{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
{1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
};
const char* csSource = R"glsl(
#version 450
#extension GL_EXT_nonuniform_qualifier : enable
layout(set=0, binding=0) buffer block { vec2 x; };
layout(set=0, binding=1) uniform sampler2D t;
void main() {
vec4 vColor4 = texture(t, nonuniformEXT(x));
}
)glsl";
ComputePipelineShaderTest(csSource, bindings);
}
TEST_F(PositiveDescriptorIndexing, PipelineShaderImageBufferArray) {
TEST_DESCRIPTION("Indexing into a ImageVuffer array (texel buffer).");
std::vector<VkDescriptorSetLayoutBinding> bindings = {
{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
{1, VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
};
const char* csSource = R"glsl(
#version 450
#extension GL_EXT_nonuniform_qualifier : enable
layout(set=0, binding=0) buffer block { int x; };
layout(set=0, binding=1, rgba8ui) uniform uimageBuffer image_buffer_array[];
void main() {
vec4 color = vec4(1.0);
color += imageLoad(image_buffer_array[x], 0);
// uses a OpCopyObject
color += imageLoad(image_buffer_array[nonuniformEXT(x)], 0);
}
)glsl";
AddRequiredFeature(vkt::Feature::runtimeDescriptorArray);
AddRequiredFeature(vkt::Feature::shaderStorageTexelBufferArrayDynamicIndexing);
AddRequiredFeature(vkt::Feature::shaderStorageTexelBufferArrayNonUniformIndexing);
ComputePipelineShaderTest(csSource, bindings);
}
TEST_F(PositiveDescriptorIndexing, PipelineShaderMultiArrayIndexing) {
TEST_DESCRIPTION("Indexing into a nested array.");
std::vector<VkDescriptorSetLayoutBinding> bindings = {
{0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
{2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
};
const char* csSource = R"glsl(
#version 450
#extension GL_EXT_nonuniform_qualifier : enable
layout(set = 0, binding = 0) uniform A { uint value; };
layout(set = 0, binding = 1) uniform B { uint tex_index[1]; };
layout(set = 0, binding = 2) uniform sampler2D tex[6];
void main() {
vec4 color = vec4(1.0);
color += texture(tex[tex_index[value]], vec2(0, 0));
color += texture(tex[tex_index[nonuniformEXT(value)]], vec2(0, 0));
color += texture(tex[nonuniformEXT(tex_index[value])], vec2(0, 0));
}
)glsl";
AddRequiredFeature(vkt::Feature::shaderSampledImageArrayNonUniformIndexing);
ComputePipelineShaderTest(csSource, bindings);
}
TEST_F(PositiveDescriptorIndexing, DescriptorSetVariableDescriptorCountAllocateInfo) {
AddRequiredExtensions(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
AddRequiredFeature(vkt::Feature::descriptorBindingVariableDescriptorCount);
RETURN_IF_SKIP(Init());
// Don't set VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT
vkt::DescriptorSetLayout ds_layout(*m_device, {0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr});
VkDescriptorPoolSize pool_size = {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1};
VkDescriptorPoolCreateInfo dspci = vku::InitStructHelper();
dspci.poolSizeCount = 1;
dspci.pPoolSizes = &pool_size;
dspci.maxSets = 1;
vkt::DescriptorPool pool(*m_device, dspci);
// Ignored because VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT isn't set
VkDescriptorSetVariableDescriptorCountAllocateInfo count_alloc_info = vku::InitStructHelper();
count_alloc_info.descriptorSetCount = 1;
uint32_t variable_count = 1024;
count_alloc_info.pDescriptorCounts = &variable_count;
VkDescriptorSetAllocateInfo ds_alloc_info = vku::InitStructHelper(&count_alloc_info);
ds_alloc_info.descriptorPool = pool;
ds_alloc_info.descriptorSetCount = 1;
ds_alloc_info.pSetLayouts = &ds_layout.handle();
VkDescriptorSet ds = VK_NULL_HANDLE;
vk::AllocateDescriptorSets(*m_device, &ds_alloc_info, &ds);
}
TEST_F(PositiveDescriptorIndexing, StaticAccessSomeOfTheArray) {
TEST_DESCRIPTION("https://gitlab.khronos.org/vulkan/vulkan/-/issues/4383");
RETURN_IF_SKIP(Init());
InitRenderTarget();
// Descriptor has 4 items, but only update the 2 used
OneOffDescriptorSet descriptor_set(m_device, {
{0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4, VK_SHADER_STAGE_ALL, nullptr},
});
const vkt::PipelineLayout pipeline_layout(*m_device, {&descriptor_set.layout_});
vkt::Buffer buffer_0(*m_device, 32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT);
vkt::Buffer buffer_1(*m_device, 32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT);
descriptor_set.WriteDescriptorBufferInfo(0, buffer_0, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0);
descriptor_set.WriteDescriptorBufferInfo(0, buffer_0, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1);
descriptor_set.UpdateDescriptorSets();
const char* vs_source = R"glsl(
#version 450
layout(set = 0, binding = 0) uniform Foo { float x; } bufs[2];
void main() {
gl_Position = vec4(bufs[0].x, bufs[0].x, bufs[1].x, bufs[1].x);
}
)glsl";
VkShaderObj vs(*m_device, vs_source, VK_SHADER_STAGE_VERTEX_BIT);
CreatePipelineHelper pipe(*this);
pipe.shader_stages_[0] = vs.GetStageCreateInfo();
pipe.gp_ci_.layout = pipeline_layout;
pipe.CreateGraphicsPipeline();
m_command_buffer.Begin();
m_command_buffer.BeginRenderPass(m_renderPassBeginInfo);
vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe);
vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set.set_, 0,
nullptr);
vk::CmdDraw(m_command_buffer, 1, 0, 0, 0);
m_command_buffer.EndRenderPass();
m_command_buffer.End();
m_default_queue->SubmitAndWait(m_command_buffer);
}
TEST_F(PositiveDescriptorIndexing, VariableDescriptorCountBuffer) {
TEST_DESCRIPTION("https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/10490");
SetTargetApiVersion(VK_API_VERSION_1_2);
AddRequiredFeature(vkt::Feature::descriptorBindingVariableDescriptorCount);
RETURN_IF_SKIP(Init());
vkt::Buffer buffer(*m_device, 1024, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
VkDescriptorBindingFlags binding_flags = VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT;
VkDescriptorSetLayoutBindingFlagsCreateInfo dsl_binding_flags = vku::InitStructHelper();
dsl_binding_flags.bindingCount = 1u;
dsl_binding_flags.pBindingFlags = &binding_flags;
uint32_t variable_count = 1u;
VkDescriptorSetVariableDescriptorCountAllocateInfo variable_count_info = vku::InitStructHelper();
variable_count_info.descriptorSetCount = 1u;
variable_count_info.pDescriptorCounts = &variable_count;
OneOffDescriptorSet descriptor_set(m_device,
{
{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
},
0u, &dsl_binding_flags, 0u, &variable_count_info);
descriptor_set.WriteDescriptorBufferInfo(0, buffer, 0u, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0u);
descriptor_set.UpdateDescriptorSets();
const vkt::PipelineLayout pipeline_layout(*m_device, {&descriptor_set.layout_});
const char* cs_source = R"glsl(
#version 450 core
layout(set = 0, binding = 0) buffer SSBO { int x; } bufs[3];
void main() {
bufs[0].x = 0;
}
)glsl";
CreateComputePipelineHelper pipe(*this);
pipe.cs_ = VkShaderObj(*m_device, cs_source, VK_SHADER_STAGE_COMPUTE_BIT);
pipe.cp_ci_.layout = pipeline_layout;
pipe.CreateComputePipeline();
m_command_buffer.Begin();
vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_layout, 0u, 1u, &descriptor_set.set_, 0u,
nullptr);
vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
vk::CmdDispatch(m_command_buffer, 1u, 1u, 1u);
m_command_buffer.End();
}