diff --git a/include/vsg/io/DatabasePager.h b/include/vsg/io/DatabasePager.h index 78374461d8..9b01c0649b 100644 --- a/include/vsg/io/DatabasePager.h +++ b/include/vsg/io/DatabasePager.h @@ -48,6 +48,47 @@ namespace vsg std::vector newHighresRequired; }; + struct PagedLODContainer : public Inherit + { + explicit PagedLODContainer(uint32_t maxNumPagedLOD = 10000); + + struct List + { + uint32_t head = 0; + uint32_t tail = 0; + uint32_t count = 0; + std::string name; + }; + + struct Element + { + uint32_t previous = 0; + uint32_t next = 0; + ref_ptr plod; + List* list = nullptr; + }; + + using Elements = std::vector; + + Elements elements; + List availableList; + List inactiveList; + List activeList; + + void resize(uint32_t new_size); + void resize(); + void inactive(const PagedLOD* plod); + void active(const PagedLOD* plod); + void remove(PagedLOD* plod); + + void _move(const PagedLOD* plod, List* targetList); + + bool check(); + bool check(const List& list); + + void print(std::ostream& out); + }; + /// Thread safe queue for tracking PagedLOD that needs to be loaded, compiled or merged by the DatabasePager class VSG_DECLSPEC DatabaseQueue : public Inherit { diff --git a/include/vsg/nodes/PagedLOD.h b/include/vsg/nodes/PagedLOD.h index b33cf2b691..607761ffdf 100644 --- a/include/vsg/nodes/PagedLOD.h +++ b/include/vsg/nodes/PagedLOD.h @@ -108,45 +108,4 @@ namespace vsg }; VSG_type_name(vsg::PagedLOD); - struct PagedLODContainer : public Inherit - { - explicit PagedLODContainer(uint32_t maxNumPagedLOD = 10000); - - struct List - { - uint32_t head = 0; - uint32_t tail = 0; - uint32_t count = 0; - std::string name; - }; - - struct Element - { - uint32_t previous = 0; - uint32_t next = 0; - ref_ptr plod; - List* list = nullptr; - }; - - using Elements = std::vector; - - Elements elements; - List availableList; - List inactiveList; - List activeList; - - void resize(uint32_t new_size); - void resize(); - void inactive(const PagedLOD* plod); - void active(const PagedLOD* plod); - void remove(PagedLOD* plod); - - void _move(const PagedLOD* plod, List* targetList); - - bool check(); - bool check(const List& list); - - void print(std::ostream& out); - }; - } // namespace vsg diff --git a/include/vsg/vk/vulkan.h b/include/vsg/vk/vulkan.h index 0a27c4ee1f..cac8dbd1ab 100644 --- a/include/vsg/vk/vulkan.h +++ b/include/vsg/vk/vulkan.h @@ -116,6 +116,41 @@ typedef struct VkRenderPassCreateInfo2KHR #endif +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Definitions not provided prior to 1.1.97 +// +#if VK_HEADER_VERSION < 97 + +#define VK_EXT_memory_budget 1 +#define VK_EXT_MEMORY_BUDGET_SPEC_VERSION 1 +#define VK_EXT_MEMORY_BUDGET_EXTENSION_NAME "VK_EXT_memory_budget" + +typedef struct VkPhysicalDeviceMemoryBudgetPropertiesEXT { + VkStructureType sType; + void* pNext; + VkDeviceSize heapBudget[VK_MAX_MEMORY_HEAPS]; + VkDeviceSize heapUsage[VK_MAX_MEMORY_HEAPS]; +} VkPhysicalDeviceMemoryBudgetPropertiesEXT; + +#define VK_EXT_memory_priority 1 +#define VK_EXT_MEMORY_PRIORITY_SPEC_VERSION 1 +#define VK_EXT_MEMORY_PRIORITY_EXTENSION_NAME "VK_EXT_memory_priority" + +typedef struct VkPhysicalDeviceMemoryPriorityFeaturesEXT { + VkStructureType sType; + void* pNext; + VkBool32 memoryPriority; +} VkPhysicalDeviceMemoryPriorityFeaturesEXT; + +typedef struct VkMemoryPriorityAllocateInfoEXT { + VkStructureType sType; + const void* pNext; + float priority; +} VkMemoryPriorityAllocateInfoEXT; + +#endif + //////////////////////////////////////////////////////////////////////////////////////////////////// // // Definitions not provided prior to 1.3.215 diff --git a/src/vsg/io/DatabasePager.cpp b/src/vsg/io/DatabasePager.cpp index ab2227aad3..ca96f187b6 100644 --- a/src/vsg/io/DatabasePager.cpp +++ b/src/vsg/io/DatabasePager.cpp @@ -20,6 +20,350 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI using namespace vsg; +#define PRINT_CONTAINER 0 +#define CHECK_CONTAINER 0 + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// +// PagedLODContainer +// +PagedLODContainer::PagedLODContainer(uint32_t maxNumPagedLOD) : + elements(1) +{ + availableList.name = "availableList"; + activeList.name = "activeList"; + inactiveList.name = "inactiveList"; + + resize(std::max(maxNumPagedLOD, 10u)); +} + +void PagedLODContainer::resize(uint32_t new_size) +{ + // note first entry in elements is the null entry, so have to add/take away 1 when accounting for it. + uint32_t original_size = static_cast(elements.size()) - 1; + elements.resize(new_size + 1); + + uint32_t i = 1 + original_size; + uint32_t previous = availableList.tail; + + if (availableList.head == 0) + { + availableList.head = i; + } + + if (availableList.tail > 0) + { + elements[availableList.tail].next = i; + } + + for (; i < new_size; ++i) + { + auto& element = elements[i]; + element.previous = previous; + element.next = i + 1; + element.list = &availableList; + previous = i; + } + + // set up tail + elements[i].previous = previous; + elements[i].next = 0; + elements[i].list = &availableList; + + availableList.tail = i; + + availableList.count += (new_size - original_size); + +#if PRINT_CONTAINER + debug("PagedLODContainer::resize(", new_size, ")"); + debug_stream([&](auto& fout) { print(fout); }); +#endif +} + +void PagedLODContainer::resize() +{ + uint32_t original_size = static_cast(elements.size() - 1); + uint32_t new_size = original_size * 2; + resize(new_size); +} + +void PagedLODContainer::print(std::ostream& fout) +{ + uint32_t total_size = static_cast(elements.size()); + fout << " PagedLODContainer::print() elements.size() = " << total_size << std::endl; + fout << " availableList, " << &availableList << ", head = " << availableList.head << ", tail = " << availableList.tail << " count = " << availableList.count << std::endl; + fout << " activeList, " << &activeList << ", head = " << activeList.head << ", tail = " << activeList.tail << " count = " << activeList.count << std::endl; + fout << " inactiveList = " << &inactiveList << ", head = " << inactiveList.head << ", tail = " << inactiveList.tail << " count = " << inactiveList.count << std::endl; + + for (unsigned i = 0; i < total_size; ++i) + { + const auto& element = elements[i]; + fout << " element[" << i << "] plod = " << element.plod.get() << ", previous =" << element.previous << ", next = " << element.next << ", list = "; + if (element.list) + fout << element.list->name; + else + fout << " unassigned"; + fout << std::endl; + } +} + +void PagedLODContainer::_move(const PagedLOD* plod, List* targetList) +{ + if (plod->index == 0) + { +#if PRINT_CONTAINER + debug("plod not yet assigned, assigning to ", targetList->name); +#endif + // resize if there are no available empty elements. + if (availableList.head == 0) + { + resize(); + } + + // take the first element from availableList and move head to next item. + uint32_t index = availableList.head; + auto& element = elements[availableList.head]; + if (availableList.head == availableList.tail) + { + availableList.head = 0; + availableList.tail = 0; + } + else + { + availableList.head = element.next; + } + + if (element.next > 0) + { + auto& next_element = elements[element.next]; + next_element.previous = 0; + } + + // place element at the end of the active list. + if (targetList->tail > 0) + { + auto& previous_element = elements[targetList->tail]; + previous_element.next = index; + } + + if (targetList->head == 0) + { + targetList->head = index; + } + + element.previous = targetList->tail; + element.next = 0; + element.list = targetList; + targetList->tail = index; + + // assign index to PagedLOD + plod->index = index; + element.plod = const_cast(plod); + + --(availableList.count); + ++(targetList->count); + + return; + } + + auto& element = elements[plod->index]; + List* previousList = element.list; + + if (previousList == targetList) + { +#if PRINT_CONTAINER + debug("PagedLODContainer::move(", plod, ") index = ", plod->index, ", already in ", targetList->name); +#endif + return; + } + +#if PRINT_CONTAINER + debug("PagedLODContainer::move(", plod, ") index = ", plod->index, ", moving from ", previousList->name, " to ", targetList->name); +#endif + + // remove from inactiveList + + if (element.previous > 0) elements[element.previous].next = element.next; + if (element.next > 0) elements[element.next].previous = element.previous; + + // if this element is tail on inactive list then shift it back + if (previousList->head == plod->index) + { +#if PRINT_CONTAINER + debug(" removing head from ", previousList->name); +#endif + previousList->head = element.next; + } + + if (previousList->tail == plod->index) + { +#if PRINT_CONTAINER + debug(" removing tail from ", previousList->name); +#endif + previousList->tail = element.previous; + } + + element.list = targetList; + element.previous = targetList->tail; + element.next = 0; + + // add to end of activeList tail + if (targetList->head == 0) + { +#if PRINT_CONTAINER + debug(" setting ", targetList->name, ".head to", plod->index); +#endif + targetList->head = plod->index; + } + + if (targetList->tail > 0) + { +#if PRINT_CONTAINER + debug(" moving ", targetList->name, ".tail to ", plod->index); +#endif + elements[targetList->tail].next = plod->index; + } + targetList->tail = plod->index; + + --(previousList->count); + ++(targetList->count); +} + +void PagedLODContainer::active(const PagedLOD* plod) +{ + debug("Moving to activeList", plod, ", ", plod->index); + + _move(plod, &activeList); + +#if PRINT_CONTAINER + debug_stream([&](auto& fout) { check(); print(fout); }); +#endif +} + +void PagedLODContainer::inactive(const PagedLOD* plod) +{ + debug("Moving to inactiveList", plod, ", ", plod->index); + + _move(plod, &inactiveList); + +#if PRINT_CONTAINER + debug_stream([&](std::ostream& fout) { check(); print(fout); }); +#endif +} + +void PagedLODContainer::remove(PagedLOD* plod) +{ + debug("Remove and make available to availableList", plod, ", ", plod->index); + + if (plod->index == 0) + { + warn("PagedLODContainer::remove() plod not assigned so ignore"); + check(); + return; + } + + _move(plod, &availableList); + + // reset element and plod + auto& element = elements[plod->index]; + plod->index = 0; + element.plod = nullptr; + +#if PRINT_CONTAINER + check(); +#endif +#if CHECK_CONTAINER + info_stream([&](std::ostream& fout) { print(fout); }); +#endif +} + +bool PagedLODContainer::check(const List& list) +{ + if (list.head == 0) + { + // we have an empty list + if (list.tail == 0) + { + if (list.count == 0) return true; + warn("list ", list.name, " has a head==0 and tail==0 but length is ", list.count); + return false; + } + + warn("list ", list.name, " has a head==0, but tail is non zero"); + return false; + } + + const auto& head_element = elements[list.head]; + if (head_element.previous != 0) + { + warn("list ", list.name, " has a head.previous that is non zero ", head_element.previous); + return false; + } + + const auto& tail_element = elements[list.tail]; + if (tail_element.next != 0) + { + warn("list ", list.name, " has a tail.next that is non zero ", tail_element.next); + return false; + } + + uint32_t count = 0; + for (uint32_t i = list.head; i > 0 && count < elements.size();) + { + auto& element = elements[i]; + if (element.previous == 0) + { + if (i != list.head) + { + warn("list ", list.name, " non head element ", i, " has a previous==0"); + return false; + } + } + else + { + auto& previous_element = elements[element.previous]; + if (previous_element.next != i) + { + warn("list ", list.name, " element = ", i, ", element.previous = ", element.previous, ", does not match to previous.next = ", previous_element.next); + return false; + } + } + + if (element.next == 0) + { + if (i != list.tail) + { + warn("list ", list.name, " non tail element ", i, " has a next==0"); + return false; + } + } + else + { + auto& next_element = elements[element.next]; + if (next_element.previous != i) + { + warn("list ", list.name, " element = ", i, ", element.next = ", element.next, ", does not match to next.previous = ", next_element.previous); + return false; + } + } + + ++count; + + i = element.next; + } + + if (count == list.count) return true; + return false; +} + +bool PagedLODContainer::check() +{ + bool result1 = check(availableList); + bool result2 = check(activeList); + bool result3 = check(inactiveList); + return result1 && result2 && result3; +} + ///////////////////////////////////////////////////////////////////////// // // DatabaseQueue diff --git a/src/vsg/nodes/PagedLOD.cpp b/src/vsg/nodes/PagedLOD.cpp index 9550bf5479..21e72c3caa 100644 --- a/src/vsg/nodes/PagedLOD.cpp +++ b/src/vsg/nodes/PagedLOD.cpp @@ -15,18 +15,8 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI using namespace vsg; -#define PRINT_CONTAINER 0 -#define CHECK_CONTAINER 0 - -//static std::atomic_uint s_numPagedLODS{0}; - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// PagedLOD -// PagedLOD::PagedLOD() { - // ++s_numPagedLODS; } PagedLOD::PagedLOD(const PagedLOD& rhs, const CopyOp& copyop) : @@ -42,8 +32,6 @@ PagedLOD::PagedLOD(const PagedLOD& rhs, const CopyOp& copyop) : PagedLOD::~PagedLOD() { - // --s_numPagedLODS; - // vsg::debug("s_numPagedLODS = ", s_numPagedLODS); } int PagedLOD::compare(const Object& rhs_object) const @@ -104,343 +92,3 @@ void PagedLOD::write(Output& output) const output.write("child.node", children[1].node); } -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// PagedLODContainer -// -PagedLODContainer::PagedLODContainer(uint32_t maxNumPagedLOD) : - elements(1) -{ - availableList.name = "availableList"; - activeList.name = "activeList"; - inactiveList.name = "inactiveList"; - - resize(std::max(maxNumPagedLOD, 10u)); -} - -void PagedLODContainer::resize(uint32_t new_size) -{ - // note first entry in elements is the null entry, so have to add/take away 1 when accounting for it. - uint32_t original_size = static_cast(elements.size()) - 1; - elements.resize(new_size + 1); - - uint32_t i = 1 + original_size; - uint32_t previous = availableList.tail; - - if (availableList.head == 0) - { - availableList.head = i; - } - - if (availableList.tail > 0) - { - elements[availableList.tail].next = i; - } - - for (; i < new_size; ++i) - { - auto& element = elements[i]; - element.previous = previous; - element.next = i + 1; - element.list = &availableList; - previous = i; - } - - // set up tail - elements[i].previous = previous; - elements[i].next = 0; - elements[i].list = &availableList; - - availableList.tail = i; - - availableList.count += (new_size - original_size); - -#if PRINT_CONTAINER - debug("PagedLODContainer::resize(", new_size, ")"); - debug_stream([&](auto& fout) { print(fout); }); -#endif -} - -void PagedLODContainer::resize() -{ - uint32_t original_size = static_cast(elements.size() - 1); - uint32_t new_size = original_size * 2; - resize(new_size); -} - -void PagedLODContainer::print(std::ostream& fout) -{ - uint32_t total_size = static_cast(elements.size()); - fout << " PagedLODContainer::print() elements.size() = " << total_size << std::endl; - fout << " availableList, " << &availableList << ", head = " << availableList.head << ", tail = " << availableList.tail << " count = " << availableList.count << std::endl; - fout << " activeList, " << &activeList << ", head = " << activeList.head << ", tail = " << activeList.tail << " count = " << activeList.count << std::endl; - fout << " inactiveList = " << &inactiveList << ", head = " << inactiveList.head << ", tail = " << inactiveList.tail << " count = " << inactiveList.count << std::endl; - - for (unsigned i = 0; i < total_size; ++i) - { - const auto& element = elements[i]; - fout << " element[" << i << "] plod = " << element.plod.get() << ", previous =" << element.previous << ", next = " << element.next << ", list = "; - if (element.list) - fout << element.list->name; - else - fout << " unassigned"; - fout << std::endl; - } -} - -void PagedLODContainer::_move(const PagedLOD* plod, List* targetList) -{ - if (plod->index == 0) - { -#if PRINT_CONTAINER - debug("plod not yet assigned, assigning to ", targetList->name); -#endif - // resize if there are no available empty elements. - if (availableList.head == 0) - { - resize(); - } - - // take the first element from availableList and move head to next item. - uint32_t index = availableList.head; - auto& element = elements[availableList.head]; - if (availableList.head == availableList.tail) - { - availableList.head = 0; - availableList.tail = 0; - } - else - { - availableList.head = element.next; - } - - if (element.next > 0) - { - auto& next_element = elements[element.next]; - next_element.previous = 0; - } - - // place element at the end of the active list. - if (targetList->tail > 0) - { - auto& previous_element = elements[targetList->tail]; - previous_element.next = index; - } - - if (targetList->head == 0) - { - targetList->head = index; - } - - element.previous = targetList->tail; - element.next = 0; - element.list = targetList; - targetList->tail = index; - - // assign index to PagedLOD - plod->index = index; - element.plod = const_cast(plod); - - --(availableList.count); - ++(targetList->count); - - return; - } - - auto& element = elements[plod->index]; - List* previousList = element.list; - - if (previousList == targetList) - { -#if PRINT_CONTAINER - debug("PagedLODContainer::move(", plod, ") index = ", plod->index, ", already in ", targetList->name); -#endif - return; - } - -#if PRINT_CONTAINER - debug("PagedLODContainer::move(", plod, ") index = ", plod->index, ", moving from ", previousList->name, " to ", targetList->name); -#endif - - // remove from inactiveList - - if (element.previous > 0) elements[element.previous].next = element.next; - if (element.next > 0) elements[element.next].previous = element.previous; - - // if this element is tail on inactive list then shift it back - if (previousList->head == plod->index) - { -#if PRINT_CONTAINER - debug(" removing head from ", previousList->name); -#endif - previousList->head = element.next; - } - - if (previousList->tail == plod->index) - { -#if PRINT_CONTAINER - debug(" removing tail from ", previousList->name); -#endif - previousList->tail = element.previous; - } - - element.list = targetList; - element.previous = targetList->tail; - element.next = 0; - - // add to end of activeList tail - if (targetList->head == 0) - { -#if PRINT_CONTAINER - debug(" setting ", targetList->name, ".head to", plod->index); -#endif - targetList->head = plod->index; - } - - if (targetList->tail > 0) - { -#if PRINT_CONTAINER - debug(" moving ", targetList->name, ".tail to ", plod->index); -#endif - elements[targetList->tail].next = plod->index; - } - targetList->tail = plod->index; - - --(previousList->count); - ++(targetList->count); -} - -void PagedLODContainer::active(const PagedLOD* plod) -{ - debug("Moving to activeList", plod, ", ", plod->index); - - _move(plod, &activeList); - -#if PRINT_CONTAINER - debug_stream([&](auto& fout) { check(); print(fout); }); -#endif -} - -void PagedLODContainer::inactive(const PagedLOD* plod) -{ - debug("Moving to inactiveList", plod, ", ", plod->index); - - _move(plod, &inactiveList); - -#if PRINT_CONTAINER - debug_stream([&](std::ostream& fout) { check(); print(fout); }); -#endif -} - -void PagedLODContainer::remove(PagedLOD* plod) -{ - debug("Remove and make available to availableList", plod, ", ", plod->index); - - if (plod->index == 0) - { - warn("PagedLODContainer::remove() plod not assigned so ignore"); - check(); - return; - } - - _move(plod, &availableList); - - // reset element and plod - auto& element = elements[plod->index]; - plod->index = 0; - element.plod = nullptr; - -#if PRINT_CONTAINER - check(); -#endif -#if CHECK_CONTAINER - info_stream([&](std::ostream& fout) { print(fout); }); -#endif -} - -bool PagedLODContainer::check(const List& list) -{ - if (list.head == 0) - { - // we have an empty list - if (list.tail == 0) - { - if (list.count == 0) return true; - warn("list ", list.name, " has a head==0 and tail==0 but length is ", list.count); - return false; - } - - warn("list ", list.name, " has a head==0, but tail is non zero"); - return false; - } - - const auto& head_element = elements[list.head]; - if (head_element.previous != 0) - { - warn("list ", list.name, " has a head.previous that is non zero ", head_element.previous); - return false; - } - - const auto& tail_element = elements[list.tail]; - if (tail_element.next != 0) - { - warn("list ", list.name, " has a tail.next that is non zero ", tail_element.next); - return false; - } - - uint32_t count = 0; - for (uint32_t i = list.head; i > 0 && count < elements.size();) - { - auto& element = elements[i]; - if (element.previous == 0) - { - if (i != list.head) - { - warn("list ", list.name, " non head element ", i, " has a previous==0"); - return false; - } - } - else - { - auto& previous_element = elements[element.previous]; - if (previous_element.next != i) - { - warn("list ", list.name, " element = ", i, ", element.previous = ", element.previous, ", does not match to previous.next = ", previous_element.next); - return false; - } - } - - if (element.next == 0) - { - if (i != list.tail) - { - warn("list ", list.name, " non tail element ", i, " has a next==0"); - return false; - } - } - else - { - auto& next_element = elements[element.next]; - if (next_element.previous != i) - { - warn("list ", list.name, " element = ", i, ", element.next = ", element.next, ", does not match to next.previous = ", next_element.previous); - return false; - } - } - - ++count; - - i = element.next; - } - - if (count == list.count) return true; - return false; -} - -bool PagedLODContainer::check() -{ - bool result1 = check(availableList); - bool result2 = check(activeList); - bool result3 = check(inactiveList); - return result1 && result2 && result3; -} diff --git a/src/vsg/state/Buffer.cpp b/src/vsg/state/Buffer.cpp index 28bf024c1e..fa6aa3dce1 100644 --- a/src/vsg/state/Buffer.cpp +++ b/src/vsg/state/Buffer.cpp @@ -159,8 +159,21 @@ ref_ptr vsg::createBufferAndMemory(Device* device, VkDeviceSize size, Vk buffer->compile(device); auto memRequirements = buffer->getMemoryRequirements(device->deviceID); - auto memory = vsg::DeviceMemory::create(device, memRequirements, memoryProperties, pNextAllocInfo); + if ((memoryProperties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) + { + auto deviceMemoryBufferPools = device->deviceMemoryBufferPools.ref_ptr(); + if (deviceMemoryBufferPools) + { + auto [memory, offset] = deviceMemoryBufferPools->reserveMemory(memRequirements, memoryProperties); + + buffer->bind(memory, offset); + return buffer; + } + } + + auto memory = vsg::DeviceMemory::create(device, memRequirements, memoryProperties, pNextAllocInfo); buffer->bind(memory, 0); + return buffer; } diff --git a/src/vsg/state/Image.cpp b/src/vsg/state/Image.cpp index 42417d61e5..c63813fb8b 100644 --- a/src/vsg/state/Image.cpp +++ b/src/vsg/state/Image.cpp @@ -157,12 +157,25 @@ VkResult Image::bind(DeviceMemory* deviceMemory, VkDeviceSize memoryOffset) VkResult Image::allocateAndBindMemory(Device* device, VkMemoryPropertyFlags memoryProperties, void* pNextAllocInfo) { auto memRequirements = getMemoryRequirements(device->deviceID); + + if ((memoryProperties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) + { + auto deviceMemoryBufferPools = device->deviceMemoryBufferPools.ref_ptr(); + if (deviceMemoryBufferPools) + { + auto [memory, offset] = deviceMemoryBufferPools->reserveMemory(memRequirements, memoryProperties); + + return bind(memory, offset); + } + } + auto memory = DeviceMemory::create(device, memRequirements, memoryProperties, pNextAllocInfo); auto [allocated, offset] = memory->reserve(memRequirements.size); if (!allocated) { throw Exception{"Error: Failed to allocate DeviceMemory."}; } + return bind(memory, offset); }