DVDThread: Use queues

This makes it possible to submit a read without waiting for
the previous read to finish.
This commit is contained in:
JosJuice 2016-08-12 23:49:32 +02:00
parent a2f352c422
commit aabb17b7c9
1 changed files with 108 additions and 36 deletions

View File

@ -3,6 +3,7 @@
// Refer to the license.txt file included.
#include <cinttypes>
#include <map>
#include <mutex>
#include <thread>
#include <utility>
@ -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<ReadRequest, std::vector<u8>>;
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<ReadRequest, false> s_request_queue;
static Common::FifoQueue<ReadResult, false> s_result_queue;
static std::map<u64, ReadResult> 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<u8>& 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<u8> 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<u8> 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;
}
}
}
}