Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,7 @@ add_library(core STATIC
hle/service/nx2_streamplay.h
hle/service/nx2_stub_interface.h
hle/service/nx2_gsv.h
hle/service/nx2_present_queue.h
hle/service/services.cpp
hle/service/services.h
hle/service/set/factory_settings_server.cpp
Expand Down
16 changes: 14 additions & 2 deletions src/core/hle/service/nvdrv/core/syncpoint_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,31 @@ u32 SyncpointManager::ReserveSyncpoint(u32 id, bool client_managed) {
return id;
}

/// Finds an unreserved nonzero ID, returning zero if the pool is exhausted.
u32 SyncpointManager::FindFreeSyncpoint() {
for (u32 i{1}; i < syncpoints.size(); i++) {
if (!syncpoints[i].reserved) {
return i;
}
}
ASSERT_MSG(false, "Failed to find a free syncpoint!");
return 0;
}

/// Allocates a syncpoint, retaining the assertion and zero result for callers requiring one.
u32 SyncpointManager::AllocateSyncpoint(bool client_managed) {
const auto id = TryAllocateSyncpoint(client_managed);
ASSERT_MSG(id.has_value(), "Failed to find a free syncpoint!");
return id.value_or(0);
}

/// Reserves a nonzero ID atomically, or reports exhaustion without changing reservations.
std::optional<u32> SyncpointManager::TryAllocateSyncpoint(bool client_managed) {
std::lock_guard lock(reservation_lock);
return ReserveSyncpoint(FindFreeSyncpoint(), client_managed);
const u32 id = FindFreeSyncpoint();
if (id == 0) {
return std::nullopt;
}
return ReserveSyncpoint(id, client_managed);
}

void SyncpointManager::FreeSyncpoint(u32 id) {
Expand Down
4 changes: 4 additions & 0 deletions src/core/hle/service/nvdrv/core/syncpoint_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <array>
#include <atomic>
#include <mutex>
#include <optional>

#include "common/common_types.h"
#include "core/hle/service/nvdrv/nvdata.h"
Expand Down Expand Up @@ -52,6 +53,9 @@ class SyncpointManager final {
*/
u32 AllocateSyncpoint(bool client_managed);

/// Reserves a free syncpoint, or returns no value on exhaustion without reserving ID 0.
std::optional<u32> TryAllocateSyncpoint(bool client_managed);

/**
* @url
* https://github.com/Jetson-TX1-AndroidTV/android_kernel_jetson_tx1_hdmi_primary/blob/8f74a72394efb871cb3f886a3de2998cd7ff2990/drivers/gpu/host1x/syncpt.c#L259
Expand Down
113 changes: 99 additions & 14 deletions src/core/hle/service/nx2_gsv.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
#include "core/hle/service/nvdrv/devices/nx2_stub_device.h"
#include "core/hle/service/nvdrv/nvdrv.h"
#include "core/hle/service/nvdrv/nvdrv_interface.h"
#include "core/hle/service/nx2_present_queue.h"
#include "core/hle/service/nx2_stub_interface.h"
#include "core/hle/service/sm/sm.h"
#include "core/hle/service/server_manager.h"
Expand Down Expand Up @@ -1697,7 +1698,12 @@ class INx2Vi2Session final : public ServiceFramework<INx2Vi2Session> {
});
}

/// Cancels display callbacks before releasing their syncpoints and kernel events.
~INx2Vi2Session() override {
if (m_release_queue) {
m_release_queue.reset();
NvdrvSyncpoints().FreeSyncpoint(m_release_syncpoint);
}
// The vsync callback may be running on the HostTiming thread: empty its event list under
// its lock first (it holds the state, not this session), then close the events.
{
Expand All @@ -1717,9 +1723,8 @@ class INx2Vi2Session final : public ServiceFramework<INx2Vi2Session> {
TipcHandshake(ctx, "nx2:vi2");
}

// 229(u64 client token, u32 slot, u32 image, ...fences): result, u64 mask of buffer slots the
// client may acquire next, four zero words. Nothing is shown yet; every slot is handed back
// at once (changes.patch's default mask is 0x7fff).
/// Presents a native buffer and returns available slots plus its release fence.
/// The queued path keeps display pacing outside the guest's CPU frame measurement.
void PresentClientBuffer(HLERequestContext& ctx) {
const auto* w = ctx.CommandBuffer();
++m_presents;
Expand Down Expand Up @@ -1773,9 +1778,18 @@ class INx2Vi2Session final : public ServiceFramework<INx2Vi2Session> {
m_previous_fence_id = ~w[22];
m_previous_fence_value = w[24];
const auto waited = std::chrono::steady_clock::now();
const auto release = PresentQueue() ? QueueRelease(w[12], ~w[22], w[24])
: std::optional{std::array<u32, 4>{}};
if (!release) {
IPC::ResponseBuilder rb{ctx, 2};
rb.PushRaw<u32>(Kernel::ResultOutOfResource.raw);
return;
}
Composite(w[5], ~w[22], w[24], w[7]);
const auto composited = std::chrono::steady_clock::now();
PaceToVblank(w[12]);
if (!PresentQueue()) {
PaceToVblank(w[12]);
}
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.
Expand Down Expand Up @@ -1814,9 +1828,68 @@ class INx2Vi2Session final : public ServiceFramework<INx2Vi2Session> {
}
constexpr u64 AvailableSlots = 0x7fff;
TipcReplyWords(ctx, {static_cast<u32>(AvailableSlots),
static_cast<u32>(AvailableSlots >> 32), 0, 0, 0, 0});
static_cast<u32>(AvailableSlots >> 32), (*release)[0], (*release)[1],
(*release)[2], (*release)[3]});
}

/// Returns presentation fences instead of blocking the present thread until the next flip.
/// This keeps the display wait out of Wonder's CPU frame measurement.
/// EDEN_NX2_PRESENT_QUEUE=0 restores blocking presentation pacing.
static bool PresentQueue() {
static const bool enabled = [] {
const char* value = std::getenv("EDEN_NX2_PRESENT_QUEUE");
return value == nullptr || value[0] != '0';
}();
return enabled;
}

/// Returns the allocator for display release syncpoints.
Nvidia::NvCore::SyncpointManager& NvdrvSyncpoints() {
return system.ServiceManager()
.GetService<Nvidia::NVDRV>("nvdrv", true)
->GetModule()
->GetContainer()
.GetSyncpointManager();
}

/// Returns {~syncpoint, 0, value, 0}, released after the swap deadline and frame's GPU fence.
/// Returns no fence if the syncpoint pool or bounded release queue is exhausted.
std::optional<std::array<u32, 4>> QueueRelease(u32 timing, u32 fence_id, u32 fence_value) {
if (!m_release_queue) {
const auto id = NvdrvSyncpoints().TryAllocateSyncpoint(true);
if (!id) {
return std::nullopt;
}
m_release_syncpoint = *id;
m_release_queue.emplace(system.CoreTiming(), system.Host1x().GetSyncpointManager(), *id);
LOG_INFO(Service, "nx2:vi2 display release syncpoint {}", m_release_syncpoint);
}
const u32 swap_interval = SwapInterval(timing);
const u32 hz = timing & 0xffff;
const u32 refresh_rate = hz >= 30 && hz <= 240 ? hz : 60;
if (timing != std::exchange(m_swap_word, timing)) {
LOG_INFO(Service, "nx2:vi2 present #{} queued swap interval {} at {} Hz",
m_presents, swap_interval, refresh_rate);
}
const auto now = system.CoreTiming().GetGlobalTimeNs();
std::chrono::nanoseconds interval{};
if (Settings::values.use_speed_limit.GetValue() && Settings::SpeedLimit() != 0) {
interval = std::chrono::nanoseconds{
100'000'000'000LL * swap_interval / refresh_rate / Settings::SpeedLimit()};
}
m_release_time = interval.count() == 0 ? now : (std::max)(now, m_release_time);
const auto value = m_release_queue->Queue(m_release_time, fence_id, fence_value);
if (!value) {
return std::nullopt;
}
m_release_time += interval;
return std::array<u32, 4>{~m_release_syncpoint, 0, *value, 0};
}

u32 m_release_syncpoint{~0U};
std::chrono::nanoseconds m_release_time{};
std::optional<Nx2PresentReleaseQueue> m_release_queue;

// The console's compositor frees a buffer only once its frame's GPU fence has passed, which
// is what keeps a game from running ahead of the GPU. This reply always reports every buffer
// free, so pace the game here instead: wait (bounded, in case the GPU waits on the CPU) until
Expand All @@ -1838,13 +1911,8 @@ class INx2Vi2Session final : public ServiceFramework<INx2Vi2Session> {
std::chrono::steady_clock::time_point m_next_vblank{};
u32 m_swap_word{};

// timing: present word 12, (swap interval << 16) | refresh rate. Fire Emblem: Fortune's Weave
// sends 0x1003c during its boot logos and 0x2003c (two vblanks per frame, 30 fps) after; paced
// at every vblank it ran at double speed. EDEN_NX2_SWAP_INTERVAL=<n> forces the interval.
void PaceToVblank(u32 timing) {
if (!Settings::values.use_speed_limit.GetValue() || Settings::SpeedLimit() == 0) {
return;
}
/// Resolves the requested interval and existing title/debug overrides for both pacing paths.
u32 SwapInterval(u32 timing) {
static const u32 forced_interval = [] {
const char* value = std::getenv("EDEN_NX2_SWAP_INTERVAL");
return value ? static_cast<u32>(std::strtoul(value, nullptr, 10)) : 0U;
Expand All @@ -1860,6 +1928,18 @@ class INx2Vi2Session final : public ServiceFramework<INx2Vi2Session> {
if (forced_interval != 0) {
swap_interval = forced_interval;
}
return swap_interval;
}

/// Blocks until the next requested swap interval when queued presentation is disabled.
// timing: present word 12, (swap interval << 16) | refresh rate. Fire Emblem: Fortune's Weave
// sends 0x1003c during its boot logos and 0x2003c (two vblanks per frame, 30 fps) after; paced
// at every vblank it ran at double speed. EDEN_NX2_SWAP_INTERVAL=<n> forces the interval.
void PaceToVblank(u32 timing) {
if (!Settings::values.use_speed_limit.GetValue() || Settings::SpeedLimit() == 0) {
return;
}
const u32 swap_interval = SwapInterval(timing);
const u32 refresh_rate = (timing & 0xffff) >= 30 && (timing & 0xffff) <= 240 ? timing & 0xffff : 60;
if (timing != std::exchange(m_swap_word, timing)) {
LOG_INFO(Service, "nx2:vi2 present #{} swap interval {} at {} Hz (word 12 = {:#x})",
Expand Down Expand Up @@ -1944,7 +2024,8 @@ class INx2Vi2Session final : public ServiceFramework<INx2Vi2Session> {
return syncpoints.IsReadyGuest(fence_id, fence_value);
}

// Shows a presented native image: word 5 is the image id, ~word 22 / word 24 the GPU fence
/// Shows a native image, keeping CPU speed limiting disabled when release fences pace it.
// Word 5 is the image id, ~word 22 / word 24 the GPU fence
// (syncpoint, value) to wait for (layout from changes.patch). The image is a block-linear
// RGBA8 surface in the console's native GOB layout at GPU address == CPU address.
// shown_size: the present's word 7, ((height - 1) << 16) | (width - 1) of the area the game drew
Expand Down Expand Up @@ -2040,7 +2121,11 @@ class INx2Vi2Session final : public ServiceFramework<INx2Vi2Session> {
system.GPU().RequestComposite(std::move(layers), std::move(fences));
CaptureFrame();
DumpRanges();
system.SpeedLimiter().DoSpeedLimiting(system.CoreTiming().GetGlobalTimeUs());
// With the release fence pacing the game, limiting here would hold the present thread
// again (the wait the games' frame timers must not see).
if (!PresentQueue()) {
system.SpeedLimiter().DoSpeedLimiting(system.CoreTiming().GetGlobalTimeUs());
}
system.GetPerfStats().EndSystemFrame();
system.GetPerfStats().BeginSystemFrame();
}
Expand Down
110 changes: 110 additions & 0 deletions src/core/hle/service/nx2_present_queue.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once

#include <chrono>
#include <deque>
#include <memory>
#include <mutex>
#include <optional>

#include "common/common_types.h"
#include "core/core_timing.h"
#include "video_core/host1x/syncpoint_manager.h"

namespace Service {

/// Releases native display buffers in submission order after their deadline and GPU fence pass.
class Nx2PresentReleaseQueue {
public:
// The service advertises fifteen buffer slots. Clients must wait for releases before reuse.
static constexpr size_t Capacity = 15;

/// Starts an empty queue using an already allocated, session-owned release syncpoint.
Nx2PresentReleaseQueue(Core::Timing::CoreTiming& timing_,
Tegra::Host1x::SyncpointManager& syncpoints_, u32 release_id_)
: timing{timing_}, syncpoints{syncpoints_}, release_id{release_id_},
next_value{syncpoints.GetGuestSyncpointValue(release_id)} {
timer = Core::Timing::CreateEvent(
"nx2:vi2 buffer release",
[this](s64, std::chrono::nanoseconds) -> std::optional<std::chrono::nanoseconds> {
ReleaseReady();
return std::nullopt;
});
}

/// Stops rescheduling before waiting for the callback, then wakes outstanding release waits.
/// At most Capacity increments are needed, even if another producer advanced the syncpoint.
~Nx2PresentReleaseQueue() {
{
std::scoped_lock lock{mutex};
stopped = true;
}
timing.UnscheduleEvent(timer);
for (const auto& entry : pending) {
Signal(entry.value);
}
}

/// Enqueues without blocking the present thread; returns no value when all slots are pending.
std::optional<u32> Queue(std::chrono::nanoseconds deadline, u32 fence_id, u32 fence_value) {
std::scoped_lock lock{mutex};
if (stopped || pending.size() == Capacity) {
return std::nullopt;
}
pending.push_back({deadline, fence_id, fence_value, ++next_value});
if (pending.size() == 1) {
timing.ScheduleEvent(deadline, timer, true);
}
return next_value;
}

private:
struct PendingRelease {
std::chrono::nanoseconds deadline;
u32 fence_id;
u32 fence_value;
u32 value;
};

/// Advances once per queued release, without chasing a counter that has already passed it.
void Signal(u32 value) {
if (static_cast<s32>(value - syncpoints.GetGuestSyncpointValue(release_id)) > 0) {
syncpoints.IncrementGuest(release_id);
}
}

/// Polls only the oldest release; GPU waits never block the timing or presentation thread.
void ReleaseReady() {
std::scoped_lock lock{mutex};
if (stopped) {
return;
}
while (!pending.empty()) {
const auto& entry = pending.front();
const auto now = timing.GetGlobalTimeNs();
if (now < entry.deadline) {
timing.ScheduleEvent(entry.deadline, timer, true);
return;
}
if (entry.fence_id < Tegra::Host1x::SyncpointManager::NUM_MAX_SYNCPOINTS &&
!syncpoints.IsReadyGuest(entry.fence_id, entry.fence_value)) {
timing.ScheduleEvent(std::chrono::microseconds{100}, timer);
return;
}
Signal(entry.value);
pending.pop_front();
}
}

Core::Timing::CoreTiming& timing;
Tegra::Host1x::SyncpointManager& syncpoints;
const u32 release_id;
u32 next_value;
std::mutex mutex;
bool stopped{};
std::deque<PendingRelease> pending;
std::shared_ptr<Core::Timing::EventType> timer;
};

} // namespace Service
1 change: 1 addition & 0 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ add_executable(tests
core/ldn.cpp
core/internal_network/socket_proxy.cpp
core/nup_writer.cpp
core/nx2_present_queue.cpp
core/priority_queue.cpp
core/internal_network/network.cpp
video_core/memory_tracker.cpp
Expand Down
Loading