// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include #include #include #include #include #include #include "common/alignment.h" #include "common/common_types.h" #include "common/polyfill_thread.h" #include "common/settings.h" #include "video_core/gpu_split.h" #include "video_core/renderer_vulkan/vk_master_semaphore.h" #include "video_core/vulkan_common/vulkan_wrapper.h" namespace VideoCommon { template class QueryCacheBase; } namespace Vulkan { class CommandPool; class Device; class Framebuffer; class GraphicsPipeline; class StateTracker; struct QueryCacheParams; /// The scheduler abstracts command buffer and fence management with an interface that's able to do /// OpenGL-like operations on Vulkan command buffers. class Scheduler { public: explicit Scheduler(const Device& device, StateTracker& state_tracker); ~Scheduler(); /// Sends the current execution context to the GPU. u64 Flush(VkSemaphore signal_semaphore = {}, VkSemaphore wait_semaphore = {}); /// Sends the current execution context to the GPU and waits for it to complete. void Finish(VkSemaphore signal_semaphore = {}, VkSemaphore wait_semaphore = {}); /// Waits for the worker thread to finish executing everything. After this function returns it's /// safe to touch worker resources. void WaitWorker(); /// Sends currently recorded work to the worker thread. void DispatchWork(); /// Requests to begin a renderpass. void RequestRenderpass(const Framebuffer* framebuffer); /// Defers a full-attachment color clear so it becomes the next render pass. bool DeferColorClear(const Framebuffer* framebuffer, u32 rt_slot, const VkClearValue& value); /// Defers a full depth/stencil clear so it becomes the next render pass. bool DeferDepthStencilClear(const Framebuffer* framebuffer, const VkClearValue& value); /// Realizes any pending deferred clear before its framebuffer can be moved or freed. void FlushDeferredClear(); /// Requests the current execution context to be able to execute operations only allowed outside /// of a renderpass. void RequestOutsideRenderPassOperationContext(); /// Returns true when a render pass is currently active in the scheduler state. bool IsRenderPassActive() const { return state.renderpass != VK_NULL_HANDLE; } /// Update the pipeline to the current execution context. bool UpdateGraphicsPipeline(GraphicsPipeline* pipeline); /// Update the rescaling state. Returns true if the state has to be updated. bool UpdateRescaling(bool is_rescaling); /// Returns true when the descriptor buffer chunk has to be bound into the command buffer. bool UpdateDescriptorBufferChunk(u32 descriptor_chunk); /// Invalidates current command buffer state except for render passes void InvalidateState(); /// Assigns the query cache. void SetQueryCache(VideoCommon::QueryCacheBase& query_cache_) { query_cache = &query_cache_; } // Registers a callback to perform on queue submission. void RegisterOnSubmit(std::function&& func) { on_submit = std::move(func); } /// Send work to a separate thread. template requires std::is_invocable_v void RecordWithUploadBuffer(T&& command) { if (VideoCommon::GpuSplit::check_owner) [[unlikely]] { VideoCommon::GpuSplit::CheckOwner("Scheduler::Record"); } if (chunk->Record(command)) { return; } DispatchWork(); (void)chunk->Record(command); } template requires std::is_invocable_v void Record(T&& c) { this->RecordWithUploadBuffer( [command = std::move(c)](vk::CommandBuffer cmdbuf, vk::CommandBuffer) { command(cmdbuf); }); } /// Whether the recording thread submitted `tick` within 20 ms. [[nodiscard]] bool WaitForSubmission(u64 tick) const { const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds{20}; while (tick >= master_semaphore->CurrentTick()) { if (std::chrono::steady_clock::now() >= deadline) { return false; } std::this_thread::sleep_for(std::chrono::microseconds{100}); } return true; } /// Returns the current command buffer tick. [[nodiscard]] u64 CurrentTick() const noexcept { return master_semaphore->CurrentTick(); } /// Returns true when a tick has been triggered by the GPU. [[nodiscard]] bool IsFree(u64 tick) const noexcept { return master_semaphore->IsFree(tick); } /// Waits for the given GPU tick, optionally pacing frames. Threads other than the one that /// records (the async present thread) pass may_flush = false: flushing from there records /// into the current chunk while that thread does (corrupt command buffers, crashes). They /// wait for the recording thread to submit the tick instead, and give up after 20 ms (it may /// be waiting for them). void Wait(u64 tick, double target_fps = 0.0, bool may_flush = true) { if (tick > 0 && (may_flush || WaitForSubmission(tick))) { if (tick >= master_semaphore->CurrentTick()) { Flush(); } master_semaphore->Wait(tick); } if (Settings::values.use_speed_limit.GetValue() && target_fps > 0.0) { auto now = std::chrono::steady_clock::now(); if (last_target_fps != target_fps) { frame_interval = std::chrono::duration_cast(std::chrono::duration(1.0 / target_fps)); max_frame_count = static_cast(0.1 * target_fps); last_target_fps = target_fps; frame_counter = 0; start_time = now; } frame_counter++; auto target_time = start_time + frame_interval * frame_counter; if (target_time >= now) { constexpr auto spin_tail = std::chrono::milliseconds(1); auto sleep_time = target_time - now; if (sleep_time > spin_tail * 2) { std::this_thread::sleep_for(sleep_time - spin_tail); } while (std::chrono::steady_clock::now() < target_time) { std::this_thread::yield(); } } else if (frame_counter > max_frame_count) { frame_counter = 0; start_time = now; } } } /// Returns the master timeline semaphore. [[nodiscard]] MasterSemaphore& GetMasterSemaphore() const noexcept { return *master_semaphore; } std::mutex submit_mutex; private: class Command { public: virtual ~Command() = default; virtual void Execute(vk::CommandBuffer cmdbuf, vk::CommandBuffer upload_cmdbuf) const = 0; Command* GetNext() const { return next; } void SetNext(Command* next_) { next = next_; } private: Command* next = nullptr; }; template class TypedCommand final : public Command { public: explicit TypedCommand(T&& command_) : command{std::move(command_)} {} ~TypedCommand() override = default; TypedCommand(TypedCommand&&) = delete; TypedCommand& operator=(TypedCommand&&) = delete; void Execute(vk::CommandBuffer cmdbuf, vk::CommandBuffer upload_cmdbuf) const override { command(cmdbuf, upload_cmdbuf); } private: T command; }; class CommandChunk final { public: void ExecuteAll(vk::CommandBuffer cmdbuf, vk::CommandBuffer upload_cmdbuf); template bool Record(T& command) { using FuncType = TypedCommand; static_assert(sizeof(FuncType) < sizeof(data), "Lambda is too large"); command_offset = Common::AlignUp(command_offset, alignof(FuncType)); if (command_offset > sizeof(data) - sizeof(FuncType)) { return false; } Command* const current_last = last; last = new (data.data() + command_offset) FuncType(std::move(command)); if (current_last) { current_last->SetNext(last); } else { first = last; } command_offset += sizeof(FuncType); return true; } void MarkSubmit() { submit = true; } bool Empty() const { return command_offset == 0; } bool HasSubmit() const { return submit; } private: Command* first = nullptr; Command* last = nullptr; size_t command_offset = 0; bool submit = false; alignas(std::max_align_t) std::array data{}; }; struct State { VkRenderPass renderpass{}; VkFramebuffer framebuffer{}; VkExtent2D render_area = {0, 0}; GraphicsPipeline* graphics_pipeline = nullptr; bool is_rescaling = false; bool rescaling_defined = false; bool needs_state_enable_refresh = false; u32 descriptor_buffer_chunk = 0; bool descriptor_buffer_bound = false; }; struct DeferredClear { const Framebuffer* framebuffer = nullptr; u32 color_clear_mask = 0; std::array color_values{}; bool depth_stencil = false; VkClearValue depth_stencil_value{}; }; /// Begins a render pass for the given framebuffer, optionally with clear values. void BeginRenderPassImpl(const Framebuffer* framebuffer, VkRenderPass renderpass, const VkClearValue* clear_values, u32 clear_value_count); /// If a deferred clear is pending. void RealizeDeferredClear(); void WorkerThread(std::stop_token stop_token); void AllocateWorkerCommandBuffer(); u64 SubmitExecution(VkSemaphore signal_semaphore, VkSemaphore wait_semaphore); void AllocateNewContext(); void EndPendingOperations(); void EndRenderPass(); void AcquireNewChunk(); const Device& device; StateTracker& state_tracker; std::unique_ptr master_semaphore; std::unique_ptr command_pool; VideoCommon::QueryCacheBase* query_cache = nullptr; vk::CommandBuffer current_cmdbuf; vk::CommandBuffer current_upload_cmdbuf; DeferredClear deferred_clear; std::unique_ptr chunk; std::function on_submit; State state; u32 num_renderpass_images = 0; std::array renderpass_images{}; std::array renderpass_image_ranges{}; std::queue> work_queue; std::vector> chunk_reserve; std::mutex execution_mutex; std::mutex reserve_mutex; std::mutex queue_mutex; std::condition_variable_any event_cv; bool worker_waiting{}; ///< The worker is waiting for work (guarded by queue_mutex) std::jthread worker_thread; std::chrono::steady_clock::duration frame_interval{}; std::chrono::steady_clock::time_point start_time{}; double last_target_fps{}; u64 max_frame_count{}; u64 frame_counter{}; }; } // namespace Vulkan