From aabb17b7c949b3e66e9c9267ea1cf3773b3e4a14 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Fri, 12 Aug 2016 23:49:32 +0200 Subject: [PATCH] DVDThread: Use queues This makes it possible to submit a read without waiting for the previous read to finish. --- Source/Core/Core/HW/DVDThread.cpp | 144 ++++++++++++++++++++++-------- 1 file changed, 108 insertions(+), 36 deletions(-) diff --git a/Source/Core/Core/HW/DVDThread.cpp b/Source/Core/Core/HW/DVDThread.cpp index 491a1fea45..3a84cdc3c5 100644 --- a/Source/Core/Core/HW/DVDThread.cpp +++ b/Source/Core/Core/HW/DVDThread.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include #include @@ -11,6 +12,7 @@ #include "Common/ChunkFile.h" #include "Common/CommonTypes.h" #include "Common/Event.h" +#include "Common/FifoQueue.h" #include "Common/Flag.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" @@ -39,6 +41,11 @@ struct ReadRequest // We can't have a function pointer here, because they can't be in savestates. bool reply_to_ios; + // IDs are used to uniquely identify a request. They must not be + // identical to IDs of any other requests that currently exist, but + // it's fine to re-use IDs of requests that have existed in the past. + u64 id; + // Only used for logging u64 time_started_ticks; u64 realtime_started_us; @@ -49,20 +56,35 @@ using ReadResult = std::pair>; static void DVDThread(); -static void FinishRead(u64 userdata, s64 cycles_late); +static void FinishRead(u64 id, s64 cycles_late); static CoreTiming::EventType* s_finish_read; -static std::thread s_dvd_thread; -static Common::Event s_dvd_thread_start_working; -static Common::Event s_dvd_thread_done_working; -static Common::Flag s_dvd_thread_exiting(false); +static u64 s_next_id = 0; -static ReadRequest s_read_request; -static ReadResult s_read_result; +static std::thread s_dvd_thread; +static Common::Event s_request_queue_expanded; // Is set by CPU thread +static Common::Event s_result_queue_expanded; // Is set by DVD thread +static Common::Event s_dvd_thread_done_working; // Is set by DVD thread +static Common::Flag s_dvd_thread_exiting(false); // Is set by CPU thread + +static Common::FifoQueue s_request_queue; +static Common::FifoQueue s_result_queue; +static std::map s_result_map; void Start() { s_finish_read = CoreTiming::RegisterEvent("FinishReadDVDThread", FinishRead); + + s_dvd_thread_exiting.Clear(); + s_request_queue_expanded.Reset(); + s_result_queue_expanded.Reset(); + s_request_queue.Clear(); + s_result_queue.Clear(); + + // This is reset on every launch for determinism, but it doesn't matter + // much, because this will never get exposed to the emulated game. + s_next_id = 0; + _assert_(!s_dvd_thread.joinable()); s_dvd_thread = std::thread(DVDThread); } @@ -71,26 +93,37 @@ void Stop() { _assert_(s_dvd_thread.joinable()); - // The DVD thread will return if s_DVD_thread_exiting - // is set when it starts working + // By setting s_DVD_thread_exiting, we ask the DVD thread to cleanly exit. + // In case the request queue is empty, we need to set s_request_queue_expanded + // so that the DVD thread will wake up and check s_DVD_thread_exiting. s_dvd_thread_exiting.Set(); - s_dvd_thread_start_working.Set(); + s_request_queue_expanded.Set(); s_dvd_thread.join(); - - s_dvd_thread_exiting.Clear(); } void DoState(PointerWrap& p) { - // By waiting for the DVD thread to be done working, we ensure - // that the value of s_read_request won't be used again. - // We thus don't need to save or load s_read_request here. + // By waiting for the DVD thread to be done working, we ensure that + // there are no pending requests. The DVD thread won't be touching + // s_result_queue, and everything we need to save will be in either + // s_result_queue or s_result_map (other than s_next_id). WaitUntilIdle(); - // TODO: Savestates can be smaller if s_read_result.buffer isn't saved, - // but instead gets re-read from the disc when loading the savestate. - p.Do(s_read_result); + // Move everything from s_result_queue to s_result_map because + // PointerWrap::Do supports std::map but not Common::FifoQueue. + // This won't affect the behavior of FinishRead. + ReadResult result; + while (s_result_queue.Pop(result)) + s_result_map.emplace(result.first.id, std::move(result)); + + // Everything is now in s_result_map, so we simply savestate that. + // We also savestate s_next_id to avoid ID collisions. + p.Do(s_result_map); + p.Do(s_next_id); + + // TODO: Savestates can be smaller if the buffers of results aren't saved, + // but instead get re-read from the disc when loading the savestate. // After loading a savestate, the debug log in FinishRead will report // screwed up times for requests that were submitted before the savestate @@ -104,7 +137,9 @@ void WaitUntilIdle() // Wait until DVD thread isn't working s_dvd_thread_done_working.Wait(); - // Set the event again so that we still know that the DVD thread isn't working + // Set the event again so that we still know that the DVD thread + // isn't working. This is needed in case this function gets called + // again before the DVD thread starts working. s_dvd_thread_done_working.Set(); } @@ -113,8 +148,6 @@ void StartRead(u64 dvd_offset, u32 output_address, u32 length, bool decrypt, boo { _assert_(Core::IsCPUThread()); - s_dvd_thread_done_working.Wait(); - ReadRequest request; request.dvd_offset = dvd_offset; @@ -123,21 +156,52 @@ void StartRead(u64 dvd_offset, u32 output_address, u32 length, bool decrypt, boo request.decrypt = decrypt; request.reply_to_ios = reply_to_ios; + u64 id = s_next_id++; + request.id = id; + request.time_started_ticks = CoreTiming::GetTicks(); request.realtime_started_us = Common::Timer::GetTimeUs(); - s_read_request = std::move(request); + s_request_queue.Push(std::move(request)); + s_request_queue_expanded.Set(); - s_dvd_thread_start_working.Set(); - - CoreTiming::ScheduleEvent(ticks_until_completion, s_finish_read); + CoreTiming::ScheduleEvent(ticks_until_completion, s_finish_read, id); } -static void FinishRead(u64 userdata, s64 cycles_late) +static void FinishRead(u64 id, s64 cycles_late) { - WaitUntilIdle(); + // We can't simply pop s_result_queue and always get the ReadResult + // we want, because the DVD thread may add ReadResults to the queue + // in a different order than we want to get them. What we do instead + // is to pop the queue until we find the ReadResult we want (the one + // whose ID matches userdata), which means we may end up popping + // ReadResults that we don't want. We can't add those unwanted results + // back to the queue, because the queue can only have one writer. + // Instead, we add them to a map that only is used by the CPU thread. + // When this function is called again later, it will check the map for + // the wanted ReadResult before it starts searching through the queue. + ReadResult result; + auto it = s_result_map.find(id); + if (it != s_result_map.end()) + { + result = std::move(it->second); + s_result_map.erase(it); + } + else + { + while (true) + { + while (!s_result_queue.Pop(result)) + s_result_queue_expanded.Wait(); + + if (result.first.id == id) + break; + else + s_result_map.emplace(result.first.id, std::move(result)); + } + } + // We have now obtained the right ReadResult. - const ReadResult result = std::move(s_read_result); const ReadRequest& request = result.first; const std::vector& buffer = result.second; @@ -167,21 +231,29 @@ static void DVDThread() { s_dvd_thread_done_working.Set(); - s_dvd_thread_start_working.Wait(); + s_request_queue_expanded.Wait(); if (s_dvd_thread_exiting.IsSet()) return; - ReadRequest request = std::move(s_read_request); + s_dvd_thread_done_working.Reset(); - std::vector buffer(request.length); - const DiscIO::IVolume& volume = DVDInterface::GetVolume(); - if (!volume.Read(request.dvd_offset, request.length, buffer.data(), request.decrypt)) - buffer.resize(0); + ReadRequest request; + while (s_request_queue.Pop(request)) + { + std::vector buffer(request.length); + const DiscIO::IVolume& volume = DVDInterface::GetVolume(); + if (!volume.Read(request.dvd_offset, request.length, buffer.data(), request.decrypt)) + buffer.resize(0); - request.realtime_done_us = Common::Timer::GetTimeUs(); + request.realtime_done_us = Common::Timer::GetTimeUs(); - s_read_result = ReadResult(std::move(request), std::move(buffer)); + s_result_queue.Push(ReadResult(std::move(request), std::move(buffer))); + s_result_queue_expanded.Set(); + + if (s_dvd_thread_exiting.IsSet()) + return; + } } } }