mirror of
https://github.com/izzy2lost/xenia-edge.git
synced 2026-07-06 00:20:26 -07:00
[Vulkan] Resolve downscale shader to use rotating descriptor pool chain
Avoids mid-frame GPU stalls on pool exhaustion
This commit is contained in:
@@ -1302,27 +1302,16 @@ bool VulkanCommandProcessor::SetupContext() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Descriptor pool for resolve downscale shader.
|
||||
// Max 16 sets, each with 2 storage buffers (source + destination).
|
||||
// Descriptor pool chain for resolve downscale shader.
|
||||
// Uses pool chain to avoid mid-frame GPU stalls on pool exhaustion.
|
||||
// Each pool has 64 sets with 2 storage buffers each.
|
||||
VkDescriptorPoolSize resolve_downscale_pool_size;
|
||||
resolve_downscale_pool_size.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
resolve_downscale_pool_size.descriptorCount = 32; // 16 sets * 2 buffers
|
||||
VkDescriptorPoolCreateInfo resolve_downscale_pool_create_info;
|
||||
resolve_downscale_pool_create_info.sType =
|
||||
VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
resolve_downscale_pool_create_info.pNext = nullptr;
|
||||
resolve_downscale_pool_create_info.flags =
|
||||
VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
resolve_downscale_pool_create_info.maxSets = 16;
|
||||
resolve_downscale_pool_create_info.poolSizeCount = 1;
|
||||
resolve_downscale_pool_create_info.pPoolSizes =
|
||||
&resolve_downscale_pool_size;
|
||||
if (dfn.vkCreateDescriptorPool(
|
||||
device, &resolve_downscale_pool_create_info, nullptr,
|
||||
&resolve_downscale_descriptor_pool_) != VK_SUCCESS) {
|
||||
XELOGE("Failed to create the resolve downscale descriptor pool");
|
||||
return false;
|
||||
}
|
||||
resolve_downscale_pool_size.descriptorCount = 128; // 64 sets * 2 buffers
|
||||
resolve_downscale_descriptor_pool_chain_ =
|
||||
std::make_unique<ui::vulkan::VulkanDescriptorPoolChain>(
|
||||
vulkan_device, 0, 64, &resolve_downscale_pool_size, 1,
|
||||
resolve_downscale_descriptor_set_layout_);
|
||||
}
|
||||
|
||||
occlusion_query_resources_available_ = InitializeOcclusionQueryResources();
|
||||
@@ -1392,8 +1381,7 @@ void VulkanCommandProcessor::ShutdownContext() {
|
||||
ui::vulkan::util::DestroyAndNullHandle(
|
||||
dfn.vkDestroyDescriptorSetLayout, device,
|
||||
resolve_downscale_descriptor_set_layout_);
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyDescriptorPool, device,
|
||||
resolve_downscale_descriptor_pool_);
|
||||
resolve_downscale_descriptor_pool_chain_.reset();
|
||||
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyDescriptorPool, device,
|
||||
swap_descriptor_pool_);
|
||||
@@ -4201,43 +4189,22 @@ bool VulkanCommandProcessor::IssueCopy() {
|
||||
resolve_downscale_buffer_size_ = downscale_buffer_size;
|
||||
}
|
||||
|
||||
// Allocate descriptor set for source and destination buffers
|
||||
VkDescriptorSetAllocateInfo descriptor_alloc_info = {};
|
||||
descriptor_alloc_info.sType =
|
||||
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
descriptor_alloc_info.descriptorPool = resolve_downscale_descriptor_pool_;
|
||||
descriptor_alloc_info.descriptorSetCount = 1;
|
||||
descriptor_alloc_info.pSetLayouts =
|
||||
&resolve_downscale_descriptor_set_layout_;
|
||||
|
||||
VkDescriptorSet descriptor_set;
|
||||
if (dfn.vkAllocateDescriptorSets(device, &descriptor_alloc_info,
|
||||
&descriptor_set) != VK_SUCCESS) {
|
||||
// Pool may be exhausted, reset it and try again
|
||||
if (!AwaitAllQueueOperationsCompletion()) {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to wait for GPU before resetting "
|
||||
"descriptor pool");
|
||||
if (debug_markers_enabled_) {
|
||||
PopDebugMarker();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
dfn.vkResetDescriptorPool(device, resolve_downscale_descriptor_pool_, 0);
|
||||
if (dfn.vkAllocateDescriptorSets(device, &descriptor_alloc_info,
|
||||
&descriptor_set) != VK_SUCCESS) {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to allocate resolve downscale "
|
||||
"descriptor set");
|
||||
if (debug_markers_enabled_) {
|
||||
PopDebugMarker();
|
||||
}
|
||||
return true;
|
||||
// Allocate descriptor set for source and destination buffers.
|
||||
// Uses pool chain to avoid mid-frame GPU stalls on pool exhaustion.
|
||||
VkDescriptorSet descriptor_set =
|
||||
resolve_downscale_descriptor_pool_chain_->Allocate(
|
||||
GetCurrentSubmission());
|
||||
if (descriptor_set == VK_NULL_HANDLE) {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to allocate resolve downscale "
|
||||
"descriptor set from pool chain");
|
||||
if (debug_markers_enabled_) {
|
||||
PopDebugMarker();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure submission is open after any potential
|
||||
// AwaitAllQueueOperationsCompletion calls during buffer allocation above
|
||||
// Ensure submission is open
|
||||
if (!BeginSubmission(true)) {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to begin submission for scaled "
|
||||
@@ -4856,27 +4823,9 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(
|
||||
|
||||
size_t fences_total = submissions_in_flight_fences_.size();
|
||||
size_t fences_awaited = 0;
|
||||
if (await_submission > submission_completed_) {
|
||||
// Await in a blocking way if requested.
|
||||
// TODO(Triang3l): Await only one fence. "Fence signal operations that are
|
||||
// defined by vkQueueSubmit additionally include in the first
|
||||
// synchronization scope all commands that occur earlier in submission
|
||||
// order."
|
||||
VkResult wait_result = dfn.vkWaitForFences(
|
||||
device, uint32_t(await_submission - submission_completed_),
|
||||
submissions_in_flight_fences_.data(), VK_TRUE, UINT64_MAX);
|
||||
if (wait_result == VK_SUCCESS) {
|
||||
fences_awaited += await_submission - submission_completed_;
|
||||
} else {
|
||||
XELOGE("Failed to await submission completion Vulkan fences");
|
||||
if (wait_result == VK_ERROR_DEVICE_LOST) {
|
||||
device_lost_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check how far into the submissions the GPU currently is, in order because
|
||||
// submission themselves can be executed out of order, but Xenia serializes
|
||||
// that for simplicity.
|
||||
// Check how far into the submissions the GPU currently is (non-blocking).
|
||||
// This is similar to D3D12's GetCompletedValue() - check first, wait only if
|
||||
// needed.
|
||||
while (fences_awaited < fences_total) {
|
||||
VkResult fence_status = dfn.vkWaitForFences(
|
||||
device, 1, &submissions_in_flight_fences_[fences_awaited], VK_TRUE, 0);
|
||||
@@ -4888,6 +4837,25 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(
|
||||
}
|
||||
++fences_awaited;
|
||||
}
|
||||
// If we need to wait for a specific submission and it's not done yet, block.
|
||||
if (!device_lost_ &&
|
||||
await_submission > submission_completed_ + fences_awaited) {
|
||||
size_t fences_to_wait =
|
||||
size_t(await_submission - submission_completed_) - fences_awaited;
|
||||
if (fences_to_wait > 0 && fences_awaited < fences_total) {
|
||||
VkResult wait_result = dfn.vkWaitForFences(
|
||||
device, uint32_t(fences_to_wait),
|
||||
&submissions_in_flight_fences_[fences_awaited], VK_TRUE, UINT64_MAX);
|
||||
if (wait_result == VK_SUCCESS) {
|
||||
fences_awaited += fences_to_wait;
|
||||
} else {
|
||||
XELOGE("Failed to await submission completion Vulkan fences");
|
||||
if (wait_result == VK_ERROR_DEVICE_LOST) {
|
||||
device_lost_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (device_lost_) {
|
||||
graphics_system_->OnHostGpuLossFromAnyThread(true);
|
||||
return;
|
||||
@@ -4937,6 +4905,11 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(
|
||||
|
||||
texture_cache_->CompletedSubmissionUpdated(submission_completed_);
|
||||
|
||||
// Reclaim descriptor pools that the GPU has finished using.
|
||||
if (resolve_downscale_descriptor_pool_chain_) {
|
||||
resolve_downscale_descriptor_pool_chain_->Reclaim(submission_completed_);
|
||||
}
|
||||
|
||||
ProcessReadyOcclusionQueries(submission_completed_);
|
||||
|
||||
// Destroy objects scheduled for destruction.
|
||||
@@ -5377,6 +5350,12 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
|
||||
submissions_in_flight_fences_.push_back(fence);
|
||||
fences_free_.pop_back();
|
||||
|
||||
// Mark descriptor pool chains with submission index for reclaim tracking.
|
||||
if (resolve_downscale_descriptor_pool_chain_) {
|
||||
resolve_downscale_descriptor_pool_chain_->EndSubmission(
|
||||
submission_current);
|
||||
}
|
||||
|
||||
submission_open_ = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "xenia/gpu/xenos.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/ui/vulkan/linked_type_descriptor_set_allocator.h"
|
||||
#include "xenia/ui/vulkan/vulkan_descriptor_pool_chain.h"
|
||||
#include "xenia/ui/vulkan/vulkan_presenter.h"
|
||||
#include "xenia/ui/vulkan/vulkan_provider.h"
|
||||
#include "xenia/ui/vulkan/vulkan_upload_buffer_pool.h"
|
||||
@@ -689,8 +690,10 @@ class VulkanCommandProcessor final : public CommandProcessor {
|
||||
VK_NULL_HANDLE;
|
||||
VkPipelineLayout resolve_downscale_pipeline_layout_ = VK_NULL_HANDLE;
|
||||
VkPipeline resolve_downscale_pipeline_ = VK_NULL_HANDLE;
|
||||
// Descriptor pool for resolve downscale shader (2 storage buffers per set).
|
||||
VkDescriptorPool resolve_downscale_descriptor_pool_ = VK_NULL_HANDLE;
|
||||
// Descriptor pool chain for resolve downscale shader (2 storage buffers per
|
||||
// set). Uses pool chain to avoid mid-frame GPU stalls on pool exhaustion.
|
||||
std::unique_ptr<ui::vulkan::VulkanDescriptorPoolChain>
|
||||
resolve_downscale_descriptor_pool_chain_;
|
||||
// Intermediate buffer for downscaled output (device local, for compute).
|
||||
VkBuffer resolve_downscale_buffer_ = VK_NULL_HANDLE;
|
||||
VkDeviceMemory resolve_downscale_buffer_memory_ = VK_NULL_HANDLE;
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2025 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/ui/vulkan/vulkan_descriptor_pool_chain.h"
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace vulkan {
|
||||
|
||||
VulkanDescriptorPoolChain::VulkanDescriptorPoolChain(
|
||||
const VulkanDevice* device, VkDescriptorPoolCreateFlags flags,
|
||||
uint32_t max_sets_per_pool, const VkDescriptorPoolSize* pool_sizes,
|
||||
uint32_t pool_size_count, VkDescriptorSetLayout set_layout)
|
||||
: device_(device),
|
||||
flags_(flags),
|
||||
max_sets_per_pool_(max_sets_per_pool),
|
||||
set_layout_(set_layout) {
|
||||
pool_sizes_.assign(pool_sizes, pool_sizes + pool_size_count);
|
||||
}
|
||||
|
||||
VulkanDescriptorPoolChain::~VulkanDescriptorPoolChain() { ClearCache(); }
|
||||
|
||||
void VulkanDescriptorPoolChain::Reclaim(uint64_t completed_submission_index) {
|
||||
// Move pools that the GPU has finished using back to the writable list.
|
||||
while (submitted_first_) {
|
||||
if (submitted_first_->last_submission_index > completed_submission_index) {
|
||||
break;
|
||||
}
|
||||
// Reset the pool so it can be reused.
|
||||
const VulkanDevice::Functions& dfn = device_->functions();
|
||||
dfn.vkResetDescriptorPool(device_->device(), submitted_first_->pool, 0);
|
||||
submitted_first_->allocated_sets = 0;
|
||||
|
||||
// Move to writable list.
|
||||
if (writable_last_) {
|
||||
writable_last_->next = submitted_first_;
|
||||
} else {
|
||||
writable_first_ = submitted_first_;
|
||||
}
|
||||
writable_last_ = submitted_first_;
|
||||
submitted_first_ = submitted_first_->next;
|
||||
writable_last_->next = nullptr;
|
||||
}
|
||||
if (!submitted_first_) {
|
||||
submitted_last_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanDescriptorPoolChain::ClearCache() {
|
||||
const VulkanDevice::Functions& dfn = device_->functions();
|
||||
VkDevice vk_device = device_->device();
|
||||
|
||||
while (submitted_first_) {
|
||||
Pool* next = submitted_first_->next;
|
||||
DestroyPool(submitted_first_);
|
||||
submitted_first_ = next;
|
||||
}
|
||||
submitted_last_ = nullptr;
|
||||
|
||||
while (writable_first_) {
|
||||
Pool* next = writable_first_->next;
|
||||
DestroyPool(writable_first_);
|
||||
writable_first_ = next;
|
||||
}
|
||||
writable_last_ = nullptr;
|
||||
}
|
||||
|
||||
VkDescriptorSet VulkanDescriptorPoolChain::Allocate(uint64_t submission_index) {
|
||||
const VulkanDevice::Functions& dfn = device_->functions();
|
||||
VkDevice vk_device = device_->device();
|
||||
|
||||
// If current pool is full, move to next writable pool or create new.
|
||||
if (writable_first_ &&
|
||||
writable_first_->allocated_sets >= max_sets_per_pool_) {
|
||||
// Current pool is full, move it to submitted list.
|
||||
if (submitted_last_) {
|
||||
submitted_last_->next = writable_first_;
|
||||
} else {
|
||||
submitted_first_ = writable_first_;
|
||||
}
|
||||
submitted_last_ = writable_first_;
|
||||
writable_first_ = writable_first_->next;
|
||||
submitted_last_->next = nullptr;
|
||||
if (!writable_first_) {
|
||||
writable_last_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new pool if needed.
|
||||
if (!writable_first_) {
|
||||
writable_first_ = CreatePool();
|
||||
if (!writable_first_) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
writable_last_ = writable_first_;
|
||||
}
|
||||
|
||||
// Allocate from current pool.
|
||||
VkDescriptorSetAllocateInfo alloc_info = {};
|
||||
alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
alloc_info.descriptorPool = writable_first_->pool;
|
||||
alloc_info.descriptorSetCount = 1;
|
||||
alloc_info.pSetLayouts = &set_layout_;
|
||||
|
||||
VkDescriptorSet descriptor_set;
|
||||
VkResult result =
|
||||
dfn.vkAllocateDescriptorSets(vk_device, &alloc_info, &descriptor_set);
|
||||
|
||||
if (result != VK_SUCCESS) {
|
||||
// Pool exhausted unexpectedly, try creating a new pool.
|
||||
// Move current to submitted.
|
||||
if (submitted_last_) {
|
||||
submitted_last_->next = writable_first_;
|
||||
} else {
|
||||
submitted_first_ = writable_first_;
|
||||
}
|
||||
submitted_last_ = writable_first_;
|
||||
writable_first_ = writable_first_->next;
|
||||
submitted_last_->next = nullptr;
|
||||
if (!writable_first_) {
|
||||
writable_last_ = nullptr;
|
||||
}
|
||||
|
||||
// Create new pool.
|
||||
writable_first_ = CreatePool();
|
||||
if (!writable_first_) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
writable_last_ = writable_first_;
|
||||
|
||||
// Retry allocation.
|
||||
alloc_info.descriptorPool = writable_first_->pool;
|
||||
result =
|
||||
dfn.vkAllocateDescriptorSets(vk_device, &alloc_info, &descriptor_set);
|
||||
if (result != VK_SUCCESS) {
|
||||
XELOGE("VulkanDescriptorPoolChain: Failed to allocate descriptor set");
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
writable_first_->allocated_sets++;
|
||||
writable_first_->last_submission_index = submission_index;
|
||||
return descriptor_set;
|
||||
}
|
||||
|
||||
void VulkanDescriptorPoolChain::EndSubmission(uint64_t submission_index) {
|
||||
// Mark current pool with submission index for reclaim tracking.
|
||||
if (writable_first_) {
|
||||
writable_first_->last_submission_index = submission_index;
|
||||
}
|
||||
}
|
||||
|
||||
VulkanDescriptorPoolChain::Pool* VulkanDescriptorPoolChain::CreatePool() {
|
||||
const VulkanDevice::Functions& dfn = device_->functions();
|
||||
VkDevice vk_device = device_->device();
|
||||
|
||||
VkDescriptorPoolCreateInfo create_info = {};
|
||||
create_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
create_info.flags = flags_;
|
||||
create_info.maxSets = max_sets_per_pool_;
|
||||
create_info.poolSizeCount = static_cast<uint32_t>(pool_sizes_.size());
|
||||
create_info.pPoolSizes = pool_sizes_.data();
|
||||
|
||||
VkDescriptorPool vk_pool;
|
||||
if (dfn.vkCreateDescriptorPool(vk_device, &create_info, nullptr, &vk_pool) !=
|
||||
VK_SUCCESS) {
|
||||
XELOGE("VulkanDescriptorPoolChain: Failed to create descriptor pool");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Pool* pool = new Pool;
|
||||
pool->pool = vk_pool;
|
||||
pool->last_submission_index = 0;
|
||||
pool->allocated_sets = 0;
|
||||
pool->next = nullptr;
|
||||
return pool;
|
||||
}
|
||||
|
||||
void VulkanDescriptorPoolChain::DestroyPool(Pool* pool) {
|
||||
const VulkanDevice::Functions& dfn = device_->functions();
|
||||
dfn.vkDestroyDescriptorPool(device_->device(), pool->pool, nullptr);
|
||||
delete pool;
|
||||
}
|
||||
|
||||
} // namespace vulkan
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2025 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_UI_VULKAN_VULKAN_DESCRIPTOR_POOL_CHAIN_H_
|
||||
#define XENIA_UI_VULKAN_VULKAN_DESCRIPTOR_POOL_CHAIN_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/ui/vulkan/vulkan_device.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace vulkan {
|
||||
|
||||
// A chain of descriptor pools that rotates based on submission index,
|
||||
// avoiding mid-frame GPU stalls when pools are exhausted.
|
||||
// Similar to D3D12DescriptorHeapPool but for Vulkan's descriptor model.
|
||||
class VulkanDescriptorPoolChain {
|
||||
public:
|
||||
VulkanDescriptorPoolChain(const VulkanDevice* device,
|
||||
VkDescriptorPoolCreateFlags flags,
|
||||
uint32_t max_sets_per_pool,
|
||||
const VkDescriptorPoolSize* pool_sizes,
|
||||
uint32_t pool_size_count,
|
||||
VkDescriptorSetLayout set_layout);
|
||||
~VulkanDescriptorPoolChain();
|
||||
|
||||
// Reclaim pools that have been fully processed by the GPU.
|
||||
// Call this when submissions complete (e.g., after fence signaled).
|
||||
void Reclaim(uint64_t completed_submission_index);
|
||||
|
||||
// Clear all pools. Must only be called when GPU is idle.
|
||||
void ClearCache();
|
||||
|
||||
// Allocate a descriptor set from the chain.
|
||||
// Returns VK_NULL_HANDLE on failure.
|
||||
// The submission_index should be the current frame/submission being built.
|
||||
VkDescriptorSet Allocate(uint64_t submission_index);
|
||||
|
||||
// Mark current pool as submitted and ready for reclaim after GPU completes.
|
||||
// Call this when submitting a command buffer.
|
||||
void EndSubmission(uint64_t submission_index);
|
||||
|
||||
private:
|
||||
struct Pool {
|
||||
VkDescriptorPool pool;
|
||||
uint64_t last_submission_index;
|
||||
uint32_t allocated_sets;
|
||||
Pool* next;
|
||||
};
|
||||
|
||||
Pool* CreatePool();
|
||||
void DestroyPool(Pool* pool);
|
||||
|
||||
const VulkanDevice* device_;
|
||||
VkDescriptorPoolCreateFlags flags_;
|
||||
uint32_t max_sets_per_pool_;
|
||||
std::vector<VkDescriptorPoolSize> pool_sizes_;
|
||||
VkDescriptorSetLayout set_layout_;
|
||||
|
||||
// Pools with free space, first is current.
|
||||
Pool* writable_first_ = nullptr;
|
||||
Pool* writable_last_ = nullptr;
|
||||
|
||||
// Pools waiting for GPU to finish.
|
||||
Pool* submitted_first_ = nullptr;
|
||||
Pool* submitted_last_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace vulkan
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_UI_VULKAN_VULKAN_DESCRIPTOR_POOL_CHAIN_H_
|
||||
Reference in New Issue
Block a user