nx2: video decoder sessions; present waits on an event; present timeline
- gsv 901/1001 succeed and 902/1002 return sessions; their TIPC 1 (CreateVideoDecoderService) returns another, auto-stubbed. Title 1's movie player no longer aborts with 2001-0114 (no frames are decoded). - The present's wait for its fence is woken by the GPU thread through a syncpoint action instead of 200 us sleeps (~1 ms each on Windows). - Every 600 presents, log the average frame time and how much of it the present thread spent blocked on the GPU fence, compositing and pacing.
This commit is contained in:
parent
1522ce5e43
commit
65a52cffa5
1 changed files with 88 additions and 3 deletions
|
|
@ -103,6 +103,26 @@ public:
|
|||
explicit INx2NvsrvSession(Core::System& system_) : ServiceFramework{system_, "nx2:nvsrv"} {}
|
||||
};
|
||||
|
||||
// Placeholder for the Switch 2 video decoder (nn::codec::video::detail::ipc), handed out by gsv
|
||||
// 902 and 1002 (two sessions; `name` tells them apart in logs). TIPC 1 is
|
||||
// CreateVideoDecoderService (ipc::CreateVideoDecoderService): handle at word 3, result at word
|
||||
// 4, a session the decoders then talk to. Everything else is auto-stubbed through the nx2: prefix.
|
||||
class INx2VideoSession final : public ServiceFramework<INx2VideoSession> {
|
||||
public:
|
||||
explicit INx2VideoSession(Core::System& system_, const char* name)
|
||||
: ServiceFramework{system_, name} {
|
||||
RegisterHandlersTipc({
|
||||
{1, &INx2VideoSession::CreateDecoderService, "CreateVideoDecoderService"},
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
void CreateDecoderService(HLERequestContext& ctx) {
|
||||
LOG_WARNING(Service, "(STUBBED) {} TIPC 1, returning a video decoder service session", GetServiceName());
|
||||
ReturnRawSession(system, ctx, std::make_shared<INx2VideoSession>(system, "nx2:vdecsvc"));
|
||||
}
|
||||
};
|
||||
|
||||
// Placeholder for the Switch 2 syncpoint server (nn::syncpt), handed out by gsv 202. TIPC 1
|
||||
// returns a u32 the SDK requires to be < 0x400 and then passes min(value, 63) to TIPC 21.
|
||||
// Everything else is auto-stubbed through the nx2: prefix.
|
||||
|
|
@ -672,6 +692,7 @@ private:
|
|||
// game prepares frame N+1 while the GPU thread runs frame N (the console keeps several
|
||||
// frames in flight). Experimental.
|
||||
static const bool frames_ahead = std::getenv("EDEN_NX2_FRAMES_AHEAD") != nullptr;
|
||||
const auto entered = std::chrono::steady_clock::now();
|
||||
if (frames_ahead) {
|
||||
WaitForFrame(m_previous_fence_id, m_previous_fence_value);
|
||||
m_previous_fence_id = ~w[22];
|
||||
|
|
@ -679,8 +700,33 @@ private:
|
|||
} else {
|
||||
WaitForFrame(~w[22], w[24]);
|
||||
}
|
||||
const auto waited = std::chrono::steady_clock::now();
|
||||
Composite(w[5], ~w[22], w[24]);
|
||||
const auto composited = std::chrono::steady_clock::now();
|
||||
PaceToVblank();
|
||||
const auto paced = std::chrono::steady_clock::now();
|
||||
// Where each frame's time goes on the present thread, averaged per 600 presents: the
|
||||
// game is blocked here while Eden's GPU thread catches up (fence) and composites.
|
||||
{
|
||||
using us = std::chrono::microseconds;
|
||||
const auto to_us = [](auto d) { return std::chrono::duration_cast<us>(d).count(); };
|
||||
m_stats.fence_us += to_us(waited - entered);
|
||||
m_stats.composite_us += to_us(composited - waited);
|
||||
m_stats.pace_us += to_us(paced - composited);
|
||||
m_stats.max_fence_us = (std::max)(m_stats.max_fence_us, to_us(waited - entered));
|
||||
if (m_stats.last_present != std::chrono::steady_clock::time_point{}) {
|
||||
m_stats.frame_us += to_us(entered - m_stats.last_present);
|
||||
}
|
||||
m_stats.last_present = entered;
|
||||
if (++m_stats.presents == 600) {
|
||||
LOG_INFO(Service,
|
||||
"nx2:vi2 last 600 presents: frame {} us, blocked on the GPU fence {} us "
|
||||
"(max {}), composite {} us, vblank pacing {} us",
|
||||
m_stats.frame_us / 599, m_stats.fence_us / 600, m_stats.max_fence_us,
|
||||
m_stats.composite_us / 600, m_stats.pace_us / 600);
|
||||
m_stats = {.last_present = entered};
|
||||
}
|
||||
}
|
||||
constexpr u64 AvailableSlots = 0x7fff;
|
||||
TipcReplyWords(ctx, {static_cast<u32>(AvailableSlots),
|
||||
static_cast<u32>(AvailableSlots >> 32), 0, 0, 0, 0});
|
||||
|
|
@ -692,6 +738,11 @@ private:
|
|||
// Eden's GPU thread has processed the frame. Otherwise the game laps the GPU thread and
|
||||
// rewrites command memory it hasn't read yet.
|
||||
u32 m_wait_timeouts{};
|
||||
struct PresentStats {
|
||||
long long frame_us{}, fence_us{}, composite_us{}, pace_us{}, max_fence_us{};
|
||||
u32 presents{};
|
||||
std::chrono::steady_clock::time_point last_present{};
|
||||
} m_stats{};
|
||||
u32 m_previous_fence_id{~0U}; // none yet (WaitForFrame ignores ids >= 192)
|
||||
u32 m_previous_fence_value{};
|
||||
|
||||
|
|
@ -724,9 +775,28 @@ private:
|
|||
// Long enough to cover shader/pipeline compile stalls: a game that gets more than a few
|
||||
// command buffers ahead misses the equal-value gates its other channels acquire.
|
||||
const auto deadline = start + std::chrono::milliseconds{4000};
|
||||
while (!syncpoints.IsReadyGuest(fence_id, fence_value) &&
|
||||
std::chrono::steady_clock::now() < deadline && system.IsPoweredOn()) {
|
||||
std::this_thread::sleep_for(std::chrono::microseconds{200});
|
||||
if (!syncpoints.IsReadyGuest(fence_id, fence_value)) {
|
||||
// Woken by the GPU thread as it reaches the fence. Polling with 200 us sleeps slept
|
||||
// about 1 ms per check on Windows, added to every frame. An action left registered
|
||||
// after a timeout fires later on a state nobody waits on.
|
||||
struct Wake {
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
bool done{};
|
||||
};
|
||||
auto wake = std::make_shared<Wake>();
|
||||
syncpoints.RegisterGuestAction(fence_id, fence_value, [wake] {
|
||||
{
|
||||
std::scoped_lock lk{wake->mutex};
|
||||
wake->done = true;
|
||||
}
|
||||
wake->cv.notify_all();
|
||||
});
|
||||
std::unique_lock lk{wake->mutex};
|
||||
while (!wake->done && system.IsPoweredOn() &&
|
||||
std::chrono::steady_clock::now() < deadline) {
|
||||
wake->cv.wait_for(lk, std::chrono::milliseconds{50});
|
||||
}
|
||||
}
|
||||
if (!syncpoints.IsReadyGuest(fence_id, fence_value) && m_wait_timeouts++ < 64) {
|
||||
LOG_WARNING(Service, "nx2:vi2 present #{} fence ({}, {}) not reached in 4 s (now {})",
|
||||
|
|
@ -964,6 +1034,10 @@ public:
|
|||
{1510, &INx2GraphicsSystemSession::CalculateRequirements, "CalculateRequirements"},
|
||||
{991, &INx2GraphicsSystemSession::CalculateDecoderImagePoolBuffer,
|
||||
"CalculateDecoderImagePoolBuffer"},
|
||||
{901, &INx2GraphicsSystemSession::Stub, "VideoInitialize (guessed)"},
|
||||
{902, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSession (guessed)"},
|
||||
{1001, &INx2GraphicsSystemSession::Stub, "VideoInitialize2 (guessed)"},
|
||||
{1002, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSession2 (guessed)"},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -997,6 +1071,17 @@ private:
|
|||
rb.Push(ResultSuccess);
|
||||
}
|
||||
|
||||
// nn::codec::video::detail::ipc::Initialize sends 901, 902, 1001, 1002. 902 and 1002 return
|
||||
// a session handle at word 3 and the result at word 4; left to the auto stub, the handle was 0
|
||||
// and InitializeVp9Decoder failed with 2001-0114 (title 1 aborts when a movie starts).
|
||||
void OpenVideoSession(HLERequestContext& ctx) {
|
||||
const bool second = ctx.GetCommand() == 1002;
|
||||
LOG_WARNING(Service, "(STUBBED) nx2:gsv TIPC {}, returning a video decoder session",
|
||||
ctx.GetCommand());
|
||||
ReturnRawSession(system, ctx,
|
||||
std::make_shared<INx2VideoSession>(system, second ? "nx2:vdec2" : "nx2:vdec"));
|
||||
}
|
||||
|
||||
void OpenSyncptSession(HLERequestContext& ctx) {
|
||||
// Used by nn::syncpt::sdk::InitializeEx; handle at word 3, result at word 4.
|
||||
LOG_WARNING(Service, "(STUBBED) nx2:gsv TIPC 202, returning a nx2:syncpt session");
|
||||
|
|
|
|||
Loading…
Reference in a new issue