aboutsummaryrefslogtreecommitdiff
path: root/src/core
diff options
context:
space:
mode:
authornowhere02 <140357165+nowhere02@users.noreply.github.com>2026-11-02 00:17:14 -0300
committerGitHub <noreply@github.com>2026-11-02 00:17:14 -0300
commit9c55e70d790a4cc9507d80dbb9a9d6224175574e (patch)
tree4c53ccbf16ab56bb00e512cc7926b1d0d438792d /src/core
parentb8d2c08d6efd312620b066cdab2f0238ebc94a26 (diff)
parent06e0541e18e5bb8ab8d3d03660d3d82cb112f62b (diff)
Merge pull request #270 from Duck912/fe-render
Fe render
Diffstat (limited to 'src/core')
-rw-r--r--src/core/hle/service/am/frontend/applet_software_keyboard.cpp8
-rw-r--r--src/core/hle/service/am/frontend/applet_software_keyboard_types.h60
-rw-r--r--src/core/hle/service/nvdrv/core/container.cpp2
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp46
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h2
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp25
-rw-r--r--src/core/hle/service/nvdrv/devices/nx2_stub_device.h51
-rw-r--r--src/core/hle/service/nvnflinger/pixel_format.h1
-rw-r--r--src/core/hle/service/nx2_gsv.h294
9 files changed, 456 insertions, 33 deletions
diff --git a/src/core/hle/service/am/frontend/applet_software_keyboard.cpp b/src/core/hle/service/am/frontend/applet_software_keyboard.cpp
index db42781e18..061394bff8 100644
--- a/src/core/hle/service/am/frontend/applet_software_keyboard.cpp
+++ b/src/core/hle/service/am/frontend/applet_software_keyboard.cpp
@@ -222,6 +222,14 @@ void SoftwareKeyboard::InitializeForeground() {
sizeof(SwkbdConfigNew));
swkbd_config_common.use_utf8 = true;
break;
+ case SwkbdAppletVersion::Version1441810: {
+ // Fire Emblem: Fortune's Weave asks for UTF-8 and keeps the reply up to the first NUL, so the
+ // Switch 1 layout's use_utf8 (a byte of the native initial_string_offset) left one letter.
+ const bool decoded =
+ DecodeNativeSwkbdConfig(swkbd_config_data, swkbd_config_common, swkbd_config_new);
+ ASSERT(decoded);
+ break;
+ }
default:
UNIMPLEMENTED_MSG("Unknown SwkbdConfig revision={} with size={}", swkbd_applet_version,
swkbd_config_data.size());
diff --git a/src/core/hle/service/am/frontend/applet_software_keyboard_types.h b/src/core/hle/service/am/frontend/applet_software_keyboard_types.h
index f263a704cd..8ff8391aa1 100644
--- a/src/core/hle/service/am/frontend/applet_software_keyboard_types.h
+++ b/src/core/hle/service/am/frontend/applet_software_keyboard_types.h
@@ -7,6 +7,9 @@
#pragma once
#include <array>
+#include <cstddef>
+#include <cstring>
+#include <span>
#include "common/bit_field.h"
#include "common/common_funcs.h"
@@ -31,6 +34,7 @@ enum class SwkbdAppletVersion : u32_le {
Version393227 = 0x6000B, // 6.0.0 - 7.0.1
Version524301 = 0x8000D, // 8.0.0+
Version1310736 = 0x140010, // Switch 2 (Pokemon Pokopia): 0x530-byte config, UTF-8 text
+ Version1441810 = 0x160012, // Switch 2 (Fire Emblem: Fortune's Weave): native 0x530-byte config
};
enum class SwkbdType : u32 {
@@ -184,8 +188,64 @@ struct SwkbdConfigNew {
};
static_assert(sizeof(SwkbdConfigNew) == 0x4C8 - sizeof(SwkbdConfigCommon),
"SwkbdConfigNew has incorrect size.");
+
+// SwkbdAppletVersion 0x160012: the text prefix of the common part keeps its layout, the flags,
+// lengths, string offsets and dictionary fields move (checked against
+// nn::swkbd::keyboardConfig2kbdConfig in Fire Emblem: Fortune's Weave's sdk).
+struct SwkbdConfigNative {
+ std::array<u8, 0x3AA> common_prefix{};
+ bool use_utf8{};
+ bool use_blur_background{};
+ u32 max_text_length{};
+ u32 min_text_length{};
+ SwkbdPasswordMode password_mode{};
+ SwkbdTextDrawType text_draw_type{};
+ u32 initial_string_offset{};
+ u32 initial_string_length{};
+ u32 user_dictionary_offset{};
+ u32 user_dictionary_entries{};
+ std::array<u64, 24> customized_dictionary_set_entries{};
+ u8 total_customized_dictionary_set_entries{};
+ bool use_text_check{};
+ bool disable_cancel_button{};
+ u8 trigger{};
+ std::array<u32, 8> text_grouping{};
+ INSERT_PADDING_BYTES(0x80);
+};
+static_assert(sizeof(SwkbdConfigNative) == 0x530);
+static_assert(offsetof(SwkbdConfigNative, use_text_check) == 0x48D);
+static_assert(offsetof(SwkbdConfigNative, text_grouping) == 0x490);
#pragma pack(pop)
+inline bool DecodeNativeSwkbdConfig(std::span<const u8> data, SwkbdConfigCommon& common,
+ SwkbdConfigNew& config) {
+ if (data.size() != sizeof(SwkbdConfigNative)) {
+ return false;
+ }
+ SwkbdConfigNative native{};
+ std::memcpy(&native, data.data(), sizeof(native));
+ common = {};
+ config = {};
+ std::memcpy(static_cast<void*>(&common), native.common_prefix.data(), native.common_prefix.size());
+ common.enable_return_button = native.common_prefix[0x1B] != 0;
+ common.use_utf8 = native.use_utf8;
+ common.use_blur_background = native.use_blur_background;
+ common.max_text_length = native.max_text_length;
+ common.min_text_length = native.min_text_length;
+ common.password_mode = native.password_mode;
+ common.text_draw_type = native.text_draw_type;
+ common.initial_string_offset = native.initial_string_offset;
+ common.initial_string_length = native.initial_string_length;
+ common.user_dictionary_offset = native.user_dictionary_offset;
+ common.user_dictionary_entries = native.user_dictionary_entries;
+ common.use_text_check = native.use_text_check;
+ config.text_grouping = native.text_grouping;
+ config.customized_dictionary_set_entries = native.customized_dictionary_set_entries;
+ config.total_customized_dictionary_set_entries = native.total_customized_dictionary_set_entries;
+ config.disable_cancel_button = native.disable_cancel_button;
+ return true;
+}
+
struct SwkbdTextCheck {
SwkbdTextCheckResult text_check_result{};
std::array<char16_t, STRING_BUFFER_SIZE / 2> text_check_message{};
diff --git a/src/core/hle/service/nvdrv/core/container.cpp b/src/core/hle/service/nvdrv/core/container.cpp
index 9edce03f64..22db243e3f 100644
--- a/src/core/hle/service/nvdrv/core/container.cpp
+++ b/src/core/hle/service/nvdrv/core/container.cpp
@@ -11,6 +11,7 @@
#include "core/hle/service/nvdrv/core/heap_mapper.h"
#include "core/hle/service/nvdrv/core/nvmap.h"
#include "core/hle/service/nvdrv/core/syncpoint_manager.h"
+#include "core/hle/service/nvdrv/devices/nx2_stub_device.h"
#include "core/memory.h"
#include "video_core/host1x/host1x.h"
@@ -130,6 +131,7 @@ void Container::CloseSession(SessionId session_id) {
session.has_preallocated_area = false;
}
session.is_active = false;
+ Devices::Nx2GpuMemoryRegistry::Get().ForgetDeviceRanges(session.asid.id);
smmu.UnregisterProcess(impl->sessions[session_id.id].asid);
impl->id_pool.emplace_front(session_id.id);
}
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
index e66c72cd26..9d696c027e 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
@@ -5,6 +5,7 @@
// SPDX-FileCopyrightText: 2021 Skyline Team and Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
+#include <cstdlib>
#include <cstring>
#include <utility>
@@ -35,6 +36,13 @@ nvhost_as_gpu::nvhost_as_gpu(Core::System& system_, Module& module_, NvCore::Con
nvhost_as_gpu::~nvhost_as_gpu() {
Nx2GpuMemoryRegistry::Get().RemoveListener(this);
+ // Leave shared ranges to the process's other address spaces; like this address space's own
+ // ranges, one nobody else maps stays allocated.
+ for (const auto& [key, range] : nx2_device_ranges) {
+ if (range.shared) {
+ Nx2GpuMemoryRegistry::Get().ReleaseDeviceRange(range.asid, key.first, key.second);
+ }
+ }
}
NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> input,
@@ -134,25 +142,37 @@ void nvhost_as_gpu::Nx2Map(u64 gpu_address, u64 cpu_address, u64 size) {
}
// The same CPU range is often mapped twice (at its own address and in a sparse region), so
// reuse its SMMU mapping.
- DAddr device_address{};
- bool heap = false;
+ Nx2DeviceRange range{};
if (const auto it = nx2_device_ranges.find({cpu_address, size}); it != nx2_device_ranges.end()) {
- device_address = it->second.address;
- heap = it->second.heap;
+ range = it->second;
} else if (session->has_preallocated_area && session->mapper->IsInBounds(cpu_address, size)) {
- device_address = session->mapper->Map(cpu_address, size);
- heap = true;
+ range = {session->mapper->Map(cpu_address, size), true};
} else {
- auto& smmu = system.Host1x().MemoryManager();
- device_address = smmu.Allocate(Common::AlignUp(size, 0x20000));
- if (device_address == 0) {
+ const auto allocate = [&]() -> u64 {
+ auto& smmu = system.Host1x().MemoryManager();
+ const DAddr address = smmu.Allocate(Common::AlignUp(size, 0x20000));
+ if (address != 0) {
+ smmu.Map(address, cpu_address, size, session->asid, true);
+ }
+ return address;
+ };
+ // EDEN_NX2_PRIVATE_DEVICE_RANGES=1 (A/B aid): a range per address space.
+ static const bool private_ranges = std::getenv("EDEN_NX2_PRIVATE_DEVICE_RANGES") != nullptr;
+ if (private_ranges) {
+ range = {allocate(), false};
+ } else {
+ range = {Nx2GpuMemoryRegistry::Get().AcquireDeviceRange(session->asid.id, cpu_address,
+ size, allocate),
+ false, true, session->asid.id};
+ }
+ if (range.address == 0) {
LOG_CRITICAL(Service_NVDRV, "Switch 2 GPU memory {:#x}+{:#x}: out of SMMU space",
cpu_address, size);
return;
}
- smmu.Map(device_address, cpu_address, size, session->asid, true);
}
- nx2_device_ranges[{cpu_address, size}] = {device_address, heap};
+ const DAddr device_address = range.address;
+ nx2_device_ranges[{cpu_address, size}] = range;
Nx2GpuMemoryRegistry::Get().RecordDeviceRange(cpu_address, size, device_address);
gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
LOG_DEBUG(Service_NVDRV, "Switch 2 GPU mapping {:#x}+{:#x} -> CPU {:#x}", gpu_address, size,
@@ -177,6 +197,10 @@ void nvhost_as_gpu::Nx2Release(u64 cpu_address, u64 size) {
}
const Nx2DeviceRange range = it->second;
nx2_device_ranges.erase(it);
+ if (range.shared &&
+ !Nx2GpuMemoryRegistry::Get().ReleaseDeviceRange(range.asid, cpu_address, size)) {
+ return; // another address space of the process still maps it
+ }
auto* session = container.GetSession(nx2_session);
auto* const mapper = range.heap && session != nullptr ? session->mapper.get() : nullptr;
auto& smmu = system.Host1x().MemoryManager();
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
index 39b63ea369..37b6d0d4c4 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
@@ -238,6 +238,8 @@ private:
struct Nx2DeviceRange {
DAddr address;
bool heap; ///< Mapped through the session's heap mapper (else an SMMU allocation)
+ bool shared{}; ///< A reference to the process's range in Nx2GpuMemoryRegistry
+ size_t asid{};
};
std::map<std::pair<u64, u64>, Nx2DeviceRange> nx2_device_ranges;
void Nx2Release(u64 cpu_address, u64 size);
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
index 5998940afa..cf4a473496 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
@@ -113,12 +113,22 @@ NvResult nvhost_ctrl_gpu::Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8>
case 0x6:
return WrapFixedInlOut(this, &nvhost_ctrl_gpu::GetTPCMasks3, input, output,
inline_output);
- case 0x13:
- LOG_DEBUG(Service_NVDRV, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
- return NvResult::NotImplemented;
+ case 0x13: {
+ // NVGPU_GPU_IOCTL_VSMS_MAPPING: an 8-byte argument struct plus a buffer of
+ // {gpc_index, tpc_index} byte pairs, one per virtual SM. The SDK treats a pair left at
+ // its 0xc0c0 sentinel as failure, so fill every pair: one GPC, two SMs per TPC.
+ if (command.raw != 0xc0084713 || input.size() != 8 || output.size() != 8 ||
+ inline_output.size() < 2) {
+ return NvResult::BadParameter;
+ }
+ std::memcpy(output.data(), input.data(), output.size());
+ for (size_t i = 0; i + 1 < inline_output.size(); i += 2) {
+ inline_output[i] = 0; // gpc_index
+ inline_output[i + 1] = static_cast<u8>(i / 4); // tpc_index
+ }
+ LOG_DEBUG(Service_NVDRV, "VSMS mapping for {} SMs", inline_output.size() / 2);
+ return NvResult::Success;
+ }
default:
break;
}
@@ -218,6 +228,9 @@ void ApplyNx2Characteristics(GC& gc) {
if (remap) {
gc.flags |= 1ULL << 35;
}
+ // Bits 40..43: the 2D, 3D, compute and inline-to-memory class families. The SDK filters its
+ // class enumeration by them; without them Fire Emblem: Fortune's Weave aborts at boot.
+ gc.flags |= 0xfULL << 40;
// The Switch 2 layout keeps nine s16 interface versions at 0x70..0x80 (like nvgpu's
// *_ioctl_nr_last); its NvRm requires e.g. [0x78] >= 6 and enables features above 0xa, 0x1f
diff --git a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
index 869e17caf6..8dee9ccfb7 100644
--- a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
+++ b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
@@ -5,6 +5,7 @@
#include <algorithm>
#include <atomic>
+#include <compare>
#include <functional>
#include <map>
#include <mutex>
@@ -166,12 +167,50 @@ public:
{
std::scoped_lock lk{device_mutex};
device_ranges.clear();
+ shared_ranges.clear();
}
std::scoped_lock lk{mutex};
mappings.clear();
sparse_regions.clear();
}
+ // Device (SMMU) range of a mapping outside the heap, shared by every address space of one
+ // process: the caches key memory by device address, so a range per address space made separate
+ // copies of the same memory, and presentation could read one the game never drew into. The
+ // first address space to map the range allocates it with `allocate` (0: out of space); every
+ // call takes a reference.
+ u64 AcquireDeviceRange(size_t asid, u64 cpu_address, u64 size,
+ const std::function<u64()>& allocate) {
+ std::scoped_lock lk{device_mutex};
+ const SharedRangeKey key{asid, cpu_address, size};
+ if (const auto it = shared_ranges.find(key); it != shared_ranges.end()) {
+ ++it->second.references;
+ return it->second.device_address;
+ }
+ const u64 device_address = allocate();
+ if (device_address != 0) {
+ shared_ranges.emplace(key, SharedRange{device_address, 1});
+ }
+ return device_address;
+ }
+
+ // Drops a reference. True for the last one: the caller frees the range.
+ bool ReleaseDeviceRange(size_t asid, u64 cpu_address, u64 size) {
+ std::scoped_lock lk{device_mutex};
+ const auto it = shared_ranges.find(SharedRangeKey{asid, cpu_address, size});
+ if (it == shared_ranges.end() || --it->second.references != 0) {
+ return false;
+ }
+ shared_ranges.erase(it);
+ return true;
+ }
+
+ // The process's session closed and its ASID can be reused: stop sharing its ranges.
+ void ForgetDeviceRanges(size_t asid) {
+ std::scoped_lock lk{device_mutex};
+ std::erase_if(shared_ranges, [asid](const auto& entry) { return entry.first.asid == asid; });
+ }
+
// Device (SMMU) address of a CPU range an address space mapped; used to present images.
void RecordDeviceRange(u64 cpu_address, u64 size, u64 device_address) {
std::scoped_lock lk{device_mutex};
@@ -197,6 +236,18 @@ private:
std::mutex device_mutex;
std::map<u64, std::pair<u64, u64>> device_ranges; // cpu start -> (size, device address)
+ struct SharedRangeKey {
+ size_t asid;
+ u64 cpu_address;
+ u64 size;
+ auto operator<=>(const SharedRangeKey&) const = default;
+ };
+ struct SharedRange {
+ u64 device_address;
+ u32 references;
+ };
+ std::map<SharedRangeKey, SharedRange> shared_ranges;
+
struct Mapping {
u64 gpu_address;
u64 cpu_address;
diff --git a/src/core/hle/service/nvnflinger/pixel_format.h b/src/core/hle/service/nvnflinger/pixel_format.h
index b6e8b5f8a0..a38c8be777 100644
--- a/src/core/hle/service/nvnflinger/pixel_format.h
+++ b/src/core/hle/service/nvnflinger/pixel_format.h
@@ -19,6 +19,7 @@ enum class PixelFormat : u32 {
Bgra8888 = 5,
Rgba5551 = 6,
Rgba4444 = 7,
+ Rgba1010102 = 43,
};
} // namespace Service::android
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index 628bdeda5b..5a2963f179 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -21,6 +21,7 @@
#include <optional>
#include <memory>
#include <mutex>
+#include <utility>
#include <vector>
#include "common/alignment.h"
@@ -45,13 +46,17 @@
#include "core/hle/kernel/k_resource_limit.h"
#include "core/hle/kernel/k_server_session.h"
#include "core/hle/kernel/k_event.h"
+#include "core/hle/kernel/k_hardware_timer.h"
#include "core/hle/kernel/k_readable_event.h"
#include "core/hle/kernel/k_session.h"
+#include "core/hle/kernel/k_thread.h"
#include "core/hle/kernel/k_transfer_memory.h"
+#include "core/hle/kernel/svc_results.h"
#include "core/memory.h"
#include "video_core/host1x/host1x.h"
#include "video_core/host1x/nx2_video_decoder.h"
#include "video_core/nx2.h"
+#include "video_core/textures/decoders.h"
#include "core/hle/service/hle_ipc.h"
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
@@ -304,6 +309,8 @@ public:
{21, &INx2GpuSession::Start, "Start (guessed)"},
{22, &INx2GpuSession::OpenSubSession, "OpenSubSession (guessed)"},
{33, &INx2GpuSession::GetCount33, "Unknown33 (u32 count)"},
+ {41, &INx2GpuSession::CreateGraphicsDevice, "CreateGraphicsDevice"},
+ {42, &INx2GpuSession::DestroyGraphicsDevice, "DestroyGraphicsDevice"},
{201, &INx2GpuSession::GetUserPartition, "GetUserPartition (newer SDKs)"},
{202, &INx2GpuSession::GetGsvPartition, "GetGsvPartition (newer SDKs)"},
{203, &INx2GpuSession::GetVaRange, "GetVaRange (guessed)"},
@@ -395,9 +402,39 @@ private:
static_cast<u32>(second), static_cast<u32>(second >> 32)});
}
+ // An empty partition is min = ~0, max = 0 (min > max). Fire Emblem: Fortune's Weave's SDK
+ // reads (0, 0) as a one-byte partition at address 0 and aborts in nn::gpu::sdk.
void GetGsvPartition(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) nx2:gpu TIPC 202 [2]={:#x}, empty", ctx.CommandBuffer()[2]);
- TipcReplyWords(ctx, {0, 0, 0, 0});
+ TipcReplyWords(ctx, {~0U, ~0U, 0, 0});
+ }
+
+ // Fire Emblem: Fortune's Weave: 41(name u64, flags u64, debug label u64) returns a device id
+ // that later NvRm calls name; 0 is not a valid id. Only flags 0 has been seen.
+ void CreateGraphicsDevice(HLERequestContext& ctx) {
+ const auto* b = ctx.CommandBuffer();
+ const u64 name = u64{b[2]} | (u64{b[3]} << 32);
+ const u64 flags = u64{b[4]} | (u64{b[5]} << 32);
+ u32 id = 0;
+ for (u32 candidate = 1; candidate < m_devices.size(); ++candidate) {
+ if (!m_devices[candidate]) {
+ m_devices[candidate] = true;
+ id = candidate;
+ break;
+ }
+ }
+ LOG_INFO(Service, "nx2:gpu 41 create graphics device name={:#x} flags={:#x} -> id {}", name,
+ flags, id);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(id != 0 ? ResultSuccess : Kernel::ResultOutOfResource);
+ rb.Push(id);
+ }
+
+ void DestroyGraphicsDevice(HLERequestContext& ctx) {
+ const u32 id = ctx.CommandBuffer()[2] & 0xff;
+ const bool existed = id < m_devices.size() && std::exchange(m_devices[id], false);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(existed ? ResultSuccess : Kernel::ResultNotFound);
}
void OpenSubSession(HLERequestContext& ctx) {
@@ -1246,6 +1283,7 @@ private:
std::array<Kernel::KEvent*, NumIds> m_events{};
std::map<u32, GpuMemory> m_memory;
u32 m_next_memory_handle{1};
+ std::array<bool, 64> m_devices{}; // gpu 41 graphics device ids (0 unused)
};
// Native memory and images handed out by the Switch 2 graphics server (gsv 111/114/423/429/315),
@@ -1264,6 +1302,10 @@ struct Nx2NativeObjects {
u64 offset{}; // into its memory (images of a decoder pool share one)
bool window_pool{};
bool decoder{}; // a video decoder output image (gsv 911)
+ // A decoder image the client reads on the GPU (gsv 428/426): written block-linear.
+ bool gpu_read{};
+ u32 gpu_maps{}; // outstanding 426 maps
+ bool mapped_here{}; // 426 added its GPU mapping (no other mapping covered it)
};
std::mutex mutex;
std::map<u32, Memory> memories;
@@ -1407,11 +1449,12 @@ public:
{11, &INx2Vi2Session::ReturnEvent, "Unknown11"},
{31, &INx2Vi2Session::GetVsyncEvent, "GetVsyncEvent (guessed)"},
{32, &INx2Vi2Session::GetVsyncEvent, "GetDisplayVsyncEvent (guessed)"},
- // Title 1 calls these ~75 times a second with the display name "default"; the
+ {104, &INx2Vi2Session::GetHdrStatus, "GetHdrStatus"},
+ // Title 1 calls this ~75 times a second with the display name "default"; the
// auto stub's empty success reply works, but logged two lines per call (a 50 MB log
// in an 18 minute session).
- {104, &INx2Vi2Session::SucceedQuietly, "Unknown104 (display \"default\")"},
{105, &INx2Vi2Session::SucceedQuietly, "Unknown105 (display \"default\")"},
+ {106, &INx2Vi2Session::GetBufferMaxResolution, "GetBufferMaxResolution"},
{228, &INx2Vi2Session::CancelClientBuffer, "CancelClientBuffer"},
{229, &INx2Vi2Session::PresentClientBuffer, "PresentClientBuffer"},
});
@@ -1464,9 +1507,9 @@ private:
WaitForFrame(~w[22], w[24]);
}
const auto waited = std::chrono::steady_clock::now();
- Composite(w[5], ~w[22], w[24]);
+ Composite(w[5], ~w[22], w[24], w[7]);
const auto composited = std::chrono::steady_clock::now();
- PaceToVblank();
+ 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.
@@ -1522,18 +1565,39 @@ private:
// present faster than that. Every slot is reported free here: hold each present until the
// next vblank instead (title 4 ran at up to 160 fps, its timing and audio with it).
std::chrono::steady_clock::time_point m_next_vblank{};
+ u32 m_swap_word{};
- void PaceToVblank() {
+ // 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 auto interval = std::chrono::nanoseconds{100'000'000'000LL / 60 / Settings::SpeedLimit()};
+ 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;
+ }();
+ const u32 swap_interval =
+ forced_interval != 0 ? forced_interval : std::clamp<u32>(timing >> 16, 1, 4);
+ 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})",
+ m_presents, swap_interval, refresh_rate, timing);
+ }
+ const auto interval = std::chrono::nanoseconds{
+ 100'000'000'000LL * swap_interval / refresh_rate / Settings::SpeedLimit()};
const auto now = std::chrono::steady_clock::now();
if (now > m_next_vblank + interval) {
- // Running slower than 60 fps: show at once and don't bank the lost time.
+ // Running slower than the swap interval: show at once and don't bank the lost time.
m_next_vblank = now;
} else if (now < m_next_vblank) {
- std::this_thread::sleep_until(m_next_vblank);
+ // Sleep this service thread in the guest kernel. It runs on guest core 3, and a host
+ // sleep held that core for the whole wait: game threads scheduled there ran only
+ // between presents (Fortune's Weave's loader read one RomFS block per frame).
+ const auto wait = std::chrono::duration_cast<std::chrono::nanoseconds>(m_next_vblank - now);
+ static_cast<void>(Kernel::GetCurrentThread(kernel).Sleep(
+ kernel, kernel.HardwareTimer().GetTick() + wait.count() + 2));
}
m_next_vblank += interval;
}
@@ -1586,9 +1650,12 @@ private:
// Shows a presented native image: 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.
- void Composite(u32 image_id, u32 fence_id, u32 fence_value) {
+ // present_size: word 7 of the present, (height - 1) << 16 | (width - 1) of the frame drawn
+ // into the image's top-left corner (Fire Emblem: Fortune's Weave: 1280x720 in 3840x2160).
+ void Composite(u32 image_id, u32 fence_id, u32 fence_value, u32 present_size = 0) {
u64 address{};
Nx2ImageLayout layout{};
+ u32 image_format{};
{
std::scoped_lock lk{m_objects->mutex};
const auto image = m_objects->images.find(image_id);
@@ -1601,6 +1668,7 @@ private:
}
address = memory->second.address;
layout = Nx2CalculateImageLayout(image->second.preference.data());
+ image_format = image->second.preference[6];
}
const auto device = Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address);
if (!device) {
@@ -1610,6 +1678,11 @@ private:
}
u32 crop_width = layout.width;
u32 crop_height = layout.height;
+ // Present word 7 holds the size of the frame drawn into the image's top-left corner.
+ if (present_size != 0) {
+ crop_width = (std::min)(crop_width, (present_size & 0xffffU) + 1);
+ crop_height = (std::min)(crop_height, (present_size >> 16) + 1);
+ }
// Handheld titles keep their docked present images (up to 3840x2160) and draw the frame
// into the top-left 1920x1080, the handheld panel's resolution; the console's display
// scans out only that region. Showing whole images put the frame in a corner inside black
@@ -1648,7 +1721,9 @@ private:
.width = layout.width,
.height = layout.height,
.stride = static_cast<u32>(layout.pitch / 4),
- .pixel_format = android::PixelFormat::Rgba8888,
+ // Preference word 6: 1 = RGBA8, 4 = RGB10A2.
+ .pixel_format = image_format == 4 ? android::PixelFormat::Rgba1010102
+ : android::PixelFormat::Rgba8888,
.transform_flags = android::BufferTransformFlags::Unset,
.crop_rect = {0, 0, static_cast<int>(crop_width), static_cast<int>(crop_height)},
.blending = Tegra::BlendMode::Opaque,
@@ -1752,6 +1827,50 @@ private:
TipcReplyWords(ctx, {1});
}
+ // 104(u64 display name): result, u64 output format, u32 0. Format 1 is SDR. A bare success
+ // left the format to stale reply words and a game could set up HDR output from them.
+ void GetHdrStatus(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 name = u64{packet[2]} | (u64{packet[3]} << 32);
+ constexpr u64 DefaultDisplay = 0x00746c7561666564ULL; // "default"
+ Result result = Kernel::ResultInvalidCombination;
+ u64 format{};
+ if (packet[0] == 0x78 && packet[1] == 2) {
+ result = Result{ErrorModule::VI, 7};
+ if (name == DefaultDisplay) {
+ format = 1;
+ result = ResultSuccess;
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 5};
+ rb.Push(result.raw);
+ rb.Push(format);
+ rb.Push<u32>(0);
+ }
+
+ // The highest buffer resolution mode of the "default" display. Fire Emblem: Fortune's Weave picks
+ // its output from it: 1 -> 1280x720, 2 -> 1920x1080, 4 and 8 -> 2560x1440; 0 (the auto stub's
+ // reply), 3 and 5 fall back to 720p. Docked reports 4, handheld 2 (the screen is 1080p);
+ // EDEN_VI2_MAX_RESOLUTION_MODE=<n> overrides.
+ void GetBufferMaxResolution(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 name = u64{packet[2]} | (u64{packet[3]} << 32);
+ constexpr u64 DefaultDisplay = 0x00746c7561666564ULL; // "default"
+ Result result = Kernel::ResultInvalidCombination;
+ u64 mode{};
+ if (packet[0] == 0x7a && packet[1] == 2) {
+ result = Result{ErrorModule::VI, 7};
+ if (name == DefaultDisplay) {
+ static const char* forced = std::getenv("EDEN_VI2_MAX_RESOLUTION_MODE");
+ mode = forced ? std::strtoull(forced, nullptr, 0) : Settings::IsDockedMode() ? 4 : 2;
+ result = ResultSuccess;
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(result.raw);
+ rb.Push(mode);
+ }
+
// The same reply as the auto stub (success, no data), without its log lines.
void SucceedQuietly(HLERequestContext& ctx) {
IPC::ResponseBuilder rb{ctx, 2};
@@ -2221,6 +2340,7 @@ private:
const Decoded& frame, std::vector<u8>& bytes) {
u64 address{};
Nx2DecoderImageLayout layout{};
+ bool block_linear{};
{
std::scoped_lock lk{m_objects->mutex};
const auto image = m_objects->images.find(image_id);
@@ -2228,6 +2348,8 @@ private:
return {m_width, m_height};
}
layout = Nx2CalculateDecoderImageLayout(image->second.preference.data());
+ block_linear = image->second.gpu_read &&
+ layout.format != Tegra::Host1x::Nx2PictureFormat::Nv12;
const auto memory = m_objects->memories.find(image->second.memory);
if (memory != m_objects->memories.end() && memory->second.address != 0) {
address = memory->second.address + image->second.offset;
@@ -2241,10 +2363,25 @@ private:
const u32 width = std::min(frame.picture->width, layout.width);
const u32 height = std::min(frame.picture->height, layout.height);
bytes.resize(layout.size);
+ auto& memory = system.ApplicationMemory();
+ if (block_linear) {
+ // Read on the GPU: RGBA (the blit's format 0xd5) in the native block-linear layout,
+ // 16-GOB blocks, as our Eden tree writes these images.
+ decoder->Render(*frame.picture, Tegra::Host1x::Nx2PictureFormat::Rgba8, width, height,
+ layout.pitch, 0, bytes);
+ m_tiled.resize(layout.size);
+ Tegra::Texture::SwizzleTexture(m_tiled, bytes, 4, static_cast<u32>(layout.pitch / 4),
+ layout.height, 1, 4, 0, 1, true);
+ memory.WriteBlock(address, m_tiled.data(), m_tiled.size());
+ if (const auto device =
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address)) {
+ system.GPU().InvalidateRegion(*device, m_tiled.size());
+ }
+ return {width, height};
+ }
decoder->Render(*frame.picture, layout.format, width, height, layout.pitch,
layout.chroma_offset, bytes);
DumpFrame(bytes, layout, width, height);
- auto& memory = system.ApplicationMemory();
memory.WriteBlock(address, bytes.data(), layout.pitch * height);
if (layout.format == Tegra::Host1x::Nx2PictureFormat::Nv12) {
memory.WriteBlock(address + layout.chroma_offset, bytes.data() + layout.chroma_offset,
@@ -2290,6 +2427,7 @@ private:
u64 m_submitted{};
u32 m_syncpoint{NoSyncpoint}; ///< Counts finished frames (allocated at the first register)
u32 m_sync_base{}; ///< Its value before the first frame
+ std::vector<u8> m_tiled; ///< Decode thread: block-linear copy of a frame (WriteImage)
// Shared with the decode thread.
std::mutex m_mutex;
@@ -2333,6 +2471,7 @@ public:
{429, &INx2GraphicsSystemSession::CreateNativeImage, "CreateNativeImage"},
{430, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyNativeImage"},
{426, &INx2GraphicsSystemSession::QueryNativeImage, "MapNativeImage"},
+ {427, &INx2GraphicsSystemSession::UnmapNativeImage, "UnmapNativeImage"},
{428, &INx2GraphicsSystemSession::QueryNativeImage, "QueryNativeImage"},
{435, &INx2GraphicsSystemSession::CalculateNativeImageSize, "CalculateNativeImageSize"},
{1402, &INx2GraphicsSystemSession::OpenNvsrvSession, "OpenNvsrvSession (guessed)"},
@@ -2597,8 +2736,19 @@ private:
void DestroyNativeMemory(HLERequestContext& ctx) {
const u32 id = ctx.CommandBuffer()[2];
- std::scoped_lock lk{m_objects->mutex};
- m_objects->memories.erase(id);
+ std::vector<std::optional<std::pair<u64, u64>>> unmaps;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ for (auto& [image_id, image] : m_objects->images) {
+ if (image.memory == id && image.mapped_here) {
+ unmaps.push_back(TakeDecoderMapping(image));
+ }
+ }
+ m_objects->memories.erase(id);
+ }
+ for (const auto& range : unmaps) {
+ DropDecoderMapping(range);
+ }
LOG_DEBUG(Service, "nx2:gsv 112 DestroyNativeMemory id {}", id);
TipcReplyWords(ctx, {});
}
@@ -2673,6 +2823,9 @@ private:
// the 429 preference words (0x10, 0x14), a guess.
void QueryNativeImage(HLERequestContext& ctx) {
const u32 handle = ctx.CommandBuffer()[2];
+ if (QueryDecoderImage(ctx, handle)) {
+ return;
+ }
std::array<u32, 16> preference{};
u64 image_offset = 0;
u64 memory_address = 0;
@@ -2717,6 +2870,108 @@ private:
}
}
+ // 428/426 for a decoder output image (Fire Emblem: Fortune's Weave's movie player blits each
+ // one, block-linear RGBA, format 0xd5, block height 16 GOBs, into a texture of its own). The
+ // reply follows our Eden tree, where the movies play: words 3-4 the GPU address, 5-6 the last
+ // byte's address, then the descriptor {1, 0, dimensions, .., 0x01000001, 1, offset, size}
+ // (offset 0 once mapped). 426 maps the image at its own address for the GPU: decoder pool
+ // memory is process memory (114) that no GPU mapping covers, and the blit read zeros.
+ bool QueryDecoderImage(HLERequestContext& ctx, u32 handle) {
+ const bool map = ctx.GetCommand() == 426;
+ u64 address{};
+ u64 size{};
+ u64 offset{};
+ u32 dimensions{};
+ bool register_mapping = false;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ const auto image = m_objects->images.find(handle);
+ if (image == m_objects->images.end() || !image->second.decoder) {
+ return false;
+ }
+ const auto memory = m_objects->memories.find(image->second.memory);
+ if (memory == m_objects->memories.end() || memory->second.address == 0) {
+ return false;
+ }
+ image->second.gpu_read = true;
+ offset = image->second.offset;
+ size = image->second.size;
+ dimensions = image->second.preference[2];
+ if (map) {
+ address = memory->second.address + offset;
+ register_mapping = image->second.gpu_maps++ == 0 &&
+ !Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address);
+ image->second.mapped_here |= register_mapping;
+ }
+ }
+ if (register_mapping) {
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Register(address, size);
+ }
+ LOG_INFO(Service, "nx2:gsv {} decoder image {} at {:#x}+{:#x}{}", ctx.GetCommand(), handle,
+ address, size, register_mapping ? ", mapped for the GPU" : "");
+ std::array<u32, 37> words{};
+ if (map) {
+ const u64 last = address + size - 1;
+ words[0] = static_cast<u32>(address);
+ words[1] = static_cast<u32>(address >> 32);
+ words[2] = static_cast<u32>(last);
+ words[3] = static_cast<u32>(last >> 32);
+ }
+ u32* const desc = words.data() + 4;
+ desc[0] = 1;
+ desc[2] = dimensions;
+ desc[8] = 0x01000001;
+ desc[9] = 1;
+ desc[10] = map ? 0 : static_cast<u32>(offset);
+ desc[11] = static_cast<u32>(size);
+ IPC::ResponseBuilder rb{ctx, static_cast<u32>(2 + words.size())};
+ rb.PushRaw<u32>(ResultSuccess.raw);
+ for (const u32 w : words) {
+ rb.Push<u32>(w);
+ }
+ return true;
+ }
+
+ // 427(u32 handle, u32 address space, u64 address, u64 last address): undoes a 426. The last
+ // unmap of a decoder image drops the GPU mapping 426 made.
+ void UnmapNativeImage(HLERequestContext& ctx) {
+ const auto* w = ctx.CommandBuffer();
+ const u32 handle = w[2];
+ std::optional<std::pair<u64, u64>> unmap;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ if (const auto image = m_objects->images.find(handle);
+ image != m_objects->images.end() && image->second.gpu_maps != 0 &&
+ --image->second.gpu_maps == 0) {
+ unmap = TakeDecoderMapping(image->second);
+ }
+ }
+ LOG_DEBUG(Service, "nx2:gsv 427 image {} [{:#x}, {:#x}]", handle, Pair(w, 4), Pair(w, 6));
+ DropDecoderMapping(unmap);
+ TipcReplyWords(ctx, {});
+ }
+
+ // The range 426 mapped for an image, if it did; called with m_objects->mutex held.
+ std::optional<std::pair<u64, u64>> TakeDecoderMapping(Nx2NativeObjects::Image& image) {
+ image.gpu_maps = 0;
+ if (!std::exchange(image.mapped_here, false)) {
+ return std::nullopt;
+ }
+ const auto memory = m_objects->memories.find(image.memory);
+ if (memory == m_objects->memories.end() || memory->second.address == 0) {
+ return std::nullopt;
+ }
+ return std::pair{memory->second.address + image.offset, image.size};
+ }
+
+ static void DropDecoderMapping(const std::optional<std::pair<u64, u64>>& range) {
+ if (range) {
+ auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get();
+ registry.Unmap(range->first, range->second);
+ registry.ReleaseBacking(range->first, range->second);
+ }
+ }
+
// 911(preference[16], u32 count, u32 memory id) (nn::codec::InitializeDecoderImagePool):
// result, the id of the first of `count` decoder output images; the client names image i of
// the pool id + i. They share the pool's memory, one image size apart.
@@ -2775,8 +3030,15 @@ private:
void DestroyNativeImage(HLERequestContext& ctx) {
const u32 id = ctx.CommandBuffer()[2];
- std::scoped_lock lk{m_objects->mutex};
- m_objects->images.erase(id);
+ std::optional<std::pair<u64, u64>> unmap;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ if (const auto image = m_objects->images.find(id); image != m_objects->images.end()) {
+ unmap = TakeDecoderMapping(image->second);
+ m_objects->images.erase(image);
+ }
+ }
+ DropDecoderMapping(unmap);
TipcReplyWords(ctx, {});
}