[D3D12/Vulkan] Simplify host GPU fence management

Replace the `SubmissionTracker`s with new `GPUCompletionTimeline`s with a
more unified interface (using a base class), and without the internal logic
for queue ownership transfers since that idea was scrapped during the
development of the `Presenter`.

Also use this fence management logic for GPU emulation, though without
architectural reworks for now, just on the bottom level.

Still very messy, but can be cleaned up in further GPU command processor
and presenter reworks.
This commit is contained in:
Triang3l
2025-12-14 21:10:06 +03:00
parent 01ae24e46e
commit fe1fd36137
18 changed files with 894 additions and 974 deletions
+40 -76
View File
@@ -481,8 +481,8 @@ bool D3D12CommandProcessor::RequestOneUseSingleViewDescriptors(
} else {
descriptor_index = view_bindless_heap_allocated_++;
}
view_bindless_one_use_descriptors_.push_back(
std::make_pair(descriptor_index, submission_current_));
view_bindless_one_use_descriptors_.emplace_back(descriptor_index,
GetCurrentSubmission());
handles_out[i] =
std::make_pair(provider.OffsetViewDescriptor(
view_bindless_heap_cpu_start_, descriptor_index),
@@ -665,7 +665,8 @@ ID3D12Resource* D3D12CommandProcessor::RequestScratchGPUBuffer(
return nullptr;
}
if (scratch_buffer_ != nullptr) {
resources_for_deletion_.emplace_back(submission_current_, scratch_buffer_);
resources_for_deletion_.emplace_back(GetCurrentSubmission(),
scratch_buffer_);
}
scratch_buffer_ = buffer;
scratch_buffer_size_ = size;
@@ -787,22 +788,11 @@ bool D3D12CommandProcessor::SetupContext() {
ID3D12Device* device = provider.GetDevice();
ID3D12CommandQueue* direct_queue = provider.GetDirectQueue();
fence_completion_event_ = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (fence_completion_event_ == nullptr) {
XELOGE("Failed to create the fence completion event");
return false;
}
if (FAILED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&submission_fence_)))) {
XELOGE("Failed to create the submission fence");
return false;
}
if (FAILED(device->CreateFence(
0, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&queue_operations_since_submission_fence_)))) {
XELOGE(
"Failed to create the fence for awaiting queue operations done since "
"the latest submission");
completion_timeline_ = ui::d3d12::D3D12GPUCompletionTimeline::Create(device);
queue_operations_since_submission_completion_timeline_ =
ui::d3d12::D3D12GPUCompletionTimeline::Create(device);
if (!completion_timeline_ ||
!queue_operations_since_submission_completion_timeline_) {
return false;
}
@@ -1669,21 +1659,10 @@ void D3D12CommandProcessor::ShutdownContext() {
frame_completed_ = 0;
std::memset(closed_frame_submissions_, 0, sizeof(closed_frame_submissions_));
// First release the fences since they may reference fence_completion_event_.
queue_operations_since_submission_completion_timeline_.reset();
queue_operations_done_since_submission_signal_ = false;
queue_operations_since_submission_fence_last_ = 0;
ui::d3d12::util::ReleaseAndNull(queue_operations_since_submission_fence_);
ui::d3d12::util::ReleaseAndNull(submission_fence_);
submission_open_ = false;
submission_current_ = 1;
submission_completed_ = 0;
if (fence_completion_event_) {
CloseHandle(fence_completion_event_);
fence_completion_event_ = nullptr;
}
completion_timeline_.reset();
device_removed_ = false;
@@ -1777,7 +1756,7 @@ void D3D12CommandProcessor::IssueSwap(uint32_t frontbuffer_ptr,
fxaa_source_texture_->GetDesc();
if (fxaa_source_texture_desc.Width != swap_texture_desc.Width ||
fxaa_source_texture_desc.Height != swap_texture_desc.Height) {
if (submission_completed_ < fxaa_source_texture_submission_) {
if (GetCompletedSubmission() < fxaa_source_texture_submission_) {
fxaa_source_texture_->AddRef();
resources_for_deletion_.emplace_back(
fxaa_source_texture_submission_,
@@ -1908,7 +1887,7 @@ void D3D12CommandProcessor::IssueSwap(uint32_t frontbuffer_ptr,
.resource_uav_capable();
if (use_fxaa) {
fxaa_source_texture_submission_ = submission_current_;
fxaa_source_texture_submission_ = GetCurrentSubmission();
}
ID3D12Resource* apply_gamma_dest =
@@ -2634,8 +2613,9 @@ bool D3D12CommandProcessor::IssueCopy() {
return true;
}
void D3D12CommandProcessor::CheckSubmissionFence(uint64_t await_submission) {
if (await_submission >= submission_current_) {
void D3D12CommandProcessor::CheckSubmissionCompletion(
uint64_t await_submission) {
if (await_submission >= GetCurrentSubmission()) {
if (submission_open_) {
EndSubmission(false);
}
@@ -2644,48 +2624,31 @@ void D3D12CommandProcessor::CheckSubmissionFence(uint64_t await_submission) {
// submission, but just in case of a failure, or queue operations being done
// outside of a submission, await explicitly.
if (queue_operations_done_since_submission_signal_) {
UINT64 fence_value = ++queue_operations_since_submission_fence_last_;
ID3D12CommandQueue* direct_queue = GetD3D12Provider().GetDirectQueue();
if (SUCCEEDED(
direct_queue->Signal(queue_operations_since_submission_fence_,
fence_value) &&
SUCCEEDED(queue_operations_since_submission_fence_
->SetEventOnCompletion(fence_value,
fence_completion_event_)))) {
WaitForSingleObject(fence_completion_event_, INFINITE);
if (SUCCEEDED(queue_operations_since_submission_completion_timeline_
->SignalAndAdvance(direct_queue) &&
queue_operations_since_submission_completion_timeline_
->AwaitAllSubmissions())) {
queue_operations_done_since_submission_signal_ = false;
} else {
XELOGE(
"Failed to await an out-of-submission queue operation completion "
"Direct3D 12 fence");
"Failed to await the completion of an out-of-submission "
"Direct3D 12 queue operation");
}
}
// A submission won't be ended if it hasn't been started, or if ending
// has failed - clamp the index.
await_submission = submission_current_ - 1;
await_submission = GetCurrentSubmission() - 1;
}
uint64_t submission_completed_before = submission_completed_;
submission_completed_ = submission_fence_->GetCompletedValue();
if (submission_completed_ < await_submission) {
if (SUCCEEDED(submission_fence_->SetEventOnCompletion(
await_submission, fence_completion_event_))) {
WaitForSingleObject(fence_completion_event_, INFINITE);
submission_completed_ = submission_fence_->GetCompletedValue();
}
}
if (submission_completed_ < await_submission) {
XELOGE("Failed to await a submission completion Direct3D 12 fence");
}
if (submission_completed_ <= submission_completed_before) {
// Not updated - no need to reclaim or download things.
return;
}
completion_timeline_->AwaitSubmissionAndUpdateCompleted(await_submission);
const uint64_t completed_submission = GetCompletedSubmission();
// Reclaim command allocators.
while (command_allocator_submitted_first_) {
if (command_allocator_submitted_first_->last_usage_submission >
submission_completed_) {
completed_submission) {
break;
}
if (command_allocator_writable_last_) {
@@ -2706,7 +2669,7 @@ void D3D12CommandProcessor::CheckSubmissionFence(uint64_t await_submission) {
// Release single-use bindless descriptors.
while (!view_bindless_one_use_descriptors_.empty()) {
if (view_bindless_one_use_descriptors_.front().second >
submission_completed_) {
completed_submission) {
break;
}
ReleaseViewBindlessDescriptorImmediately(
@@ -2716,7 +2679,7 @@ void D3D12CommandProcessor::CheckSubmissionFence(uint64_t await_submission) {
// Delete transient resources marked for deletion.
while (!resources_for_deletion_.empty()) {
if (resources_for_deletion_.front().first > submission_completed_) {
if (resources_for_deletion_.front().first > completed_submission) {
break;
}
resources_for_deletion_.front().second->Release();
@@ -2729,7 +2692,7 @@ void D3D12CommandProcessor::CheckSubmissionFence(uint64_t await_submission) {
primitive_processor_->CompletedSubmissionUpdated();
texture_cache_->CompletedSubmissionUpdated(submission_completed_);
texture_cache_->CompletedSubmissionUpdated(completed_submission);
}
bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) {
@@ -2759,7 +2722,7 @@ bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) {
// Check the fence - needed for all kinds of submissions (to reclaim transient
// resources early) and specifically for frames (not to queue too many), and
// await the availability of the current frame.
CheckSubmissionFence(
CheckSubmissionCompletion(
is_opening_frame
? closed_frame_submissions_[frame_current_ % kQueueFrames]
: 0);
@@ -2775,7 +2738,7 @@ bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) {
for (uint64_t frame = frame_completed_ + 1; frame < frame_current_;
++frame) {
if (closed_frame_submissions_[frame % kQueueFrames] >
submission_completed_) {
GetCompletedSubmission()) {
break;
}
frame_completed_ = frame;
@@ -2812,7 +2775,7 @@ bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) {
primitive_processor_->BeginSubmission();
texture_cache_->BeginSubmission(submission_current_);
texture_cache_->BeginSubmission(GetCurrentSubmission());
}
if (is_opening_frame) {
@@ -2907,6 +2870,8 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) {
// destroyed between frames.
SubmitBarriers();
// TODO(Triang3l): Error checking.
ID3D12CommandQueue* direct_queue = provider.GetDirectQueue();
// Submit the deferred command list.
@@ -2923,7 +2888,7 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) {
ID3D12CommandList* execute_command_lists[] = {command_list_};
direct_queue->ExecuteCommandLists(1, execute_command_lists);
command_allocator_writable_first_->last_usage_submission =
submission_current_;
GetCurrentSubmission();
if (command_allocator_submitted_last_) {
command_allocator_submitted_last_->next =
command_allocator_writable_first_;
@@ -2936,8 +2901,7 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) {
if (!command_allocator_writable_first_) {
command_allocator_writable_last_ = nullptr;
}
direct_queue->Signal(submission_fence_, submission_current_++);
completion_timeline_->SignalAndAdvance(direct_queue);
submission_open_ = false;
@@ -2958,7 +2922,7 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) {
frame_open_ = false;
// Submission already closed now, so minus 1.
closed_frame_submissions_[(frame_current_++) % kQueueFrames] =
submission_current_ - 1;
GetCurrentSubmission() - 1;
if (cache_clear_requested_ && AwaitAllQueueOperationsCompletion()) {
cache_clear_requested_ = false;
@@ -3912,7 +3876,7 @@ bool D3D12CommandProcessor::UpdateBindings(const D3D12Shader* vertex_shader,
ID3D12DescriptorHeap* sampler_heap_new;
if (!sampler_bindless_heaps_overflowed_.empty() &&
sampler_bindless_heaps_overflowed_.front().second <=
submission_completed_) {
GetCompletedSubmission()) {
sampler_heap_new = sampler_bindless_heaps_overflowed_.front().first;
sampler_bindless_heaps_overflowed_.pop_front();
} else {
@@ -3934,7 +3898,7 @@ bool D3D12CommandProcessor::UpdateBindings(const D3D12Shader* vertex_shader,
// leave the values in an undefined state in case CreateDescriptorHeap
// has failed.
sampler_bindless_heaps_overflowed_.push_back(std::make_pair(
sampler_bindless_heap_current_, submission_current_));
sampler_bindless_heap_current_, GetCurrentSubmission()));
sampler_bindless_heap_current_ = sampler_heap_new;
sampler_bindless_heap_cpu_start_ =
sampler_bindless_heap_current_
+17 -17
View File
@@ -36,6 +36,7 @@
#include "xenia/gpu/xenos.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/ui/d3d12/d3d12_descriptor_heap_pool.h"
#include "xenia/ui/d3d12/d3d12_gpu_completion_timeline.h"
#include "xenia/ui/d3d12/d3d12_provider.h"
#include "xenia/ui/d3d12/d3d12_upload_buffer_pool.h"
#include "xenia/ui/d3d12/d3d12_util.h"
@@ -73,12 +74,16 @@ class D3D12CommandProcessor : public CommandProcessor {
return deferred_command_list_;
}
uint64_t GetCurrentSubmission() const { return submission_current_; }
uint64_t GetCompletedSubmission() const { return submission_completed_; }
uint64_t GetCurrentSubmission() const {
return completion_timeline_->GetUpcomingSubmission();
}
uint64_t GetCompletedSubmission() const {
return completion_timeline_->GetCompletedSubmissionFromLastUpdate();
}
// Must be called when a subsystem does something like UpdateTileMappings so
// it can be awaited in CheckSubmissionFence(submission_current_) if it was
// done after the latest ExecuteCommandLists + Signal.
// it can be awaited in CheckSubmissionCompletion(GetCurrentSubmission()) if
// it was done after the latest ExecuteCommandLists + Signal.
void NotifyQueueOperationsDoneDirectly() {
queue_operations_done_since_submission_signal_ = true;
}
@@ -324,9 +329,9 @@ class D3D12CommandProcessor : public CommandProcessor {
// decay.
// Rechecks submission number and reclaims per-submission resources. Pass 0 as
// the submission to await to simply check status, or pass submission_current_
// to wait for all queue operations to be completed.
void CheckSubmissionFence(uint64_t await_submission);
// the submission to await to simply check status, or pass
// GetCurrentSubmission() to wait for all queue operations to be completed.
void CheckSubmissionCompletion(uint64_t await_submission);
// If is_guest_command is true, a new full frame - with full cleanup of
// resources and, if needed, starting capturing - is opened if pending (as
// opposed to simply resuming after mid-frame synchronization). Returns
@@ -342,8 +347,8 @@ class D3D12CommandProcessor : public CommandProcessor {
// need to be fulfilled before actually submitting the command list.
bool CanEndSubmissionImmediately() const;
bool AwaitAllQueueOperationsCompletion() {
CheckSubmissionFence(submission_current_);
return submission_completed_ + 1 >= submission_current_;
CheckSubmissionCompletion(GetCurrentSubmission());
return GetCompletedSubmission() + 1u >= GetCurrentSubmission();
}
// Need to await submission completion before calling.
void ClearCommandAllocatorCache();
@@ -389,20 +394,15 @@ class D3D12CommandProcessor : public CommandProcessor {
bool cache_clear_requested_ = false;
HANDLE fence_completion_event_ = nullptr;
std::unique_ptr<ui::d3d12::D3D12GPUCompletionTimeline> completion_timeline_;
bool submission_open_ = false;
// Values of submission_fence_.
uint64_t submission_current_ = 1;
uint64_t submission_completed_ = 0;
ID3D12Fence* submission_fence_ = nullptr;
// For awaiting non-submission queue operations such as UpdateTileMappings in
// AwaitAllQueueOperationsCompletion when they're queued after the latest
// ExecuteCommandLists + Signal, thus won't be awaited by just awaiting the
// submission.
ID3D12Fence* queue_operations_since_submission_fence_ = nullptr;
uint64_t queue_operations_since_submission_fence_last_ = 0;
std::unique_ptr<ui::d3d12::D3D12GPUCompletionTimeline>
queue_operations_since_submission_completion_timeline_;
bool queue_operations_done_since_submission_signal_ = false;
bool frame_open_ = false;
+38 -122
View File
@@ -69,6 +69,9 @@ const VkDescriptorPoolSize
VulkanCommandProcessor::VulkanCommandProcessor(
VulkanGraphicsSystem* graphics_system, kernel::KernelState* kernel_state)
: CommandProcessor(graphics_system, kernel_state),
completion_timeline_(static_cast<const ui::vulkan::VulkanProvider*>(
graphics_system->provider())
->vulkan_device()),
deferred_command_buffer_(*this),
transient_descriptor_allocator_uniform_buffer_(
static_cast<const ui::vulkan::VulkanProvider*>(
@@ -1150,26 +1153,17 @@ void VulkanCommandProcessor::ShutdownContext() {
dfn.vkDestroySemaphore(device, semaphore.second, nullptr);
}
submissions_in_flight_semaphores_.clear();
for (VkFence& fence : submissions_in_flight_fences_) {
dfn.vkDestroyFence(device, fence, nullptr);
}
submissions_in_flight_fences_.clear();
current_submission_wait_stage_masks_.clear();
for (VkSemaphore semaphore : current_submission_wait_semaphores_) {
dfn.vkDestroySemaphore(device, semaphore, nullptr);
}
current_submission_wait_semaphores_.clear();
submission_completed_ = 0;
submission_open_ = false;
for (VkSemaphore semaphore : semaphores_free_) {
dfn.vkDestroySemaphore(device, semaphore, nullptr);
}
semaphores_free_.clear();
for (VkFence fence : fences_free_) {
dfn.vkDestroyFence(device, fence, nullptr);
}
fences_free_.clear();
device_lost_ = false;
@@ -1418,7 +1412,8 @@ void VulkanCommandProcessor::IssueSwap(uint32_t frontbuffer_ptr,
SwapFramebuffer& new_swap_framebuffer =
swap_framebuffers_[swap_framebuffer_new_index];
if (new_swap_framebuffer.framebuffer != VK_NULL_HANDLE) {
if (submission_completed_ >= new_swap_framebuffer.last_submission) {
if (GetCompletedSubmission() >=
new_swap_framebuffer.last_submission) {
dfn.vkDestroyFramebuffer(device, new_swap_framebuffer.framebuffer,
nullptr);
} else {
@@ -2026,7 +2021,7 @@ VulkanCommandProcessor::AcquireScratchGpuBuffer(
return ScratchBufferAcquisition();
}
if (submission_completed_ >= scratch_buffer_last_usage_submission_) {
if (GetCompletedSubmission() >= scratch_buffer_last_usage_submission_) {
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
const VkDevice device = vulkan_device->device();
if (scratch_buffer_ != VK_NULL_HANDLE) {
@@ -2341,7 +2336,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type,
texture_cache_->GetSubmissionToAwaitOnSamplerOverflow(
samplers_overflowed_count);
assert_true(sampler_overflow_await_submission <= GetCurrentSubmission());
CheckSubmissionFenceAndDeviceLoss(sampler_overflow_await_submission);
CheckSubmissionCompletionAndDeviceLoss(sampler_overflow_await_submission);
}
// Set up the render targets - this may perform dispatches and draws.
@@ -2646,7 +2641,7 @@ void VulkanCommandProcessor::InitializeTrace() {
}
}
void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(
void VulkanCommandProcessor::CheckSubmissionCompletionAndDeviceLoss(
uint64_t await_submission) {
// Only report once, no need to retry a wait that won't succeed anyway.
if (device_lost_) {
@@ -2662,69 +2657,23 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(
await_submission = GetCurrentSubmission() - 1;
}
const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice();
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
const VkDevice device = vulkan_device->device();
completion_timeline_.AwaitSubmissionAndUpdateCompleted(await_submission);
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.
while (fences_awaited < fences_total) {
VkResult fence_status = dfn.vkWaitForFences(
device, 1, &submissions_in_flight_fences_[fences_awaited], VK_TRUE, 0);
if (fence_status != VK_SUCCESS) {
if (fence_status == VK_ERROR_DEVICE_LOST) {
device_lost_ = true;
}
break;
}
++fences_awaited;
}
if (device_lost_) {
const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice();
if (vulkan_device->IsLost()) {
device_lost_ = true;
graphics_system_->OnHostGpuLossFromAnyThread(true);
return;
}
if (!fences_awaited) {
// Not updated - no need to reclaim or download things.
return;
}
// Reclaim fences.
fences_free_.reserve(fences_free_.size() + fences_awaited);
auto submissions_in_flight_fences_awaited_end =
submissions_in_flight_fences_.cbegin();
std::advance(submissions_in_flight_fences_awaited_end, fences_awaited);
fences_free_.insert(fences_free_.cend(),
submissions_in_flight_fences_.cbegin(),
submissions_in_flight_fences_awaited_end);
submissions_in_flight_fences_.erase(submissions_in_flight_fences_.cbegin(),
submissions_in_flight_fences_awaited_end);
submission_completed_ += fences_awaited;
const uint64_t completed_submission = GetCompletedSubmission();
// Reclaim semaphores.
while (!submissions_in_flight_semaphores_.empty()) {
const auto& semaphore_submission =
submissions_in_flight_semaphores_.front();
if (semaphore_submission.first > submission_completed_) {
if (semaphore_submission.first > completed_submission) {
break;
}
semaphores_free_.push_back(semaphore_submission.second);
@@ -2734,7 +2683,7 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(
// Reclaim command pools.
while (!command_buffers_submitted_.empty()) {
const auto& command_buffer_pair = command_buffers_submitted_.front();
if (command_buffer_pair.first > submission_completed_) {
if (command_buffer_pair.first > completed_submission) {
break;
}
command_buffers_writable_.push_back(command_buffer_pair.second);
@@ -2747,12 +2696,14 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(
render_target_cache_->CompletedSubmissionUpdated();
texture_cache_->CompletedSubmissionUpdated(submission_completed_);
texture_cache_->CompletedSubmissionUpdated(completed_submission);
// Destroy objects scheduled for destruction.
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
const VkDevice device = vulkan_device->device();
while (!destroy_framebuffers_.empty()) {
const auto& destroy_pair = destroy_framebuffers_.front();
if (destroy_pair.first > submission_completed_) {
if (destroy_pair.first > completed_submission) {
break;
}
dfn.vkDestroyFramebuffer(device, destroy_pair.second, nullptr);
@@ -2760,7 +2711,7 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(
}
while (!destroy_buffers_.empty()) {
const auto& destroy_pair = destroy_buffers_.front();
if (destroy_pair.first > submission_completed_) {
if (destroy_pair.first > completed_submission) {
break;
}
dfn.vkDestroyBuffer(device, destroy_pair.second, nullptr);
@@ -2768,7 +2719,7 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(
}
while (!destroy_memory_.empty()) {
const auto& destroy_pair = destroy_memory_.front();
if (destroy_pair.first > submission_completed_) {
if (destroy_pair.first > completed_submission) {
break;
}
dfn.vkFreeMemory(device, destroy_pair.second, nullptr);
@@ -2798,8 +2749,9 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) {
is_opening_frame
? closed_frame_submissions_[frame_current_ % kMaxFramesInFlight]
: 0;
CheckSubmissionFenceAndDeviceLoss(await_submission);
if (device_lost_ || submission_completed_ < await_submission) {
CheckSubmissionCompletionAndDeviceLoss(await_submission);
const uint64_t completed_submission = GetCompletedSubmission();
if (device_lost_ || completed_submission < await_submission) {
return false;
}
@@ -2812,7 +2764,7 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) {
for (uint64_t frame = frame_completed_ + 1; frame < frame_current_;
++frame) {
if (closed_frame_submissions_[frame % kMaxFramesInFlight] >
submission_completed_) {
completed_submission) {
break;
}
frame_completed_ = frame;
@@ -2925,27 +2877,12 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) {
}
bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice();
ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice();
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
const VkDevice device = vulkan_device->device();
// Make sure everything needed for submitting exist.
if (submission_open_) {
if (fences_free_.empty()) {
VkFenceCreateInfo fence_create_info;
fence_create_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fence_create_info.pNext = nullptr;
fence_create_info.flags = 0;
VkFence fence;
if (dfn.vkCreateFence(device, &fence_create_info, nullptr, &fence) !=
VK_SUCCESS) {
XELOGE("Failed to create a Vulkan fence");
// Try to submit later. Completely dropping the submission is not
// permitted because resources would be left in an undefined state.
return false;
}
fences_free_.push_back(fence);
}
if (!sparse_memory_binds_.empty() && semaphores_free_.empty()) {
VkSemaphoreCreateInfo semaphore_create_info;
semaphore_create_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
@@ -3088,58 +3025,37 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
return false;
}
VkSubmitInfo submit_info;
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.pNext = nullptr;
const uint64_t submission_index = GetCurrentSubmission();
VkSubmitInfo submit_info = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
if (!current_submission_wait_semaphores_.empty()) {
submit_info.waitSemaphoreCount =
uint32_t(current_submission_wait_semaphores_.size());
submit_info.pWaitSemaphores = current_submission_wait_semaphores_.data();
submit_info.pWaitDstStageMask =
current_submission_wait_stage_masks_.data();
} else {
submit_info.waitSemaphoreCount = 0;
submit_info.pWaitSemaphores = nullptr;
submit_info.pWaitDstStageMask = nullptr;
}
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &command_buffer.buffer;
submit_info.signalSemaphoreCount = 0;
submit_info.pSignalSemaphores = nullptr;
assert_false(fences_free_.empty());
VkFence fence = fences_free_.back();
if (dfn.vkResetFences(device, 1, &fence) != VK_SUCCESS) {
XELOGE("Failed to reset a Vulkan submission fence");
return false;
}
VkResult submit_result;
{
ui::vulkan::VulkanDevice::Queue::Acquisition queue_acquisition =
vulkan_device->AcquireQueue(
vulkan_device->queue_family_graphics_compute(), 0);
submit_result =
dfn.vkQueueSubmit(queue_acquisition.queue(), 1, &submit_info, fence);
}
const VkResult submit_result = completion_timeline_.AcquireFenceAndSubmit(
vulkan_device->queue_family_graphics_compute(), 0, 1, &submit_info);
if (submit_result != VK_SUCCESS) {
XELOGE("Failed to submit a Vulkan command buffer");
if (submit_result == VK_ERROR_DEVICE_LOST && !device_lost_) {
XELOGE("Failed to submit a GPU emulation Vulkan command buffer: {}",
vk::to_string(vk::Result(submit_result)));
if (vulkan_device->IsLost() && !device_lost_) {
device_lost_ = true;
graphics_system_->OnHostGpuLossFromAnyThread(true);
}
return false;
}
uint64_t submission_current = GetCurrentSubmission();
current_submission_wait_stage_masks_.clear();
for (VkSemaphore semaphore : current_submission_wait_semaphores_) {
submissions_in_flight_semaphores_.emplace_back(submission_current,
submissions_in_flight_semaphores_.emplace_back(submission_index,
semaphore);
}
current_submission_wait_semaphores_.clear();
command_buffers_submitted_.emplace_back(submission_current, command_buffer);
command_buffers_submitted_.emplace_back(submission_index, command_buffer);
command_buffers_writable_.pop_back();
// Increments the current submission number, going to the next submission.
submissions_in_flight_fences_.push_back(fence);
fences_free_.pop_back();
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_gpu_completion_timeline.h"
#include "xenia/ui/vulkan/vulkan_presenter.h"
#include "xenia/ui/vulkan/vulkan_provider.h"
#include "xenia/ui/vulkan/vulkan_upload_buffer_pool.h"
@@ -157,10 +158,11 @@ class VulkanCommandProcessor : public CommandProcessor {
bool submission_open() const { return submission_open_; }
uint64_t GetCurrentSubmission() const {
return submission_completed_ +
uint64_t(submissions_in_flight_fences_.size()) + 1;
return completion_timeline_.GetUpcomingSubmission();
}
uint64_t GetCompletedSubmission() const {
return completion_timeline_.GetCompletedSubmissionFromLastUpdate();
}
uint64_t GetCompletedSubmission() const { return submission_completed_; }
// Sparse binds are:
// - In a single submission, all submitted in one vkQueueBindSparse.
@@ -403,7 +405,7 @@ class VulkanCommandProcessor : public CommandProcessor {
// Rechecks submission number and reclaims per-submission resources. Pass 0 as
// the submission to await to simply check status, or pass
// GetCurrentSubmission() to wait for all queue operations to be completed.
void CheckSubmissionFenceAndDeviceLoss(uint64_t await_submission);
void CheckSubmissionCompletionAndDeviceLoss(uint64_t await_submission);
// If is_guest_command is true, a new full frame - with full cleanup of
// resources and, if needed, starting capturing - is opened if pending (as
// opposed to simply resuming after mid-frame synchronization). Returns
@@ -414,8 +416,9 @@ class VulkanCommandProcessor : public CommandProcessor {
// successfully, if it has failed, leaves it open.
bool EndSubmission(bool is_swap);
bool AwaitAllQueueOperationsCompletion() {
CheckSubmissionFenceAndDeviceLoss(GetCurrentSubmission());
return !submission_open_ && submissions_in_flight_fences_.empty();
CheckSubmissionCompletionAndDeviceLoss(GetCurrentSubmission());
return !submission_open_ &&
GetCompletedSubmission() + 1u >= GetCurrentSubmission();
}
void ClearTransientDescriptorPools();
@@ -460,16 +463,14 @@ class VulkanCommandProcessor : public CommandProcessor {
VkPipelineStageFlags guest_shader_pipeline_stages_ = 0;
VkShaderStageFlags guest_shader_vertex_stages_ = 0;
std::vector<VkFence> fences_free_;
std::vector<VkSemaphore> semaphores_free_;
ui::vulkan::VulkanGPUCompletionTimeline completion_timeline_;
bool submission_open_ = false;
uint64_t submission_completed_ = 0;
// In case vkQueueSubmit fails after something like a successful
// vkQueueBindSparse, to wait correctly on the next attempt.
std::vector<VkSemaphore> current_submission_wait_semaphores_;
std::vector<VkPipelineStageFlags> current_submission_wait_stage_masks_;
std::vector<VkFence> submissions_in_flight_fences_;
std::deque<std::pair<uint64_t, VkSemaphore>>
submissions_in_flight_semaphores_;
@@ -0,0 +1,67 @@
/**
******************************************************************************
* 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/d3d12/d3d12_gpu_completion_timeline.h"
#include <utility>
#include "xenia/base/logging.h"
namespace xe {
namespace ui {
namespace d3d12 {
std::unique_ptr<D3D12GPUCompletionTimeline> D3D12GPUCompletionTimeline::Create(
ID3D12Device* const device) {
Microsoft::WRL::ComPtr<ID3D12Fence> fence;
const HRESULT fence_create_result =
device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence));
if (FAILED(fence_create_result)) {
XELOGE("Failed to create a Direct3D 12 fence, result 0x{:08X}",
fence_create_result);
return nullptr;
}
return std::unique_ptr<D3D12GPUCompletionTimeline>(
new D3D12GPUCompletionTimeline(fence.Get()));
}
D3D12GPUCompletionTimeline::~D3D12GPUCompletionTimeline() {
fence_->SetEventOnCompletion(GetUpcomingSubmission() - 1, nullptr);
}
void D3D12GPUCompletionTimeline::UpdateCompletedSubmission() {
uint64_t actual_completed_submission = fence_->GetCompletedValue();
// The device has been removed if the completed value is UINT64_MAX.
if (actual_completed_submission == UINT64_MAX) {
return;
}
SetCompletedSubmission(actual_completed_submission);
}
void D3D12GPUCompletionTimeline::AwaitSubmissionImpl(
const uint64_t awaited_submission) {
fence_->SetEventOnCompletion(awaited_submission, nullptr);
}
HRESULT D3D12GPUCompletionTimeline::SignalAndAdvance(
ID3D12CommandQueue* const queue) {
const HRESULT signal_result =
queue->Signal(fence_.Get(), GetUpcomingSubmission());
if (FAILED(signal_result)) {
XELOGE("Failed to signal a Direct3D 12 fence, result 0x{:08X}",
signal_result);
} else {
IncrementUpcomingSubmission();
}
return signal_result;
}
} // namespace d3d12
} // namespace ui
} // namespace xe
@@ -0,0 +1,58 @@
/**
******************************************************************************
* 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_D3D12_D3D12_GPU_COMPLETION_TIMELINE_H_
#define XENIA_UI_D3D12_D3D12_GPU_COMPLETION_TIMELINE_H_
#include <memory>
#include "xenia/ui/d3d12/d3d12_api.h"
#include "xenia/ui/gpu_completion_timeline.h"
namespace xe {
namespace ui {
namespace d3d12 {
class D3D12GPUCompletionTimeline : public GPUCompletionTimeline {
public:
static std::unique_ptr<D3D12GPUCompletionTimeline> Create(
ID3D12Device* device);
D3D12GPUCompletionTimeline(const D3D12GPUCompletionTimeline&) = delete;
D3D12GPUCompletionTimeline& operator=(const D3D12GPUCompletionTimeline&) =
delete;
D3D12GPUCompletionTimeline(D3D12GPUCompletionTimeline&&) = delete;
D3D12GPUCompletionTimeline& operator=(D3D12GPUCompletionTimeline&&) = delete;
~D3D12GPUCompletionTimeline();
ID3D12Fence* GetFence() const { return fence_.Get(); }
using GPUCompletionTimeline::IncrementUpcomingSubmission;
void UpdateCompletedSubmission() override;
// If signaling has succeeded, will advance to the next submission.
HRESULT SignalAndAdvance(ID3D12CommandQueue* queue);
protected:
void AwaitSubmissionImpl(uint64_t awaited_submission) override;
private:
explicit D3D12GPUCompletionTimeline(ID3D12Fence* const fence)
: fence_(fence) {}
Microsoft::WRL::ComPtr<ID3D12Fence> fence_;
};
} // namespace d3d12
} // namespace ui
} // namespace xe
#endif // XENIA_UI_D3D12_D3D12_GPU_COMPLETION_TIMELINE_H_
+80 -59
View File
@@ -11,6 +11,7 @@
#include <algorithm>
#include <climits>
#include <cstdint>
#include <memory>
#include <utility>
@@ -48,12 +49,17 @@ namespace shaders {
} // namespace shaders
D3D12Presenter::~D3D12Presenter() {
// Await completion of the usage of everything before destroying anything.
// Await completion of the usage of everything before destroying anything,
// irrespective of the declaration order in the class.
// From most likely the latest to most likely the earliest to be signaled, so
// just one sleep will likely be needed.
paint_context_.AwaitSwapChainUsageCompletion();
guest_output_resource_refresher_submission_tracker_.Shutdown();
ui_submission_tracker_.Shutdown();
if (guest_output_resource_refresher_completion_timeline_) {
guest_output_resource_refresher_completion_timeline_->AwaitAllSubmissions();
}
if (ui_completion_timeline_) {
ui_completion_timeline_->AwaitAllSubmissions();
}
}
Surface::TypeFlags D3D12Presenter::GetSupportedSurfaceTypes() const {
@@ -152,27 +158,33 @@ bool D3D12Presenter::CaptureGuestOutput(RawImage& image_out) {
return false;
}
ID3D12CommandQueue* direct_queue = provider_.GetDirectQueue();
// Make sure that if any work is submitted, any `return` will cause an await
// before releasing the command allocator / list and the resource the RAII
// way in the destruction of the submission tracker - so create after the
// objects referenced in the submission - but don't submit anything if
// failed to initialize the fence.
D3D12SubmissionTracker submission_tracker;
if (!submission_tracker.Initialize(device, direct_queue)) {
Microsoft::WRL::ComPtr<ID3D12Fence> fence;
const HRESULT fence_create_result =
device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence));
if (FAILED(fence_create_result)) {
XELOGE(
"D3D12Presenter: Failed to create the guest output capturing fence, "
"result 0x{:08X}",
fence_create_result);
return false;
}
ID3D12CommandQueue* const direct_queue = provider_.GetDirectQueue();
ID3D12CommandList* execute_command_list = command_list.Get();
direct_queue->ExecuteCommandLists(1, &execute_command_list);
if (!submission_tracker.NextSubmission()) {
const HRESULT fence_signal_result = direct_queue->Signal(fence.Get(), 1);
if (FAILED(fence_signal_result)) {
XELOGE(
"D3D12Presenter: Failed to signal the guest output capturing fence");
"D3D12Presenter: Failed to enqueue signaling of the guest output "
"capturing fence, result 0x{:08X}",
fence_signal_result);
return false;
}
if (!submission_tracker.AwaitAllSubmissionsCompletion()) {
const HRESULT fence_wait_result = fence->SetEventOnCompletion(1, nullptr);
if (FAILED(fence_wait_result)) {
XELOGE(
"D3D12Presenter: Failed to await the guest output capturing fence");
"D3D12Presenter: Failed to await the guest output capturing fence, "
"result 0x{:08X}",
fence_wait_result);
return false;
}
}
@@ -379,7 +391,7 @@ bool D3D12Presenter::RefreshGuestOutputImpl(
bool& is_8bpc_out_ref) {
assert_not_zero(frontbuffer_width);
assert_not_zero(frontbuffer_height);
std::pair<UINT64, Microsoft::WRL::ComPtr<ID3D12Resource>>&
std::pair<uint64_t, Microsoft::WRL::ComPtr<ID3D12Resource>>&
guest_output_resource_ref = guest_output_resources_[mailbox_index];
if (guest_output_resource_ref.second) {
D3D12_RESOURCE_DESC guest_output_resource_current_desc =
@@ -387,9 +399,9 @@ bool D3D12Presenter::RefreshGuestOutputImpl(
if (guest_output_resource_current_desc.Width != frontbuffer_width ||
guest_output_resource_current_desc.Height != frontbuffer_height) {
// Main target painting has its own reference to the textures for reading
// in its own submission tracker timeline, safe to release here.
guest_output_resource_refresher_submission_tracker_
.AwaitSubmissionCompletion(guest_output_resource_ref.first);
// in its own completion timeline, safe to release here.
guest_output_resource_refresher_completion_timeline_
->AwaitSubmissionAndUpdateCompleted(guest_output_resource_ref.first);
guest_output_resource_ref.second.Reset();
}
}
@@ -427,9 +439,10 @@ bool D3D12Presenter::RefreshGuestOutputImpl(
// signal and wait slightly longer, for nothing important, while shutting down
// than to destroy the resource while it's still in use.
guest_output_resource_ref.first =
guest_output_resource_refresher_submission_tracker_
.GetCurrentSubmission();
guest_output_resource_refresher_submission_tracker_.NextSubmission();
guest_output_resource_refresher_completion_timeline_
->GetUpcomingSubmission();
guest_output_resource_refresher_completion_timeline_->SignalAndAdvance(
provider_.GetDirectQueue());
return refresher_succeeded;
}
@@ -452,14 +465,12 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
bool execute_ui_drawers) {
// Begin the command list with the command allocator not currently potentially
// used on the GPU.
UINT64 current_paint_submission =
paint_context_.paint_submission_tracker.GetCurrentSubmission();
UINT64 command_allocator_count =
const uint64_t current_paint_submission =
paint_context_.paint_completion_timeline->GetUpcomingSubmission();
const uint64_t command_allocator_count =
UINT64(paint_context_.command_allocators.size());
if (current_paint_submission >= command_allocator_count) {
paint_context_.paint_submission_tracker.AwaitSubmissionCompletion(
current_paint_submission - command_allocator_count);
}
paint_context_.paint_completion_timeline
->AwaitMaxSubmissionsPendingAndUpdateCompleted(command_allocator_count);
ID3D12CommandAllocator* command_allocator =
paint_context_
.command_allocators[current_paint_submission %
@@ -542,7 +553,7 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
// released (or a taken, but never actually used) slot.
for (size_t i = 0;
i < paint_context_.guest_output_resource_paint_refs.size(); ++i) {
const std::pair<UINT64, Microsoft::WRL::ComPtr<ID3D12Resource>>&
const std::pair<uint64_t, Microsoft::WRL::ComPtr<ID3D12Resource>>&
guest_output_resource_paint_ref =
paint_context_.guest_output_resource_paint_refs[i];
if (guest_output_resource_paint_ref.second == guest_output_resource) {
@@ -573,11 +584,12 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
}
// Await the completion of the usage of the old guest output
// resource and its SRV descriptors.
paint_context_.paint_submission_tracker.AwaitSubmissionCompletion(
paint_context_
.guest_output_resource_paint_refs
[guest_output_resource_paint_ref_new_index]
.first);
paint_context_.paint_completion_timeline
->AwaitSubmissionAndUpdateCompleted(
paint_context_
.guest_output_resource_paint_refs
[guest_output_resource_paint_ref_new_index]
.first);
}
guest_output_resource_paint_ref_index =
guest_output_resource_paint_ref_new_index;
@@ -585,7 +597,7 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
// used, not dropped due to some error.
paint_context_.guest_output_resource_paint_refs
[guest_output_resource_paint_ref_index] =
std::make_pair(UINT64(0), guest_output_resource);
std::make_pair(uint64_t(0), guest_output_resource);
// Create the SRV descriptor of the new texture.
D3D12_SHADER_RESOURCE_VIEW_DESC guest_output_resource_srv_desc;
guest_output_resource_srv_desc.Format = kGuestOutputFormat;
@@ -628,8 +640,10 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
// Need to replace immediately as a new texture with the requested
// size is needed.
if (intermediate_texture_ptr_ref) {
paint_context_.paint_submission_tracker.AwaitSubmissionCompletion(
paint_context_.guest_output_intermediate_texture_last_usage);
paint_context_.paint_completion_timeline
->AwaitSubmissionAndUpdateCompleted(
paint_context_
.guest_output_intermediate_texture_last_usage);
intermediate_texture_ptr_ref.Reset();
}
// Resource.
@@ -692,8 +706,8 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
} else {
// Was previously needed, but not anymore - destroy when possible.
if (intermediate_texture_ptr_ref &&
paint_context_.paint_submission_tracker
.GetCompletedSubmission() >=
paint_context_.paint_completion_timeline
->GetCompletedSubmissionFromLastUpdate() >=
paint_context_
.guest_output_intermediate_texture_last_usage) {
intermediate_texture_ptr_ref.Reset();
@@ -985,11 +999,12 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
// Release main target guest output texture references that aren't needed
// anymore (this is done after various potential guest-output-related main
// target submission tracker waits so the completed submission value is the
// target completion timeline waits so the completed submission value is the
// most actual).
UINT64 completed_paint_submission =
paint_context_.paint_submission_tracker.GetCompletedSubmission();
for (std::pair<UINT64, Microsoft::WRL::ComPtr<ID3D12Resource>>&
uint64_t completed_paint_submission =
paint_context_.paint_completion_timeline
->GetCompletedSubmissionFromLastUpdate();
for (std::pair<uint64_t, Microsoft::WRL::ComPtr<ID3D12Resource>>&
guest_output_resource_paint_ref :
paint_context_.guest_output_resource_paint_refs) {
if (!guest_output_resource_paint_ref.second ||
@@ -1034,8 +1049,8 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
D3D12UIDrawContext ui_draw_context(
*this, paint_context_.swap_chain_width,
paint_context_.swap_chain_height, command_list,
ui_submission_tracker_.GetCurrentSubmission(),
ui_submission_tracker_.GetCompletedSubmission());
ui_completion_timeline_->GetUpcomingSubmission(),
ui_completion_timeline_->UpdateAndGetCompletedSubmission());
ExecuteUIDrawersFromUIThread(ui_draw_context);
}
@@ -1052,13 +1067,15 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
command_list->ResourceBarrier(1, &barrier_rtv_to_present);
// Execute and present.
// TODO(Triang3l): Error checking.
command_list->Close();
ID3D12CommandQueue* const direct_queue = provider_.GetDirectQueue();
ID3D12CommandList* execute_command_list = command_list;
provider_.GetDirectQueue()->ExecuteCommandLists(1, &execute_command_list);
direct_queue->ExecuteCommandLists(1, &execute_command_list);
if (execute_ui_drawers) {
ui_submission_tracker_.NextSubmission();
ui_completion_timeline_->SignalAndAdvance(direct_queue);
}
paint_context_.paint_submission_tracker.NextSubmission();
paint_context_.paint_completion_timeline->SignalAndAdvance(direct_queue);
// Present as soon as possible, without waiting for vsync (the host refresh
// rate may be something like 144 Hz, which is not a multiple of the common
// 30 Hz or 60 Hz guest refresh rate), and allowing dropping outdated queued
@@ -1074,7 +1091,7 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
// Even if presentation has failed, work might have been enqueued anyway
// internally before the failure according to Jesse Natalie from the DirectX
// Discord server.
paint_context_.present_submission_tracker.NextSubmission();
paint_context_.present_completion_timeline->SignalAndAdvance(direct_queue);
switch (present_result) {
case DXGI_ERROR_DEVICE_REMOVED:
return PaintResult::kGpuLostExternally;
@@ -1387,11 +1404,13 @@ bool D3D12Presenter::InitializeSurfaceIndependent() {
ID3D12CommandQueue* direct_queue = provider_.GetDirectQueue();
// Paint submission trackers.
if (!paint_context_.paint_submission_tracker.Initialize(device,
direct_queue) ||
!paint_context_.present_submission_tracker.Initialize(device,
direct_queue)) {
// Paint completion timelines.
paint_context_.paint_completion_timeline =
D3D12GPUCompletionTimeline::Create(device);
paint_context_.present_completion_timeline =
D3D12GPUCompletionTimeline::Create(device);
if (!paint_context_.paint_completion_timeline ||
!paint_context_.present_completion_timeline) {
return false;
}
@@ -1449,12 +1468,14 @@ bool D3D12Presenter::InitializeSurfaceIndependent() {
return false;
}
if (!guest_output_resource_refresher_submission_tracker_.Initialize(
device, direct_queue)) {
guest_output_resource_refresher_completion_timeline_ =
D3D12GPUCompletionTimeline::Create(device);
if (!guest_output_resource_refresher_completion_timeline_) {
return false;
}
if (!ui_submission_tracker_.Initialize(device, direct_queue)) {
ui_completion_timeline_ = D3D12GPUCompletionTimeline::Create(device);
if (!ui_completion_timeline_) {
return false;
}
+36 -24
View File
@@ -11,12 +11,13 @@
#define XENIA_UI_D3D12_D3D12_PRESENTER_H_
#include <array>
#include <cstdint>
#include <memory>
#include <utility>
#include "xenia/base/math.h"
#include "xenia/ui/d3d12/d3d12_gpu_completion_timeline.h"
#include "xenia/ui/d3d12/d3d12_provider.h"
#include "xenia/ui/d3d12/d3d12_submission_tracker.h"
#include "xenia/ui/presenter.h"
#include "xenia/ui/surface.h"
@@ -29,8 +30,8 @@ class D3D12UIDrawContext final : public UIDrawContext {
D3D12UIDrawContext(Presenter& presenter, uint32_t render_target_width,
uint32_t render_target_height,
ID3D12GraphicsCommandList* command_list,
UINT64 submission_index_current,
UINT64 submission_index_completed)
uint64_t submission_index_current,
uint64_t submission_index_completed)
: UIDrawContext(presenter, render_target_width, render_target_height),
command_list_(command_list),
submission_index_current_(submission_index_current),
@@ -39,15 +40,17 @@ class D3D12UIDrawContext final : public UIDrawContext {
ID3D12GraphicsCommandList* command_list() const {
return command_list_.Get();
}
UINT64 submission_index_current() const { return submission_index_current_; }
UINT64 submission_index_completed() const {
uint64_t submission_index_current() const {
return submission_index_current_;
}
uint64_t submission_index_completed() const {
return submission_index_completed_;
}
private:
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> command_list_;
UINT64 submission_index_current_;
UINT64 submission_index_completed_;
uint64_t submission_index_current_;
uint64_t submission_index_completed_;
};
class D3D12Presenter final : public Presenter {
@@ -99,8 +102,9 @@ class D3D12Presenter final : public Presenter {
bool CaptureGuestOutput(RawImage& image_out) override;
void AwaitUISubmissionCompletionFromUIThread(UINT64 submission_index) {
ui_submission_tracker_.AwaitSubmissionCompletion(submission_index);
void AwaitUISubmissionCompletionFromUIThread(uint64_t submission_index) {
ui_completion_timeline_->AwaitSubmissionAndUpdateCompleted(
submission_index);
}
protected:
@@ -213,12 +217,19 @@ class D3D12Presenter final : public Presenter {
};
void AwaitSwapChainUsageCompletion() {
// May be called during destruction.
// Presentation engine usage.
present_submission_tracker.AwaitAllSubmissionsCompletion();
if (present_completion_timeline) {
present_completion_timeline->AwaitAllSubmissions();
}
// Paint (render target) usage. While the presentation fence is signaled
// on the same queue, and presentation happens after painting, awaiting
// anyway for safety just to make less assumptions in the architecture.
paint_submission_tracker.AwaitAllSubmissionsCompletion();
if (paint_completion_timeline) {
paint_completion_timeline->AwaitAllSubmissions();
}
}
void DestroySwapChain();
@@ -226,9 +237,9 @@ class D3D12Presenter final : public Presenter {
// Connection-independent.
// Signaled before presenting.
D3D12SubmissionTracker paint_submission_tracker;
std::unique_ptr<D3D12GPUCompletionTimeline> paint_completion_timeline;
// Signaled after presenting.
D3D12SubmissionTracker present_submission_tracker;
std::unique_ptr<D3D12GPUCompletionTimeline> present_completion_timeline;
std::array<Microsoft::WRL::ComPtr<ID3D12CommandAllocator>,
kSwapChainBufferCount>
@@ -251,7 +262,7 @@ class D3D12Presenter final : public Presenter {
// the reference is not in this array yet, the most outdated reference, if
// needed, is replaced with the new one, awaiting the completion of the last
// paint usage.
std::array<std::pair<UINT64, Microsoft::WRL::ComPtr<ID3D12Resource>>,
std::array<std::pair<uint64_t, Microsoft::WRL::ComPtr<ID3D12Resource>>,
kGuestOutputMailboxSize>
guest_output_resource_paint_refs;
@@ -261,7 +272,7 @@ class D3D12Presenter final : public Presenter {
std::array<Microsoft::WRL::ComPtr<ID3D12Resource>,
kMaxGuestOutputPaintEffects - 1>
guest_output_intermediate_textures;
UINT64 guest_output_intermediate_texture_last_usage = 0;
uint64_t guest_output_intermediate_texture_last_usage = 0;
// Connection-specific.
@@ -301,22 +312,23 @@ class D3D12Presenter final : public Presenter {
size_t(GuestOutputPaintEffect::kCount)>
guest_output_paint_final_pipelines_;
// The first is the refresher submission tracker fence value at which the
// guest output texture was last refreshed, the second is the reference to the
// texture, which may be null. The indices are the mailbox indices.
std::array<std::pair<UINT64, Microsoft::WRL::ComPtr<ID3D12Resource>>,
// The first is the refresher completion timeline submission index at which
// the guest output texture was last refreshed, the second is the reference to
// the texture, which may be null. The indices are the mailbox indices.
std::array<std::pair<uint64_t, Microsoft::WRL::ComPtr<ID3D12Resource>>,
kGuestOutputMailboxSize>
guest_output_resources_;
// The guest output resources are protected by two submission trackers - the
// refresher ones (for writing to them via the guest_output_resources_
// The guest output resources are protected by two completion timelines - the
// refresher one (for writing to them via the guest_output_resources_
// references) and the paint one (for presenting it via the
// paint_context_.guest_output_resource_paint_refs references taken from
// guest_output_resources_).
D3D12SubmissionTracker guest_output_resource_refresher_submission_tracker_;
std::unique_ptr<D3D12GPUCompletionTimeline>
guest_output_resource_refresher_completion_timeline_;
// UI submission tracker with the submission index that can be given to UI
// UI completion timeline with the submission index that can be given to UI
// drawers (accessible from the UI thread only, at any time).
D3D12SubmissionTracker ui_submission_tracker_;
std::unique_ptr<D3D12GPUCompletionTimeline> ui_completion_timeline_;
// Accessible only by painting and by surface connection lifetime management
// (ConnectOrReconnectPaintingToSurfaceFromUIThread,
@@ -1,126 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/ui/d3d12/d3d12_submission_tracker.h"
#include "xenia/base/assert.h"
#include "xenia/base/logging.h"
namespace xe {
namespace ui {
namespace d3d12 {
bool D3D12SubmissionTracker::Initialize(ID3D12Device* device,
ID3D12CommandQueue* queue) {
Shutdown();
fence_completion_event_ = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (!fence_completion_event_) {
XELOGE(
"D3D12SubmissionTracker: Failed to create the fence completion event");
Shutdown();
return false;
}
// Continue where the tracker was left at the last shutdown.
if (FAILED(device->CreateFence(submission_current_ - 1, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&fence_)))) {
XELOGE("D3D12SubmissionTracker: Failed to create the fence");
Shutdown();
return false;
}
queue_ = queue;
submission_signal_queued_ = submission_current_ - 1;
return true;
}
void D3D12SubmissionTracker::Shutdown() {
AwaitAllSubmissionsCompletion();
queue_.Reset();
fence_.Reset();
if (fence_completion_event_) {
CloseHandle(fence_completion_event_);
fence_completion_event_ = nullptr;
}
}
bool D3D12SubmissionTracker::AwaitSubmissionCompletion(
UINT64 submission_index) {
if (!fence_ || !fence_completion_event_) {
// Not fully initialized yet or already shut down.
return false;
}
// The tracker itself can't give a submission index for a submission that
// hasn't even started being recorded yet, the client has provided a
// completely invalid value or has done overly optimistic math if such an
// index has been obtained somehow.
assert_true(submission_index <= submission_current_);
// Waiting for the current submission is fine if there was a refusal to
// submit, and the submission index wasn't incremented, but still need to
// release objects referenced in the dropped submission (while shutting down,
// for instance - in this case, waiting for the last successful submission,
// which could have also referenced the objects from the new submission - we
// can't know since the client has already overwritten its last usage index,
// would correctly ensure that GPU usage of the objects is not pending).
// Waiting for successful submissions, but failed signals, will result in a
// true race condition, however, but waiting for the closest successful signal
// is the best approximation - also retrying to signal in this case.
UINT64 fence_value = submission_index;
if (submission_index > submission_signal_queued_) {
TrySignalEnqueueing();
fence_value = submission_signal_queued_;
}
if (fence_->GetCompletedValue() < fence_value) {
if (FAILED(fence_->SetEventOnCompletion(fence_value,
fence_completion_event_))) {
return false;
}
if (WaitForSingleObject(fence_completion_event_, INFINITE) !=
WAIT_OBJECT_0) {
return false;
}
}
return fence_value == submission_index;
}
void D3D12SubmissionTracker::SetQueue(ID3D12CommandQueue* new_queue) {
if (queue_.Get() == new_queue) {
return;
}
if (queue_) {
// Make sure the first signal on the new queue won't happen before the last
// signal, if pending, on the old one, as that would result first in too
// early submission completion indication, and then in rewinding.
AwaitAllSubmissionsCompletion();
}
queue_ = new_queue;
}
bool D3D12SubmissionTracker::NextSubmission() {
++submission_current_;
assert_not_null(queue_);
assert_not_null(fence_);
return TrySignalEnqueueing();
}
bool D3D12SubmissionTracker::TrySignalEnqueueing() {
if (submission_signal_queued_ + 1 >= submission_current_) {
return true;
}
if (!queue_ || !fence_) {
return false;
}
if (FAILED(queue_->Signal(fence_.Get(), submission_current_ - 1))) {
return false;
}
submission_signal_queued_ = submission_current_ - 1;
return true;
}
} // namespace d3d12
} // namespace ui
} // namespace xe
@@ -1,93 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_UI_D3D12_D3D12_SUBMISSION_TRACKER_H_
#define XENIA_UI_D3D12_D3D12_SUBMISSION_TRACKER_H_
#include "xenia/ui/d3d12/d3d12_api.h"
namespace xe {
namespace ui {
namespace d3d12 {
// GPU > CPU fence wrapper, safely handling cases when the fence has not been
// initialized yet or has already been shut down, dropped submissions, and also
// transfers between queues so signals stay ordered.
//
// The current submission index can be associated with the usage of objects to
// release them when the GPU isn't potentially referencing them anymore, and
// should be incremented only
//
// 0 can be used as a "never referenced" submission index.
//
// The submission index timeline survives Shutdown / Initialize, so submission
// indices can be given to clients that are not aware of the lifetime of the
// tracker.
class D3D12SubmissionTracker {
public:
D3D12SubmissionTracker() = default;
D3D12SubmissionTracker(const D3D12SubmissionTracker& submission_tracker) =
delete;
D3D12SubmissionTracker& operator=(
const D3D12SubmissionTracker& submission_tracker) = delete;
~D3D12SubmissionTracker() { Shutdown(); }
// The queue may be null if it's going to be set dynamically. Will also take a
// reference to the queue.
bool Initialize(ID3D12Device* device, ID3D12CommandQueue* queue);
void Shutdown();
// Will perform an ownership transfer if the queue is different than the
// current one, and take a reference to the queue.
void SetQueue(ID3D12CommandQueue* new_queue);
UINT64 GetCurrentSubmission() const { return submission_current_; }
// May be lower than a value awaited by AwaitSubmissionCompletion if it
// returned false.
UINT64 GetCompletedSubmission() const {
// If shut down already or haven't fully initialized yet, don't care, for
// simplicity of external code, as any downloads are unlikely in this case,
// but destruction can be simplified.
return fence_ ? fence_->GetCompletedValue() : (GetCurrentSubmission() - 1);
}
// Returns whether the expected GPU signal has actually been reached (rather
// than some fallback condition) for cases when stronger completeness
// guarantees as needed (when downloading, as opposed to just destroying).
// If false is returned, it's also not guaranteed that GetCompletedSubmission
// will return a value >= submission_index.
bool AwaitSubmissionCompletion(UINT64 submission_index);
bool AwaitAllSubmissionsCompletion() {
return AwaitSubmissionCompletion(GetCurrentSubmission() - 1);
}
// Call after a successful ExecuteCommandList. Unconditionally increments the
// current submission index, and tries to enqueue the fence signal. Returns
// true if enqueued successfully, but even if not, waiting for submissions
// without a successfully enqueued signal is handled in the tracker in a way
// that it won't be infinite, so there's no need for clients to revert updates
// to submission indices associated with GPU usage of objects.
bool NextSubmission();
// If NextSubmission has failed, but it's important that the signal is
// enqueued, can be used to retry enqueueing the signal.
bool TrySignalEnqueueing();
private:
UINT64 submission_current_ = 1;
UINT64 submission_signal_queued_ = 0;
HANDLE fence_completion_event_ = nullptr;
Microsoft::WRL::ComPtr<ID3D12Fence> fence_;
Microsoft::WRL::ComPtr<ID3D12CommandQueue> queue_;
};
} // namespace d3d12
} // namespace ui
} // namespace xe
#endif // XENIA_UI_D3D12_D3D12_SUBMISSION_TRACKER_H_
+101
View File
@@ -0,0 +1,101 @@
/**
******************************************************************************
* 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_GPU_COMPLETION_TIMELINE_H_
#define XENIA_UI_GPU_COMPLETION_TIMELINE_H_
#include <algorithm>
#include <cstdint>
#include "xenia/base/assert.h"
namespace xe {
namespace ui {
class GPUCompletionTimeline {
public:
GPUCompletionTimeline(const GPUCompletionTimeline&) = delete;
GPUCompletionTimeline& operator=(const GPUCompletionTimeline&) = delete;
GPUCompletionTimeline(GPUCompletionTimeline&&) = delete;
GPUCompletionTimeline& operator=(GPUCompletionTimeline&&) = delete;
// The implemention of completion timeline destruction must await all pending
// submissions (including for releasing its own GPU API fences, but also
// because code that uses the completion timeline may expect that, since the
// completion timeline is generally the only way for it to know when its GPU
// objects can be destroyed safely).
virtual ~GPUCompletionTimeline() = default;
// Always >= 1, so objects never used yet may be associated with the
// submission index 0.
uint64_t GetUpcomingSubmission() const { return upcoming_submission_; }
// Awaiting completion or updating the completed submission may mark the
// device as lost.
// The implementation is required to call `SetCompletedSubmission` if it has
// been made aware that a submission has been completed.
virtual void UpdateCompletedSubmission() = 0;
uint64_t GetCompletedSubmissionFromLastUpdate() const {
return completed_submission_;
}
uint64_t UpdateAndGetCompletedSubmission() {
UpdateCompletedSubmission();
return GetCompletedSubmissionFromLastUpdate();
}
bool AwaitSubmissionAndUpdateCompleted(const uint64_t awaited_submission) {
assert_true(awaited_submission < upcoming_submission_);
if (UpdateAndGetCompletedSubmission() < awaited_submission) {
AwaitSubmissionImpl(awaited_submission);
}
// Recheck, the wait might have been incomplete if there was an error.
return UpdateAndGetCompletedSubmission() >= awaited_submission;
}
bool AwaitMaxSubmissionsPendingAndUpdateCompleted(
const uint64_t max_submissions_pending) {
assert_not_zero(max_submissions_pending);
return AwaitSubmissionAndUpdateCompleted(
GetUpcomingSubmission() -
std::min(max_submissions_pending, GetUpcomingSubmission()));
}
bool AwaitAllSubmissions() {
return AwaitSubmissionAndUpdateCompleted(GetUpcomingSubmission() - 1);
}
protected:
explicit GPUCompletionTimeline() = default;
// Call only after a successful submission, so that it can be awaited later.
void IncrementUpcomingSubmission() { ++upcoming_submission_; }
void SetCompletedSubmission(const uint64_t new_completed_submission) {
assert_true(new_completed_submission < upcoming_submission_);
assert_true(new_completed_submission >= completed_submission_);
completed_submission_ = new_completed_submission;
}
// The implementation may call `SetCompletedSubmission`, but is not required
// to.
virtual void AwaitSubmissionImpl(uint64_t awaited_submission) = 0;
private:
uint64_t upcoming_submission_ = 1;
uint64_t completed_submission_ = 0;
};
} // namespace ui
} // namespace xe
#endif // XENIA_UI_GPU_COMPLETION_TIMELINE_H_
+22
View File
@@ -10,6 +10,7 @@
#ifndef XENIA_UI_VULKAN_VULKAN_DEVICE_H_
#define XENIA_UI_VULKAN_VULKAN_DEVICE_H_
#include <atomic>
#include <memory>
#include <mutex>
#include <vector>
@@ -269,6 +270,25 @@ class VulkanDevice {
const MemoryTypes& memory_types() const { return memory_types_; }
// Returns whether a device loss has been observed for the first time, so, for
// instance, logging of the device loss can be limited only to the location
// where it was first caught.
bool SetLost() noexcept {
return !lost_.exchange(true, std::memory_order_acq_rel);
}
bool IsLost() const noexcept { return lost_.load(std::memory_order_acquire); }
VkResult SubmitAndUpdateLost(const VkQueue queue, const uint32_t submit_count,
const VkSubmitInfo* const submits,
const VkFence fence) {
const VkResult submit_result =
functions().vkQueueSubmit(queue, submit_count, submits, fence);
if (submit_result == VK_ERROR_DEVICE_LOST) {
SetLost();
}
return submit_result;
}
private:
explicit VulkanDevice(const VulkanInstance* vulkan_instance,
VkPhysicalDevice physical_device);
@@ -288,6 +308,8 @@ class VulkanDevice {
uint32_t queue_family_sparse_binding_ = UINT32_MAX;
MemoryTypes memory_types_;
std::atomic<bool> lost_{false};
};
} // namespace vulkan
@@ -0,0 +1,182 @@
/**
******************************************************************************
* 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_gpu_completion_timeline.h"
#include <algorithm>
#include <iterator>
#include "xenia/base/assert.h"
#include "xenia/base/logging.h"
namespace xe {
namespace ui {
namespace vulkan {
VulkanGPUCompletionTimeline::~VulkanGPUCompletionTimeline() {
#ifndef NDEBUG
assert_zero(fences_acquired_);
#endif
if (!pending_submission_fences_.empty()) {
if (vulkan_device_->functions().vkWaitForFences(
vulkan_device_->device(), 1,
&pending_submission_fences_.back().second, VK_TRUE,
UINT64_MAX) == VK_ERROR_DEVICE_LOST) {
vulkan_device_->SetLost();
}
while (!pending_submission_fences_.empty()) {
vulkan_device_->functions().vkDestroyFence(
vulkan_device_->device(), pending_submission_fences_.back().second,
nullptr);
pending_submission_fences_.pop_back();
}
}
while (!free_fences_.empty()) {
vulkan_device_->functions().vkDestroyFence(vulkan_device_->device(),
free_fences_.back(), nullptr);
free_fences_.pop_back();
}
}
std::optional<VulkanGPUCompletionTimeline::FenceAcquisition>
VulkanGPUCompletionTimeline::AcquireFenceForSubmission(
VkResult* const result_out_opt) {
// Reuse fences if completion was not awaited or updated explicitly.
UpdateAndGetCompletedSubmission();
VkFence fence = VK_NULL_HANDLE;
if (!free_fences_.empty()) {
const VkResult fence_reset_result =
vulkan_device_->functions().vkResetFences(vulkan_device_->device(), 1,
&free_fences_.back());
if (fence_reset_result != VK_SUCCESS) {
XELOGE("Failed to reset a Vulkan fence: {}",
vk::to_string(vk::Result(fence_reset_result)));
} else {
fence = free_fences_.back();
free_fences_.pop_back();
}
}
if (fence == VK_NULL_HANDLE) {
const VkFenceCreateInfo fence_create_info = {
VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
const VkResult fence_create_result =
vulkan_device_->functions().vkCreateFence(
vulkan_device_->device(), &fence_create_info, nullptr, &fence);
if (fence_create_result != VK_SUCCESS) {
XELOGE("Failed to create a Vulkan fence: {}",
vk::to_string(vk::Result(fence_create_result)));
if (result_out_opt != nullptr) {
*result_out_opt = fence_create_result;
}
return std::nullopt;
}
}
#ifndef NDEBUG
++fences_acquired_;
#endif
if (result_out_opt != nullptr) {
*result_out_opt = VK_SUCCESS;
}
return FenceAcquisition(this, fence);
}
VkResult VulkanGPUCompletionTimeline::AcquireFenceAndSubmit(
const uint32_t queue_family_index, const uint32_t queue_index,
const uint32_t submit_count, const VkSubmitInfo* const submits) {
VkResult fence_acquire_result;
std::optional<FenceAcquisition> fence_acquisition =
AcquireFenceForSubmission(&fence_acquire_result);
if (!fence_acquisition.has_value()) {
return fence_acquire_result;
}
VkResult submit_result;
{
const VulkanDevice::Queue::Acquisition queue_acquisition =
vulkan_device_->AcquireQueue(queue_family_index, queue_index);
submit_result = vulkan_device_->SubmitAndUpdateLost(
queue_acquisition.queue(), submit_count, submits,
fence_acquisition->GetFenceForSubmitting());
}
if (submit_result != VK_SUCCESS) {
fence_acquisition->SetSubmissionFailedOrAborted();
}
return submit_result;
}
void VulkanGPUCompletionTimeline::UpdateCompletedSubmission() {
while (!pending_submission_fences_.empty()) {
const VkResult fence_status = vulkan_device_->functions().vkGetFenceStatus(
vulkan_device_->device(), pending_submission_fences_.front().second);
if (fence_status != VK_SUCCESS) {
// Not ready, or an error.
if (fence_status == VK_ERROR_DEVICE_LOST) {
vulkan_device_->SetLost();
}
break;
}
SetCompletedSubmission(pending_submission_fences_.front().first);
free_fences_.push_back(pending_submission_fences_.front().second);
pending_submission_fences_.pop_front();
}
}
void VulkanGPUCompletionTimeline::AwaitSubmissionImpl(
const uint64_t awaited_submission) {
// According to the Vulkan 1.4.335 specification:
// "The first synchronization scope includes every batch submitted in the same
// queue submission command. Fence signal operations that are defined by
// vkQueueSubmit or vkQueueSubmit2 additionally include in the first
// synchronization scope all commands that occur earlier in submission order.
// Fence signal operations that are defined by vkQueueSubmit or vkQueueSubmit2
// or vkQueueBindSparse additionally include in the first synchronization
// scope any semaphore and fence signal operations that occur earlier in
// signal operation order."
auto submission_end_iterator = pending_submission_fences_.cbegin();
while (submission_end_iterator != pending_submission_fences_.cend() &&
submission_end_iterator->first <= awaited_submission) {
submission_end_iterator = std::next(submission_end_iterator);
}
if (submission_end_iterator != pending_submission_fences_.cbegin()) {
const VkResult fence_wait_result =
vulkan_device_->functions().vkWaitForFences(
vulkan_device_->device(), 1,
&std::prev(submission_end_iterator)->second, VK_TRUE, UINT64_MAX);
if (fence_wait_result != VK_SUCCESS) {
XELOGE("Failed to wait for a Vulkan fence: {}",
vk::to_string(vk::Result(fence_wait_result)));
if (fence_wait_result == VK_ERROR_DEVICE_LOST) {
vulkan_device_->SetLost();
}
return;
}
for (auto free_fence_iterator = pending_submission_fences_.cbegin();
free_fence_iterator != submission_end_iterator;
free_fence_iterator = std::next(free_fence_iterator)) {
free_fences_.push_back(free_fence_iterator->second);
}
pending_submission_fences_.erase(pending_submission_fences_.cbegin(),
submission_end_iterator);
}
if (GetCompletedSubmissionFromLastUpdate() < awaited_submission) {
SetCompletedSubmission(awaited_submission);
}
}
} // namespace vulkan
} // namespace ui
} // namespace xe
@@ -0,0 +1,155 @@
/**
******************************************************************************
* 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_GPU_COMPLETION_TIMELINE_H_
#define XENIA_UI_VULKAN_VULKAN_GPU_COMPLETION_TIMELINE_H_
#include <deque>
#include <optional>
#include <utility>
#include <vector>
#include "xenia/base/assert.h"
#include "xenia/ui/gpu_completion_timeline.h"
#include "xenia/ui/vulkan/vulkan_device.h"
namespace xe {
namespace ui {
namespace vulkan {
class VulkanGPUCompletionTimeline : public GPUCompletionTimeline {
public:
explicit VulkanGPUCompletionTimeline(VulkanDevice* const vulkan_device)
: vulkan_device_(vulkan_device) {}
VulkanGPUCompletionTimeline(const VulkanGPUCompletionTimeline&) = delete;
VulkanGPUCompletionTimeline& operator=(const VulkanGPUCompletionTimeline&) =
delete;
VulkanGPUCompletionTimeline(VulkanGPUCompletionTimeline&&) = delete;
VulkanGPUCompletionTimeline& operator=(VulkanGPUCompletionTimeline&&) =
delete;
~VulkanGPUCompletionTimeline();
class FenceAcquisition {
public:
explicit FenceAcquisition(
VulkanGPUCompletionTimeline* const completion_timeline,
const VkFence fence)
: completion_timeline_(completion_timeline), fence_(fence) {
assert_not_null(completion_timeline);
assert_true(fence != VK_NULL_HANDLE);
}
FenceAcquisition(const FenceAcquisition&) = delete;
FenceAcquisition& operator=(const FenceAcquisition&) = delete;
FenceAcquisition(FenceAcquisition&& other)
: completion_timeline_(other.completion_timeline_),
fence_(other.fence_),
submission_successful_(other.submission_successful_) {
other.completion_timeline_ = nullptr;
other.fence_ = VK_NULL_HANDLE;
other.submission_successful_.reset();
}
FenceAcquisition& operator==(FenceAcquisition&& other) {
if (this == &other) {
return *this;
}
completion_timeline_ = other.completion_timeline_;
other.completion_timeline_ = nullptr;
fence_ = other.fence_;
other.fence_ = VK_NULL_HANDLE;
submission_successful_ = other.submission_successful_;
other.submission_successful_.reset();
return *this;
}
~FenceAcquisition() {
if (completion_timeline_ && fence_) {
#ifndef NDEBUG
assert_not_zero(completion_timeline_->fences_acquired_);
--completion_timeline_->fences_acquired_;
#endif
if (submission_successful_.value_or(false)) {
assert_true(
completion_timeline_->pending_submission_fences_.empty() ||
completion_timeline_->pending_submission_fences_.front().first <
completion_timeline_->GetUpcomingSubmission());
completion_timeline_->pending_submission_fences_.emplace_back(
completion_timeline_->GetUpcomingSubmission(), fence_);
completion_timeline_->IncrementUpcomingSubmission();
} else {
completion_timeline_->free_fences_.push_back(fence_);
}
}
}
VkFence GetFenceForSubmitting() {
// Don't mark the fence as used in a submission if already tried to
// submit, but failed.
assert_true(!submission_successful_.has_value() ||
*submission_successful_);
submission_successful_ = true;
return fence_;
}
void SetSubmissionFailedOrAborted() { submission_successful_ = false; }
private:
VulkanGPUCompletionTimeline* completion_timeline_ = nullptr;
VkFence fence_ = VK_NULL_HANDLE;
std::optional<bool> submission_successful_;
};
// If the submission has succeeded (`GetFenceForSubmitting` was called, but
// `SetSubmissionFailedOrAborted` was not), will advance to the next
// submission once the acquisition is released.
//
// It's possible to acquire a fence not right before submitting, but also well
// in advance, such as before recording the command buffer, for instance, to
// skip recording it if fence acquisition has failed.
//
// Acquiring a fence also updates the completed submission in order to reuse
// fences if this completion timeline is used without regular checks or waits
// (for instance, if it's supplementary to another completion timeline, and
// awaited only before destroying something).
[[nodiscard]] std::optional<FenceAcquisition> AcquireFenceForSubmission(
VkResult* result_out_opt = nullptr);
VkResult AcquireFenceAndSubmit(uint32_t queue_family_index,
uint32_t queue_index, uint32_t submit_count,
const VkSubmitInfo* submits);
void UpdateCompletedSubmission() override;
protected:
void AwaitSubmissionImpl(uint64_t awaited_submission) override;
private:
VulkanDevice* const vulkan_device_;
std::vector<VkFence> free_fences_;
// <Submission index, fence>, in submission index order.
std::deque<std::pair<uint64_t, VkFence>> pending_submission_fences_;
#ifndef NDEBUG
size_t fences_acquired_ = 0;
#endif
};
} // namespace vulkan
} // namespace ui
} // namespace xe
#endif // XENIA_UI_VULKAN_VULKAN_GPU_COMPLETION_TIMELINE_H_
+71 -116
View File
@@ -158,8 +158,8 @@ VulkanPresenter::~VulkanPresenter() {
// (paint submission completion already awaited).
// From most likely the latest to most likely the earliest to be signaled, so
// just one sleep will likely be needed.
ui_submission_tracker_.Shutdown();
guest_output_image_refresher_submission_tracker_.Shutdown();
ui_completion_timeline_.AwaitAllSubmissions();
guest_output_image_refresher_completion_timeline_.AwaitAllSubmissions();
const VulkanDevice::Functions& dfn = vulkan_device_->functions();
const VkDevice device = vulkan_device_->device();
@@ -382,51 +382,24 @@ bool VulkanPresenter::CaptureGuestOutput(RawImage& image_out) {
return false;
}
VkSubmitInfo submit_info = {};
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &command_buffer;
VulkanSubmissionTracker submission_tracker(vulkan_device_);
{
VulkanSubmissionTracker::FenceAcquisition fence_acqusition(
submission_tracker.AcquireFenceToAdvanceSubmission());
if (!fence_acqusition.fence()) {
XELOGE(
"VulkanPresenter: Failed to acquire a fence for guest output "
"capturing");
fence_acqusition.SubmissionFailedOrDropped();
dfn.vkDestroyCommandPool(device, command_pool, nullptr);
dfn.vkDestroyBuffer(device, buffer, nullptr);
dfn.vkFreeMemory(device, buffer_memory, nullptr);
return false;
}
VkResult submit_result;
{
const VulkanDevice::Queue::Acquisition queue_acquisition =
vulkan_device_->AcquireQueue(
vulkan_device_->queue_family_graphics_compute(), 0);
submit_result =
dfn.vkQueueSubmit(queue_acquisition.queue(), 1, &submit_info,
fence_acqusition.fence());
}
VulkanGPUCompletionTimeline completion_timeline(vulkan_device_);
VkSubmitInfo submit_info = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &command_buffer;
const VkResult submit_result = completion_timeline.AcquireFenceAndSubmit(
vulkan_device_->queue_family_graphics_compute(), 0, 1, &submit_info);
if (submit_result != VK_SUCCESS) {
XELOGE(
"VulkanPresenter: Failed to submit the guest output capturing "
"command buffer");
fence_acqusition.SubmissionFailedOrDropped();
"command buffer: {}",
vk::to_string(vk::Result(submit_result)));
dfn.vkDestroyCommandPool(device, command_pool, nullptr);
dfn.vkDestroyBuffer(device, buffer, nullptr);
dfn.vkFreeMemory(device, buffer_memory, nullptr);
return false;
}
}
if (!submission_tracker.AwaitAllSubmissionsCompletion()) {
XELOGE(
"VulkanPresenter: Failed to await the guest output capturing fence");
dfn.vkDestroyCommandPool(device, command_pool, nullptr);
dfn.vkDestroyBuffer(device, buffer, nullptr);
dfn.vkFreeMemory(device, buffer_memory, nullptr);
return false;
// Destroying the completion timeline causes the submission to be awaited.
}
dfn.vkDestroyCommandPool(device, command_pool, nullptr);
@@ -479,8 +452,8 @@ VkCommandBuffer VulkanPresenter::AcquireUISetupCommandBufferFromUIThread() {
// Try to reuse an existing command buffer.
if (!paint_context_.ui_setup_command_buffers.empty()) {
uint64_t submission_index_completed =
ui_submission_tracker_.UpdateAndGetCompletedSubmission();
const uint64_t submission_index_completed =
ui_completion_timeline_.UpdateAndGetCompletedSubmission();
for (size_t i = 0; i < paint_context_.ui_setup_command_buffers.size();
++i) {
PaintContext::UISetupCommandBuffer& ui_setup_command_buffer =
@@ -503,7 +476,7 @@ VkCommandBuffer VulkanPresenter::AcquireUISetupCommandBufferFromUIThread() {
}
paint_context_.ui_setup_command_buffer_current_index = i;
ui_setup_command_buffer.last_usage_submission_index =
ui_submission_tracker_.GetCurrentSubmission();
ui_completion_timeline_.GetUpcomingSubmission();
return ui_setup_command_buffer.command_buffer;
}
}
@@ -546,7 +519,7 @@ VkCommandBuffer VulkanPresenter::AcquireUISetupCommandBufferFromUIThread() {
paint_context_.ui_setup_command_buffers.size();
paint_context_.ui_setup_command_buffers.emplace_back(
new_command_pool, new_command_buffer,
ui_submission_tracker_.GetCurrentSubmission());
ui_completion_timeline_.GetUpcomingSubmission());
return new_command_buffer;
}
@@ -877,8 +850,9 @@ bool VulkanPresenter::RefreshGuestOutputImpl(
if (image_instance.image &&
(image_instance.image->extent().width != frontbuffer_width ||
image_instance.image->extent().height != frontbuffer_height)) {
guest_output_image_refresher_submission_tracker_.AwaitSubmissionCompletion(
image_instance.last_refresher_submission);
guest_output_image_refresher_completion_timeline_
.AwaitSubmissionAndUpdateCompleted(
image_instance.last_refresher_submission);
image_instance.image.reset();
}
if (!image_instance.image) {
@@ -904,26 +878,21 @@ bool VulkanPresenter::RefreshGuestOutputImpl(
// signal and wait slightly longer, for nothing important, while shutting down
// than to destroy the image while it's still in use.
image_instance.last_refresher_submission =
guest_output_image_refresher_submission_tracker_.GetCurrentSubmission();
guest_output_image_refresher_completion_timeline_.GetUpcomingSubmission();
// No need to make the refresher signal the fence by itself - signal it here
// instead to have more control:
// "Fence signal operations that are defined by vkQueueSubmit additionally
// include in the first synchronization scope all commands that occur earlier
// in submission order."
const VulkanDevice::Functions& dfn = vulkan_device_->functions();
{
VulkanSubmissionTracker::FenceAcquisition fence_acqusition(
guest_output_image_refresher_submission_tracker_
.AcquireFenceToAdvanceSubmission());
const VulkanDevice::Queue::Acquisition queue_acquisition =
vulkan_device_->AcquireQueue(
vulkan_device_->queue_family_graphics_compute(), 0);
if (dfn.vkQueueSubmit(queue_acquisition.queue(), 0, nullptr,
fence_acqusition.fence()) != VK_SUCCESS) {
fence_acqusition.SubmissionSucceededSignalFailed();
}
const VkResult submit_result =
guest_output_image_refresher_completion_timeline_.AcquireFenceAndSubmit(
vulkan_device_->queue_family_graphics_compute(), 0, 0, nullptr);
if (submit_result != VK_SUCCESS) {
XELOGE(
"VulkanPresenter: Failed to submit the guest output image refresh "
"fence signal: {}",
vk::to_string(vk::Result(submit_result)));
}
return refresher_succeeded;
}
@@ -1277,7 +1246,7 @@ VkSwapchainKHR VulkanPresenter::PaintContext::CreateSwapchainForVulkanSurface(
VkSwapchainKHR VulkanPresenter::PaintContext::PrepareForSwapchainRetirement() {
if (swapchain != VK_NULL_HANDLE) {
submission_tracker.AwaitAllSubmissionsCompletion();
completion_timeline.AwaitAllSubmissions();
}
const VulkanDevice::Functions& dfn = vulkan_device->functions();
const VkDevice device = vulkan_device->device();
@@ -1385,13 +1354,11 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(
bool execute_ui_drawers) {
// Begin the submission in place of the one not currently potentially used on
// the GPU.
uint64_t current_paint_submission_index =
paint_context_.submission_tracker.GetCurrentSubmission();
const uint64_t current_paint_submission_index =
paint_context_.completion_timeline.GetUpcomingSubmission();
uint64_t paint_submission_count = uint64_t(paint_context_.submissions.size());
if (current_paint_submission_index >= paint_submission_count) {
paint_context_.submission_tracker.AwaitSubmissionCompletion(
current_paint_submission_index - paint_submission_count);
}
paint_context_.completion_timeline
.AwaitMaxSubmissionsPendingAndUpdateCompleted(paint_submission_count);
const PaintContext::Submission& paint_submission =
*paint_context_.submissions[current_paint_submission_index %
paint_submission_count];
@@ -1555,7 +1522,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(
}
// Await the completion of the usage of the old guest output image and
// its descriptors.
paint_context_.submission_tracker.AwaitSubmissionCompletion(
paint_context_.completion_timeline.AwaitSubmissionAndUpdateCompleted(
paint_context_
.guest_output_image_paint_refs
[guest_output_image_paint_ref_new_index]
@@ -1617,9 +1584,10 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(
// Need to replace immediately as a new image with the requested
// size is needed.
if (intermediate_image_ptr_ref) {
paint_context_.submission_tracker.AwaitSubmissionCompletion(
paint_context_
.guest_output_intermediate_image_last_submission);
paint_context_.completion_timeline
.AwaitSubmissionAndUpdateCompleted(
paint_context_
.guest_output_intermediate_image_last_submission);
intermediate_image_ptr_ref.reset();
util::DestroyAndNullHandle(
dfn.vkDestroyFramebuffer, device,
@@ -1696,7 +1664,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(
} else {
// Was previously needed, but not anymore - destroy when possible.
if (intermediate_image_ptr_ref &&
paint_context_.submission_tracker
paint_context_.completion_timeline
.UpdateAndGetCompletedSubmission() >=
paint_context_
.guest_output_intermediate_image_last_submission) {
@@ -1731,7 +1699,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(
if (swapchain_effect_pipeline.swapchain_pipeline != VK_NULL_HANDLE &&
swapchain_effect_pipeline.swapchain_format !=
paint_context_.swapchain_render_pass_format) {
paint_context_.submission_tracker.AwaitSubmissionCompletion(
paint_context_.completion_timeline.AwaitSubmissionAndUpdateCompleted(
paint_context_.guest_output_image_paint_last_submission);
util::DestroyAndNullHandle(
dfn.vkDestroyPipeline, device,
@@ -1959,10 +1927,10 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(
// Release main target guest output image references that aren't needed
// anymore (this is done after various potential guest-output-related main
// target submission tracker waits so the completed submission value is the
// target completion timeline waits so the completed submission index is the
// most actual).
uint64_t completed_paint_submission =
paint_context_.submission_tracker.UpdateAndGetCompletedSubmission();
paint_context_.completion_timeline.UpdateAndGetCompletedSubmission();
for (std::pair<uint64_t, std::shared_ptr<GuestOutputImage>>&
guest_output_image_paint_ref :
paint_context_.guest_output_image_paint_refs) {
@@ -2001,8 +1969,8 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(
VulkanUIDrawContext ui_draw_context(
*this, paint_context_.swapchain_extent.width,
paint_context_.swapchain_extent.height, draw_command_buffer,
ui_submission_tracker_.GetCurrentSubmission(),
ui_submission_tracker_.UpdateAndGetCompletedSubmission(),
ui_completion_timeline_.GetUpcomingSubmission(),
ui_completion_timeline_.UpdateAndGetCompletedSubmission(),
paint_context_.swapchain_render_pass,
paint_context_.swapchain_render_pass_format);
ExecuteUIDrawersFromUIThread(ui_draw_context);
@@ -2045,48 +2013,35 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(
submit_info.pCommandBuffers = command_buffers;
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &present_semaphore;
{
VulkanSubmissionTracker::FenceAcquisition fence_acqusition(
paint_context_.submission_tracker.AcquireFenceToAdvanceSubmission());
// Also update the submission tracker giving submission indices to UI draw
// callbacks if submission is successful.
VulkanSubmissionTracker::FenceAcquisition ui_fence_acquisition;
if (execute_ui_drawers) {
ui_fence_acquisition =
ui_submission_tracker_.AcquireFenceToAdvanceSubmission();
const VkResult submit_result =
paint_context_.completion_timeline.AcquireFenceAndSubmit(
vulkan_device_->queue_family_graphics_compute(), 0, 1, &submit_info);
if (submit_result != VK_SUCCESS) {
XELOGE(
"VulkanPresenter: Failed to submit the presentation command buffer: {}",
vk::to_string(vk::Result(submit_result)));
if (ui_setup_command_buffer_index != SIZE_MAX) {
// If failed to submit, make the UI setup command buffer available for
// immediate reuse, as the completed submission index won't be updated to
// the current index, and failing submissions with setup command buffer
// over and over will result in never reusing the setup command buffers.
paint_context_.ui_setup_command_buffers[ui_setup_command_buffer_index]
.last_usage_submission_index = 0;
}
VkResult submit_result;
{
const VulkanDevice::Queue::Acquisition queue_acquisition =
vulkan_device_->AcquireQueue(
vulkan_device_->queue_family_graphics_compute(), 0);
submit_result = dfn.vkQueueSubmit(queue_acquisition.queue(), 1,
&submit_info, fence_acqusition.fence());
if (ui_fence_acquisition.fence() != VK_NULL_HANDLE &&
submit_result == VK_SUCCESS) {
if (dfn.vkQueueSubmit(queue_acquisition.queue(), 0, nullptr,
ui_fence_acquisition.fence()) != VK_SUCCESS) {
ui_fence_acquisition.SubmissionSucceededSignalFailed();
}
}
}
if (submit_result != VK_SUCCESS) {
XELOGE("VulkanPresenter: Failed to submit command buffers");
fence_acqusition.SubmissionFailedOrDropped();
ui_fence_acquisition.SubmissionFailedOrDropped();
if (ui_setup_command_buffer_index != SIZE_MAX) {
// If failed to submit, make the UI setup command buffer available for
// immediate reuse, as the completed submission index won't be updated
// to the current index, and failing submissions with setup command
// buffer over and over will result in never reusing the setup command
// buffers.
paint_context_.ui_setup_command_buffers[ui_setup_command_buffer_index]
.last_usage_submission_index = 0;
}
// The image is in an acquired state - but now, it will be in it forever.
// To avoid that, recreate the swapchain - don't return just
// kNotPresented.
return PaintResult::kNotPresentedConnectionOutdated;
// The image is in an acquired state - but now, it will be in it forever.
// To avoid that, recreate the swapchain - don't return just kNotPresented.
return PaintResult::kNotPresentedConnectionOutdated;
}
if (execute_ui_drawers) {
// Also update the completion timeline providing submission indices to UI
// draw callbacks if submission is successful.
const VkResult ui_signal_submit_result =
ui_completion_timeline_.AcquireFenceAndSubmit(
vulkan_device_->queue_family_graphics_compute(), 0, 0, nullptr);
if (ui_signal_submit_result != VK_SUCCESS) {
XELOGE(
"VulkanPresenter: Failed to submit the UI drawing fence signal: {}",
vk::to_string(vk::Result(ui_signal_submit_result)));
}
}
+17 -17
View File
@@ -23,8 +23,8 @@
#include "xenia/ui/surface.h"
#include "xenia/ui/vulkan/ui_samplers.h"
#include "xenia/ui/vulkan/vulkan_device.h"
#include "xenia/ui/vulkan/vulkan_gpu_completion_timeline.h"
#include "xenia/ui/vulkan/vulkan_instance.h"
#include "xenia/ui/vulkan/vulkan_submission_tracker.h"
namespace xe {
namespace ui {
@@ -124,8 +124,8 @@ class VulkanPresenter final : public Presenter {
};
static std::unique_ptr<VulkanPresenter> Create(
HostGpuLossCallback host_gpu_loss_callback,
const VulkanDevice* vulkan_device, const UISamplers* ui_samplers) {
HostGpuLossCallback host_gpu_loss_callback, VulkanDevice* vulkan_device,
const UISamplers* ui_samplers) {
auto presenter = std::unique_ptr<VulkanPresenter>(new VulkanPresenter(
host_gpu_loss_callback, vulkan_device, ui_samplers));
if (!presenter->InitializeSurfaceIndependent()) {
@@ -136,7 +136,7 @@ class VulkanPresenter final : public Presenter {
~VulkanPresenter();
const VulkanDevice* vulkan_device() const { return vulkan_device_; }
VulkanDevice* vulkan_device() const { return vulkan_device_; }
static Surface::TypeFlags GetSurfaceTypesSupportedByInstance(
const VulkanInstance::Extensions& instance_extensions);
@@ -145,7 +145,7 @@ class VulkanPresenter final : public Presenter {
bool CaptureGuestOutput(RawImage& image_out) override;
void AwaitUISubmissionCompletionFromUIThread(uint64_t submission_index) {
ui_submission_tracker_.AwaitSubmissionCompletion(submission_index);
ui_completion_timeline_.AwaitSubmissionAndUpdateCompleted(submission_index);
}
VkCommandBuffer AcquireUISetupCommandBufferFromUIThread();
@@ -361,8 +361,8 @@ class VulkanPresenter final : public Presenter {
VkFramebuffer framebuffer;
};
explicit PaintContext(const VulkanDevice* const vulkan_device)
: vulkan_device(vulkan_device), submission_tracker(vulkan_device) {}
explicit PaintContext(VulkanDevice* const vulkan_device)
: vulkan_device(vulkan_device), completion_timeline(vulkan_device) {}
PaintContext(const PaintContext& paint_context) = delete;
PaintContext& operator=(const PaintContext& paint_context) = delete;
@@ -386,11 +386,11 @@ class VulkanPresenter final : public Presenter {
// Connection-indepedent.
const VulkanDevice* vulkan_device;
VulkanDevice* vulkan_device;
std::array<std::unique_ptr<PaintContext::Submission>, kSubmissionCount>
submissions;
VulkanSubmissionTracker submission_tracker;
VulkanGPUCompletionTimeline completion_timeline;
std::array<GuestOutputPaintPipeline, size_t(GuestOutputPaintEffect::kCount)>
guest_output_paint_pipelines;
@@ -445,13 +445,13 @@ class VulkanPresenter final : public Presenter {
};
explicit VulkanPresenter(HostGpuLossCallback host_gpu_loss_callback,
const VulkanDevice* vulkan_device,
VulkanDevice* vulkan_device,
const UISamplers* ui_samplers)
: Presenter(host_gpu_loss_callback),
vulkan_device_(vulkan_device),
ui_samplers_(ui_samplers),
guest_output_image_refresher_submission_tracker_(vulkan_device),
ui_submission_tracker_(vulkan_device),
guest_output_image_refresher_completion_timeline_(vulkan_device),
ui_completion_timeline_(vulkan_device),
paint_context_(vulkan_device) {
assert_not_null(vulkan_device);
assert_not_null(ui_samplers);
@@ -462,7 +462,7 @@ class VulkanPresenter final : public Presenter {
[[nodiscard]] VkPipeline CreateGuestOutputPaintPipeline(
GuestOutputPaintEffect effect, VkRenderPass render_pass);
const VulkanDevice* vulkan_device_;
VulkanDevice* vulkan_device_;
const UISamplers* ui_samplers_;
// Static objects for guest output presentation, used only when painting the
@@ -489,11 +489,11 @@ class VulkanPresenter final : public Presenter {
uint64_t guest_output_image_next_version_ = 0;
std::array<GuestOutputImageInstance, kGuestOutputMailboxSize>
guest_output_images_;
VulkanSubmissionTracker guest_output_image_refresher_submission_tracker_;
VulkanGPUCompletionTimeline guest_output_image_refresher_completion_timeline_;
// UI submission tracker with the submission index that can be given to UI
// drawers (accessible from the UI thread only, at any time).
VulkanSubmissionTracker ui_submission_tracker_;
// UI submission completion timeline with the submission index that can be
// given to UI drawers (accessible from the UI thread only, at any time).
VulkanGPUCompletionTimeline ui_completion_timeline_;
// Accessible only by painting and by surface connection lifetime management
// (ConnectOrReconnectPaintingToSurfaceFromUIThread,
@@ -1,174 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/ui/vulkan/vulkan_submission_tracker.h"
#include <cstdint>
#include "xenia/base/assert.h"
#include "xenia/ui/vulkan/vulkan_util.h"
namespace xe {
namespace ui {
namespace vulkan {
VulkanSubmissionTracker::FenceAcquisition::~FenceAcquisition() {
if (!submission_tracker_) {
// Dropped submission or left after std::move.
return;
}
assert_true(submission_tracker_->fence_acquired_ == fence_);
if (fence_ != VK_NULL_HANDLE) {
if (signal_failed_) {
// Left in the unsignaled state.
submission_tracker_->fences_reclaimed_.push_back(fence_);
} else {
// Left in the pending state.
submission_tracker_->fences_pending_.emplace_back(
submission_tracker_->submission_current_, fence_);
}
submission_tracker_->fence_acquired_ = VK_NULL_HANDLE;
}
++submission_tracker_->submission_current_;
}
void VulkanSubmissionTracker::Shutdown() {
AwaitAllSubmissionsCompletion();
const VulkanDevice::Functions& dfn = vulkan_device_->functions();
const VkDevice device = vulkan_device_->device();
for (VkFence fence : fences_reclaimed_) {
dfn.vkDestroyFence(device, fence, nullptr);
}
fences_reclaimed_.clear();
for (const std::pair<uint64_t, VkFence>& fence_pair : fences_pending_) {
dfn.vkDestroyFence(device, fence_pair.second, nullptr);
}
fences_pending_.clear();
assert_true(fence_acquired_ == VK_NULL_HANDLE);
util::DestroyAndNullHandle(dfn.vkDestroyFence, device, fence_acquired_);
}
void VulkanSubmissionTracker::FenceAcquisition::SubmissionFailedOrDropped() {
if (!submission_tracker_) {
return;
}
assert_true(submission_tracker_->fence_acquired_ == fence_);
if (fence_ != VK_NULL_HANDLE) {
submission_tracker_->fences_reclaimed_.push_back(fence_);
}
submission_tracker_->fence_acquired_ = VK_NULL_HANDLE;
fence_ = VK_NULL_HANDLE;
// No submission acquisition from now on, don't increment the current
// submission index as well.
submission_tracker_ = VK_NULL_HANDLE;
}
uint64_t VulkanSubmissionTracker::UpdateAndGetCompletedSubmission() {
if (!fences_pending_.empty()) {
const VulkanDevice::Functions& dfn = vulkan_device_->functions();
const VkDevice device = vulkan_device_->device();
while (!fences_pending_.empty()) {
const std::pair<uint64_t, VkFence>& pending_pair =
fences_pending_.front();
assert_true(pending_pair.first > submission_completed_on_gpu_);
if (dfn.vkGetFenceStatus(device, pending_pair.second) != VK_SUCCESS) {
break;
}
fences_reclaimed_.push_back(pending_pair.second);
submission_completed_on_gpu_ = pending_pair.first;
fences_pending_.pop_front();
}
}
return submission_completed_on_gpu_;
}
bool VulkanSubmissionTracker::AwaitSubmissionCompletion(
uint64_t submission_index) {
// The tracker itself can't give a submission index for a submission that
// hasn't even started being recorded yet, the client has provided a
// completely invalid value or has done overly optimistic math if such an
// index has been obtained somehow.
assert_true(submission_index <= submission_current_);
// Waiting for the current submission is fine if there was a failure or a
// refusal to submit, and the submission index wasn't incremented, but still
// need to release objects referenced in the dropped submission (while
// shutting down, for instance - in this case, waiting for the last successful
// submission, which could have also referenced the objects from the new
// submission - we can't know since the client has already overwritten its
// last usage index, would correctly ensure that GPU usage of the objects is
// not pending). Waiting for successful submissions, but failed signals, will
// result in a true race condition, however, but waiting for the closest
// successful signal is the best approximation - also retrying to signal in
// this case.
// Go from the most recent to wait only for one fence, which includes all the
// preceding ones.
// "Fence signal operations that are defined by vkQueueSubmit additionally
// include in the first synchronization scope all commands that occur earlier
// in submission order."
size_t reclaim_end = fences_pending_.size();
if (reclaim_end) {
const VulkanDevice::Functions& dfn = vulkan_device_->functions();
const VkDevice device = vulkan_device_->device();
while (reclaim_end) {
const std::pair<uint64_t, VkFence>& pending_pair =
fences_pending_[reclaim_end - 1];
assert_true(pending_pair.first > submission_completed_on_gpu_);
if (pending_pair.first <= submission_index) {
// Wait if requested.
if (dfn.vkWaitForFences(device, 1, &pending_pair.second, VK_TRUE,
UINT64_MAX) == VK_SUCCESS) {
break;
}
}
// Just refresh the completed submission.
if (dfn.vkGetFenceStatus(device, pending_pair.second) == VK_SUCCESS) {
break;
}
--reclaim_end;
}
if (reclaim_end) {
submission_completed_on_gpu_ = fences_pending_[reclaim_end - 1].first;
for (; reclaim_end; --reclaim_end) {
fences_reclaimed_.push_back(fences_pending_.front().second);
fences_pending_.pop_front();
}
}
}
return submission_completed_on_gpu_ == submission_index;
}
VulkanSubmissionTracker::FenceAcquisition
VulkanSubmissionTracker::AcquireFenceToAdvanceSubmission() {
assert_true(fence_acquired_ == VK_NULL_HANDLE);
// Reclaim fences if the client only gets the completed submission index or
// awaits in special cases such as shutdown.
UpdateAndGetCompletedSubmission();
const VulkanDevice::Functions& dfn = vulkan_device_->functions();
const VkDevice device = vulkan_device_->device();
if (!fences_reclaimed_.empty()) {
VkFence reclaimed_fence = fences_reclaimed_.back();
if (dfn.vkResetFences(device, 1, &reclaimed_fence) == VK_SUCCESS) {
fence_acquired_ = fences_reclaimed_.back();
fences_reclaimed_.pop_back();
}
}
if (fence_acquired_ == VK_NULL_HANDLE) {
VkFenceCreateInfo fence_create_info;
fence_create_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fence_create_info.pNext = nullptr;
fence_create_info.flags = 0;
// May fail, a null fence is handled in FenceAcquisition.
dfn.vkCreateFence(device, &fence_create_info, nullptr, &fence_acquired_);
}
return FenceAcquisition(*this, fence_acquired_);
}
} // namespace vulkan
} // namespace ui
} // namespace xe
@@ -1,141 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_UI_VULKAN_VULKAN_SUBMISSION_TRACKER_H_
#define XENIA_UI_VULKAN_VULKAN_SUBMISSION_TRACKER_H_
#include <cstdint>
#include <deque>
#include <utility>
#include <vector>
#include "xenia/base/assert.h"
#include "xenia/ui/vulkan/vulkan_device.h"
namespace xe {
namespace ui {
namespace vulkan {
// Fence wrapper, safely handling cases when the fence has not been initialized
// yet or has already been shut down, and failed or dropped submissions.
//
// The current submission index can be associated with the usage of objects to
// release them when the GPU isn't potentially referencing them anymore, and
// should be incremented only
//
// 0 can be used as a "never referenced" submission index.
//
// The submission index timeline survives Shutdown, so submission indices can be
// given to clients that are not aware of the lifetime of the tracker.
//
// To transfer the tracker to another queue (to make sure the first signal on
// the new queue does not happen before the last signal on the old one), call
// AwaitAllSubmissionsCompletion before doing the first submission on the new
// one.
class VulkanSubmissionTracker {
public:
class FenceAcquisition {
public:
FenceAcquisition() : submission_tracker_(nullptr), fence_(VK_NULL_HANDLE) {}
FenceAcquisition(VulkanSubmissionTracker& submission_tracker, VkFence fence)
: submission_tracker_(&submission_tracker), fence_(fence) {}
FenceAcquisition(const FenceAcquisition& fence_acquisition) = delete;
FenceAcquisition& operator=(const FenceAcquisition& fence_acquisition) =
delete;
FenceAcquisition(FenceAcquisition&& fence_acquisition) {
*this = std::move(fence_acquisition);
}
FenceAcquisition& operator=(FenceAcquisition&& fence_acquisition) {
if (this == &fence_acquisition) {
return *this;
}
submission_tracker_ = fence_acquisition.submission_tracker_;
fence_acquisition.submission_tracker_ = nullptr;
fence_ = fence_acquisition.fence_;
fence_acquisition.fence_ = VK_NULL_HANDLE;
return *this;
}
~FenceAcquisition();
// In unsignaled state. May be null if failed to create or to reset a fence.
VkFence fence() { return fence_; }
// Call if vkQueueSubmit has failed (or it was decided not to commit the
// submission), and the submission index shouldn't be incremented by
// releasing this submission (for instance, to retry commands with long-term
// effects like copying or image layout changes later if in the attempt the
// submission index stays the same).
void SubmissionFailedOrDropped();
// Call if for some reason (like signaling multiple fences) the fence
// signaling was done in a separate submission than the command buffer, and
// the command buffer vkQueueSubmit succeeded (so commands with long-term
// effects will be executed), but the fence-only vkQueueSubmit has failed,
// thus the tracker shouldn't attempt to wait for that fence (it will be in
// the unsignaled state).
void SubmissionSucceededSignalFailed() { signal_failed_ = true; }
private:
// If nullptr, has been moved to another FenceAcquisition - not holding a
// fence from now on.
VulkanSubmissionTracker* submission_tracker_;
VkFence fence_;
bool signal_failed_ = false;
};
VulkanSubmissionTracker(const VulkanDevice* vulkan_device)
: vulkan_device_(vulkan_device) {
assert_not_null(vulkan_device);
}
VulkanSubmissionTracker(const VulkanSubmissionTracker& submission_tracker) =
delete;
VulkanSubmissionTracker& operator=(
const VulkanSubmissionTracker& submission_tracker) = delete;
~VulkanSubmissionTracker() { Shutdown(); }
void Shutdown();
uint64_t GetCurrentSubmission() const { return submission_current_; }
uint64_t UpdateAndGetCompletedSubmission();
// Returns whether the expected GPU signal has actually been reached (rather
// than some fallback condition) for cases when stronger completeness
// guarantees as needed (when downloading, as opposed to just destroying).
// If false is returned, it's also not guaranteed that GetCompletedSubmission
// will return a value >= submission_index.
bool AwaitSubmissionCompletion(uint64_t submission_index);
bool AwaitAllSubmissionsCompletion() {
return AwaitSubmissionCompletion(submission_current_ - 1);
}
[[nodiscard]] FenceAcquisition AcquireFenceToAdvanceSubmission();
private:
const VulkanDevice* vulkan_device_;
uint64_t submission_current_ = 1;
// Last submission with a successful fence signal as well as a successful
// fence wait / query.
uint64_t submission_completed_on_gpu_ = 0;
// The flow is:
// Reclaimed (or create if empty) > acquired > pending > reclaimed.
// Or, if dropped the submission while acquired:
// Reclaimed (or create if empty) > acquired > reclaimed.
VkFence fence_acquired_ = VK_NULL_HANDLE;
// Ordered by the submission index (the first pair member).
std::deque<std::pair<uint64_t, VkFence>> fences_pending_;
// Fences are reclaimed when awaiting or when refreshing the completed value.
std::vector<VkFence> fences_reclaimed_;
};
} // namespace vulkan
} // namespace ui
} // namespace xe
#endif // XENIA_UI_VULKAN_VULKAN_SUBMISSION_TRACKER_H_