diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.cc b/src/xenia/gpu/d3d12/d3d12_command_processor.cc index a2c48e3bd..b7800e1c8 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.cc +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.cc @@ -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_ diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.h b/src/xenia/gpu/d3d12/d3d12_command_processor.h index 0574d33bc..c220ad2a6 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.h +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.h @@ -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 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 + queue_operations_since_submission_completion_timeline_; bool queue_operations_done_since_submission_signal_ = false; bool frame_open_ = false; diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.cc b/src/xenia/gpu/vulkan/vulkan_command_processor.cc index ab102949c..53a1548ee 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.cc +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.cc @@ -69,6 +69,9 @@ const VkDescriptorPoolSize VulkanCommandProcessor::VulkanCommandProcessor( VulkanGraphicsSystem* graphics_system, kernel::KernelState* kernel_state) : CommandProcessor(graphics_system, kernel_state), + completion_timeline_(static_cast( + graphics_system->provider()) + ->vulkan_device()), deferred_command_buffer_(*this), transient_descriptor_allocator_uniform_buffer_( static_cast( @@ -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; } diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.h b/src/xenia/gpu/vulkan/vulkan_command_processor.h index cd872dc84..9bc8f313e 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.h +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.h @@ -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 fences_free_; std::vector 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 current_submission_wait_semaphores_; std::vector current_submission_wait_stage_masks_; - std::vector submissions_in_flight_fences_; std::deque> submissions_in_flight_semaphores_; diff --git a/src/xenia/ui/d3d12/d3d12_gpu_completion_timeline.cc b/src/xenia/ui/d3d12/d3d12_gpu_completion_timeline.cc new file mode 100644 index 000000000..b4c77d031 --- /dev/null +++ b/src/xenia/ui/d3d12/d3d12_gpu_completion_timeline.cc @@ -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 + +#include "xenia/base/logging.h" + +namespace xe { +namespace ui { +namespace d3d12 { + +std::unique_ptr D3D12GPUCompletionTimeline::Create( + ID3D12Device* const device) { + Microsoft::WRL::ComPtr 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( + 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 diff --git a/src/xenia/ui/d3d12/d3d12_gpu_completion_timeline.h b/src/xenia/ui/d3d12/d3d12_gpu_completion_timeline.h new file mode 100644 index 000000000..ad303330a --- /dev/null +++ b/src/xenia/ui/d3d12/d3d12_gpu_completion_timeline.h @@ -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 + +#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 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 fence_; +}; + +} // namespace d3d12 +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_D3D12_D3D12_GPU_COMPLETION_TIMELINE_H_ diff --git a/src/xenia/ui/d3d12/d3d12_presenter.cc b/src/xenia/ui/d3d12/d3d12_presenter.cc index 574b66d0d..337cbd922 100644 --- a/src/xenia/ui/d3d12/d3d12_presenter.cc +++ b/src/xenia/ui/d3d12/d3d12_presenter.cc @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -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 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>& + std::pair>& 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>& + const std::pair>& 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_t completed_paint_submission = + paint_context_.paint_completion_timeline + ->GetCompletedSubmissionFromLastUpdate(); + for (std::pair>& 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; } diff --git a/src/xenia/ui/d3d12/d3d12_presenter.h b/src/xenia/ui/d3d12/d3d12_presenter.h index 81d9c4ff5..a02a7e5b6 100644 --- a/src/xenia/ui/d3d12/d3d12_presenter.h +++ b/src/xenia/ui/d3d12/d3d12_presenter.h @@ -11,12 +11,13 @@ #define XENIA_UI_D3D12_D3D12_PRESENTER_H_ #include +#include #include #include #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 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 paint_completion_timeline; // Signaled after presenting. - D3D12SubmissionTracker present_submission_tracker; + std::unique_ptr present_completion_timeline; std::array, 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::array>, kGuestOutputMailboxSize> guest_output_resource_paint_refs; @@ -261,7 +272,7 @@ class D3D12Presenter final : public Presenter { std::array, 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>, + // 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>, 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 + 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 ui_completion_timeline_; // Accessible only by painting and by surface connection lifetime management // (ConnectOrReconnectPaintingToSurfaceFromUIThread, diff --git a/src/xenia/ui/d3d12/d3d12_submission_tracker.cc b/src/xenia/ui/d3d12/d3d12_submission_tracker.cc deleted file mode 100644 index d8f604f7f..000000000 --- a/src/xenia/ui/d3d12/d3d12_submission_tracker.cc +++ /dev/null @@ -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 diff --git a/src/xenia/ui/d3d12/d3d12_submission_tracker.h b/src/xenia/ui/d3d12/d3d12_submission_tracker.h deleted file mode 100644 index cf4e92836..000000000 --- a/src/xenia/ui/d3d12/d3d12_submission_tracker.h +++ /dev/null @@ -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 fence_; - Microsoft::WRL::ComPtr queue_; -}; - -} // namespace d3d12 -} // namespace ui -} // namespace xe - -#endif // XENIA_UI_D3D12_D3D12_SUBMISSION_TRACKER_H_ diff --git a/src/xenia/ui/gpu_completion_timeline.h b/src/xenia/ui/gpu_completion_timeline.h new file mode 100644 index 000000000..aa784bf34 --- /dev/null +++ b/src/xenia/ui/gpu_completion_timeline.h @@ -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 +#include + +#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_ diff --git a/src/xenia/ui/vulkan/vulkan_device.h b/src/xenia/ui/vulkan/vulkan_device.h index 255c75464..7c2ad22cd 100644 --- a/src/xenia/ui/vulkan/vulkan_device.h +++ b/src/xenia/ui/vulkan/vulkan_device.h @@ -10,6 +10,7 @@ #ifndef XENIA_UI_VULKAN_VULKAN_DEVICE_H_ #define XENIA_UI_VULKAN_VULKAN_DEVICE_H_ +#include #include #include #include @@ -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 lost_{false}; }; } // namespace vulkan diff --git a/src/xenia/ui/vulkan/vulkan_gpu_completion_timeline.cc b/src/xenia/ui/vulkan/vulkan_gpu_completion_timeline.cc new file mode 100644 index 000000000..a7d11c333 --- /dev/null +++ b/src/xenia/ui/vulkan/vulkan_gpu_completion_timeline.cc @@ -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 +#include + +#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::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 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 diff --git a/src/xenia/ui/vulkan/vulkan_gpu_completion_timeline.h b/src/xenia/ui/vulkan/vulkan_gpu_completion_timeline.h new file mode 100644 index 000000000..125545525 --- /dev/null +++ b/src/xenia/ui/vulkan/vulkan_gpu_completion_timeline.h @@ -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 +#include +#include +#include + +#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 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 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 free_fences_; + + // , in submission index order. + std::deque> 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_ diff --git a/src/xenia/ui/vulkan/vulkan_presenter.cc b/src/xenia/ui/vulkan/vulkan_presenter.cc index 8846937ca..d3385e09a 100644 --- a/src/xenia/ui/vulkan/vulkan_presenter.cc +++ b/src/xenia/ui/vulkan/vulkan_presenter.cc @@ -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>& 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))); } } diff --git a/src/xenia/ui/vulkan/vulkan_presenter.h b/src/xenia/ui/vulkan/vulkan_presenter.h index 5339b4551..2268a5a2a 100644 --- a/src/xenia/ui/vulkan/vulkan_presenter.h +++ b/src/xenia/ui/vulkan/vulkan_presenter.h @@ -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 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(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, kSubmissionCount> submissions; - VulkanSubmissionTracker submission_tracker; + VulkanGPUCompletionTimeline completion_timeline; std::array 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 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, diff --git a/src/xenia/ui/vulkan/vulkan_submission_tracker.cc b/src/xenia/ui/vulkan/vulkan_submission_tracker.cc deleted file mode 100644 index 8fe4e412a..000000000 --- a/src/xenia/ui/vulkan/vulkan_submission_tracker.cc +++ /dev/null @@ -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 - -#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& 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& 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& 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 diff --git a/src/xenia/ui/vulkan/vulkan_submission_tracker.h b/src/xenia/ui/vulkan/vulkan_submission_tracker.h deleted file mode 100644 index c44d438e8..000000000 --- a/src/xenia/ui/vulkan/vulkan_submission_tracker.h +++ /dev/null @@ -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 -#include -#include -#include - -#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> fences_pending_; - // Fences are reclaimed when awaiting or when refreshing the completed value. - std::vector fences_reclaimed_; -}; - -} // namespace vulkan -} // namespace ui -} // namespace xe - -#endif // XENIA_UI_VULKAN_VULKAN_SUBMISSION_TRACKER_H_