Compare commits

..

15 Commits

Author SHA1 Message Date
xbzk a0bc8bfde4 [video_core] Avoid stale macro upload references 2026-08-01 19:44:23 -03:00
xbzk e5b247db35 [frontend] Expose nxlink server mode 2026-08-01 19:44:23 -03:00
xbzk 44c0815dcd [loader] Add nxlink log server mode 2026-08-01 19:44:23 -03:00
xbzk b468602ffa [loader] Preserve nxlink argv markers 2026-08-01 19:44:23 -03:00
xbzk bc7219fd01 [network] Return in-progress for nonblocking connect 2026-08-01 19:44:23 -03:00
xbzk a1caf852ff [fs] Preserve guest file open modes in real VFS 2026-08-01 19:44:23 -03:00
xbzk 801c472017 [input] added option to disable wgi/xinput to prevent SDL GUIDE hack 2026-08-01 19:44:23 -03:00
xbzk 53a98f9de1 [qt_common] Avoid FS factory refresh while powered
Skip FileSystemController factory recreation during game-list repopulation while emulation is powered on.

This avoids poking live FS/VFS state during homebrew self-update and in-place NextLoad flows.
2026-08-01 19:44:22 -03:00
xbzk 6bad422d08 [core] Support libnx homebrew NextLoad handoff
Implement the homebrew NextLoad path used by libnx NROs to request another NRO from svcExitProcess.

Keep the existing process alive, rebuild the homebrew config and argv buffers, reset thread context, refresh process metadata, and add memory/address-space fallbacks needed for repeated in-place handoffs.

Reference: https://switchbrew.github.io/libnx/env_8h.html
2026-08-01 19:44:22 -03:00
xbzk 3469f3789f [nvdrv] Reset process resources for homebrew handoff
Track NVDRV sessions by process and aruid so in-place homebrew handoffs can close process-owned device files and sessions before loading the next NRO.

Also unlock nvmap device-shared pages during session cleanup to avoid stale GPU mappings leaking across repeated handoffs.
2026-08-01 19:44:22 -03:00
xbzk 221ffea4c1 [fsp] Preserve homebrew cwd for SDMC root aliases
Carry the initial homebrew working directory through filesystem process registration and FSP current-process state.

Use that cwd to resolve the homebrew cwd-plus-double-slash alias back to the SDMC root, allowing file browsers to navigate above their launch directory.
2026-08-01 19:44:22 -03:00
xbzk c87f6202d3 [fs] Allow real VFS files to be replaced while open
Add a Windows share-delete file open mode and use it for cached real VFS files. Close cached references before create, move, and delete so guest-side self-update flows can rename or replace files that Eden previously opened.

Also preserve Android real VFS full paths so homebrew path derivation does not lose the original file path.
2026-08-01 16:46:42 -03:00
xbzk 8ab9521cea [video_core] Restrict macro JIT zero-register skips
Only apply the zero-register ALU skip when the operation is safe to elide without changing carry/result semantics.

This avoids invalid-instruction floods seen with Macro JIT enabled while keeping the optimization for operations where a zero source is harmless.
2026-08-01 16:26:57 -03:00
Maufeat 612409c7ba [hid] Add Quaternion to ReloadInput (#4240)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Adds Quaternion to ReloadInput. What does it fix? Displays correct space in VR (only test on SSBU) not tested any further.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4240
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
2026-07-31 19:45:38 +02:00
simply0001 54046ac60e [video_core/macro] check HLE hashes before compiling (#4236)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Known HLE macros are identified by a hash, but MacroEngine compiled them first and and afterwards it threw the compiled program away when the hash matched. This fix makes it so it checks the hash first and caches the HLE implementation directly, so it only compiles when the hash is unknown or if HLE is disabled.

Cached macros were also constantly checking the hash again and walking through each `std::get_if` until their variant matched. So I dispatched them through `std::visit` instead, and keep one resolved code span for hashing, compiling, and dumping so mid-method uploads use the right range.

Continues the macro hot path work from [#4067](https://git.eden-emu.dev/eden-emu/eden/pulls/4067)

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4236
Reviewed-by: Shinmegumi <shinmegumi@eden-emu.dev>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
2026-07-30 06:25:43 +02:00
104 changed files with 2811 additions and 1584 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ android {
defaultConfig {
applicationId = "dev.eden.eden_emulator"
minSdk = 33
minSdk = 24
targetSdk = 36
versionName = getGitVersion()
versionCode = autoVersion
@@ -50,6 +50,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
GPU_UNSWIZZLE_TEXTURE_SIZE("gpu_unswizzle_texture_size"),
GPU_UNSWIZZLE_STREAM_SIZE("gpu_unswizzle_stream_size"),
GPU_UNSWIZZLE_CHUNK_SIZE("gpu_unswizzle_chunk_size"),
HOMEBREW_NXLINK_SERVER_MODE("homebrew_nxlink_server_mode"),
BAT_TEMPERATURE_UNIT("bat_temperature_unit"),
CABINET_APPLET("cabinet_applet_mode"),
CONTROLLER_APPLET("controller_applet_mode"),
@@ -132,6 +132,15 @@ abstract class SettingsItem(
descriptionId = R.string.program_args_description
)
)
put(
SingleChoiceSetting(
IntSetting.HOMEBREW_NXLINK_SERVER_MODE,
titleId = R.string.nxlink_server_mode,
descriptionId = R.string.nxlink_server_mode_description,
choicesId = R.array.nxlinkServerModeEntries,
valuesId = R.array.nxlinkServerModeValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
@@ -594,7 +603,7 @@ abstract class SettingsItem(
IntSetting.ANDROID_PIPELINE_WORKERS,
titleId = R.string.pipeline_worker_cores,
descriptionId = R.string.pipeline_worker_cores_description,
min = 2,
min = 4,
max = 8,
units = "cores"
)
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
@@ -169,7 +169,7 @@ class InputDialogFragment : DialogFragment() {
NativeInput.onGamePadButtonEvent(
controllerData.getGUID(),
controllerData.getPort(),
event.keyCode,
InputHandler.getButtonIdFromEvent(event),
action
)
onInputReceived(event.device)
@@ -1288,6 +1288,7 @@ class SettingsFragmentPresenter(
add(ShortSetting.DEBUG_KNOBS.key)
add(StringSetting.PROGRAM_ARGS.key)
add(IntSetting.HOMEBREW_NXLINK_SERVER_MODE.key)
if (!NativeConfig.isPerGameConfigLoaded()) {
add(HeaderSetting(R.string.gpu_logging_header))
@@ -49,6 +49,12 @@ object InputHandler {
MotionEvent.AXIS_RTRIGGER
)
// Currently, Android doesn't support Joy-Con D-pad buttons. We fall back to the scan code
private const val LINUX_BUTTON_DPAD_UP = 0x220
private const val LINUX_BUTTON_DPAD_DOWN = 0x221
private const val LINUX_BUTTON_DPAD_LEFT = 0x222
private const val LINUX_BUTTON_DPAD_RIGHT = 0x223
fun isPhysicalGameController(device: InputDevice?): Boolean {
device ?: return false
@@ -87,12 +93,25 @@ object InputHandler {
NativeInput.onGamePadButtonEvent(
controllerData.getGUID(),
controllerData.getPort(),
event.keyCode,
getButtonIdFromEvent(event),
action
)
return true
}
fun getButtonIdFromEvent(event: KeyEvent): Int {
if (event.keyCode == 0) {
return when (event.scanCode) {
LINUX_BUTTON_DPAD_UP -> KeyEvent.KEYCODE_DPAD_UP
LINUX_BUTTON_DPAD_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
LINUX_BUTTON_DPAD_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
LINUX_BUTTON_DPAD_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
else -> return 0
}
}
return event.keyCode
}
fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
val controllerData =
androidControllers[event.device.controllerNumber] ?: return false
@@ -630,6 +630,16 @@
<item>3</item>
</integer-array>
<string-array name="nxlinkServerModeEntries">
<item>Disabled</item>
<item>Eden Log</item>
</string-array>
<integer-array name="nxlinkServerModeValues">
<item>0</item>
<item>1</item>
</integer-array>
<string-array name="installKeysResults">
<item>""</item>
<item>""</item>
@@ -436,6 +436,8 @@
<string name="program_args">Homebrew Args</string>
<string name="program_args_description">Command-line arguments passed to homebrew at launch (e.g. -noglsl).</string>
<string name="nxlink_server_mode">nxlink Server</string>
<string name="nxlink_server_mode_description">Starts a local nxlink server for homebrew stdout/stderr streams.</string>
<!-- System settings strings -->
<string name="device_name">Device name</string>
+19 -15
View File
@@ -336,22 +336,22 @@ ALLOC_MEMBER(VaType)::Allocate(VaType size) {
current_linear_alloc_end = alloc_start + size;
} else { // If linear allocation overflows the AS then find a gap
if (this->blocks.size() <= 2) {
ASSERT_MSG(false, "Unexpected allocator state!");
}
auto search_predecessor{std::next(this->blocks.begin())};
auto search_successor{std::next(search_predecessor)};
while (search_successor != this->blocks.end() &&
(search_successor->virt - search_predecessor->virt < size ||
search_predecessor->Mapped())) {
search_predecessor = search_successor++;
}
if (search_successor != this->blocks.end()) {
alloc_start = search_predecessor->virt;
alloc_start = virt_start;
} else {
return {}; // AS is full
auto search_predecessor{std::next(this->blocks.begin())};
auto search_successor{std::next(search_predecessor)};
while (search_successor != this->blocks.end() &&
(search_successor->virt - search_predecessor->virt < size ||
search_predecessor->Mapped())) {
search_predecessor = search_successor++;
}
if (search_successor != this->blocks.end()) {
alloc_start = search_predecessor->virt;
} else {
return {}; // AS is full
}
}
}
@@ -364,6 +364,10 @@ ALLOC_MEMBER(void)::AllocateFixed(VaType virt, VaType size) {
}
ALLOC_MEMBER(void)::Free(VaType virt, VaType size) {
const VaType virt_end = virt + size;
this->Unmap(virt, size);
if (virt_end >= virt && virt_end == current_linear_alloc_end) {
current_linear_alloc_end = virt < virt_start ? virt_start : virt;
}
}
} // namespace Common
+62 -1
View File
@@ -4,6 +4,8 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cerrno>
#include <cstdint>
#include <vector>
#include "common/assert.h"
@@ -15,8 +17,10 @@
#include "common/logging.h"
#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#include <share.h>
#include <windows.h>
#else
#include <unistd.h>
#endif
@@ -95,10 +99,65 @@ namespace {
case FileShareFlag::ShareWriteOnly:
return _SH_DENYRD;
case FileShareFlag::ShareReadWrite:
case FileShareFlag::ShareReadWriteDelete:
return _SH_DENYNO;
}
}
[[nodiscard]] std::FILE* OpenWithWindowsShareDelete(const fs::path& path, FileAccessMode mode,
FileType type) {
DWORD desired_access{};
DWORD creation_disposition{OPEN_EXISTING};
int open_flags = type == FileType::BinaryFile ? _O_BINARY : _O_TEXT;
switch (mode) {
case FileAccessMode::Read:
desired_access = GENERIC_READ;
open_flags |= _O_RDONLY;
break;
case FileAccessMode::Write:
desired_access = GENERIC_WRITE;
creation_disposition = CREATE_ALWAYS;
open_flags |= _O_WRONLY;
break;
case FileAccessMode::Append:
desired_access = GENERIC_WRITE;
creation_disposition = OPEN_ALWAYS;
open_flags |= _O_WRONLY | _O_APPEND;
break;
case FileAccessMode::ReadWrite:
desired_access = GENERIC_READ | GENERIC_WRITE;
open_flags |= _O_RDWR;
break;
case FileAccessMode::ReadAppend:
desired_access = GENERIC_READ | GENERIC_WRITE;
creation_disposition = OPEN_ALWAYS;
open_flags |= _O_RDWR | _O_APPEND;
break;
}
const auto handle =
CreateFileW(path.c_str(), desired_access,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
creation_disposition, FILE_ATTRIBUTE_NORMAL, nullptr);
if (handle == INVALID_HANDLE_VALUE) {
errno = EACCES;
return nullptr;
}
const auto fd = _open_osfhandle(reinterpret_cast<intptr_t>(handle), open_flags);
if (fd == -1) {
CloseHandle(handle);
return nullptr;
}
auto* const file = _wfdopen(fd, AccessModeToWStr(mode, type));
if (file == nullptr) {
_close(fd);
}
return file;
}
#else
/**
@@ -254,7 +313,9 @@ void IOFile::Open(const fs::path& path, FileAccessMode mode, FileType type, File
errno = 0;
#ifdef _WIN32
if (flag != FileShareFlag::ShareNone) {
if (flag == FileShareFlag::ShareReadWriteDelete) {
file = OpenWithWindowsShareDelete(path, mode, type);
} else if (flag != FileShareFlag::ShareNone) {
file = _wfsopen(path.c_str(), AccessModeToWStr(mode, type), ToWindowsFileShareFlag(flag));
} else {
_wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type));
+6 -5
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -49,10 +49,11 @@ enum class FileType {
};
enum class FileShareFlag {
ShareNone, // Provides exclusive access to the file.
ShareReadOnly, // Provides read only shared access to the file.
ShareWriteOnly, // Provides write only shared access to the file.
ShareReadWrite, // Provides read and write shared access to the file.
ShareNone, // Provides exclusive access to the file.
ShareReadOnly, // Provides read only shared access to the file.
ShareWriteOnly, // Provides write only shared access to the file.
ShareReadWrite, // Provides read and write shared access to the file.
ShareReadWriteDelete, // Provides read, write, and delete shared access to the file.
};
enum class DirEntryFilter {
+12 -1
View File
@@ -702,7 +702,15 @@ struct Values {
// Controls
InputSetting<std::array<PlayerInput, 10>> players;
Setting<bool> disable_wgi_xinput{
linkage, false, "disable_wgi_xinput", Category::Controls, Specialization::Default,
// Only read/write disable_wgi_xinput on Windows platforms
#ifdef _WIN32
true
#else
false
#endif
};
Setting<bool> enable_raw_input{
linkage, false, "enable_raw_input", Category::Controls, Specialization::Default,
// Only read/write enable_raw_input on Windows platforms
@@ -832,6 +840,9 @@ struct Values {
Setting<bool> gpu_log_driver_debug{linkage, true, "gpu_log_driver_debug", Category::Debugging};
Setting<s32> gpu_log_ring_buffer_size{linkage, 512, "gpu_log_ring_buffer_size",
Category::Debugging};
Setting<HomebrewNxlinkServerMode> homebrew_nxlink_server_mode{
linkage, HomebrewNxlinkServerMode::Disabled, "homebrew_nxlink_server_mode",
Category::Debugging};
SwitchableSetting<u16, true> debug_knobs{linkage,
0,
+1
View File
@@ -159,6 +159,7 @@ ENUM(GpuUnswizzleChunk, VeryLow, Low, Normal, Medium, High)
ENUM(TemperatureUnits, Celsius, Fahrenheit)
ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3);
ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All)
ENUM(HomebrewNxlinkServerMode, Disabled, EdenLog, HostStdout, File)
ENUM(GameListMode, TreeView, GridView, CarouselView);
ENUM(SpeedMode, Standard, Turbo, Slow);
+20 -139
View File
@@ -1,6 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -40,110 +39,6 @@
#include <unistd.h>
#endif
#ifdef __ANDROID__
#include <sys/resource.h>
#include <algorithm>
#include <fstream>
#include <utility>
#include <vector>
namespace {
[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_URGENT_AUDIO = -19;
[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_AUDIO = -16;
[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_URGENT_DISPLAY = -8;
[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_DISPLAY = -4;
[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_FOREGROUND = -2;
[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_MORE_FAVORABLE = -1;
[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_DEFAULT = 0;
[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_LESS_FAVORABLE = 1;
[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_BACKGROUND = 10;
[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_LOWEST = 19;
constexpr size_t ANDROID_MINIMUM_PERFORMANCE_CORES = 4;
cpu_set_t ComputePerformanceCoreMask() {
cpu_set_t mask;
CPU_ZERO(&mask);
cpu_set_t allowed;
CPU_ZERO(&allowed);
if (sched_getaffinity(gettid(), sizeof(allowed), &allowed) != 0) {
return mask;
}
std::vector<std::pair<long, int>> cores;
const int total = static_cast<int>(std::thread::hardware_concurrency());
for (int cpu = 0; cpu < total; ++cpu) {
if (!CPU_ISSET(cpu, &allowed)) {
continue;
}
long max_frequency = 0;
std::ifstream file("/sys/devices/system/cpu/cpu" + std::to_string(cpu) +
"/cpufreq/cpuinfo_max_freq");
if (!file || !(file >> max_frequency) || max_frequency <= 0) {
CPU_ZERO(&mask);
return mask;
}
cores.emplace_back(max_frequency, cpu);
}
if (cores.empty()) {
return mask;
}
std::sort(cores.begin(), cores.end(),
[](const auto& lhs, const auto& rhs) { return lhs.first > rhs.first; });
size_t taken = 0;
long cluster_frequency = cores.front().first;
for (const auto& [frequency, cpu] : cores) {
if (frequency != cluster_frequency) {
if (taken >= ANDROID_MINIMUM_PERFORMANCE_CORES) {
break;
}
cluster_frequency = frequency;
}
CPU_SET(cpu, &mask);
++taken;
}
return mask;
}
const cpu_set_t& PerformanceCoreMask() {
static const cpu_set_t mask = ComputePerformanceCoreMask();
return mask;
}
cpu_set_t ComputeEfficiencyCoreMask() {
cpu_set_t mask;
CPU_ZERO(&mask);
const cpu_set_t& performance = PerformanceCoreMask();
if (CPU_COUNT(&performance) == 0) {
return mask;
}
cpu_set_t allowed;
CPU_ZERO(&allowed);
if (sched_getaffinity(gettid(), sizeof(allowed), &allowed) != 0) {
return mask;
}
const int total = static_cast<int>(std::thread::hardware_concurrency());
for (int cpu = 0; cpu < total; ++cpu) {
if (CPU_ISSET(cpu, &allowed) && !CPU_ISSET(cpu, &performance)) {
CPU_SET(cpu, &mask);
}
}
return mask;
}
const cpu_set_t& EfficiencyCoreMask() {
static const cpu_set_t mask = ComputeEfficiencyCoreMask();
return mask;
}
} // Anonymous namespace
#endif
#include "common/cpu_features.h"
#ifdef ARCHITECTURE_x86_64
#ifdef _MSC_VER
@@ -183,21 +78,6 @@ void SetCurrentThreadPriority(ThreadPriority new_priority) {
}
}();
set_thread_priority(find_thread(NULL), priority);
#elif defined(__ANDROID__)
const int nice_value = [&]() {
switch (new_priority) {
case ThreadPriority::Low: return ANDROID_THREAD_PRIORITY_BACKGROUND;
case ThreadPriority::Normal: return ANDROID_THREAD_PRIORITY_DEFAULT;
case ThreadPriority::High: return ANDROID_THREAD_PRIORITY_DISPLAY;
case ThreadPriority::VeryHigh: return ANDROID_THREAD_PRIORITY_URGENT_DISPLAY;
case ThreadPriority::Critical: return ANDROID_THREAD_PRIORITY_AUDIO;
default: return ANDROID_THREAD_PRIORITY_DEFAULT;
}
}();
if (setpriority(PRIO_PROCESS, static_cast<id_t>(gettid()), nice_value) != 0) {
LOG_DEBUG(Common, "Could not set thread nice value to {}: {}", nice_value,
GetLastErrorMsg());
}
#else
pthread_t this_thread = pthread_self();
const auto scheduling_type = SCHED_OTHER;
@@ -252,28 +132,29 @@ void SetCurrentThreadName(const char* name) {
#endif
}
void SetCurrentThreadToPerformanceCores() {
void PinCurrentThreadToPerformanceCore(size_t core_id) {
ASSERT(core_id < 4);
// If we set a flag for a CPU that doesn't exist, the thread may not be allowed to
// run in ANY processor!
auto const total_cores = std::thread::hardware_concurrency();
if (core_id < total_cores) {
#if defined(__ANDROID__)
const cpu_set_t& mask = PerformanceCoreMask();
if (CPU_COUNT(&mask) == 0) {
return;
}
if (sched_setaffinity(gettid(), sizeof(mask), &mask) != 0) {
LOG_DEBUG(Common, "Could not restrict thread to performance cores: {}", GetLastErrorMsg());
}
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(core_id, &set);
sched_setaffinity(pthread_self(), sizeof(set), &set);
#elif defined(__linux__) || defined(__FreeBSD__)
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(core_id, &set);
pthread_setaffinity_np(pthread_self(), sizeof(set), &set);
#elif defined(_WIN32)
DWORD set = 1UL << core_id;
SetThreadAffinityMask(GetCurrentThread(), set);
#else
// No pin functionality implemented
#endif
}
void SetCurrentThreadToEfficiencyCores() {
#if defined(__ANDROID__)
const cpu_set_t& mask = EfficiencyCoreMask();
if (CPU_COUNT(&mask) == 0) {
return;
}
if (sched_setaffinity(gettid(), sizeof(mask), &mask) != 0) {
LOG_DEBUG(Common, "Could not restrict thread to efficiency cores: {}", GetLastErrorMsg());
}
#endif
}
#ifdef ARCHITECTURE_x86_64
+1 -7
View File
@@ -99,14 +99,8 @@ enum class ThreadPriority : u32 {
Critical = 4,
};
enum class ThreadPlacement : u32 {
Default = 0,
Background = 1,
};
void SetCurrentThreadPriority(ThreadPriority new_priority);
void SetCurrentThreadName(const char* name);
void SetCurrentThreadToPerformanceCores();
void SetCurrentThreadToEfficiencyCores();
void PinCurrentThreadToPerformanceCore(size_t core_id);
} // namespace Common
+3 -8
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -37,15 +37,10 @@ class StatefulThreadWorker {
using StateMaker = std::conditional_t<with_state, std::function<StateType()>, DummyCallable>;
public:
explicit StatefulThreadWorker(size_t num_workers, std::string name, StateMaker func = {},
ThreadPlacement placement = ThreadPlacement::Default)
explicit StatefulThreadWorker(size_t num_workers, std::string name, StateMaker func = {})
: workers_queued{num_workers}, thread_name{std::move(name)} {
const auto lambda = [this, func, placement](std::stop_token stop_token) {
const auto lambda = [this, func](std::stop_token stop_token) {
Common::SetCurrentThreadName(thread_name.c_str());
if (placement == ThreadPlacement::Background) {
Common::SetCurrentThreadPriority(ThreadPriority::Low);
Common::SetCurrentThreadToEfficiencyCores();
}
{
[[maybe_unused]] std::conditional_t<with_state, StateType, int> state{func()};
while (!stop_token.stop_requested()) {
+2
View File
@@ -1139,6 +1139,8 @@ add_library(core STATIC
launch_timestamp_cache.h
loader/deconstructed_rom_directory.cpp
loader/deconstructed_rom_directory.h
loader/homebrew_nxlink.cpp
loader/homebrew_nxlink.h
loader/kip.cpp
loader/kip.h
loader/loader.cpp
+2
View File
@@ -52,6 +52,7 @@
#include "core/hle/service/set/system_settings_server.h"
#include "core/hle/service/sm/sm.h"
#include "core/internal_network/network.h"
#include "core/loader/homebrew_nxlink.h"
#include "core/loader/loader.h"
#include "core/memory.h"
#include "core/memory/cheat_engine.h"
@@ -397,6 +398,7 @@ struct System::Impl {
stop_event.request_stop();
core_timing.SyncPause(false);
Loader::HomebrewNxlink::StopServer();
Network::CancelPendingSocketOperations();
kernel.SuspendEmulation(true);
kernel.CloseServices();
+6 -1
View File
@@ -174,7 +174,12 @@ void CpuManager::RunThread(std::stop_token token, std::size_t core) {
std::string name = is_multicore ? ("CPUCore_" + std::to_string(core)) : std::string{"CPUThread"};
Common::SetCurrentThreadName(name.c_str());
Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical);
Common::SetCurrentThreadToPerformanceCores();
#ifdef __ANDROID__
// Aimed specifically for Snapdragon 8 Elite devices
// This kills performance on desktop, but boosts perf for UMA devices
// like the S8E. Mediatek and Mali likely won't suffer.
Common::PinCurrentThreadToPerformanceCore(core);
#endif
auto& data = core_data[core];
data.host_context = Common::Fiber::ThreadToFiber();
+1
View File
@@ -12,6 +12,7 @@
namespace FileSys {
enum class OpenMode : u32 {
Default = 0,
Read = (1 << 0),
Write = (1 << 1),
AllowAppend = (1 << 2),
+7 -4
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -40,7 +40,7 @@ VfsEntryType VfsFilesystem::GetEntryType(std::string_view path_) const {
VirtualFile VfsFilesystem::OpenFile(std::string_view path_, OpenMode perms) {
const auto path = Common::FS::SanitizePath(path_);
return root->GetFileRelative(path);
return root->GetFileRelative(path, perms);
}
VirtualFile VfsFilesystem::CreateFile(std::string_view path_, OpenMode perms) {
@@ -201,7 +201,7 @@ std::string VfsFile::GetFullPath() const {
return GetContainingDirectory()->GetFullPath() + '/' + GetName();
}
VirtualFile VfsDirectory::GetFileRelative(std::string_view path) const {
VirtualFile VfsDirectory::GetFileRelative(std::string_view path, OpenMode perms) const {
auto vec = Common::FS::SplitPathComponents(path);
if (vec.empty()) {
return nullptr;
@@ -224,7 +224,10 @@ VirtualFile VfsDirectory::GetFileRelative(std::string_view path) const {
return nullptr;
}
return dir->GetFile(vec.back());
if (perms == OpenMode::Default) {
return dir->GetFile(vec.back());
}
return dir->GetFileRelative(vec.back(), perms);
}
VirtualFile VfsDirectory::GetFileAbsolute(std::string_view path) const {
+2 -1
View File
@@ -201,7 +201,8 @@ public:
// Retrieves the file located at path as if the current directory was root. Returns nullptr if
// not found.
virtual VirtualFile GetFileRelative(std::string_view path) const;
virtual VirtualFile GetFileRelative(std::string_view path,
OpenMode perms = OpenMode::Default) const;
// Calls GetFileRelative(path) on the root of the current directory.
virtual VirtualFile GetFileAbsolute(std::string_view path) const;
+2 -2
View File
@@ -27,9 +27,9 @@ VirtualDir LayeredVfsDirectory::MakeLayeredDirectory(std::vector<VirtualDir> dir
return VirtualDir(new LayeredVfsDirectory(std::move(dirs), std::move(name)));
}
VirtualFile LayeredVfsDirectory::GetFileRelative(std::string_view path) const {
VirtualFile LayeredVfsDirectory::GetFileRelative(std::string_view path, OpenMode perms) const {
for (const auto& layer : dirs) {
const auto file = layer->GetFileRelative(path);
const auto file = layer->GetFileRelative(path, perms);
if (file != nullptr)
return file;
}
+5 -1
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -20,7 +23,8 @@ public:
/// Wrapper function to allow for more efficient handling of dirs.size() == 0, 1 cases.
static VirtualDir MakeLayeredDirectory(std::vector<VirtualDir> dirs, std::string name = "");
VirtualFile GetFileRelative(std::string_view path) const override;
VirtualFile GetFileRelative(std::string_view path,
OpenMode perms = OpenMode::Default) const override;
VirtualDir GetDirectoryRelative(std::string_view path) const override;
VirtualFile GetFile(std::string_view file_name) const override;
VirtualDir GetSubdirectory(std::string_view subdir_name) const override;
+51 -23
View File
@@ -12,7 +12,6 @@
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/vfs/vfs_real.h"
@@ -46,17 +45,11 @@ bool IsWithinRoot(std::string_view root, std::string_view full_path) {
}
constexpr FS::FileAccessMode ModeFlagsToFileAccessMode(OpenMode mode) {
switch (mode) {
case OpenMode::Read:
return FS::FileAccessMode::Read;
case OpenMode::Write:
case OpenMode::ReadWrite:
case OpenMode::AllowAppend:
case OpenMode::All:
if (True(mode & OpenMode::Write) || True(mode & OpenMode::AllowAppend)) {
return FS::FileAccessMode::ReadWrite;
default:
return {};
}
return FS::FileAccessMode::Read;
}
} // Anonymous namespace
@@ -94,9 +87,11 @@ VirtualFile RealVfsFilesystem::OpenFileFromEntry(std::string_view path_, std::op
std::optional<std::string> parent_path,
OpenMode perms) {
const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
const auto open_perms = perms == OpenMode::Default ? OpenMode::Read : perms;
std::scoped_lock lk{list_lock};
if (auto it = cache.find(path); it != cache.end()) {
const CacheKey cache_key{path, open_perms};
if (auto it = cache.find(cache_key); it != cache.end()) {
if (auto file = it->second.lock(); file) {
return file;
}
@@ -110,8 +105,9 @@ VirtualFile RealVfsFilesystem::OpenFileFromEntry(std::string_view path_, std::op
this->InsertReferenceIntoListLocked(*reference);
auto file = std::shared_ptr<RealVfsFile>(
new RealVfsFile(*this, std::move(reference), path, perms, size, std::move(parent_path)));
cache[path] = file;
new RealVfsFile(*this, std::move(reference), path, open_perms, size,
std::move(parent_path)));
cache[cache_key] = file;
return file;
}
@@ -124,7 +120,7 @@ VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, OpenMode perms
const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
{
std::scoped_lock lk{list_lock};
cache.erase(path);
CloseCachedFileReferenceLocked(path);
}
// Current usages of CreateFile expect to delete the contents of an existing file.
@@ -157,8 +153,8 @@ VirtualFile RealVfsFilesystem::MoveFile(std::string_view old_path_, std::string_
const auto new_path = FS::SanitizePath(new_path_, FS::DirectorySeparator::PlatformDefault);
{
std::scoped_lock lk{list_lock};
cache.erase(old_path);
cache.erase(new_path);
CloseCachedFileReferenceLocked(old_path);
CloseCachedFileReferenceLocked(new_path);
}
if (!FS::RenameFile(old_path, new_path)) {
return nullptr;
@@ -170,14 +166,15 @@ bool RealVfsFilesystem::DeleteFile(std::string_view path_) {
const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
{
std::scoped_lock lk{list_lock};
cache.erase(path);
CloseCachedFileReferenceLocked(path);
}
return FS::RemoveFile(path);
}
VirtualDir RealVfsFilesystem::OpenDirectory(std::string_view path_, OpenMode perms) {
const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms));
return std::shared_ptr<RealVfsDirectory>(
new RealVfsDirectory(*this, path, perms == OpenMode::Default ? OpenMode::Read : perms));
}
VirtualDir RealVfsFilesystem::CreateDirectory(std::string_view path_, OpenMode perms) {
@@ -222,8 +219,8 @@ std::unique_lock<std::mutex> RealVfsFilesystem::RefreshReference(const std::stri
if (!reference.file) {
this->EvictSingleReferenceLocked();
reference.file =
FS::FileOpen(path, ModeFlagsToFileAccessMode(perms), FS::FileType::BinaryFile);
reference.file = FS::FileOpen(path, ModeFlagsToFileAccessMode(perms),
FS::FileType::BinaryFile, FS::FileShareFlag::ShareReadWriteDelete);
if (reference.file) {
num_open_files++;
}
@@ -297,15 +294,45 @@ RealVfsFile::~RealVfsFile() {
base.DropReference(std::move(reference));
}
void RealVfsFilesystem::CloseCachedFileReferenceLocked(const std::string& path) {
for (auto it = cache.lower_bound(CacheKey{path, OpenMode::Default});
it != cache.end() && it->first.first == path;) {
const auto cached_file = it->second.lock();
if (cached_file) {
auto* const real_file = static_cast<RealVfsFile*>(cached_file.get());
auto& reference = real_file->reference;
if (reference && reference->file) {
RemoveReferenceFromListLocked(*reference);
reference->file.reset();
num_open_files--;
InsertReferenceIntoListLocked(*reference);
}
}
it = cache.erase(it);
}
}
std::string RealVfsFile::GetName() const {
#ifdef __ANDROID__
if (path[0] != '/') {
if (!path.empty() && path[0] != '/') {
return FS::Android::GetFilename(path);
}
#endif
return path_components.empty() ? "" : std::string(path_components.back());
}
std::string RealVfsFile::GetFullPath() const {
#ifdef __ANDROID__
if (!path.empty() && path[0] != '/') {
auto out = path;
std::replace(out.begin(), out.end(), '\\', '/');
return out;
}
#endif
return VfsFile::GetFullPath();
}
std::size_t RealVfsFile::GetSize() const {
if (size) {
return *size;
@@ -411,13 +438,14 @@ RealVfsDirectory::RealVfsDirectory(RealVfsFilesystem& base_, const std::string&
RealVfsDirectory::~RealVfsDirectory() = default;
VirtualFile RealVfsDirectory::GetFileRelative(std::string_view relative_path) const {
VirtualFile RealVfsDirectory::GetFileRelative(std::string_view relative_path,
OpenMode open_perms) const {
const auto full_path = FS::SanitizePath(path + '/' + std::string(relative_path));
if (!FS::Exists(full_path) || FS::IsDir(full_path)
|| !IsWithinRoot(FS::SanitizePath(path), full_path)) {
return nullptr;
}
return base.OpenFile(full_path, perms);
return base.OpenFile(full_path, open_perms == OpenMode::Default ? perms : open_perms);
}
VirtualDir RealVfsDirectory::GetDirectoryRelative(std::string_view relative_path) const {
+7 -2
View File
@@ -10,6 +10,7 @@
#include <mutex>
#include <optional>
#include <string_view>
#include <utility>
#include "common/intrusive_list.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/vfs/vfs.h"
@@ -49,8 +50,9 @@ public:
bool DeleteDirectory(std::string_view path) override;
private:
using CacheKey = std::pair<std::string, OpenMode>;
using ReferenceListType = Common::IntrusiveListBaseTraits<FileReference>::ListType;
std::map<std::string, std::weak_ptr<VfsFile>, std::less<>> cache;
std::map<CacheKey, std::weak_ptr<VfsFile>, std::less<>> cache;
ReferenceListType open_references;
ReferenceListType closed_references;
std::mutex list_lock;
@@ -63,6 +65,7 @@ private:
std::unique_lock<std::mutex> RefreshReference(const std::string& path, OpenMode perms,
FileReference& reference);
void DropReference(std::unique_ptr<FileReference>&& reference);
void CloseCachedFileReferenceLocked(const std::string& path);
private:
friend class RealVfsDirectory;
@@ -85,6 +88,7 @@ public:
~RealVfsFile() override;
std::string GetName() const override;
std::string GetFullPath() const override;
std::size_t GetSize() const override;
bool Resize(std::size_t new_size) override;
VirtualDir GetContainingDirectory() const override;
@@ -115,7 +119,8 @@ class RealVfsDirectory : public VfsDirectory {
public:
~RealVfsDirectory() override;
VirtualFile GetFileRelative(std::string_view relative_path) const override;
VirtualFile GetFileRelative(std::string_view relative_path,
OpenMode perms = OpenMode::Default) const override;
VirtualDir GetDirectoryRelative(std::string_view relative_path) const override;
VirtualFile GetFile(std::string_view name) const override;
VirtualDir GetSubdirectory(std::string_view name) const override;
+5
View File
@@ -670,6 +670,11 @@ public:
size_t GetHeapRegionSize() const {
return m_heap_region_end - m_heap_region_start;
}
size_t GetCurrentHeapSize() const {
KScopedLightLock lk(m_general_lock);
return m_current_heap_end - m_heap_region_start;
}
size_t GetAliasRegionSize() const {
return m_alias_region_end - m_alias_region_start;
}
+11
View File
@@ -210,6 +210,11 @@ Result KProcess::Initialize(KernelCore& kernel, const Svc::CreateProcessParamete
m_arg_pointer = 0;
m_arg_return_address = 0;
m_main_thread_handle_addr = 0;
m_process_handle_addr = 0;
m_homebrew_next_load_path_addr = 0;
m_homebrew_next_load_argv_addr = 0;
m_is_homebrew_in_place_next_load = false;
m_has_homebrew_nxlink_argv_marker = false;
m_code_size = params.code_num_pages * PageSize;
m_is_application = True(params.flags & Svc::CreateProcessFlag::IsApplication);
@@ -947,6 +952,7 @@ Result KProcess::Run(KernelCore& kernel, s32 priority, size_t stack_size) {
stack_top = stack_bottom + stack_size;
m_main_thread_stack_size = stack_size;
m_main_thread_stack_top = stack_top;
}
// Ensure our stack is safe to clean up on exit.
@@ -1005,6 +1011,11 @@ Result KProcess::Run(KernelCore& kernel, s32 priority, size_t stack_size) {
if (GetInteger(m_main_thread_handle_addr) != 0) {
this->GetMemory().Write32(m_main_thread_handle_addr, thread_handle);
}
if (GetInteger(m_process_handle_addr) != 0) {
Handle process_handle;
R_TRY(m_handle_table.Add(kernel, std::addressof(process_handle), this));
this->GetMemory().Write32(m_process_handle_addr, process_handle);
}
} else {
main_thread->GetContext().r[0] = 0;
main_thread->GetContext().r[1] = thread_handle;
+52
View File
@@ -6,7 +6,9 @@
#pragma once
#include <array>
#include <map>
#include <string_view>
#include "core/arm/arm_interface.h"
#include "core/file_sys/program_metadata.h"
@@ -87,6 +89,10 @@ private:
KProcessAddress m_arg_pointer{};
KProcessAddress m_arg_return_address{};
KProcessAddress m_main_thread_handle_addr{};
KProcessAddress m_process_handle_addr{};
KProcessAddress m_homebrew_next_load_path_addr{};
KProcessAddress m_homebrew_next_load_argv_addr{};
std::array<char, 16> m_homebrew_nxlink_argv_marker{};
KHandleTable m_handle_table;
KProcessAddress m_plr_address{};
ThreadList m_thread_list{};
@@ -112,6 +118,7 @@ private:
size_t m_code_size{};
size_t m_main_thread_stack_size{};
KProcessAddress m_main_thread_stack_top{};
size_t m_max_process_memory{};
size_t m_memory_release_hint{};
s64 m_schedule_count{};
@@ -139,6 +146,8 @@ private:
bool m_is_suspended : 1 = false;
bool m_is_immortal : 1 = false;
bool m_is_handle_table_initialized : 1 = false;
bool m_is_homebrew_in_place_next_load : 1 = false;
bool m_has_homebrew_nxlink_argv_marker : 1 = false;
private:
Result StartTermination(KernelCore& kernel);
@@ -231,6 +240,49 @@ public:
void SetMainThreadHandleAddr(KProcessAddress addr) {
m_main_thread_handle_addr = addr;
}
void SetProcessHandleAddr(KProcessAddress addr) {
m_process_handle_addr = addr;
}
void SetHomebrewNextLoadBufferAddrs(KProcessAddress path_addr, KProcessAddress argv_addr) {
m_homebrew_next_load_path_addr = path_addr;
m_homebrew_next_load_argv_addr = argv_addr;
}
void SetHomebrewInPlaceNextLoad(bool enabled) {
m_is_homebrew_in_place_next_load = enabled;
}
void SetHomebrewNxlinkArgvMarker(std::string_view marker) {
if (marker.size() != m_homebrew_nxlink_argv_marker.size()) {
m_has_homebrew_nxlink_argv_marker = false;
return;
}
marker.copy(m_homebrew_nxlink_argv_marker.data(), m_homebrew_nxlink_argv_marker.size());
m_has_homebrew_nxlink_argv_marker = true;
}
void ClearHomebrewNxlinkArgvMarker() {
m_has_homebrew_nxlink_argv_marker = false;
}
std::string_view GetHomebrewNxlinkArgvMarker() const {
if (!m_has_homebrew_nxlink_argv_marker) {
return {};
}
return {m_homebrew_nxlink_argv_marker.data(), m_homebrew_nxlink_argv_marker.size()};
}
bool IsHomebrewInPlaceNextLoad() const {
return m_is_homebrew_in_place_next_load;
}
KProcessAddress GetHomebrewNextLoadPathAddr() const {
return m_homebrew_next_load_path_addr;
}
KProcessAddress GetHomebrewNextLoadArgvAddr() const {
return m_homebrew_next_load_argv_addr;
}
size_t GetCodeSize() const {
return m_code_size;
}
KProcessAddress GetMainThreadStackTop() const {
return m_main_thread_stack_top;
}
size_t GetMainStackSize() const {
return m_main_thread_stack_size;
+5 -4
View File
@@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-late
// SPDX-License-Identifier: GPL-2.0-or-later
// This file is automatically generated using svc_generator.py.
// DO NOT MODIFY IT MANUALLY
@@ -128,7 +129,7 @@ static void SvcWrap_QueryMemory64From32(Core::System& system, std::span<uint64_t
}
static void SvcWrap_ExitProcess64From32(Core::System& system, std::span<uint64_t, 8> args) {
ExitProcess64From32(system);
ExitProcess64From32(system, args);
}
static void SvcWrap_CreateThread64From32(Core::System& system, std::span<uint64_t, 8> args) {
@@ -1298,7 +1299,7 @@ static void SvcWrap_QueryMemory64(Core::System& system, std::span<uint64_t, 8> a
}
static void SvcWrap_ExitProcess64(Core::System& system, std::span<uint64_t, 8> args) {
ExitProcess64(system);
ExitProcess64(system, args);
}
static void SvcWrap_CreateThread64(Core::System& system, std::span<uint64_t, 8> args) {
+6 -5
View File
@@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-late
// SPDX-License-Identifier: GPL-2.0-or-later
// This file is automatically generated using svc_generator.py.
// DO NOT MODIFY IT MANUALLY
@@ -25,7 +26,7 @@ Result SetMemoryAttribute(Core::System& system, uint64_t address, uint64_t size,
Result MapMemory(Core::System& system, uint64_t dst_address, uint64_t src_address, uint64_t size);
Result UnmapMemory(Core::System& system, uint64_t dst_address, uint64_t src_address, uint64_t size);
Result QueryMemory(Core::System& system, uint64_t out_memory_info, PageInfo* out_page_info, uint64_t address);
void ExitProcess(Core::System& system);
void ExitProcess(Core::System& system, std::span<uint64_t, 8> args);
Result CreateThread(Core::System& system, Handle* out_handle, uint64_t func, uint64_t arg, uint64_t stack_bottom, int32_t priority, int32_t core_id);
Result StartThread(Core::System& system, Handle thread_handle);
void ExitThread(Core::System& system);
@@ -146,7 +147,7 @@ Result SetMemoryAttribute64From32(Core::System& system, uint32_t address, uint32
Result MapMemory64From32(Core::System& system, uint32_t dst_address, uint32_t src_address, uint32_t size);
Result UnmapMemory64From32(Core::System& system, uint32_t dst_address, uint32_t src_address, uint32_t size);
Result QueryMemory64From32(Core::System& system, uint32_t out_memory_info, PageInfo* out_page_info, uint32_t address);
void ExitProcess64From32(Core::System& system);
void ExitProcess64From32(Core::System& system, std::span<uint64_t, 8> args);
Result CreateThread64From32(Core::System& system, Handle* out_handle, uint32_t func, uint32_t arg, uint32_t stack_bottom, int32_t priority, int32_t core_id);
Result StartThread64From32(Core::System& system, Handle thread_handle);
void ExitThread64From32(Core::System& system);
@@ -267,7 +268,7 @@ Result SetMemoryAttribute64(Core::System& system, uint64_t address, uint64_t siz
Result MapMemory64(Core::System& system, uint64_t dst_address, uint64_t src_address, uint64_t size);
Result UnmapMemory64(Core::System& system, uint64_t dst_address, uint64_t src_address, uint64_t size);
Result QueryMemory64(Core::System& system, uint64_t out_memory_info, PageInfo* out_page_info, uint64_t address);
void ExitProcess64(Core::System& system);
void ExitProcess64(Core::System& system, std::span<uint64_t, 8> args);
Result CreateThread64(Core::System& system, Handle* out_handle, uint64_t func, uint64_t arg, uint64_t stack_bottom, int32_t priority, int32_t core_id);
Result StartThread64(Core::System& system, Handle thread_handle);
void ExitThread64(Core::System& system);
+215 -6
View File
@@ -4,6 +4,9 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <vector>
#include "core/core.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/svc.h"
@@ -22,6 +25,179 @@ constexpr bool IsValidSetMemoryPermission(MemoryPermission perm) {
}
}
bool IsHomebrewInPlaceNextLoadCodeRange(const KProcess& process, u64 address, u64 size) {
if (!process.IsHomebrewInPlaceNextLoad()) {
return false;
}
const u64 code_start = GetInteger(process.GetEntryPoint());
const size_t code_size = process.GetCodeSize();
if (code_start == 0 || code_size == 0 || size > code_size || address < code_start) {
return false;
}
return address - code_start <= code_size - size;
}
struct HomebrewInPlaceMemoryBlock {
u64 address;
u64 size;
KMemoryState state;
KMemoryPermission permission;
KMemoryAttribute attribute;
bool use_process_permission;
};
Result SetHomebrewInPlaceMemoryPermissionByBlocks(KProcess& process, u64 address, u64 size,
MemoryPermission perm, Result original_result) {
auto& page_table = process.GetPageTable();
const u64 end = address + size;
const auto requested_permission = ConvertToKMemoryPermission(perm);
std::vector<HomebrewInPlaceMemoryBlock> blocks;
for (u64 cursor = address; cursor < end;) {
KMemoryInfo info;
PageInfo page_info;
const auto query_result = page_table.QueryInfo(std::addressof(info),
std::addressof(page_info), cursor);
if (query_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: split permission query failed "
"address=0x{:016X}, result={:#X}, original={:#X}",
cursor, query_result.raw, original_result.raw);
R_RETURN(original_result);
}
const u64 block_end = (std::min<u64>)(info.GetEndAddress(), end);
if (block_end <= cursor) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: split permission walk stalled "
"cursor=0x{:016X}, block=0x{:016X}/0x{:X}, original={:#X}",
cursor, info.GetAddress(), info.GetSize(), original_result.raw);
R_RETURN(original_result);
}
const bool can_reprotect = True(info.GetState() & KMemoryState::FlagCanReprotect);
const bool can_process_reprotect = True(info.GetState() & KMemoryState::FlagCode);
if (!can_reprotect && !can_process_reprotect) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: split permission unsupported block "
"address=0x{:016X}, size=0x{:X}, state=0x{:08X}, svc_state={}, "
"perm=0x{:08X}, attr=0x{:08X}, original={:#X}",
cursor, block_end - cursor, static_cast<u32>(info.GetState()),
static_cast<u32>(info.GetSvcState()),
static_cast<u32>(info.GetPermission()),
static_cast<u32>(info.GetAttribute()), original_result.raw);
R_RETURN(original_result);
}
blocks.push_back({
.address = cursor,
.size = block_end - cursor,
.state = info.GetState(),
.permission = info.GetPermission(),
.attribute = info.GetAttribute(),
.use_process_permission = !can_reprotect && can_process_reprotect,
});
cursor = block_end;
}
for (const auto& block : blocks) {
if (block.permission == requested_permission) {
continue;
}
const auto block_result =
block.use_process_permission
? page_table.SetProcessMemoryPermission(block.address, block.size, perm)
: page_table.SetMemoryPermission(block.address, block.size, perm);
if (block_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: split permission block failed "
"address=0x{:016X}, size=0x{:X}, state=0x{:08X}, perm=0x{:08X}, "
"attr=0x{:08X}, result={:#X}, original={:#X}",
block.address, block.size, static_cast<u32>(block.state),
static_cast<u32>(block.permission), static_cast<u32>(block.attribute),
block_result.raw, original_result.raw);
R_RETURN(block_result);
}
}
R_SUCCEED();
}
struct HomebrewInPlaceDeviceSharedBlock {
u64 address;
u64 size;
u16 device_use_count;
};
Result UnlockHomebrewInPlaceDeviceSharedSource(KProcess& process, u64 address, u64 size,
Result original_result) {
auto& page_table = process.GetPageTable();
const u64 end = address + size;
std::vector<HomebrewInPlaceDeviceSharedBlock> blocks;
for (u64 cursor = address; cursor < end;) {
KMemoryInfo info;
PageInfo page_info;
const auto query_result = page_table.QueryInfo(std::addressof(info),
std::addressof(page_info), cursor);
if (query_result.IsError()) {
R_RETURN(original_result);
}
const u64 block_end = (std::min<u64>)(info.GetEndAddress(), end);
if (block_end <= cursor) {
R_RETURN(original_result);
}
const bool can_device_map = True(info.GetState() & KMemoryState::FlagCanDeviceMap);
const bool is_clean_memory =
can_device_map && info.GetPermission() == KMemoryPermission::UserReadWrite &&
info.GetAttribute() == KMemoryAttribute::None && info.m_device_use_count == 0;
const bool is_stale_device_shared =
can_device_map && info.GetPermission() == KMemoryPermission::UserReadWrite &&
info.GetAttribute() == KMemoryAttribute::DeviceShared && info.m_device_use_count > 0;
if (is_clean_memory) {
cursor = block_end;
continue;
}
if (!is_stale_device_shared) {
R_RETURN(original_result);
}
blocks.push_back({
.address = cursor,
.size = block_end - cursor,
.device_use_count = info.m_device_use_count,
});
cursor = block_end;
}
if (blocks.empty()) {
R_RETURN(original_result);
}
for (const auto& block : blocks) {
for (u16 unlock = 0; unlock < block.device_use_count; unlock++) {
const auto unlock_result =
page_table.UnlockForDeviceAddressSpace(block.address, block.size);
if (unlock_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: device-shared source unlock failed "
"address=0x{:016X}, size=0x{:X}, remaining={}, result={:#X}, "
"original={:#X}",
block.address, block.size, block.device_use_count - unlock - 1,
unlock_result.raw, original_result.raw);
R_RETURN(original_result);
}
}
}
R_SUCCEED();
}
// Checks if address + size is greater than the given address
// This can return false if the size causes an overflow of a 64-bit type
// or if the given size is zero.
@@ -92,11 +268,19 @@ Result SetMemoryPermission(Core::System& system, u64 address, u64 size, MemoryPe
R_UNLESS(IsValidSetMemoryPermission(perm), ResultInvalidNewMemoryPermission);
// Validate that the region is in range for the current process.
auto& page_table = GetCurrentProcess(system.Kernel()).GetPageTable();
auto& process = GetCurrentProcess(system.Kernel());
auto& page_table = process.GetPageTable();
R_UNLESS(page_table.Contains(address, size), ResultInvalidCurrentMemory);
// Set the memory attribute.
R_RETURN(page_table.SetMemoryPermission(address, size, perm));
const auto result = page_table.SetMemoryPermission(address, size, perm);
if (result.raw == ResultInvalidCurrentMemory.raw &&
IsHomebrewInPlaceNextLoadCodeRange(process, address, size)) {
R_RETURN(
SetHomebrewInPlaceMemoryPermissionByBlocks(process, address, size, perm, result));
}
R_RETURN(result);
}
Result SetMemoryAttribute(Core::System& system, u64 address, u64 size, u32 mask, u32 attr) {
@@ -135,14 +319,30 @@ Result MapMemory(Core::System& system, u64 dst_addr, u64 src_addr, u64 size) {
LOG_TRACE(Kernel_SVC, "called, dst_addr={:#x}, src_addr={:#x}, size={:#x}", dst_addr,
src_addr, size);
auto& page_table{GetCurrentProcess(system.Kernel()).GetPageTable()};
auto& process = GetCurrentProcess(system.Kernel());
auto& page_table = process.GetPageTable();
if (const Result result{MapUnmapMemorySanityChecks(page_table, dst_addr, src_addr, size)};
result.IsError()) {
return result;
}
R_RETURN(page_table.MapMemory(dst_addr, src_addr, size));
const auto result = page_table.MapMemory(dst_addr, src_addr, size);
if (result.raw == ResultInvalidCurrentMemory.raw && process.IsHomebrewInPlaceNextLoad()) {
if (UnlockHomebrewInPlaceDeviceSharedSource(process, src_addr, size, result).IsSuccess()) {
const auto retry_result = page_table.MapMemory(dst_addr, src_addr, size);
if (retry_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: svcMapMemory retry failed after "
"DeviceShared cleanup dst=0x{:016X}, src=0x{:016X}, size=0x{:X}, "
"result={:#X}",
dst_addr, src_addr, size, retry_result.raw);
}
R_RETURN(retry_result);
}
}
R_RETURN(result);
}
/// Unmaps a region that was previously mapped with svcMapMemory
@@ -150,14 +350,23 @@ Result UnmapMemory(Core::System& system, u64 dst_addr, u64 src_addr, u64 size) {
LOG_TRACE(Kernel_SVC, "called, dst_addr={:#x}, src_addr={:#x}, size={:#x}", dst_addr,
src_addr, size);
auto& page_table{GetCurrentProcess(system.Kernel()).GetPageTable()};
auto& process = GetCurrentProcess(system.Kernel());
auto& page_table = process.GetPageTable();
if (const Result result{MapUnmapMemorySanityChecks(page_table, dst_addr, src_addr, size)};
result.IsError()) {
return result;
}
R_RETURN(page_table.UnmapMemory(dst_addr, src_addr, size));
const auto result = page_table.UnmapMemory(dst_addr, src_addr, size);
if (result.raw == ResultInvalidCurrentMemory.raw && process.IsHomebrewInPlaceNextLoad()) {
LOG_WARNING(Kernel_SVC,
"NextLoad in-place: svcUnmapMemory failed dst=0x{:016X}, "
"src=0x{:016X}, size=0x{:X}, result={:#X}",
dst_addr, src_addr, size, result.raw);
}
R_RETURN(result);
}
Result SetMemoryPermission64(Core::System& system, uint64_t address, uint64_t size,
+204 -5
View File
@@ -4,17 +4,216 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/input.h"
#include "core/core.h"
#include "core/file_sys/vfs/vfs_types.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/k_thread.h"
#include "core/hle/kernel/kernel.h"
#include "core/hle/kernel/physical_core.h"
#include "core/hle/kernel/svc.h"
#include "core/hle/service/hid/hid_server.h"
#include "core/hle/service/nvdrv/nvdrv_interface.h"
#include "core/hle/service/sm/sm.h"
#include "core/loader/nro.h"
#include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h"
#include "hid_core/resource_manager.h"
namespace Kernel::Svc {
namespace {
constexpr size_t HomebrewNextLoadPathSize = 0x200;
constexpr size_t HomebrewNextLoadArgvSize = 0x800;
std::string ReadHomebrewString(Core::Memory::Memory& memory, KProcessAddress address,
size_t max_size) {
if (GetInteger(address) == 0) {
return {};
}
return memory.ReadCString(Common::ProcessAddress{GetInteger(address)}, max_size);
}
} // namespace
/// Exits the current process
void ExitProcess(Core::System& system) {
void ExitProcess(Core::System& system, std::span<uint64_t, 8> args) {
auto* current_process = GetCurrentProcessPointer(system.Kernel());
auto* current_thread = GetCurrentThreadPointer(system.Kernel());
LOG_INFO(Kernel_SVC, "Process {} exiting", current_process->GetProcessId());
const auto next_load_path_addr = current_process->GetHomebrewNextLoadPathAddr();
const auto next_load_argv_addr = current_process->GetHomebrewNextLoadArgvAddr();
if (GetInteger(next_load_path_addr) != 0) {
auto& memory = current_process->GetMemory();
const auto next_load_path =
ReadHomebrewString(memory, next_load_path_addr, HomebrewNextLoadPathSize);
const auto next_load_argv =
ReadHomebrewString(memory, next_load_argv_addr, HomebrewNextLoadArgvSize);
if (!next_load_path.empty()) {
auto guest_path = Common::FS::SanitizePath(next_load_path);
constexpr std::string_view SdmcPrefix{"sdmc:"};
FileSys::VirtualFile file{};
const bool is_sdmc_path = guest_path.rfind(SdmcPrefix, 0) == 0;
const bool is_absolute_guest_path = !guest_path.empty() && guest_path.front() == '/';
if (is_sdmc_path || is_absolute_guest_path) {
auto relative_path =
is_sdmc_path ? guest_path.substr(SdmcPrefix.size()) : guest_path;
while (!relative_path.empty() && relative_path.front() == '/') {
relative_path.erase(relative_path.begin());
}
const auto host_path = Common::FS::GetEdenPath(Common::FS::EdenPath::SDMCDir) /
std::filesystem::path{Common::FS::ToU8String(relative_path)};
const auto host_path_string = Common::FS::PathToUTF8String(host_path);
file = Core::GetGameFileFromPath(system.GetFilesystem(), host_path_string);
if (!file) {
LOG_WARNING(Kernel_SVC,
"NextLoad: failed to open guest_path='{}', host_path='{}'",
next_load_path, host_path_string);
}
} else {
file = Core::GetGameFileFromPath(system.GetFilesystem(), guest_path);
if (!file) {
LOG_WARNING(Kernel_SVC, "NextLoad: failed to open guest_path='{}'",
next_load_path);
}
}
if (file) {
const auto nvdrv =
system.ServiceManager().GetService<Service::Nvidia::NVDRV>("nvdrv:s");
if (!nvdrv) {
LOG_WARNING(Kernel_SVC, "NextLoad: NVDRV service unavailable for reset");
} else {
nvdrv->GetModule()->ResetForProcess(current_process);
}
auto& page_table = current_process->GetPageTable();
const u64 heap_start = GetInteger(page_table.GetHeapRegionStart());
const u64 heap_size = page_table.GetHeapRegionSize();
const u64 heap_end = heap_start + heap_size;
if (heap_start != 0 && heap_size != 0 && heap_end > heap_start) {
struct DeviceSharedBlock {
u64 address;
u64 size;
u16 device_use_count;
};
std::vector<DeviceSharedBlock> blocks;
for (u64 cursor = heap_start; cursor < heap_end;) {
KMemoryInfo info;
PageInfo page_info;
const auto query_result =
page_table.QueryInfo(std::addressof(info), std::addressof(page_info),
cursor);
if (query_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad: DeviceShared heap cleanup query failed "
"address=0x{:016X}, result={:#X}",
cursor, query_result.raw);
break;
}
const u64 block_end = (std::min<u64>)(info.GetEndAddress(), heap_end);
if (block_end <= cursor) {
LOG_WARNING(Kernel_SVC,
"NextLoad: DeviceShared heap cleanup walk stalled "
"cursor=0x{:016X}, block=0x{:016X}/0x{:X}",
cursor, info.GetAddress(), info.GetSize());
break;
}
const bool is_device_shared =
True(info.GetState() & KMemoryState::FlagCanDeviceMap) &&
info.GetAttribute() == KMemoryAttribute::DeviceShared &&
info.m_device_use_count > 0;
if (is_device_shared) {
blocks.push_back(DeviceSharedBlock{
.address = cursor,
.size = block_end - cursor,
.device_use_count = info.m_device_use_count,
});
}
cursor = block_end;
}
for (const auto& block : blocks) {
for (u16 unlock = 0; unlock < block.device_use_count; unlock++) {
const auto unlock_result =
page_table.UnlockForDeviceAddressSpace(block.address, block.size);
if (unlock_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad: DeviceShared heap cleanup unlock failed "
"address=0x{:016X}, size=0x{:X}, remaining={}, "
"result={:#X}",
block.address, block.size,
block.device_use_count - unlock - 1, unlock_result.raw);
break;
}
}
}
}
if (Loader::LoadNroInPlace(system, *current_process, *current_thread, file,
next_load_path, next_load_argv)) {
const auto aruid = current_process->GetProcessId();
if (const auto hid =
system.ServiceManager().GetService<Service::HID::IHidServer>("hid")) {
const auto resource_manager = hid->GetResourceManager();
resource_manager->UnregisterAppletResourceUserId(aruid);
const auto register_result =
resource_manager->RegisterAppletResourceUserId(aruid, true);
if (register_result.IsError()) {
LOG_WARNING(Kernel_SVC,
"NextLoad: failed to register HID applet resource "
"aruid={}, result={:#X}",
aruid, register_result.raw);
}
} else {
LOG_WARNING(Kernel_SVC, "NextLoad: HID service unavailable for reset");
}
auto& hid_core = system.HIDCore();
hid_core.DisableAllControllerConfiguration();
hid_core.SetSupportedStyleTag({Core::HID::NpadStyleSet::All});
hid_core.ReloadInputDevices();
const auto activate_controller = [&](Core::HID::NpadIdType npad_id) {
auto* controller = hid_core.GetEmulatedController(npad_id);
if (controller == nullptr) {
return;
}
(void)controller->SetPollingMode(Core::HID::EmulatedDeviceIndex::AllDevices,
Common::Input::PollingMode::Active);
};
activate_controller(Core::HID::NpadIdType::Player1);
activate_controller(Core::HID::NpadIdType::Handheld);
system.Kernel().CurrentPhysicalCore().LoadContext(current_thread);
const auto& context = current_thread->GetContext();
for (size_t i = 0; i < args.size(); i++) {
args[i] = context.r[i];
}
return;
}
}
}
}
ASSERT_MSG(current_process->GetState() == KProcess::State::Running,
"Process has already exited");
@@ -132,8 +331,8 @@ Result TerminateProcess(Core::System& system, Handle process_handle) {
R_THROW(ResultNotImplemented);
}
void ExitProcess64(Core::System& system) {
ExitProcess(system);
void ExitProcess64(Core::System& system, std::span<uint64_t, 8> args) {
ExitProcess(system, args);
}
Result GetProcessId64(Core::System& system, uint64_t* out_process_id, Handle process_handle) {
@@ -164,8 +363,8 @@ Result GetProcessInfo64(Core::System& system, int64_t* out_info, Handle process_
R_RETURN(GetProcessInfo(system, out_info, process_handle, info_type));
}
void ExitProcess64From32(Core::System& system) {
ExitProcess(system);
void ExitProcess64From32(Core::System& system, std::span<uint64_t, 8> args) {
ExitProcess(system, args);
}
Result GetProcessId64From32(Core::System& system, uint64_t* out_process_id, Handle process_handle) {
+15 -8
View File
@@ -4,6 +4,7 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string_view>
#include <utility>
#include "common/assert.h"
@@ -257,7 +258,7 @@ Result VfsDirectoryServiceWrapper::OpenFile(FileSys::VirtualFile* out_file,
npath.remove_prefix(1);
}
auto file = backing->GetFileRelative(npath);
auto file = backing->GetFileRelative(npath, mode);
if (file == nullptr) {
return FileSys::ResultPathNotFound;
}
@@ -333,14 +334,16 @@ FileSystemController::~FileSystemController() = default;
Result FileSystemController::RegisterProcess(
ProcessId process_id, ProgramId program_id,
std::shared_ptr<FileSys::RomFSFactory>&& romfs_factory) {
std::shared_ptr<FileSys::RomFSFactory>&& romfs_factory, std::string homebrew_initial_cwd) {
std::scoped_lock lk{registration_lock};
registrations.emplace(process_id, Registration{
.program_id = program_id,
.romfs_factory = std::move(romfs_factory),
.save_data_factory = CreateSaveDataFactory(program_id),
});
registrations.insert_or_assign(process_id,
Registration{
.program_id = program_id,
.romfs_factory = std::move(romfs_factory),
.save_data_factory = CreateSaveDataFactory(program_id),
.homebrew_initial_cwd = std::move(homebrew_initial_cwd),
});
LOG_DEBUG(Service_FS, "Registered for process {}", process_id);
return ResultSuccess;
@@ -348,7 +351,8 @@ Result FileSystemController::RegisterProcess(
Result FileSystemController::OpenProcess(
ProgramId* out_program_id, std::shared_ptr<SaveDataController>* out_save_data_controller,
std::shared_ptr<RomFsController>* out_romfs_controller, ProcessId process_id) {
std::shared_ptr<RomFsController>* out_romfs_controller, ProcessId process_id,
std::string* out_homebrew_initial_cwd) {
std::scoped_lock lk{registration_lock};
const auto it = registrations.find(process_id);
@@ -361,6 +365,9 @@ Result FileSystemController::OpenProcess(
std::make_shared<SaveDataController>(system, it->second.save_data_factory);
*out_romfs_controller =
std::make_shared<RomFsController>(it->second.romfs_factory, it->second.program_id);
if (out_homebrew_initial_cwd != nullptr) {
*out_homebrew_initial_cwd = it->second.homebrew_initial_cwd;
}
return ResultSuccess;
}
+7 -4
View File
@@ -8,6 +8,7 @@
#include <memory>
#include <mutex>
#include <string>
#include "common/common_types.h"
#include "core/file_sys/fs_directory.h"
#include "core/file_sys/fs_filesystem.h"
@@ -71,11 +72,12 @@ public:
~FileSystemController();
Result RegisterProcess(ProcessId process_id, ProgramId program_id,
std::shared_ptr<FileSys::RomFSFactory>&& factory);
std::shared_ptr<FileSys::RomFSFactory>&& factory,
std::string homebrew_initial_cwd = {});
Result OpenProcess(ProgramId* out_program_id,
std::shared_ptr<SaveDataController>* out_save_data_controller,
std::shared_ptr<RomFsController>* out_romfs_controller,
ProcessId process_id);
std::shared_ptr<SaveDataController>* out_save_data_controller,
std::shared_ptr<RomFsController>* out_romfs_controller,
ProcessId process_id, std::string* out_homebrew_initial_cwd = nullptr);
void SetPackedUpdate(ProcessId process_id, FileSys::VirtualFile update_raw);
std::shared_ptr<SaveDataController> OpenSaveDataController();
@@ -136,6 +138,7 @@ private:
ProgramId program_id;
std::shared_ptr<FileSys::RomFSFactory> romfs_factory;
std::shared_ptr<FileSys::SaveDataFactory> save_data_factory;
std::string homebrew_initial_cwd;
};
std::mutex registration_lock;
@@ -4,6 +4,9 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string_view>
#include "common/fs/path_util.h"
#include "common/string_util.h"
#include "core/file_sys/fssrv/fssrv_sf_path.h"
#include "core/hle/service/cmif_serialization.h"
@@ -13,10 +16,29 @@
namespace Service::FileSystem {
IFileSystem::IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_)
: ServiceFramework{system_, "IFileSystem"}, backend{std::make_unique<FileSys::Fsa::IFileSystem>(
dir_)},
size_getter{std::move(size_getter_)} {
static std::string ResolveHomebrewCwdRootAlias(std::string path,
std::string_view homebrew_initial_cwd) {
if (homebrew_initial_cwd.empty()) {
return path;
}
const std::string normalized_path = Common::FS::SanitizePath(path);
if (normalized_path.empty() || normalized_path == "/" ||
normalized_path != homebrew_initial_cwd ||
path.size() != normalized_path.size() + 2 ||
path.substr(0, normalized_path.size()) != normalized_path ||
path.substr(normalized_path.size()) != "//") {
return path;
}
return "/";
}
IFileSystem::IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_,
std::string homebrew_initial_cwd_)
: ServiceFramework{system_, "IFileSystem"},
backend{std::make_unique<FileSys::Fsa::IFileSystem>(dir_)},
size_getter{std::move(size_getter_)}, homebrew_initial_cwd{std::move(homebrew_initial_cwd_)} {
static const FunctionInfo functions[] = {
{0, D<&IFileSystem::CreateFile>, "CreateFile"},
{1, D<&IFileSystem::DeleteFile>, "DeleteFile"},
@@ -43,7 +65,8 @@ Result IFileSystem::CreateFile(const InLargeData<FileSys::Sf::Path, BufferAttr_H
s32 option, s64 size) {
LOG_DEBUG(Service_FS, "called. file={}, option={:#x}, size={:#08x}", path->str, option, size);
R_RETURN(backend->CreateFile(FileSys::Path(path->str), size));
const auto fs_path = ResolveHomebrewCwdRootAlias(path->str, homebrew_initial_cwd);
R_RETURN(backend->CreateFile(FileSys::Path(fs_path.c_str()), size));
}
Result IFileSystem::DeleteFile(const InLargeData<FileSys::Sf::Path, BufferAttr_HipcPointer> path) {
@@ -94,7 +117,8 @@ Result IFileSystem::OpenFile(OutInterface<IFile> out_interface,
LOG_DEBUG(Service_FS, "called. file={}, mode={}", path->str, mode);
FileSys::VirtualFile vfs_file{};
R_TRY(backend->OpenFile(&vfs_file, FileSys::Path(path->str),
const auto fs_path = ResolveHomebrewCwdRootAlias(path->str, homebrew_initial_cwd);
R_TRY(backend->OpenFile(&vfs_file, FileSys::Path(fs_path.c_str()),
static_cast<FileSys::OpenMode>(mode)));
*out_interface = std::make_shared<IFile>(system, vfs_file);
@@ -107,7 +131,8 @@ Result IFileSystem::OpenDirectory(OutInterface<IDirectory> out_interface,
LOG_DEBUG(Service_FS, "called. directory={}, mode={}", path->str, mode);
FileSys::VirtualDir vfs_dir{};
R_TRY(backend->OpenDirectory(&vfs_dir, FileSys::Path(path->str),
const auto fs_path = ResolveHomebrewCwdRootAlias(path->str, homebrew_initial_cwd);
R_TRY(backend->OpenDirectory(&vfs_dir, FileSys::Path(fs_path.c_str()),
static_cast<FileSys::OpenDirectoryMode>(mode)));
*out_interface = std::make_shared<IDirectory>(system, vfs_dir,
@@ -120,7 +145,8 @@ Result IFileSystem::GetEntryType(
LOG_DEBUG(Service_FS, "called. file={}", path->str);
FileSys::DirectoryEntryType vfs_entry_type{};
R_TRY(backend->GetEntryType(&vfs_entry_type, FileSys::Path(path->str)));
const auto fs_path = ResolveHomebrewCwdRootAlias(path->str, homebrew_initial_cwd);
R_TRY(backend->GetEntryType(&vfs_entry_type, FileSys::Path(fs_path.c_str())));
*out_type = static_cast<u32>(vfs_entry_type);
R_SUCCEED();
@@ -1,8 +1,13 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#include "common/common_funcs.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/fsa/fs_i_filesystem.h"
@@ -23,7 +28,8 @@ class IDirectory;
class IFileSystem final : public ServiceFramework<IFileSystem> {
public:
explicit IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_);
explicit IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_,
std::string homebrew_initial_cwd_ = {});
Result CreateFile(const InLargeData<FileSys::Sf::Path, BufferAttr_HipcPointer> path, s32 option,
s64 size);
@@ -55,6 +61,7 @@ public:
private:
std::unique_ptr<FileSys::Fsa::IFileSystem> backend;
SizeGetter size_getter;
std::string homebrew_initial_cwd;
};
} // namespace Service::FileSystem
@@ -192,8 +192,9 @@ Result FSP_SRV::SetCurrentProcess(ClientProcessId pid) {
LOG_DEBUG(Service_FS, "called. current_process_id={:#016x}", current_process_id);
R_RETURN(
fsc.OpenProcess(&program_id, &save_data_controller, &romfs_controller, current_process_id));
homebrew_initial_cwd.clear();
R_RETURN(fsc.OpenProcess(&program_id, &save_data_controller, &romfs_controller,
current_process_id, &homebrew_initial_cwd));
}
Result FSP_SRV::OpenFileSystemWithPatch(OutInterface<IFileSystem> out_interface,
@@ -224,7 +225,8 @@ Result FSP_SRV::OpenSdCardFileSystem(OutInterface<IFileSystem> out_interface) {
fsc.OpenSDMC(&sdmc_dir);
*out_interface = std::make_shared<IFileSystem>(
system, sdmc_dir, SizeGetter::FromStorageId(fsc, FileSys::StorageId::SdCard));
system, sdmc_dir, SizeGetter::FromStorageId(fsc, FileSys::StorageId::SdCard),
homebrew_initial_cwd);
R_SUCCEED();
}
@@ -7,6 +7,7 @@
#pragma once
#include <memory>
#include <string>
#include "core/file_sys/fs_save_data_types.h"
#include "core/hle/service/cmif_types.h"
#include "core/hle/service/filesystem/fsp/fsp_types.h"
@@ -123,6 +124,7 @@ private:
u32 access_log_program_index = 0;
AccessLogMode access_log_mode = AccessLogMode::None;
u64 program_id = 0;
std::string homebrew_initial_cwd;
std::shared_ptr<SaveDataController> save_data_controller;
std::shared_ptr<RomFsController> romfs_controller;
};
+91 -1
View File
@@ -1,10 +1,15 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: 2022 Skyline Team and Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <atomic>
#include <deque>
#include <mutex>
#include <vector>
#include "core/hle/kernel/k_process.h"
#include "core/hle/service/nvdrv/core/container.h"
@@ -40,6 +45,16 @@ Container::Container(Tegra::Host1x::Host1x& host1x_) {
Container::~Container() = default;
static bool IsSameProcess(Kernel::KProcess* lhs, Kernel::KProcess* rhs) {
if (lhs == rhs) {
return true;
}
if (lhs == nullptr || rhs == nullptr) {
return false;
}
return lhs->GetProcessId() == rhs->GetProcessId();
}
SessionId Container::OpenSession(Kernel::KProcess* process) {
using namespace Common::Literals;
@@ -48,7 +63,7 @@ SessionId Container::OpenSession(Kernel::KProcess* process) {
if (!session.is_active) {
continue;
}
if (session.process == process) {
if (IsSameProcess(session.process, process)) {
session.ref_count++;
return session.id;
}
@@ -116,7 +131,15 @@ SessionId Container::OpenSession(Kernel::KProcess* process) {
void Container::CloseSession(SessionId session_id) {
std::scoped_lock lk(impl->session_guard);
if (session_id.id >= impl->sessions.size()) {
return;
}
auto& session = impl->sessions[session_id.id];
if (!session.is_active || session.ref_count <= 0) {
return;
}
if (--session.ref_count > 0) {
return;
}
@@ -134,6 +157,73 @@ void Container::CloseSession(SessionId session_id) {
impl->id_pool.emplace_front(session_id.id);
}
size_t Container::CloseSessions(std::span<const SessionId> session_ids) {
std::vector<SessionId> valid_session_ids;
valid_session_ids.reserve(session_ids.size());
{
std::scoped_lock lk(impl->session_guard);
for (const auto session_id : session_ids) {
if (session_id.id >= impl->sessions.size()) {
continue;
}
auto& session = impl->sessions[session_id.id];
if (!session.is_active) {
continue;
}
const auto duplicate = std::ranges::any_of(
valid_session_ids, [session_id](const auto candidate) {
return candidate.id == session_id.id;
});
if (duplicate) {
continue;
}
session.ref_count = 1;
valid_session_ids.push_back(session_id);
}
}
for (const auto session_id : valid_session_ids) {
CloseSession(session_id);
}
return valid_session_ids.size();
}
std::vector<SessionId> Container::GetSessionIdsForProcess(Kernel::KProcess* process) {
std::vector<SessionId> session_ids;
std::scoped_lock lk(impl->session_guard);
for (const auto& session : impl->sessions) {
if (!session.is_active || !IsSameProcess(session.process, process)) {
continue;
}
session_ids.push_back(session.id);
}
return session_ids;
}
std::vector<SessionId> Container::GetActiveSessionIds() const {
std::vector<SessionId> session_ids;
std::scoped_lock lk(impl->session_guard);
for (const auto& session : impl->sessions) {
if (session.is_active) {
session_ids.push_back(session.id);
}
}
return session_ids;
}
bool Container::IsSessionActive(SessionId session_id) const {
std::scoped_lock lk(impl->session_guard);
return session_id.id < impl->sessions.size() && impl->sessions[session_id.id].is_active;
}
Session* Container::GetSession(SessionId session_id) {
std::atomic_thread_fence(std::memory_order_acquire);
return &impl->sessions[session_id.id];
@@ -9,7 +9,10 @@
#include <deque>
#include <memory>
#include <span>
#include <cstddef>
#include <ankerl/unordered_dense.h>
#include <vector>
#include "core/device_memory_manager.h"
#include "core/hle/service/nvdrv/nvdata.h"
@@ -59,6 +62,10 @@ public:
SessionId OpenSession(Kernel::KProcess* process);
void CloseSession(SessionId id);
size_t CloseSessions(std::span<const SessionId> session_ids);
std::vector<SessionId> GetSessionIdsForProcess(Kernel::KProcess* process);
std::vector<SessionId> GetActiveSessionIds() const;
bool IsSessionActive(SessionId id) const;
Session* GetSession(SessionId id);
+56 -8
View File
@@ -6,10 +6,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <functional>
#include <vector>
#include "common/alignment.h"
#include "common/assert.h"
#include "common/logging.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/service/nvdrv/core/container.h"
#include "core/hle/service/nvdrv/core/heap_mapper.h"
#include "core/hle/service/nvdrv/core/nvmap.h"
@@ -326,19 +328,65 @@ std::optional<NvMap::FreeInfo> NvMap::FreeHandle(Handle::Id handle, bool interna
}
void NvMap::UnmapAllHandles(NvCore::SessionId session_id) {
auto handles_copy = [&] {
auto* session = core.GetSession(session_id);
auto* process = session != nullptr ? session->process : nullptr;
auto handle_ids = [&] {
std::scoped_lock lk{handles_lock};
return handles;
std::vector<Handle::Id> ids;
ids.reserve(handles.size());
for (const auto& entry : handles) {
ids.push_back(entry.first);
}
return ids;
}();
for (auto& [id, handle] : handles_copy) {
{
std::scoped_lock lk{handle->mutex};
if (handle->session_id.id != session_id.id || handle->dupes <= 0) {
continue;
for (const auto id : handle_ids) {
bool unlocked_pages = false;
while (true) {
bool last_user_reference = false;
VAddr address = 0;
size_t size = 0;
{
const auto handle = GetHandle(id);
if (!handle) {
break;
}
std::scoped_lock lk{handle->mutex};
if (handle->session_id.id != session_id.id || handle->dupes <= 0) {
break;
}
last_user_reference = handle->dupes == 1;
address = handle->address;
size = handle->size;
}
const auto free_info = FreeHandle(id, false);
if (!free_info) {
break;
}
if (!unlocked_pages && process != nullptr && address != 0 && size != 0 &&
(free_info->can_unlock || last_user_reference)) {
const auto unlock_result =
process->GetPageTable().UnlockForDeviceAddressSpace(address, size);
if (unlock_result.IsError()) {
LOG_WARNING(Service_NVDRV,
"NextLoad: nvmap session cleanup unlock failed, "
"handle={}, session={}, address=0x{:016X}, size=0x{:X}, "
"result={:#X}",
id, session_id.id, address, size, unlock_result.raw);
}
unlocked_pages = true;
}
if (last_user_reference) {
break;
}
}
FreeHandle(id, false);
}
}
+104 -1
View File
@@ -1,12 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: 2021 Skyline Team and Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <utility>
#include <vector>
#include <fmt/ranges.h>
#include "core/core.h"
#include "core/hle/kernel/k_event.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/nvdrv/core/container.h"
#include "core/hle/service/nvdrv/devices/nvdevice.h"
@@ -133,6 +138,9 @@ DeviceFD Module::Open(const std::string& device_name, NvCore::SessionId session_
auto device = builder(fd)->second;
device->OnOpen(session_id, fd);
if (container.IsSessionActive(session_id)) {
open_file_sessions.emplace(fd, session_id);
}
return fd;
}
@@ -204,6 +212,7 @@ NvResult Module::Close(DeviceFD fd) {
itr->second->OnClose(fd);
open_files.erase(itr);
open_file_sessions.erase(fd);
return NvResult::Success;
}
@@ -228,4 +237,98 @@ NvResult Module::QueryEvent(DeviceFD fd, u32 event_id, Kernel::KEvent*& event) {
return NvResult::Success;
}
static bool ContainsSession(std::span<const NvCore::SessionId> session_ids,
NvCore::SessionId session_id) {
return std::ranges::any_of(session_ids, [session_id](const auto candidate) {
return candidate.id == session_id.id;
});
}
static void AppendUniqueSession(std::vector<NvCore::SessionId>& session_ids,
NvCore::SessionId session_id) {
if (!ContainsSession(session_ids, session_id)) {
session_ids.push_back(session_id);
}
}
size_t Module::CloseFilesForSessions(std::span<const NvCore::SessionId> session_ids) {
std::vector<DeviceFD> fds;
fds.reserve(open_file_sessions.size());
for (const auto& [fd, session_id] : open_file_sessions) {
if (ContainsSession(session_ids, session_id)) {
fds.push_back(fd);
}
}
for (const auto fd : fds) {
Close(fd);
}
return fds.size();
}
void Module::CloseSession(NvCore::SessionId session_id) {
container.CloseSession(session_id);
}
void Module::TrackSessionAruid(NvCore::SessionId session_id, u64 aruid) {
const bool active = container.IsSessionActive(session_id);
if (active) {
session_aruids[session_id.id] = aruid;
}
}
std::vector<NvCore::SessionId> Module::GetSessionIdsForAruid(u64 aruid) const {
std::vector<NvCore::SessionId> session_ids;
for (const auto& [session_id, session_aruid] : session_aruids) {
if (session_aruid == aruid) {
session_ids.push_back(NvCore::SessionId{session_id});
}
}
return session_ids;
}
size_t Module::ResetForProcess(Kernel::KProcess* process) {
const auto process_id = process != nullptr ? process->GetProcessId() : 0;
auto session_ids = container.GetSessionIdsForProcess(process);
if (process_id != 0) {
for (const auto session_id : GetSessionIdsForAruid(process_id)) {
AppendUniqueSession(session_ids, session_id);
}
}
const auto active_session_ids = container.GetActiveSessionIds();
const auto active_before = active_session_ids.size();
const bool has_active_candidate =
std::ranges::any_of(session_ids, [this](const auto session_id) {
return container.IsSessionActive(session_id);
});
bool used_active_sessions = false;
if (!has_active_candidate && !active_session_ids.empty()) {
for (const auto session_id : active_session_ids) {
AppendUniqueSession(session_ids, session_id);
}
used_active_sessions = true;
}
const auto closed_files = CloseFilesForSessions(session_ids);
const auto closed_sessions = container.CloseSessions(session_ids);
for (const auto session_id : session_ids) {
if (!container.IsSessionActive(session_id)) {
session_aruids.erase(session_id.id);
}
}
if (used_active_sessions) {
LOG_WARNING(Service_NVDRV,
"NextLoad: NVDRV reset used active sessions because process-owned "
"sessions were not found, process_id={}, sessions={}, files={}, active_before={}",
process_id, closed_sessions, closed_files, active_before);
}
return closed_sessions;
}
} // namespace Service::Nvidia
+10
View File
@@ -12,6 +12,7 @@
#include <memory>
#include <span>
#include <string>
#include <vector>
#include <ankerl/unordered_dense.h>
#include "common/common_types.h"
@@ -26,6 +27,7 @@ class System;
namespace Kernel {
class KEvent;
class KProcess;
}
namespace Service::Nvidia {
@@ -89,6 +91,9 @@ public:
NvResult Close(DeviceFD fd);
NvResult QueryEvent(DeviceFD fd, u32 event_id, Kernel::KEvent*& event);
void CloseSession(NvCore::SessionId session_id);
void TrackSessionAruid(NvCore::SessionId session_id, u64 aruid);
size_t ResetForProcess(Kernel::KProcess* process);
NvCore::Container& GetContainer() {
return container;
@@ -106,12 +111,17 @@ private:
using FilesContainerType = ankerl::unordered_dense::map<DeviceFD, std::shared_ptr<Devices::nvdevice>>;
/// Mapping of file descriptors to the devices they reference.
FilesContainerType open_files;
ankerl::unordered_dense::map<DeviceFD, NvCore::SessionId> open_file_sessions;
ankerl::unordered_dense::map<size_t, u64> session_aruids;
KernelHelpers::ServiceContext service_context;
EventInterface events_interface;
ankerl::unordered_dense::map<std::string, std::function<FilesContainerType::iterator(DeviceFD)>> builders;
size_t CloseFilesForSessions(std::span<const NvCore::SessionId> session_ids);
std::vector<NvCore::SessionId> GetSessionIdsForAruid(u64 aruid) const;
};
void LoopProcess(Core::System& system);
@@ -212,7 +212,10 @@ void NVDRV::QueryEvent(HLERequestContext& ctx) {
void NVDRV::SetAruid(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
pid = rp.Pop<u64>();
LOG_WARNING(Service_NVDRV, "(STUBBED) called, pid={:#x}", pid);
LOG_WARNING(Service_NVDRV, "(STUBBED) called, pid={:#X}", pid);
if (is_initialized) {
nvdrv->TrackSessionAruid(session_id, pid);
}
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
+4 -2
View File
@@ -40,6 +40,7 @@ namespace Network {
namespace {
enum class CallType {
Connect,
Send,
Other,
};
@@ -131,7 +132,7 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
case WSAENOTCONN:
return Errno::NOTCONN;
case WSAEWOULDBLOCK:
return Errno::AGAIN;
return call_type == CallType::Connect ? Errno::INPROGRESS : Errno::AGAIN;
case WSAECONNREFUSED:
return Errno::CONNREFUSED;
case WSAECONNABORTED:
@@ -563,6 +564,7 @@ int TranslateTypeToNative(Type type) {
NETWORK_PROTOCOL_TRANSLATE_ELEM(UDPLITE)
#elif defined(_WIN32)
#define NETWORK_PROTOCOL_TRANSLATE_LIST \
NETWORK_PROTOCOL_TRANSLATE_ELEM(IP) \
/*NETWORK_PROTOCOL_TRANSLATE_ELEM(HOPOPTS)*/ \
NETWORK_PROTOCOL_TRANSLATE_ELEM(ICMP) \
NETWORK_PROTOCOL_TRANSLATE_ELEM(IGMP) \
@@ -888,7 +890,7 @@ Errno Socket::Connect(SockAddrIn addr_in) {
return Errno::SUCCESS;
}
return GetAndLogLastError();
return GetAndLogLastError(CallType::Connect);
}
std::pair<SockAddrIn, Errno> Socket::GetPeerName() {
+311
View File
@@ -0,0 +1,311 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "core/loader/homebrew_nxlink.h"
#include <array>
#include <cctype>
#include <memory>
#include <mutex>
#include <optional>
#include <stop_token>
#include <utility>
#include <boost/asio.hpp>
#include "common/logging.h"
#include "common/polyfill_thread.h"
#include "common/thread.h"
namespace Loader::HomebrewNxlink {
namespace {
using boost::asio::ip::tcp;
constexpr u16 NxlinkClientPort = 28771;
constexpr size_t ReceiveBufferSize = 1024;
constexpr size_t MaxLogLineSize = 16 * 1024;
struct LogConnection {
explicit LogConnection(boost::asio::io_context& io_context_) : socket{io_context_} {}
tcp::socket socket;
std::array<char, ReceiveBufferSize> buffer{};
std::string line;
};
struct LogServerState {
LogServerState() : acceptor{io_context} {}
boost::asio::io_context io_context;
tcp::acceptor acceptor;
};
std::mutex server_mutex;
std::shared_ptr<LogServerState> server_state;
std::optional<std::jthread> server_thread;
std::optional<std::string> GetLastArgvToken(std::string_view argv_string) {
while (!argv_string.empty() && argv_string.back() == '\0') {
argv_string.remove_suffix(1);
}
std::optional<std::string_view> last_token;
bool in_token = false;
bool quoted = false;
size_t token_begin = 0;
size_t token_size = 0;
for (size_t i = 0; i <= argv_string.size(); i++) {
const char c = i < argv_string.size() ? argv_string[i] : '\0';
if (!in_token) {
if (c == '\0' || std::isspace(static_cast<unsigned char>(c))) {
continue;
}
in_token = true;
token_size = 0;
if (c == '"') {
quoted = true;
token_begin = i + 1;
} else {
quoted = false;
token_begin = i;
token_size = 1;
}
continue;
}
const bool token_end =
quoted ? c == '"' || c == '\0'
: c == '\0' || std::isspace(static_cast<unsigned char>(c));
if (token_end) {
if (token_size != 0) {
last_token = argv_string.substr(token_begin, token_size);
}
in_token = false;
quoted = false;
token_size = 0;
continue;
}
token_size++;
}
if (!last_token) {
return std::nullopt;
}
return std::string{*last_token};
}
void AppendArgvToken(std::string& argv_string, std::string_view token) {
while (!argv_string.empty() && argv_string.back() == '\0') {
argv_string.pop_back();
}
if (!argv_string.empty()) {
argv_string.push_back(' ');
}
argv_string.append(token);
}
void FlushLogLine(std::string& line) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
if (!line.empty()) {
LOG_INFO(Loader, "{}", line);
line.clear();
}
}
void ConsumeLogBytes(LogConnection& connection, std::string_view bytes) {
for (const char byte : bytes) {
if (byte == '\n') {
FlushLogLine(connection.line);
continue;
}
connection.line.push_back(byte);
if (connection.line.size() >= MaxLogLineSize) {
FlushLogLine(connection.line);
}
}
}
void StartRead(std::shared_ptr<LogConnection> connection) {
connection->socket.async_read_some(
boost::asio::buffer(connection->buffer),
[connection](const boost::system::error_code& error, size_t bytes_read) {
if (error.failed()) {
FlushLogLine(connection->line);
return;
}
ConsumeLogBytes(*connection, {connection->buffer.data(), bytes_read});
StartRead(connection);
});
}
void StartAccept(std::shared_ptr<LogServerState> state) {
auto connection = std::make_shared<LogConnection>(state->io_context);
state->acceptor.async_accept(
connection->socket,
[state, connection](const boost::system::error_code& error) {
if (!error.failed()) {
LOG_INFO(Loader, "Homebrew nxlink log client connected");
StartRead(connection);
}
if (state->acceptor.is_open()) {
StartAccept(state);
}
});
}
void StartLogServer() {
std::scoped_lock lock{server_mutex};
if (server_thread) {
return;
}
auto state = std::make_shared<LogServerState>();
const tcp::endpoint endpoint{boost::asio::ip::address_v4::loopback(), NxlinkClientPort};
boost::system::error_code error;
state->acceptor.open(endpoint.protocol(), error);
if (!error.failed()) {
state->acceptor.set_option(tcp::acceptor::reuse_address(true), error);
}
if (!error.failed()) {
state->acceptor.bind(endpoint, error);
}
if (!error.failed()) {
state->acceptor.listen(boost::asio::socket_base::max_listen_connections, error);
}
if (error.failed()) {
LOG_WARNING(Loader, "Homebrew nxlink log server could not listen on 127.0.0.1:{}: {}",
NxlinkClientPort, error.message());
return;
}
StartAccept(state);
server_state = state;
server_thread.emplace([state](std::stop_token stop_token) {
Common::SetCurrentThreadName("HomebrewNxlink");
std::stop_callback stop_callback{stop_token, [state] {
boost::system::error_code ignored;
state->acceptor.close(ignored);
state->io_context.stop();
}};
LOG_INFO(Loader, "Homebrew nxlink log server listening on 127.0.0.1:{}",
NxlinkClientPort);
state->io_context.run();
LOG_INFO(Loader, "Homebrew nxlink log server stopped");
});
}
} // namespace
bool IsArgvMarker(std::string_view token) {
if (token.size() != ArgvMarkerSize || token.substr(8) != ArgvMarkerSuffix) {
return false;
}
for (size_t i = 0; i < 8; i++) {
if (!std::isxdigit(static_cast<unsigned char>(token[i]))) {
return false;
}
}
return true;
}
bool IsLoopbackArgvMarker(std::string_view token) {
if (!IsArgvMarker(token)) {
return false;
}
for (size_t i = 0; i < 8; i++) {
if (std::tolower(static_cast<unsigned char>(token[i])) !=
std::tolower(static_cast<unsigned char>(LoopbackArgvMarker[i]))) {
return false;
}
}
return true;
}
std::optional<std::string> GetArgvMarker(std::string_view argv_string) {
auto last_token = GetLastArgvToken(argv_string);
if (!last_token || !IsArgvMarker(*last_token)) {
return std::nullopt;
}
return last_token;
}
std::optional<std::string> PrepareArgv(std::string& argv_string,
std::string_view inherited_marker,
bool append_loopback_marker) {
auto marker = GetArgvMarker(argv_string);
if (!marker && IsArgvMarker(inherited_marker)) {
AppendArgvToken(argv_string, inherited_marker);
marker = std::string{inherited_marker};
}
if (!marker && append_loopback_marker) {
AppendArgvToken(argv_string, LoopbackArgvMarker);
marker = std::string{LoopbackArgvMarker};
}
return marker;
}
void ApplyServerMode(Settings::HomebrewNxlinkServerMode mode,
const std::optional<std::string>& active_marker) {
switch (mode) {
case Settings::HomebrewNxlinkServerMode::Disabled:
StopServer();
return;
case Settings::HomebrewNxlinkServerMode::EdenLog:
if (active_marker && IsLoopbackArgvMarker(*active_marker)) {
StartLogServer();
return;
}
StopServer();
if (active_marker) {
LOG_INFO(Loader,
"Homebrew nxlink server mode overridden by argv marker '{}'; not starting "
"local nxlink log server",
*active_marker);
} else {
LOG_WARNING(Loader,
"Homebrew nxlink log server requested, but no argv marker is active");
}
return;
case Settings::HomebrewNxlinkServerMode::HostStdout:
case Settings::HomebrewNxlinkServerMode::File:
StopServer();
LOG_WARNING(Loader, "Homebrew nxlink server mode {} is not implemented",
static_cast<int>(mode));
return;
}
}
void StopServer() {
std::shared_ptr<LogServerState> state;
std::optional<std::jthread> thread;
{
std::scoped_lock lock{server_mutex};
state = std::move(server_state);
thread = std::move(server_thread);
}
if (state) {
boost::system::error_code ignored;
state->acceptor.close(ignored);
state->io_context.stop();
}
if (thread) {
thread->request_stop();
}
}
} // namespace Loader::HomebrewNxlink
+30
View File
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstddef>
#include <optional>
#include <string>
#include <string_view>
#include "common/settings_enums.h"
namespace Loader::HomebrewNxlink {
constexpr size_t ArgvMarkerSize = 16;
constexpr std::string_view ArgvMarkerSuffix = "_NXLINK_";
constexpr std::string_view LoopbackArgvMarker = "0100007F_NXLINK_";
bool IsArgvMarker(std::string_view token);
bool IsLoopbackArgvMarker(std::string_view token);
std::optional<std::string> GetArgvMarker(std::string_view argv_string);
std::optional<std::string> PrepareArgv(std::string& argv_string,
std::string_view inherited_marker,
bool append_loopback_marker = false);
void ApplyServerMode(Settings::HomebrewNxlinkServerMode mode,
const std::optional<std::string>& active_marker);
void StopServer();
} // namespace Loader::HomebrewNxlink
+507 -55
View File
@@ -4,17 +4,20 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstring>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "common/alignment.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "common/settings.h"
#include "common/random.h"
@@ -23,11 +26,14 @@
#include "core/file_sys/control_metadata.h"
#include "core/file_sys/romfs_factory.h"
#include "core/file_sys/vfs/vfs_offset.h"
#include "core/hardware_properties.h"
#include "core/hle/api_version.h"
#include "core/hle/kernel/code_set.h"
#include "core/hle/kernel/k_page_table.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/k_thread.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/homebrew_nxlink.h"
#include "core/loader/nro.h"
#include "core/memory.h"
@@ -152,8 +158,432 @@ static constexpr u32 PageAlignSize(u32 size) {
return static_cast<u32>((size + Core::Memory::YUZU_PAGEMASK) & ~Core::Memory::YUZU_PAGEMASK);
}
static std::string MakeHomebrewSdmcPath(std::string path) {
std::replace(path.begin(), path.end(), '\\', '/');
while (!path.empty() && path.front() == '/') {
path.erase(path.begin());
}
return "sdmc:/" + path;
}
static std::optional<std::string> TryMakeHomebrewSdmcPathFromHostPath(std::string path) {
std::replace(path.begin(), path.end(), '\\', '/');
std::string sdmc_root =
Common::FS::PathToUTF8String(Common::FS::GetEdenPath(Common::FS::EdenPath::SDMCDir));
std::replace(sdmc_root.begin(), sdmc_root.end(), '\\', '/');
while (!sdmc_root.empty() && sdmc_root.back() == '/') {
sdmc_root.pop_back();
}
if (!sdmc_root.empty() && path.size() > sdmc_root.size() &&
path.compare(0, sdmc_root.size(), sdmc_root) == 0 && path[sdmc_root.size()] == '/') {
return MakeHomebrewSdmcPath(path.substr(sdmc_root.size() + 1));
}
constexpr std::string_view SdmcMarker{"/sdmc/"};
if (const auto pos = path.rfind(SdmcMarker); pos != std::string::npos) {
return MakeHomebrewSdmcPath(path.substr(pos + SdmcMarker.size()));
}
return std::nullopt;
}
static std::string MakeHomebrewArgv0(std::string nro_path, std::string file_name) {
if (nro_path.empty()) {
nro_path = std::move(file_name);
}
std::replace(nro_path.begin(), nro_path.end(), '\\', '/');
if (nro_path.rfind("sdmc:/", 0) == 0) {
return nro_path;
}
if (auto sdmc_path = TryMakeHomebrewSdmcPathFromHostPath(nro_path)) {
return *sdmc_path;
}
#ifdef __ANDROID__
if (nro_path.find('%') != std::string::npos) {
const auto percent_decode = [](std::string value) {
const auto hex_value = [](char c) -> int {
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
}
if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
}
return -1;
};
std::string decoded;
decoded.reserve(value.size());
for (size_t i = 0; i < value.size(); i++) {
if (value[i] == '%' && i + 2 < value.size()) {
const int high = hex_value(value[i + 1]);
const int low = hex_value(value[i + 2]);
if (high >= 0 && low >= 0) {
decoded.push_back(static_cast<char>((high << 4) | low));
i += 2;
continue;
}
}
decoded.push_back(value[i]);
}
return decoded;
};
if (auto sdmc_path = TryMakeHomebrewSdmcPathFromHostPath(percent_decode(nro_path))) {
return *sdmc_path;
}
}
#endif
if (!nro_path.empty() && nro_path.front() == '/') {
return "sdmc:" + nro_path;
}
return nro_path.empty() ? "homebrew" : nro_path;
}
static std::string QuoteHomebrewArgvComponent(const std::string& argument) {
if (argument.find_first_of(" \t\r\n") == std::string::npos) {
return argument;
}
std::string quoted{"\""};
quoted += argument;
quoted.push_back('"');
return quoted;
}
static std::string GetHomebrewInitialCwd(const std::string& argv0) {
constexpr std::string_view SdmcPrefix = "sdmc:";
if (argv0.substr(0, SdmcPrefix.size()) != SdmcPrefix) {
return {};
}
const auto last_slash = argv0.find_last_of('/');
if (last_slash == std::string_view::npos || last_slash < SdmcPrefix.size()) {
return {};
}
std::string cwd = argv0.substr(SdmcPrefix.size(), last_slash - SdmcPrefix.size());
if (cwd.empty()) {
cwd = "/";
}
while (cwd.size() > 1 && cwd.back() == '/') {
cwd.pop_back();
}
return cwd;
}
constexpr size_t HomebrewNextLoadPathSize = 0x200;
constexpr size_t HomebrewNextLoadArgvSize = 0x800;
constexpr u32 HomebrewSvcExitProcessInstruction = 0xD40000E1;
constexpr u32 HomebrewEntryEndOfList = 0;
constexpr u32 HomebrewEntryMainThreadHandle = 1;
constexpr u32 HomebrewEntryNextLoadPath = 2;
constexpr u32 HomebrewEntryOverrideHeap = 3;
constexpr u32 HomebrewEntryArgv = 5;
constexpr u32 HomebrewEntrySyscallAvailableHint = 6;
constexpr u32 HomebrewEntryAppletType = 7;
constexpr u32 HomebrewEntryProcessHandle = 10;
constexpr u32 HomebrewEntryRandomSeed = 14;
constexpr u32 HomebrewEntryHosVersion = 16;
constexpr u32 HomebrewEntrySyscallAvailableHint2 = 17;
constexpr u32 HomebrewAppletTypeApplication = 0;
constexpr u64 HomebrewAllSvcHints = ~u64{0};
constexpr u32 HomebrewHosVersion = (u32{HLE::ApiVersion::HOS_VERSION_MAJOR} << 16) |
(u32{HLE::ApiVersion::HOS_VERSION_MINOR} << 8) |
u32{HLE::ApiVersion::HOS_VERSION_MICRO};
struct HomebrewConfigEntry {
u32_le key;
u32_le flags;
u64_le value[2];
};
static_assert(sizeof(HomebrewConfigEntry) == 0x18);
constexpr size_t HomebrewBaseConfigEntryCount = 10;
constexpr size_t HomebrewInPlaceConfigEntryCount = 11;
constexpr size_t HomebrewBaseConfigTableSize =
HomebrewBaseConfigEntryCount * sizeof(HomebrewConfigEntry);
constexpr size_t HomebrewInPlaceConfigTableSize =
HomebrewInPlaceConfigEntryCount * sizeof(HomebrewConfigEntry);
struct HomebrewNroImage {
Kernel::CodeSet codeset;
size_t image_size{};
size_t args_offset{};
std::optional<size_t> exit_process_offset;
std::optional<std::string> nxlink_argv_marker;
std::string argv_string;
};
static void SetHomebrewConfigPointers(Kernel::KProcess& process, u64 config_addr,
u64 next_load_path_addr, u64 next_load_argv_addr) {
constexpr size_t MainThreadHandleEntryIndex = 0;
constexpr size_t ProcessHandleEntryIndex = 1;
constexpr size_t EntryValueOffset = offsetof(HomebrewConfigEntry, value);
process.SetArgPointer(Kernel::KProcessAddress{config_addr});
process.SetMainThreadHandleAddr(Kernel::KProcessAddress{
config_addr + MainThreadHandleEntryIndex * sizeof(HomebrewConfigEntry) + EntryValueOffset});
process.SetProcessHandleAddr(Kernel::KProcessAddress{
config_addr + ProcessHandleEntryIndex * sizeof(HomebrewConfigEntry) + EntryValueOffset});
process.SetHomebrewNextLoadBufferAddrs(Kernel::KProcessAddress{next_load_path_addr},
Kernel::KProcessAddress{next_load_argv_addr});
}
static std::optional<HomebrewNroImage> BuildHomebrewNroImage(const std::vector<u8>& data,
std::string nro_path,
std::string file_name,
std::string launch_argv,
std::string_view inherited_marker,
bool append_loopback_nxlink_marker) {
if (data.size() < sizeof(NroHeader)) {
return std::nullopt;
}
NroHeader nro_header{};
std::memcpy(&nro_header, data.data(), sizeof(NroHeader));
if (nro_header.magic != Common::MakeMagic('N', 'R', 'O', '0')) {
return std::nullopt;
}
if (data.size() < nro_header.file_size ||
nro_header.module_header_offset + sizeof(ModHeader) > PageAlignSize(nro_header.file_size)) {
return std::nullopt;
}
std::vector<u8> program_image(PageAlignSize(nro_header.file_size));
std::memcpy(program_image.data(), data.data(), nro_header.file_size);
Kernel::CodeSet codeset;
for (std::size_t i = 0; i < nro_header.segments.size(); ++i) {
codeset.segments[i].addr = nro_header.segments[i].offset;
codeset.segments[i].offset = nro_header.segments[i].offset;
codeset.segments[i].size = PageAlignSize(nro_header.segments[i].size);
}
u32 bss_size{PageAlignSize(nro_header.bss_size)};
ModHeader mod_header{};
std::memcpy(&mod_header, program_image.data() + nro_header.module_header_offset,
sizeof(ModHeader));
if (mod_header.magic == Common::MakeMagic('M', 'O', 'D', '0')) {
bss_size = PageAlignSize(mod_header.bss_end_offset - mod_header.bss_start_offset);
}
codeset.DataSegment().size += bss_size;
program_image.resize(static_cast<u32>(program_image.size()) + bss_size);
HomebrewNroImage image{.codeset = std::move(codeset)};
const auto argv0 = MakeHomebrewArgv0(std::move(nro_path), std::move(file_name));
if (!launch_argv.empty()) {
image.argv_string = std::move(launch_argv);
} else {
image.argv_string = QuoteHomebrewArgvComponent(argv0);
}
image.nxlink_argv_marker = HomebrewNxlink::PrepareArgv(
image.argv_string, inherited_marker, append_loopback_nxlink_marker);
if (image.argv_string.empty() || image.argv_string.back() != '\0') {
image.argv_string.push_back('\0');
}
const auto& code = image.codeset.CodeSegment();
const size_t code_end = (std::min)(program_image.size(), code.offset + code.size);
for (size_t offset = code.offset; offset + sizeof(u32) <= code_end; offset += sizeof(u32)) {
u32 instruction{};
std::memcpy(&instruction, program_image.data() + offset, sizeof(instruction));
if (instruction == HomebrewSvcExitProcessInstruction) {
image.exit_process_offset = offset;
break;
}
}
const size_t entries_and_buffers =
Common::AlignUp(HomebrewInPlaceConfigTableSize + HomebrewNextLoadPathSize +
HomebrewNextLoadArgvSize + image.argv_string.size(),
Core::Memory::YUZU_PAGESIZE);
image.args_offset = program_image.size();
image.codeset.DataSegment().size += static_cast<u32>(entries_and_buffers);
program_image.resize(image.args_offset + entries_and_buffers);
image.image_size = program_image.size();
image.codeset.memory = std::move(program_image);
return image;
}
bool LoadNroInPlace(Core::System& system, Kernel::KProcess& process, Kernel::KThread& thread,
const FileSys::VirtualFile& nro_file, const std::string& nro_path,
const std::string& launch_argv) {
if (!nro_file) {
return false;
}
#ifdef HAS_NCE
if (Settings::IsNceEnabled()) {
LOG_WARNING(Loader,
"Homebrew next-load: in-place handoff unavailable because NCE is enabled");
return false;
}
#endif
size_t live_threads = 0;
for (auto& candidate : process.GetThreadList()) {
if (candidate.GetState() != Kernel::ThreadState::Terminated) {
live_threads++;
}
}
if (live_threads != 1) {
LOG_WARNING(Loader, "NextLoad: in-place handoff failed because live_threads={}",
live_threads);
return false;
}
const auto stack_top = process.GetMainThreadStackTop();
if (GetInteger(stack_top) == 0) {
LOG_WARNING(Loader, "NextLoad: in-place handoff failed because stack_top=0");
return false;
}
const auto nxlink_server_mode = Settings::values.homebrew_nxlink_server_mode.GetValue();
const bool append_loopback_nxlink_marker =
nxlink_server_mode != Settings::HomebrewNxlinkServerMode::Disabled;
auto image = BuildHomebrewNroImage(
nro_file->ReadAllBytes(), nro_path, nro_file->GetName(), launch_argv,
process.GetHomebrewNxlinkArgvMarker(), append_loopback_nxlink_marker);
if (!image) {
LOG_WARNING(Loader, "NextLoad: in-place handoff failed because '{}' is invalid",
nro_path);
return false;
}
const auto capacity = process.GetCodeSize();
if (image->image_size > capacity) {
LOG_WARNING(Loader,
"NextLoad: in-place handoff failed because image_size=0x{:X} exceeds "
"capacity=0x{:X}",
image->image_size, capacity);
return false;
}
const u64 base = GetInteger(process.GetEntryPoint());
const u64 heap_addr = GetInteger(process.GetPageTable().GetHeapRegionStart());
const size_t heap_size = process.GetPageTable().GetBasePageTable().GetCurrentHeapSize();
if (heap_addr == 0 || heap_size == 0) {
LOG_WARNING(Loader,
"NextLoad: in-place handoff failed because heap override is "
"unavailable (addr=0x{:016X}, size=0x{:X})",
heap_addr, heap_size);
return false;
}
const u64 config_addr = base + image->args_offset;
const u64 next_load_path_addr = config_addr + HomebrewInPlaceConfigTableSize;
const u64 next_load_argv_addr = next_load_path_addr + HomebrewNextLoadPathSize;
const u64 argv_addr = next_load_argv_addr + HomebrewNextLoadArgvSize;
const u64 argv_entry_addr = image->argv_string.empty() ? 0 : argv_addr;
const std::string argv0 = MakeHomebrewArgv0(nro_path, nro_file->GetName());
const std::string homebrew_initial_cwd = GetHomebrewInitialCwd(argv0);
u64 program_id{};
AppLoader_NRO loader{nro_file};
if (loader.ReadProgramId(program_id) != Loader::ResultStatus::Success) {
LOG_WARNING(Loader, "NextLoad: in-place handoff could not read NRO program id");
}
Kernel::Handle main_thread_handle{};
if (process.GetHandleTable().Add(system.Kernel(), std::addressof(main_thread_handle), &thread)
.IsError()) {
LOG_WARNING(Loader,
"NextLoad: in-place handoff failed because main thread handle failed");
return false;
}
Kernel::Handle process_handle{};
if (process.GetHandleTable().Add(system.Kernel(), std::addressof(process_handle), &process)
.IsError()) {
LOG_WARNING(Loader,
"NextLoad: in-place handoff failed because process handle failed");
return false;
}
if (!process.GetMemory().ZeroBlock(Common::ProcessAddress{heap_addr}, heap_size)) {
LOG_WARNING(Loader,
"NextLoad: in-place handoff failed because heap clear failed "
"(addr=0x{:016X}, size=0x{:X})",
heap_addr, heap_size);
return false;
}
process.LoadModule(system.Kernel(), std::move(image->codeset), process.GetEntryPoint());
const HomebrewConfigEntry entries[HomebrewInPlaceConfigEntryCount] = {
{HomebrewEntryMainThreadHandle, 0, {main_thread_handle, 0}},
{HomebrewEntryProcessHandle, 0, {process_handle, 0}},
{HomebrewEntryNextLoadPath, 0, {next_load_path_addr, next_load_argv_addr}},
{HomebrewEntryOverrideHeap, 0, {heap_addr, heap_size}},
{HomebrewEntryAppletType, 0, {HomebrewAppletTypeApplication, 0}},
{HomebrewEntryArgv, 0, {0, argv_entry_addr}},
{HomebrewEntrySyscallAvailableHint, 0, {HomebrewAllSvcHints, HomebrewAllSvcHints}},
{HomebrewEntryRandomSeed, 0,
{process.GetRandomEntropy(0), process.GetRandomEntropy(1)}},
{HomebrewEntryHosVersion, 0, {HomebrewHosVersion, 0}},
{HomebrewEntrySyscallAvailableHint2, 0, {HomebrewAllSvcHints, 0}},
{HomebrewEntryEndOfList, 0, {0, 0}},
};
process.GetMemory().WriteBlock(Common::ProcessAddress{config_addr}, entries, sizeof(entries));
process.GetMemory().WriteBlock(Common::ProcessAddress{argv_addr}, image->argv_string.data(),
image->argv_string.size());
process.GetMemory().Write32(Common::ProcessAddress{GetInteger(thread.GetTlsAddress()) + 0x110},
main_thread_handle);
process.SetArgReturnAddress(Kernel::KProcessAddress{
image->exit_process_offset ? base + *image->exit_process_offset : 0});
SetHomebrewConfigPointers(process, config_addr, next_load_path_addr, next_load_argv_addr);
if (image->nxlink_argv_marker) {
process.SetHomebrewNxlinkArgvMarker(*image->nxlink_argv_marker);
} else {
process.ClearHomebrewNxlinkArgvMarker();
}
HomebrewNxlink::ApplyServerMode(nxlink_server_mode, image->nxlink_argv_marker);
system.GetFileSystemController().RegisterProcess(
process.GetProcessId(), program_id,
std::make_unique<FileSys::RomFSFactory>(loader, system.GetContentProvider(),
system.GetFileSystemController()),
homebrew_initial_cwd);
process.SetHomebrewInPlaceNextLoad(true);
auto& context = thread.GetContext();
context = {};
context.r[0] = config_addr;
context.r[1] = UINT64_MAX;
context.r[18] = Common::Random::Random64(0) | 1;
context.lr = image->exit_process_offset ? base + *image->exit_process_offset : 0;
context.pc = base;
context.sp = GetInteger(stack_top);
context.fpcr = 0;
context.fpsr = 0;
for (std::size_t core = 0; core < Core::Hardware::NUM_CPU_CORES; core++) {
if (auto* arm = process.GetArmInterface(core); arm != nullptr) {
arm->ClearInstructionCache();
}
}
return true;
}
static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
const std::vector<u8>& data) {
const std::vector<u8>& data, std::string nro_path,
std::string file_name) {
if (data.size() < sizeof(NroHeader)) {
return {};
}
@@ -195,47 +625,49 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
codeset.DataSegment().size += bss_size;
program_image.resize(static_cast<u32>(program_image.size()) + bss_size);
struct ConfigEntry {
u32_le key;
u32_le flags;
u64_le value[2];
};
static_assert(sizeof(ConfigEntry) == 0x18);
// AArch64 encoding for svc #0x7 (ExitProcess).
constexpr u32 kSvcExitProcessInstruction = 0xD40000E1;
constexpr size_t kNumEntries = 4; // MainThreadHandle, AppletType, Argv, EndOfList
constexpr size_t kConfigTableSize = kNumEntries * sizeof(ConfigEntry);
std::string argv_string;
size_t args_offset_in_image = 0;
std::optional<size_t> exit_process_offset_in_image;
const auto& program_args = Settings::values.program_args.GetValue();
const std::string argv0 = MakeHomebrewArgv0(std::move(nro_path), std::move(file_name));
argv_string = QuoteHomebrewArgvComponent(argv0);
if (!program_args.empty()) {
argv_string = "homebrew ";
argv_string.push_back(' ');
argv_string += program_args;
argv_string.push_back('\0');
const auto& code = codeset.CodeSegment();
const size_t code_end = (std::min)(program_image.size(), code.offset + code.size);
for (size_t offset = code.offset; offset + sizeof(u32) <= code_end; offset += sizeof(u32)) {
u32 instruction{};
std::memcpy(&instruction, program_image.data() + offset, sizeof(instruction));
if (instruction == kSvcExitProcessInstruction) {
exit_process_offset_in_image = offset;
break;
}
}
if (!exit_process_offset_in_image) {
LOG_WARNING(Loader,
"Unable to find svcExitProcess in NRO; returning from main may fault");
}
const size_t entries_and_argv =
Common::AlignUp(kConfigTableSize + argv_string.size(), Core::Memory::YUZU_PAGESIZE);
args_offset_in_image = program_image.size();
codeset.DataSegment().size += static_cast<u32>(entries_and_argv);
program_image.resize(args_offset_in_image + entries_and_argv);
}
const auto nxlink_server_mode = Settings::values.homebrew_nxlink_server_mode.GetValue();
const bool append_loopback_nxlink_marker =
nxlink_server_mode != Settings::HomebrewNxlinkServerMode::Disabled;
const auto nxlink_argv_marker =
HomebrewNxlink::PrepareArgv(argv_string, {}, append_loopback_nxlink_marker);
if (argv_string.empty() || argv_string.back() != '\0') {
argv_string.push_back('\0');
}
const auto& code_segment = codeset.CodeSegment();
const size_t code_end =
(std::min)(program_image.size(), code_segment.offset + code_segment.size);
for (size_t offset = code_segment.offset; offset + sizeof(u32) <= code_end;
offset += sizeof(u32)) {
u32 instruction{};
std::memcpy(&instruction, program_image.data() + offset, sizeof(instruction));
if (instruction == HomebrewSvcExitProcessInstruction) {
exit_process_offset_in_image = offset;
break;
}
}
if (!exit_process_offset_in_image) {
LOG_WARNING(Loader, "Unable to find svcExitProcess in NRO; returning from main may fault");
}
const size_t entries_and_buffers =
Common::AlignUp(HomebrewBaseConfigTableSize + HomebrewNextLoadPathSize +
HomebrewNextLoadArgvSize + argv_string.size(),
Core::Memory::YUZU_PAGESIZE);
args_offset_in_image = program_image.size();
codeset.DataSegment().size += static_cast<u32>(entries_and_buffers);
program_image.resize(args_offset_in_image + entries_and_buffers);
size_t image_size = program_image.size();
#ifdef HAS_NCE
@@ -263,6 +695,9 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
image_size += patch_segment.size;
}
#endif
// In-place NextLoad reuses the original code mapping; leave room for larger homebrew cores.
constexpr size_t HomebrewCodeArenaSize = 192 * 1024 * 1024;
image_size = (std::max)(image_size, HomebrewCodeArenaSize);
// Enable direct memory mapping in case of NCE.
const u64 fastmem_base = [&]() -> size_t {
@@ -297,31 +732,44 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
// Load codeset for current process
codeset.memory = std::move(program_image);
process.LoadModule(system.Kernel(), std::move(codeset), process.GetEntryPoint());
if (!argv_string.empty()) {
constexpr u32 kEntryEndOfList = 0;
constexpr u32 kEntryMainThreadHandle = 1;
constexpr u32 kEntryArgv = 5;
constexpr u32 kEntryAppletType = 7;
constexpr u32 kAppletTypeApplication = 0;
process.SetHomebrewInPlaceNextLoad(false);
if (nxlink_argv_marker) {
process.SetHomebrewNxlinkArgvMarker(*nxlink_argv_marker);
} else {
process.ClearHomebrewNxlinkArgvMarker();
}
HomebrewNxlink::ApplyServerMode(nxlink_server_mode, nxlink_argv_marker);
{
const u64 base = GetInteger(process.GetEntryPoint());
const u64 config_addr = base + args_offset_in_image;
const u64 argv_addr = config_addr + kConfigTableSize;
const u64 next_load_path_addr = config_addr + HomebrewBaseConfigTableSize;
const u64 next_load_argv_addr = next_load_path_addr + HomebrewNextLoadPathSize;
const u64 argv_addr = next_load_argv_addr + HomebrewNextLoadArgvSize;
const u64 argv_entry_addr = argv_string.empty() ? 0 : argv_addr;
const ConfigEntry entries[kNumEntries] = {
{kEntryMainThreadHandle, 0, {0, 0}}, // Value[0] patched in Run()
{kEntryAppletType, 0, {kAppletTypeApplication, 0}},
{kEntryArgv, 0, {0, argv_addr}},
{kEntryEndOfList, 0, {0, 0}},
const HomebrewConfigEntry entries[HomebrewBaseConfigEntryCount] = {
{HomebrewEntryMainThreadHandle, 0, {0, 0}}, // Value[0] patched in Run()
{HomebrewEntryProcessHandle, 0, {0, 0}}, // Value[0] patched in Run()
{HomebrewEntryNextLoadPath, 0, {next_load_path_addr, next_load_argv_addr}},
{HomebrewEntryAppletType, 0, {HomebrewAppletTypeApplication, 0}},
{HomebrewEntryArgv, 0, {0, argv_entry_addr}},
{HomebrewEntrySyscallAvailableHint, 0, {HomebrewAllSvcHints, HomebrewAllSvcHints}},
{HomebrewEntryRandomSeed, 0,
{process.GetRandomEntropy(0), process.GetRandomEntropy(1)}},
{HomebrewEntryHosVersion, 0, {HomebrewHosVersion, 0}},
{HomebrewEntrySyscallAvailableHint2, 0, {HomebrewAllSvcHints, 0}},
{HomebrewEntryEndOfList, 0, {0, 0}},
};
process.GetMemory().WriteBlock(Common::ProcessAddress{config_addr}, entries, sizeof(entries));
process.GetMemory().WriteBlock(Common::ProcessAddress{argv_addr}, argv_string.data(), argv_string.size());
constexpr size_t kMainThreadHandleValueOffset = offsetof(ConfigEntry, value);
process.SetArgPointer(Kernel::KProcessAddress{config_addr});
process.GetMemory().WriteBlock(Common::ProcessAddress{config_addr}, entries,
sizeof(entries));
if (!argv_string.empty()) {
process.GetMemory().WriteBlock(Common::ProcessAddress{argv_addr}, argv_string.data(),
argv_string.size());
}
if (exit_process_offset_in_image) {
process.SetArgReturnAddress(Kernel::KProcessAddress{base + *exit_process_offset_in_image});
}
process.SetMainThreadHandleAddr(Kernel::KProcessAddress{config_addr + kMainThreadHandleValueOffset});
SetHomebrewConfigPointers(process, config_addr, next_load_path_addr, next_load_argv_addr);
}
return true;
@@ -329,7 +777,8 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
bool AppLoader_NRO::LoadNro(Core::System& system, Kernel::KProcess& process,
const FileSys::VfsFile& nro_file) {
return LoadNroImpl(system, process, nro_file.ReadAllBytes());
return LoadNroImpl(system, process, nro_file.ReadAllBytes(), nro_file.GetFullPath(),
nro_file.GetName());
}
AppLoader_NRO::LoadResult AppLoader_NRO::Load(Kernel::KProcess& process, Core::System& system) {
@@ -343,10 +792,13 @@ AppLoader_NRO::LoadResult AppLoader_NRO::Load(Kernel::KProcess& process, Core::S
u64 program_id{};
ReadProgramId(program_id);
const std::string argv0 = MakeHomebrewArgv0(file->GetFullPath(), file->GetName());
const std::string homebrew_initial_cwd = GetHomebrewInitialCwd(argv0);
system.GetFileSystemController().RegisterProcess(
process.GetProcessId(), program_id,
std::make_unique<FileSys::RomFSFactory>(*this, system.GetContentProvider(),
system.GetFileSystemController()));
system.GetFileSystemController()),
homebrew_initial_cwd);
is_loaded = true;
return {ResultStatus::Success, LoadParameters{Kernel::KThread::DefaultThreadPriority,
+8
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -19,10 +22,15 @@ class NACP;
namespace Kernel {
class KProcess;
class KThread;
}
namespace Loader {
[[nodiscard]] bool LoadNroInPlace(Core::System& system, Kernel::KProcess& process,
Kernel::KThread& thread, const FileSys::VirtualFile& nro_file,
const std::string& nro_path, const std::string& launch_argv);
/// Loads an NRO file
class AppLoader_NRO final : public AppLoader {
public:
@@ -100,6 +100,7 @@ void EmulatedConsole::ReloadInput() {
motion.gyro = emulated_motion.GetGyroscope();
motion.rotation = emulated_motion.GetRotations();
motion.orientation = emulated_motion.GetOrientation();
motion.quaternion = emulated_motion.GetQuaternion();
motion.is_at_rest = !emulated_motion.IsMoving(motion_sensitivity);
// Unique index for identifying touch device source
+7
View File
@@ -648,6 +648,13 @@ SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_en
// Disable raw input. When enabled this setting causes SDL to die when a web applet opens
SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, Settings::values.enable_raw_input ? "1" : "0");
#ifdef _WIN32
if (Settings::values.disable_wgi_xinput) {
SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_RAWINPUT_CORRELATE_XINPUT, "0", SDL_HINT_OVERRIDE);
SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_WGI, "0", SDL_HINT_OVERRIDE);
}
#endif
// SDL3 defaults Steam Controller Bluetooth HIDAPI support to off, which can disable gyro.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1");
SDL_SetHint(SDL_HINT_GAMECONTROLLER_SENSOR_FUSION, "1");
@@ -302,6 +302,8 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
INSERT(Settings, device_name, tr("Device Name"), tr("The name of the console."));
INSERT(Settings, program_args, tr("Homebrew Args"),
tr("Command-line arguments passed to homebrew at launch (e.g. -noglsl)."));
INSERT(Settings, homebrew_nxlink_server_mode, tr("nxlink Server"),
tr("Starts a local nxlink server for homebrew stdout/stderr streams."));
INSERT(Settings, custom_rtc, tr("Custom RTC Date:"),
tr("This option allows to change the clock of the console.\n"
"Can be used to manipulate time in games."));
+3 -1
View File
@@ -159,7 +159,9 @@ void GameListModel::RemoveFavorite(u64 program_id) {
void GameListModel::Repopulate() {
current_worker.reset();
QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
if (!QtCommon::system->IsPoweredOn()) {
QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
}
PopulateAsync(UISettings::values.game_dirs);
}
+1 -1
View File
@@ -33,7 +33,7 @@ add_library(video_core STATIC
control/channel_state_cache.h
control/scheduler.cpp
control/scheduler.h
deferred_destruction_queue.h
delayed_destruction_ring.h
dirty_flags.cpp
dirty_flags.h
dma_pusher.cpp
+32 -62
View File
@@ -31,77 +31,44 @@ BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, R
immediately_free = (Settings::values.vram_usage_mode.GetValue() == Settings::VramUsageMode::Aggressive);
#endif
if (!runtime.CanReportMemoryUsage()) {
memory_budget = FALLBACK_MEMORY_BUDGET;
minimum_memory = DEFAULT_EXPECTED_MEMORY;
critical_memory = DEFAULT_CRITICAL_MEMORY;
return;
}
memory_budget = runtime.GetDeviceLocalMemory();
const s64 device_local_memory = static_cast<s64>(runtime.GetDeviceLocalMemory());
const s64 min_spacing_expected = device_local_memory - 1_GiB;
const s64 min_spacing_critical = device_local_memory - 512_MiB;
const s64 mem_threshold = (std::min)(device_local_memory, TARGET_THRESHOLD);
const s64 min_vacancy_expected = (6 * mem_threshold) / 10;
const s64 min_vacancy_critical = (2 * mem_threshold) / 10;
minimum_memory = static_cast<u64>(
(std::max)((std::min)(device_local_memory - min_vacancy_expected, min_spacing_expected),
DEFAULT_EXPECTED_MEMORY));
critical_memory = static_cast<u64>(
(std::max)((std::min)(device_local_memory - min_vacancy_critical, min_spacing_critical),
DEFAULT_CRITICAL_MEMORY));
}
template <class P>
BufferCache<P>::~BufferCache() = default;
template <class P>
u64 BufferCache<P>::DeviceUsage(bool force_refresh) {
if (!runtime.CanReportAllocationUsage()) {
return total_used_memory;
}
if (force_refresh || usage_refresh_countdown == 0) {
cached_device_usage = runtime.GetDeviceAllocationUsage();
usage_refresh_countdown = USAGE_REFRESH_INTERVAL;
} else {
--usage_refresh_countdown;
}
return cached_device_usage;
}
template <class P>
u64 BufferCache<P>::ReclaimMemory(u64 target_bytes, bool allow_download) {
if (target_bytes == 0 || in_reclaim) {
return 0;
}
in_reclaim = true;
sentenced_buffers.Reclaim(runtime.CompletedSyncPoint());
u64 freed = 0;
const auto clean_up = [&](BufferId buffer_id) {
if (freed >= target_bytes) {
void BufferCache<P>::RunGarbageCollector() {
const bool aggressive_gc = total_used_memory >= critical_memory;
const u64 ticks_to_destroy = aggressive_gc ? 60 : 120;
int num_iterations = aggressive_gc ? 64 : 32;
const auto clean_up = [this, &num_iterations](BufferId buffer_id) {
if (num_iterations == 0) {
return true;
}
--num_iterations;
auto& buffer = slot_buffers[buffer_id];
if (!allow_download && IsRegionGpuModified(buffer.CpuAddr(), buffer.SizeBytes())) {
return false;
}
const u64 buffer_bytes = Common::AlignUp(buffer.SizeBytes(), 1024);
DownloadBufferMemory(buffer);
DeleteBuffer(buffer_id);
freed += buffer_bytes;
return false;
};
const u64 cold_tick =
frame_tick > RECLAIM_GUARD_FRAMES ? frame_tick - RECLAIM_GUARD_FRAMES : 0;
lru_cache.ForEachItemBelow(cold_tick, clean_up);
if (freed < target_bytes) {
lru_cache.ForEachItemBelow(frame_tick > 0 ? frame_tick - 1 : 0, clean_up);
}
sentenced_buffers.Reclaim(runtime.CompletedSyncPoint());
in_reclaim = false;
usage_refresh_countdown = 0;
reclaim_stalled = freed == 0;
return freed;
}
template <class P>
void BufferCache<P>::EnsureHeadroom(bool allow_download) {
if (reclaim_stalled) {
return;
}
const u64 limit = memory_budget > RECLAIM_HEADROOM ? memory_budget - RECLAIM_HEADROOM : 0;
const u64 usage = DeviceUsage(false);
if (usage <= limit) {
return;
}
const u64 target = (limit / 100) * RECLAIM_TARGET_PERCENT;
ReclaimMemory((std::min)(usage - target, total_used_memory), allow_download);
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, clean_up);
}
template <class P>
@@ -129,11 +96,15 @@ void BufferCache<P>::TickFrame() {
const bool skip_preferred = hits * 256 < shots * 251;
channel_state->uniform_buffer_skip_cache_size = skip_preferred ? DEFAULT_SKIP_CACHE_SIZE : 0;
usage_refresh_countdown = 0;
reclaim_stalled = false;
EnsureHeadroom(true);
// If we can obtain the memory info, use it instead of the estimate.
if (runtime.CanReportMemoryUsage()) {
total_used_memory = runtime.GetDeviceMemoryUsage();
}
if (total_used_memory >= minimum_memory) {
RunGarbageCollector();
}
++frame_tick;
sentenced_buffers.Reclaim(runtime.CompletedSyncPoint());
delayed_destruction_ring.Tick();
for (auto& buffer : async_buffers_death_ring) {
runtime.FreeDeferredStagingBuffer(buffer);
@@ -1605,7 +1576,6 @@ void BufferCache<P>::JoinOverlap(BufferId new_buffer_id, BufferId overlap_id,
template <class P>
BufferId BufferCache<P>::CreateBuffer(DAddr device_addr, u32 wanted_size) {
EnsureHeadroom(false);
DAddr device_addr_end = Common::AlignUp(device_addr + wanted_size, CACHING_PAGESIZE);
device_addr = Common::AlignDown(device_addr, CACHING_PAGESIZE);
wanted_size = static_cast<u32>(device_addr_end - device_addr);
@@ -1643,7 +1613,7 @@ void BufferCache<P>::ChangeRegister(BufferId buffer_id) {
total_used_memory += Common::AlignUp(size, 1024);
buffer.setLRUID(lru_cache.Insert(buffer_id, frame_tick));
} else {
total_used_memory -= std::min<u64>(total_used_memory, Common::AlignUp(size, 1024));
total_used_memory -= Common::AlignUp(size, 1024);
lru_cache.Free(buffer.getLRUID());
}
const DAddr device_addr_begin = buffer.CpuAddr();
@@ -1902,7 +1872,7 @@ void BufferCache<P>::DeleteBuffer(BufferId buffer_id, bool do_not_mark) {
#ifdef YUZU_LEGACY
if (!do_not_mark || !immediately_free)
#endif
sentenced_buffers.Push(std::move(slot_buffers[buffer_id]), runtime.CurrentSyncPoint());
delayed_destruction_ring.Push(std::move(slot_buffers[buffer_id]));
slot_buffers.erase(buffer_id);
+14 -19
View File
@@ -9,7 +9,6 @@
#include <algorithm>
#include <array>
#include <bit>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
@@ -31,7 +30,7 @@
#include "common/slot_vector.h"
#include "video_core/buffer_cache/buffer_base.h"
#include "video_core/control/channel_state_cache.h"
#include "video_core/deferred_destruction_queue.h"
#include "video_core/delayed_destruction_ring.h"
#include "video_core/dirty_flags.h"
#include "video_core/engines/maxwell_3d.h"
#include "video_core/engines/kepler_compute.h"
@@ -183,15 +182,13 @@ class BufferCache : public VideoCommon::ChannelSetupCaches<BufferCacheChannelInf
static constexpr bool USE_MEMORY_MAPS_FOR_UPLOADS = P::USE_MEMORY_MAPS_FOR_UPLOADS;
#ifdef YUZU_LEGACY
static constexpr u64 RECLAIM_HEADROOM = 384_MiB;
static constexpr s64 TARGET_THRESHOLD = 3_GiB;
#else
static constexpr u64 RECLAIM_HEADROOM = 512_MiB;
static constexpr s64 TARGET_THRESHOLD = 4_GiB;
#endif
static constexpr u64 FALLBACK_MEMORY_BUDGET = 2_GiB;
static constexpr u32 USAGE_REFRESH_INTERVAL = 16;
static constexpr u64 RECLAIM_GUARD_FRAMES = 8;
static constexpr u64 RECLAIM_TARGET_PERCENT = 88;
static constexpr s64 DEFAULT_EXPECTED_MEMORY = 512_MiB;
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB;
// Debug Flags.
@@ -218,8 +215,6 @@ public:
void TickFrame();
u64 ReclaimMemory(u64 target_bytes, bool allow_download);
void WriteMemory(DAddr device_addr, u64 size);
void CachedWriteMemory(DAddr device_addr, u64 size);
@@ -363,9 +358,7 @@ private:
((device_addr + size) & ~Core::DEVICE_PAGEMASK);
}
u64 DeviceUsage(bool force_refresh);
void EnsureHeadroom(bool allow_download);
void RunGarbageCollector();
void BindHostIndexBuffer();
@@ -482,7 +475,12 @@ private:
Tegra::MaxwellDeviceMemoryManager& device_memory;
Common::SlotVector<Buffer> slot_buffers;
DeferredDestructionQueue<Buffer> sentenced_buffers;
#ifdef YUZU_LEGACY
static constexpr size_t TICKS_TO_DESTROY = 6;
#else
static constexpr size_t TICKS_TO_DESTROY = 8;
#endif
DelayedDestructionRing<Buffer, TICKS_TO_DESTROY> delayed_destruction_ring;
const Tegra::Engines::Maxwell3D::DrawManager::IndirectParams* current_draw_indirect{};
@@ -517,11 +515,8 @@ private:
Common::LeastRecentlyUsedCache<LRUItemParams> lru_cache;
u64 frame_tick = 0;
u64 total_used_memory = 0;
u64 memory_budget = 0;
u64 cached_device_usage = 0;
u32 usage_refresh_countdown = 0;
bool in_reclaim = false;
bool reclaim_stalled = false;
u64 minimum_memory = 0;
u64 critical_memory = 0;
BufferId inline_buffer_id;
#ifdef YUZU_LEGACY
bool immediately_free = false;
@@ -1,56 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstddef>
#include <utility>
#include <boost/container/deque.hpp>
#include <boost/container/options.hpp>
#include "common/common_types.h"
namespace VideoCommon {
template <typename T>
class DeferredDestructionQueue {
public:
void Push(T&& object, u64 sync_point) {
entries.emplace_back(std::move(object), sync_point);
}
void Reclaim(u64 completed_sync_point) {
while (!entries.empty() && entries.front().sync_point <= completed_sync_point) {
entries.pop_front();
}
}
void Clear() {
entries.clear();
}
[[nodiscard]] size_t Size() const noexcept {
return entries.size();
}
[[nodiscard]] bool Empty() const noexcept {
return entries.empty();
}
private:
struct Entry {
Entry(T&& object_, u64 sync_point_) noexcept
: object{std::move(object_)}, sync_point{sync_point_} {}
T object;
u64 sync_point;
};
using EntryDequeOptions =
boost::container::deque_options<boost::container::block_size<8u>>::type;
boost::container::deque<Entry, void, EntryDequeOptions> entries;
};
} // namespace VideoCommon
+34
View File
@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstddef>
#include <utility>
#include <vector>
namespace VideoCommon {
/// Container to push objects to be destroyed a few ticks in the future
template <typename T, size_t TICKS_TO_DESTROY>
class DelayedDestructionRing {
public:
void Tick() {
index = (index + 1) % TICKS_TO_DESTROY;
elements[index].clear();
}
void Push(T&& object) {
elements[index].push_back(std::move(object));
}
private:
size_t index = 0;
std::array<std::vector<T>, TICKS_TO_DESTROY> elements;
};
} // namespace VideoCommon
+5 -8
View File
@@ -18,7 +18,7 @@
#include "common/common_types.h"
#include "common/settings.h"
#include "common/thread.h"
#include "video_core/deferred_destruction_queue.h"
#include "video_core/delayed_destruction_ring.h"
#include "video_core/gpu.h"
#include "video_core/host1x/host1x.h"
#include "video_core/host1x/syncpoint_manager.h"
@@ -50,8 +50,7 @@ public:
/// Notify the fence manager about a new frame
void TickFrame() {
std::unique_lock lock(ring_guard);
++retire_tick;
sentenced_fences.Reclaim(retire_tick > RETIRE_DELAY ? retire_tick - RETIRE_DELAY : 0);
delayed_destruction_ring.Tick();
}
// Unlike other fences, this one doesn't
@@ -187,7 +186,7 @@ private:
}
{
std::unique_lock lock(ring_guard);
sentenced_fences.Push(std::move(current_fence), retire_tick);
delayed_destruction_ring.Push(std::move(current_fence));
}
fences.pop();
}
@@ -220,7 +219,7 @@ private:
}
{
std::unique_lock lock(ring_guard);
sentenced_fences.Push(std::move(current_fence), retire_tick);
delayed_destruction_ring.Push(std::move(current_fence));
}
}
}
@@ -265,9 +264,7 @@ private:
std::jthread fence_thread;
static constexpr u64 RETIRE_DELAY = 8;
u64 retire_tick = 1;
DeferredDestructionQueue<TFence> sentenced_fences;
DelayedDestructionRing<TFence, 8> delayed_destruction_ring;
};
} // namespace VideoCommon
-1
View File
@@ -30,7 +30,6 @@ void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Core::Fronten
thread = std::jthread([&](std::stop_token stop_token) {
Common::SetCurrentThreadName("GPU");
Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical);
Common::SetCurrentThreadToPerformanceCores();
system.RegisterHostThread();
auto current_context = context.Acquire();
@@ -32,7 +32,6 @@ set(SHADER_FILES
${CMAKE_CURRENT_SOURCE_DIR}/convert_msaa_to_non_msaa.frag
${CMAKE_CURRENT_SOURCE_DIR}/convert_non_msaa_to_msaa.comp
${CMAKE_CURRENT_SOURCE_DIR}/convert_non_msaa_to_msaa.frag
${CMAKE_CURRENT_SOURCE_DIR}/convert_non_msaa_to_msaa_depth.frag
${CMAKE_CURRENT_SOURCE_DIR}/convert_s8d24_to_abgr8.frag
${CMAKE_CURRENT_SOURCE_DIR}/full_screen_triangle.vert
${CMAKE_CURRENT_SOURCE_DIR}/fxaa.frag
@@ -1384,7 +1384,11 @@ void DecompressBlock(ivec3 coord) {
p = Cf / 65535.0f;
}
#ifdef VULKAN
imageStore(dest_image, coord + ivec3(i, j, 0), p.gbar);
#else
imageStore(dest_image, coord + ivec3(i, j, 0), clamp(p, 0.0f, 1.0f).gbar);
#endif
}
}
}
@@ -1,19 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#version 450 core
layout(binding = 0) uniform sampler2D img_in;
layout(push_constant) uniform PushConstants {
ivec2 dst_offset;
ivec2 src_offset;
ivec2 scale;
};
void main() {
const ivec2 msaa_coord = ivec2(gl_FragCoord.xy) - dst_offset;
const ivec2 sample_offset = ivec2(gl_SampleID % scale.x, gl_SampleID / scale.x);
const ivec2 coord = msaa_coord * scale + sample_offset + src_offset;
gl_FragDepth = texelFetch(img_in, coord, 0).r;
}
+94 -89
View File
@@ -417,14 +417,6 @@ void HLE_TransformFeedbackSetup::Execute(Core::System& system, Engines::Maxwell3
default: return std::monostate{};
}
}
[[nodiscard]] inline bool CanBeHLEProgram(u64 hash) noexcept {
switch (hash) {
#define HLE_MACRO_ELEM(HASH, TY, VAL) case HASH: return true;
HLE_MACRO_LIST
#undef HLE_MACRO_ELEM
default: return false;
}
}
void MacroInterpreterImpl::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, std::span<const u32> params, u32 method) {
Reset();
@@ -790,13 +782,17 @@ void MacroJITx64Impl::Compile_ALU(Core::System& system, Macro::Opcode opcode) {
const bool valid_operation = !is_a_zero && !is_b_zero;
[[maybe_unused]] const bool is_move_operation = !is_a_zero && is_b_zero;
const bool has_zero_register = is_a_zero || is_b_zero;
const bool no_zero_reg_skip = opcode.alu_operation == Macro::ALUOperation::AddWithCarry ||
opcode.alu_operation == Macro::ALUOperation::SubtractWithBorrow;
const bool zero_reg_skip =
optimizer.zero_reg_skip && !is_a_zero && is_b_zero &&
(opcode.alu_operation == Macro::ALUOperation::Xor ||
opcode.alu_operation == Macro::ALUOperation::Or ||
(optimizer.can_skip_carry && (opcode.alu_operation == Macro::ALUOperation::Add ||
opcode.alu_operation == Macro::ALUOperation::Subtract)));
Xbyak::Reg32 src_a;
Xbyak::Reg32 src_b;
if (!optimizer.zero_reg_skip || no_zero_reg_skip) {
if (!zero_reg_skip) {
src_a = Compile_GetRegister(opcode.src_a, RESULT);
src_b = Compile_GetRegister(opcode.src_b, eax);
} else {
@@ -812,7 +808,7 @@ void MacroJITx64Impl::Compile_ALU(Core::System& system, Macro::Opcode opcode) {
switch (opcode.alu_operation) {
case Macro::ALUOperation::Add:
if (optimizer.zero_reg_skip) {
if (zero_reg_skip) {
if (valid_operation) {
add(src_a, src_b);
}
@@ -830,7 +826,7 @@ void MacroJITx64Impl::Compile_ALU(Core::System& system, Macro::Opcode opcode) {
setc(byte[STATE + offsetof(JITState, carry_flag)]);
break;
case Macro::ALUOperation::Subtract:
if (optimizer.zero_reg_skip) {
if (zero_reg_skip) {
if (valid_operation) {
sub(src_a, src_b);
has_emitted = true;
@@ -849,7 +845,7 @@ void MacroJITx64Impl::Compile_ALU(Core::System& system, Macro::Opcode opcode) {
setc(byte[STATE + offsetof(JITState, carry_flag)]);
break;
case Macro::ALUOperation::Xor:
if (optimizer.zero_reg_skip) {
if (zero_reg_skip) {
if (valid_operation) {
xor_(src_a, src_b);
}
@@ -858,7 +854,7 @@ void MacroJITx64Impl::Compile_ALU(Core::System& system, Macro::Opcode opcode) {
}
break;
case Macro::ALUOperation::Or:
if (optimizer.zero_reg_skip) {
if (zero_reg_skip) {
if (valid_operation) {
or_(src_a, src_b);
}
@@ -867,7 +863,7 @@ void MacroJITx64Impl::Compile_ALU(Core::System& system, Macro::Opcode opcode) {
}
break;
case Macro::ALUOperation::And:
if (optimizer.zero_reg_skip) {
if (zero_reg_skip) {
if (!has_zero_register) {
and_(src_a, src_b);
}
@@ -876,7 +872,7 @@ void MacroJITx64Impl::Compile_ALU(Core::System& system, Macro::Opcode opcode) {
}
break;
case Macro::ALUOperation::AndNot:
if (optimizer.zero_reg_skip) {
if (zero_reg_skip) {
if (!is_a_zero) {
not_(src_b);
and_(src_a, src_b);
@@ -887,7 +883,7 @@ void MacroJITx64Impl::Compile_ALU(Core::System& system, Macro::Opcode opcode) {
}
break;
case Macro::ALUOperation::Nand:
if (optimizer.zero_reg_skip) {
if (zero_reg_skip) {
if (!is_a_zero) {
and_(src_a, src_b);
not_(src_a);
@@ -1345,80 +1341,89 @@ static void Dump(u64 hash, std::span<const u32> code, bool decompiled = false) {
macro_file.write(reinterpret_cast<const char*>(code.data()), code.size_bytes());
}
void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u32 method, std::span<const u32> parameters) {
auto const execute_variant = [&system, &maxwell3d, &parameters, method](AnyCachedMacro& acm) {
if (auto a = std::get_if<HLE_DrawArraysIndirect>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_DrawIndexedIndirect>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_MultiDrawIndexedIndirectCount>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_MultiLayerClear>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_C713C83D8F63CCF3>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_D7333D26E0A93EDE>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_BindShader>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_SetRasterBoundingBox>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_ClearConstBuffer>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_ClearMemory>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_TransformFeedbackSetup>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_DrawIndirectByteCount>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<MacroInterpreterImpl>(&acm))
return a->Execute(system, maxwell3d, parameters, method);
if (auto a = std::get_if<std::unique_ptr<DynamicCachedMacro>>(&acm))
return a->get()->Execute(system, maxwell3d, parameters, method);
};
if (auto const it = macro_cache.find(method); it != macro_cache.end()) {
auto& ci = it->second;
if (!CanBeHLEProgram(ci.hash) || Settings::values.disable_macro_hle)
maxwell3d.RefreshParameters(); //LLE must reload parameters
execute_variant(ci.program);
} else {
// Macro not compiled, check if it's uploaded and if so, compile it
std::optional<u32> mid_method;
const auto macro_code = uploaded_macro_code.find(method);
if (macro_code == uploaded_macro_code.end()) {
for (const auto& [method_base, code] : uploaded_macro_code) {
if (method >= method_base && (method - method_base) < code.size()) {
mid_method = method_base;
break;
}
}
if (!mid_method.has_value()) {
ASSERT_MSG(false, "Macro 0x{0:x} was not uploaded", method);
return;
}
}
auto& ci = macro_cache[method];
if (mid_method) {
const auto& macro_cached = uploaded_macro_code[mid_method.value()];
const auto rebased_method = method - mid_method.value();
auto& code = uploaded_macro_code[method];
code.resize(macro_cached.size() - rebased_method);
std::memcpy(code.data(), macro_cached.data() + rebased_method, code.size() * sizeof(u32));
ci.hash = Common::HashValue(code);
ci.program = Compile(system, maxwell3d, code);
} else {
ci.program = Compile(system, maxwell3d, macro_code->second);
ci.hash = Common::HashValue(macro_code->second);
}
if (CanBeHLEProgram(ci.hash) && !Settings::values.disable_macro_hle) {
ci.program = GetHLEProgram(ci.hash);
} else {
void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u32 method,
std::span<const u32> parameters) {
const auto execute_variant = [&system, &maxwell3d, &parameters,
method](AnyCachedMacro& cached) {
if (std::holds_alternative<MacroInterpreterImpl>(cached) ||
std::holds_alternative<std::unique_ptr<DynamicCachedMacro>>(cached) ||
Settings::values.disable_macro_hle) {
maxwell3d.RefreshParameters();
}
execute_variant(ci.program);
if (Settings::values.dump_macros) {
Dump(ci.hash, macro_code->second, !std::holds_alternative<std::monostate>(ci.program));
if (auto program = std::get_if<HLE_DrawArraysIndirect>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_DrawIndexedIndirect>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_MultiDrawIndexedIndirectCount>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_MultiLayerClear>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_C713C83D8F63CCF3>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_D7333D26E0A93EDE>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_BindShader>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_SetRasterBoundingBox>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_ClearConstBuffer>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_ClearMemory>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_TransformFeedbackSetup>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<HLE_DrawIndirectByteCount>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<MacroInterpreterImpl>(&cached))
return program->Execute(system, maxwell3d, parameters, method);
if (auto program = std::get_if<std::unique_ptr<DynamicCachedMacro>>(&cached))
return program->get()->Execute(system, maxwell3d, parameters, method);
UNREACHABLE();
};
if (auto const it = macro_cache.find(method); it != macro_cache.end()) {
execute_variant(it->second.program);
return;
}
// Macro not compiled, check if it's uploaded and if so, compile it
std::span<const u32> code;
auto macro_code = uploaded_macro_code.find(method);
if (macro_code == uploaded_macro_code.end()) {
for (auto it = uploaded_macro_code.begin(); it != uploaded_macro_code.end(); ++it) {
const auto& [method_base, uploaded_code] = *it;
if (method >= method_base && (method - method_base) < uploaded_code.size()) {
macro_code = it;
break;
}
}
if (macro_code == uploaded_macro_code.end()) {
ASSERT_MSG(false, "Macro 0x{0:x} was not uploaded", method);
return;
}
const auto rebased_method = method - macro_code->first;
std::vector<u32> rebased_code(macro_code->second.begin() + rebased_method,
macro_code->second.end());
const auto [it, inserted] = uploaded_macro_code.emplace(method, std::move(rebased_code));
ASSERT(inserted);
macro_code = it;
}
code = macro_code->second;
auto& ci = macro_cache[method];
ci.hash = Common::HashRange(code.begin(), code.end());
if (!Settings::values.disable_macro_hle) {
ci.program = GetHLEProgram(ci.hash);
}
if (std::holds_alternative<std::monostate>(ci.program)) {
ci.program = Compile(system, maxwell3d, code);
}
execute_variant(ci.program);
if (Settings::values.dump_macros) {
Dump(ci.hash, code, !std::holds_alternative<std::monostate>(ci.program));
}
}
@@ -93,17 +93,7 @@ public:
void PostCopyBarrier();
void Finish();
void TickFrame(Common::SlotVector<Buffer>&) noexcept {
++sync_point;
}
u64 CurrentSyncPoint() const noexcept {
return sync_point;
}
u64 CompletedSyncPoint() const noexcept {
return sync_point > SYNC_POINT_DELAY ? sync_point - SYNC_POINT_DELAY : 0;
}
void TickFrame(Common::SlotVector<Buffer>&) noexcept {}
void ClearBuffer(Buffer& dest_buffer, u32 offset, size_t size, u32 value);
@@ -138,14 +128,6 @@ public:
u64 GetDeviceMemoryUsage() const;
u64 GetDeviceAllocationUsage() const {
return GetDeviceMemoryUsage();
}
bool CanReportAllocationUsage() const {
return device.CanReportMemoryUsage();
}
void BindFastUniformBuffer(size_t stage, u32 binding_index, u32 size) {
const GLuint handle = fast_uniforms[stage][binding_index].handle;
const GLsizeiptr gl_size = static_cast<GLsizeiptr>(size);
@@ -231,13 +213,9 @@ private:
GL_FRAGMENT_PROGRAM_PARAMETER_BUFFER_NV,
};
static constexpr u64 SYNC_POINT_DELAY = 8;
const Device& device;
StagingBufferPool& staging_buffer_pool;
u64 sync_point = 1;
bool has_fast_buffer_sub_data = false;
bool use_assembly_shaders = false;
bool has_unified_vertex_buffers = false;
@@ -87,14 +87,6 @@ public:
u64 GetDeviceMemoryUsage() const;
u64 GetDeviceAllocationUsage() const {
return GetDeviceMemoryUsage();
}
bool CanReportAllocationUsage() const {
return device.CanReportMemoryUsage();
}
bool CanReportMemoryUsage() const {
return device.CanReportMemoryUsage();
}
@@ -147,19 +139,7 @@ public:
bool HasNativeASTC() const noexcept;
void TickFrame() {
++sync_point;
}
u64 CurrentSyncPoint() const noexcept {
return sync_point;
}
u64 CompletedSyncPoint() const noexcept {
return sync_point > SYNC_POINT_DELAY ? sync_point - SYNC_POINT_DELAY : 0;
}
void WaitSyncPoint(u64) {}
void TickFrame() {}
StateTracker& GetStateTracker() {
return state_tracker;
@@ -194,9 +174,6 @@ private:
std::array<OGLFramebuffer, 4> rescale_read_fbos;
const Settings::ResolutionScalingInfo& resolution;
u64 device_access_memory;
static constexpr u64 SYNC_POINT_DELAY = 8;
u64 sync_point = 1;
};
class Image : public VideoCommon::ImageBase {
@@ -393,7 +370,6 @@ struct TextureCacheParams {
static constexpr bool HAS_EMULATED_COPIES = true;
static constexpr bool HAS_DEVICE_MEMORY_INFO = true;
static constexpr bool IMPLEMENTS_ASYNC_DOWNLOADS = true;
static constexpr bool HAS_TIMELINE_SYNC_POINTS = false;
using Runtime = OpenGL::TextureCacheRuntime;
using Image = OpenGL::Image;
+27 -91
View File
@@ -21,7 +21,6 @@
#include "video_core/host_shaders/convert_depth_to_float_frag_spv.h"
#include "video_core/host_shaders/convert_float_to_depth_frag_spv.h"
#include "video_core/host_shaders/convert_msaa_to_non_msaa_frag_spv.h"
#include "video_core/host_shaders/convert_non_msaa_to_msaa_depth_frag_spv.h"
#include "video_core/host_shaders/convert_non_msaa_to_msaa_frag_spv.h"
#include "video_core/host_shaders/convert_s8d24_to_abgr8_frag_spv.h"
#include "video_core/host_shaders/full_screen_triangle_vert_spv.h"
@@ -520,8 +519,7 @@ void RecordShaderReadBarrier(Scheduler& scheduler, const ImageView& image_view)
}
[[nodiscard]] vk::ImageView MakeMSAACopyView(const vk::Device& device, VkImage image,
VkFormat format, u32 base_level,
VkImageAspectFlags aspect_mask) {
VkFormat format, u32 base_level) {
return device.CreateImageView(VkImageViewCreateInfo{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.pNext = nullptr,
@@ -536,7 +534,7 @@ void RecordShaderReadBarrier(Scheduler& scheduler, const ImageView& image_view)
.a = VK_COMPONENT_SWIZZLE_IDENTITY,
},
.subresourceRange{
.aspectMask = aspect_mask,
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = base_level,
.levelCount = 1,
.baseArrayLayer = 0,
@@ -612,8 +610,6 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
convert_s8d24_to_abgr8_frag(BuildShader(device, CONVERT_S8D24_TO_ABGR8_FRAG_SPV)),
convert_msaa_to_non_msaa_frag(BuildShader(device, CONVERT_MSAA_TO_NON_MSAA_FRAG_SPV)),
convert_non_msaa_to_msaa_frag(BuildShader(device, CONVERT_NON_MSAA_TO_MSAA_FRAG_SPV)),
convert_non_msaa_to_msaa_depth_frag(
BuildShader(device, CONVERT_NON_MSAA_TO_MSAA_DEPTH_FRAG_SPV)),
linear_sampler(device.GetLogical().CreateSampler(SAMPLER_CREATE_INFO<VK_FILTER_LINEAR>)),
nearest_sampler(device.GetLogical().CreateSampler(SAMPLER_CREATE_INFO<VK_FILTER_NEAREST>)) {}
@@ -899,34 +895,16 @@ void BlitImageHelper::CopyMSAA(RenderPassCache& render_pass_cache, VkImage dst_i
const s32 scale_y = 1 << samples_y;
const VkSampleCountFlagBits samples =
msaa_to_non_msaa ? VK_SAMPLE_COUNT_1_BIT : SampleCountFlag(num_samples);
const auto dst_surface_type = VideoCore::Surface::GetFormatType(dst_format);
const bool is_depth = dst_surface_type == VideoCore::Surface::SurfaceType::Depth ||
dst_surface_type == VideoCore::Surface::SurfaceType::DepthStencil;
const bool has_stencil = dst_surface_type == VideoCore::Surface::SurfaceType::DepthStencil;
const VkImageAspectFlags view_aspect =
is_depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT;
VkImageAspectFlags barrier_aspect = VK_IMAGE_ASPECT_COLOR_BIT;
if (is_depth) {
barrier_aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
if (has_stencil) {
barrier_aspect |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
}
RenderPassKey renderpass_key{};
renderpass_key.color_formats.fill(VideoCore::Surface::PixelFormat::Invalid);
if (is_depth) {
renderpass_key.depth_format = dst_format;
} else {
renderpass_key.color_formats[0] = dst_format;
renderpass_key.depth_format = VideoCore::Surface::PixelFormat::Invalid;
}
renderpass_key.color_formats[0] = dst_format;
renderpass_key.depth_format = VideoCore::Surface::PixelFormat::Invalid;
renderpass_key.samples = samples;
const VkRenderPass renderpass = render_pass_cache.Get(renderpass_key);
const MSAACopyPipelineKey key{
.renderpass = renderpass,
.samples = samples,
.msaa_to_non_msaa = msaa_to_non_msaa,
.is_depth = is_depth,
};
const VkPipeline pipeline = FindOrEmplaceMSAACopyPipeline(key);
const VkPipelineLayout layout = *msaa_copy_pipeline_layout;
@@ -942,10 +920,10 @@ void BlitImageHelper::CopyMSAA(RenderPassCache& render_pass_cache, VkImage dst_i
ASSERT(copy.dst_subresource.num_layers == 1);
vk::ImageView src_view =
MakeMSAACopyView(device.GetLogical(), src_image, src_vk_format,
static_cast<u32>(copy.src_subresource.base_level), view_aspect);
static_cast<u32>(copy.src_subresource.base_level));
vk::ImageView dst_view =
MakeMSAACopyView(device.GetLogical(), dst_image, dst_vk_format,
static_cast<u32>(copy.dst_subresource.base_level), view_aspect);
static_cast<u32>(copy.dst_subresource.base_level));
const VkOffset2D dst_offset{copy.dst_offset.x, copy.dst_offset.y};
const VkExtent2D dst_extent{copy.extent.width, copy.extent.height};
const VkRect2D render_area{
@@ -971,64 +949,50 @@ void BlitImageHelper::CopyMSAA(RenderPassCache& render_pass_cache, VkImage dst_i
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([this, pipeline, layout, sampler, renderpass,
framebuffer_handle = *framebuffer, src_view_handle = *src_view,
src = src_image, dst = dst_image, render_area, is_depth, barrier_aspect,
src = src_image, dst = dst_image, render_area,
push_constants](vk::CommandBuffer cmdbuf) {
const VkImageSubresourceRange src_range{
.aspectMask = barrier_aspect,
constexpr VkImageSubresourceRange color_range{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = VK_REMAINING_MIP_LEVELS,
.baseArrayLayer = 0,
.layerCount = VK_REMAINING_ARRAY_LAYERS,
};
const VkImageSubresourceRange dst_range = src_range;
const VkAccessFlags attachment_read =
is_depth ? VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT
: VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;
const VkAccessFlags attachment_write =
is_depth ? VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT
: VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
const VkPipelineStageFlags depth_stage =
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
const VkPipelineStageFlags attachment_stage =
is_depth ? depth_stage
: static_cast<VkPipelineStageFlags>(
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
const std::array pre_barriers{
VkImageMemoryBarrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = attachment_write | VK_ACCESS_SHADER_WRITE_BIT |
VK_ACCESS_TRANSFER_WRITE_BIT,
.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = src,
.subresourceRange = src_range,
.subresourceRange = color_range,
},
VkImageMemoryBarrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = attachment_write | VK_ACCESS_SHADER_WRITE_BIT |
VK_ACCESS_TRANSFER_WRITE_BIT,
.dstAccessMask = attachment_read | attachment_write,
.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = dst,
.subresourceRange = dst_range,
.subresourceRange = color_range,
},
};
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | attachment_stage,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
0, nullptr, nullptr, pre_barriers);
const VkRenderPassBeginInfo renderpass_bi{
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
@@ -1061,16 +1025,16 @@ void BlitImageHelper::CopyMSAA(RenderPassCache& render_pass_cache, VkImage dst_i
const VkImageMemoryBarrier post_barrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = attachment_write,
.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = dst,
.subresourceRange = dst_range,
.subresourceRange = color_range,
};
cmdbuf.PipelineBarrier(attachment_stage,
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT |
VK_PIPELINE_STAGE_TRANSFER_BIT,
@@ -1459,36 +1423,9 @@ VkPipeline BlitImageHelper::FindOrEmplaceMSAACopyPipeline(const MSAACopyPipeline
return *msaa_copy_pipelines[std::distance(msaa_copy_keys.begin(), it)];
}
msaa_copy_keys.push_back(key);
const VkShaderModule frag_module =
key.msaa_to_non_msaa
? *convert_msaa_to_non_msaa_frag
: (key.is_depth ? *convert_non_msaa_to_msaa_depth_frag
: *convert_non_msaa_to_msaa_frag);
const std::array stages = MakeStages(*clear_color_vert, frag_module);
const VkPipelineDepthStencilStateCreateInfo depth_stencil_ci{
.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.depthTestEnable = VK_TRUE,
.depthWriteEnable = VK_TRUE,
.depthCompareOp = VK_COMPARE_OP_ALWAYS,
.depthBoundsTestEnable = VK_FALSE,
.stencilTestEnable = VK_FALSE,
.front = {},
.back = {},
.minDepthBounds = 0.0f,
.maxDepthBounds = 0.0f,
};
static constexpr VkPipelineColorBlendStateCreateInfo no_color_blend_ci{
.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.logicOpEnable = VK_FALSE,
.logicOp = VK_LOGIC_OP_CLEAR,
.attachmentCount = 0,
.pAttachments = nullptr,
.blendConstants = {0.0f, 0.0f, 0.0f, 0.0f},
};
const std::array stages = MakeStages(*clear_color_vert, key.msaa_to_non_msaa
? *convert_msaa_to_non_msaa_frag
: *convert_non_msaa_to_msaa_frag);
const VkPipelineMultisampleStateCreateInfo multisample_ci{
.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
.pNext = nullptr,
@@ -1513,9 +1450,8 @@ VkPipeline BlitImageHelper::FindOrEmplaceMSAACopyPipeline(const MSAACopyPipeline
.pViewportState = &PIPELINE_VIEWPORT_STATE_CREATE_INFO,
.pRasterizationState = &PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
.pMultisampleState = &multisample_ci,
.pDepthStencilState = key.is_depth ? &depth_stencil_ci : nullptr,
.pColorBlendState = key.is_depth ? &no_color_blend_ci
: &PIPELINE_COLOR_BLEND_STATE_GENERIC_CREATE_INFO,
.pDepthStencilState = nullptr,
.pColorBlendState = &PIPELINE_COLOR_BLEND_STATE_GENERIC_CREATE_INFO,
.pDynamicState = &PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.layout = *msaa_copy_pipeline_layout,
.renderPass = key.renderpass,
@@ -51,7 +51,6 @@ struct MSAACopyPipelineKey {
VkRenderPass renderpass;
VkSampleCountFlagBits samples;
bool msaa_to_non_msaa;
bool is_depth;
};
struct BlitMSAAPipelineKey {
@@ -181,7 +180,6 @@ private:
vk::ShaderModule convert_s8d24_to_abgr8_frag;
vk::ShaderModule convert_msaa_to_non_msaa_frag;
vk::ShaderModule convert_non_msaa_to_msaa_frag;
vk::ShaderModule convert_non_msaa_to_msaa_depth_frag;
vk::Sampler linear_sampler;
vk::Sampler nearest_sampler;
@@ -164,9 +164,7 @@ void FixedPipelineState::Refresh(Tegra::Engines::Maxwell3D& maxwell3d, DynamicFe
}
provoking_vertex_last.Assign(use_last_provoking_vertex ? 1 : 0);
if (!features.has_dynamic_state3_conservative_raster_mode) {
conservative_raster_enable.Assign(regs.conservative_raster_enable != 0 ? 1 : 0);
}
conservative_raster_enable.Assign(regs.conservative_raster_enable != 0 ? 1 : 0);
smooth_lines.Assign(regs.line_anti_alias_enable != 0 ? 1 : 0);
alpha_to_coverage_enabled.Assign(regs.anti_alias_alpha_control.alpha_to_coverage != 0 ? 1 : 0);
alpha_to_one_enabled.Assign(regs.anti_alias_alpha_control.alpha_to_one != 0 ? 1 : 0);
@@ -362,35 +360,18 @@ void FixedPipelineState::DynamicState::Refresh2(const Maxwell& regs,
depth_bias_enable.Assign(enabled_lut[POLYGON_OFFSET_ENABLE_LUT[topology_index]] != 0 ? 1 : 0);
}
bool IsDepthClipEnabled(const Maxwell& regs) {
const auto clip = regs.viewport_clip_control.geometry_clip.Value();
return clip == Maxwell::ViewportClipControl::GeometryClip::Passthrough ||
clip == Maxwell::ViewportClipControl::GeometryClip::FrustumXYZ ||
clip == Maxwell::ViewportClipControl::GeometryClip::FrustumZ;
}
bool IsDepthClampEnabled(const Maxwell& regs, bool has_depth_clip_enable) {
if (!IsDepthClipEnabled(regs)) {
return true;
}
if (!has_depth_clip_enable) {
return false;
}
return regs.viewport_clip_control.pixel_min_z.Value() != 0 ||
regs.viewport_clip_control.pixel_max_z.Value() != 0;
}
void FixedPipelineState::DynamicState::Refresh3(const Maxwell& regs,
const DynamicFeatures& features) {
if (!features.has_dynamic_state3_logic_op_enable) {
logic_op_enable.Assign(regs.logic_op.enable != 0 ? 1 : 0);
}
if (features.has_depth_clip_enable) {
depth_clip_disabled.Assign(IsDepthClipEnabled(regs) ? 0 : 1);
}
if (!features.has_dynamic_state3_depth_clamp_enable) {
depth_clamp_disabled.Assign(
IsDepthClampEnabled(regs, features.has_depth_clip_enable) ? 0 : 1);
depth_clamp_disabled.Assign(regs.viewport_clip_control.geometry_clip ==
Maxwell::ViewportClipControl::GeometryClip::Passthrough ||
regs.viewport_clip_control.geometry_clip ==
Maxwell::ViewportClipControl::GeometryClip::FrustumXYZ ||
regs.viewport_clip_control.geometry_clip ==
Maxwell::ViewportClipControl::GeometryClip::FrustumZ);
}
if (!features.has_dynamic_state3_line_stipple_enable) {
line_stipple_enable.Assign(regs.line_stipple_enable);
@@ -30,8 +30,6 @@ struct DynamicFeatures {
bool has_extended_dynamic_state_3_blend;
bool has_extended_dynamic_state_3_enables;
bool has_dynamic_state3_depth_clamp_enable;
bool has_dynamic_state3_conservative_raster_mode;
bool has_depth_clip_enable;
bool has_dynamic_state3_logic_op_enable;
bool has_dynamic_state3_line_stipple_enable;
bool has_dynamic_vertex_input;
@@ -167,7 +165,6 @@ struct FixedPipelineState {
BitField<10, 1, u32> logic_op_enable;
BitField<11, 1, u32> depth_clamp_disabled;
BitField<12, 1, u32> line_stipple_enable;
BitField<13, 1, u32> depth_clip_disabled;
};
union {
u32 raw2;
@@ -301,9 +298,6 @@ static_assert(std::has_unique_object_representations_v<FixedPipelineState>);
static_assert(std::is_trivially_copyable_v<FixedPipelineState>);
static_assert(std::is_trivially_constructible_v<FixedPipelineState>);
bool IsDepthClipEnabled(const Maxwell& regs);
bool IsDepthClampEnabled(const Maxwell& regs, bool has_depth_clip_enable);
} // namespace Vulkan
namespace std {
@@ -246,6 +246,7 @@ protected:
StagingBufferPool& staging_pool;
vk::Buffer buffer{};
MemoryCommit memory_commit{};
VkIndexType index_type{};
u32 num_indices = 0;
};
@@ -375,10 +376,6 @@ u64 BufferCacheRuntime::GetDeviceMemoryUsage() const {
return device.GetDeviceMemoryUsage();
}
u64 BufferCacheRuntime::GetDeviceAllocationUsage() const {
return device.GetMemoryBudgetInfo().allocation_bytes;
}
bool BufferCacheRuntime::CanReportMemoryUsage() const {
return device.CanReportMemoryUsage();
}
@@ -407,16 +404,6 @@ u64 BufferCacheRuntime::KnownGpuTick() {
return scheduler.GetMasterSemaphore().KnownGpuTick();
}
u64 BufferCacheRuntime::CurrentSyncPoint() const noexcept {
return scheduler.GetMasterSemaphore().CurrentTick();
}
u64 BufferCacheRuntime::CompletedSyncPoint() const {
auto& master_semaphore = scheduler.GetMasterSemaphore();
master_semaphore.Refresh();
return master_semaphore.KnownGpuTick();
}
void BufferCacheRuntime::Wait(u64 buffer_tick) {
scheduler.Wait(buffer_tick);
}
@@ -654,7 +641,6 @@ void BufferCacheRuntime::BindTransformFeedbackBuffer(u32 index, VkBuffer buffer,
offset = 0;
size = 0;
}
scheduler.MarkTransformFeedbackUsed();
scheduler.Record([index, buffer, offset, size](vk::CommandBuffer cmdbuf) {
const VkDeviceSize vk_offset = offset;
const VkDeviceSize vk_size = size;
@@ -667,26 +653,19 @@ void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings<
// Already logged in the rasterizer
return;
}
const u32 count = std::min<u32>(static_cast<u32>(bindings.buffers.size()),
VideoCommon::NUM_TRANSFORM_FEEDBACK_BUFFERS);
std::array<VkBuffer, VideoCommon::NUM_TRANSFORM_FEEDBACK_BUFFERS> handles{};
std::array<VkDeviceSize, VideoCommon::NUM_TRANSFORM_FEEDBACK_BUFFERS> offsets{};
std::array<VkDeviceSize, VideoCommon::NUM_TRANSFORM_FEEDBACK_BUFFERS> sizes{};
for (u32 i = 0; i < count; ++i) {
boost::container::static_vector<VkBuffer, VideoCommon::NUM_VERTEX_BUFFERS> buffer_handles(bindings.buffers.size());
for (u32 i = 0; i < bindings.buffers.size(); ++i) {
auto handle = bindings.buffers[i]->Handle();
if (handle == VK_NULL_HANDLE) {
ReserveNullBuffer();
handle = *null_buffer;
} else {
offsets[i] = bindings.offsets[i];
sizes[i] = bindings.sizes[i];
bindings.offsets[i] = 0;
bindings.sizes[i] = 0;
}
handles[i] = handle;
buffer_handles[i] = handle;
}
scheduler.MarkTransformFeedbackUsed();
scheduler.Record([count, handles, offsets, sizes](vk::CommandBuffer cmdbuf) {
cmdbuf.BindTransformFeedbackBuffersEXT(0, count, handles.data(), offsets.data(),
sizes.data());
scheduler.Record([bindings_ = std::move(bindings), buffer_handles_ = std::move(buffer_handles)](vk::CommandBuffer cmdbuf) {
cmdbuf.BindTransformFeedbackBuffersEXT(0, u32(buffer_handles_.size()), buffer_handles_.data(), bindings_.offsets.data(), bindings_.sizes.data());
});
}
@@ -100,20 +100,10 @@ public:
void Finish();
u64 CurrentSyncPoint() const noexcept;
u64 CompletedSyncPoint() const;
u64 GetDeviceLocalMemory() const;
u64 GetDeviceMemoryUsage() const;
u64 GetDeviceAllocationUsage() const;
bool CanReportAllocationUsage() const noexcept {
return true;
}
bool CanReportMemoryUsage() const;
u32 GetUniformBufferAlignment() const;
@@ -37,7 +37,7 @@ void InnerFence::Wait() {
if (is_stubbed) {
return;
}
scheduler.WaitSubmitted(wait_tick);
scheduler.Wait(wait_tick);
}
FenceManager::FenceManager(VideoCore::RasterizerInterface& rasterizer_, Tegra::GPU& gpu_,
@@ -757,13 +757,16 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
.lineWidth = 1.0f,
// TODO(alekpop): Transfer from regs
};
const VkLineRasterizationModeEXT line_raster_mode =
device.GetLineRasterizationMode(key.state.smooth_lines != 0);
const bool stippled_lines_supported = device.SupportsStippleForMode(line_raster_mode);
const bool smooth_lines_supported =
device.IsExtLineRasterizationSupported() && device.SupportsSmoothLines();
const bool stippled_lines_supported =
device.IsExtLineRasterizationSupported() && device.SupportsStippledRectangularLines();
VkPipelineRasterizationLineStateCreateInfoEXT line_state{
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_LINE_STATE_CREATE_INFO_EXT,
.pNext = nullptr,
.lineRasterizationMode = line_raster_mode,
.lineRasterizationMode = key.state.smooth_lines != 0 && smooth_lines_supported
? VK_LINE_RASTERIZATION_MODE_RECTANGULAR_SMOOTH_EXT
: VK_LINE_RASTERIZATION_MODE_RECTANGULAR_EXT,
.stippledLineEnable =
(dynamic.line_stipple_enable && stippled_lines_supported) ? VK_TRUE : VK_FALSE,
.lineStippleFactor = key.state.line_stipple_factor,
@@ -802,16 +805,6 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
if (device.IsExtProvokingVertexSupported()) {
provoking_vertex.pNext = std::exchange(rasterization_ci.pNext, &provoking_vertex);
}
VkPipelineRasterizationDepthClipStateCreateInfoEXT depth_clip_state{
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_DEPTH_CLIP_STATE_CREATE_INFO_EXT,
.pNext = nullptr,
.flags = 0,
.depthClipEnable = static_cast<VkBool32>(dynamic.depth_clip_disabled == 0 ? VK_TRUE
: VK_FALSE),
};
if (device.IsExtDepthClipEnableSupported()) {
depth_clip_state.pNext = std::exchange(rasterization_ci.pNext, &depth_clip_state);
}
const bool supports_alpha_output = fragment_has_color0_output;
const bool alpha_to_one_supported = device.SupportsAlphaToOne();
@@ -305,7 +305,7 @@ size_t GetTotalPipelineWorkers() {
std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL) - 1ULL;
#ifdef __ANDROID__
const int configured = AndroidSettings::values.pipeline_worker_count.GetValue();
const int clamped = std::clamp(configured, 2, 8);
const int clamped = std::clamp(configured, 4, 8);
const size_t desired = static_cast<size_t>(clamped);
if (desired == 0) {
return 1ULL;
@@ -349,9 +349,8 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
use_asynchronous_shaders{Settings::values.use_asynchronous_shaders.GetValue()},
use_vulkan_pipeline_cache{Settings::values.use_vulkan_driver_pipeline_cache.GetValue()},
workers(device.HasBrokenParallelShaderCompiling() ? 1ULL : GetTotalPipelineWorkers(),
"VkPipelineBuilder", {}, Common::ThreadPlacement::Background),
serialization_thread(1, "VkPipelineSerialization", {},
Common::ThreadPlacement::Background) {
"VkPipelineBuilder"),
serialization_thread(1, "VkPipelineSerialization") {
const auto& float_control{device.FloatControlProperties()};
const VkDriverId driver_id{device.GetDriverID()};
const VkShaderStageFlags subgroup_stages{device.GetSubgroupSupportedStages()};
@@ -515,11 +514,6 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
dynamic_features.has_dynamic_state3_depth_clamp_enable =
dynamic_features.has_extended_dynamic_state_3_enables &&
device.SupportsDynamicState3DepthClampEnable();
dynamic_features.has_dynamic_state3_conservative_raster_mode =
dynamic_features.has_extended_dynamic_state_3_enables &&
device.SupportsDynamicState3ConservativeRasterizationMode();
dynamic_features.has_depth_clip_enable =
device.IsExtDepthClipEnableSupported();
dynamic_features.has_dynamic_state3_logic_op_enable =
dynamic_features.has_extended_dynamic_state_3_enables &&
device.SupportsDynamicState3LogicOpEnable();
@@ -532,8 +526,7 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
device.IsExtVertexInputDynamicStateSupported() &&
Settings::values.vertex_input_dynamic_state.GetValue();
dynamic_features.has_provoking_vertex =
device.IsExtProvokingVertexSupported();
dynamic_features.has_provoking_vertex = device.IsExtProvokingVertexSupported();
dynamic_features.has_provoking_vertex_first_mode =
device.SupportsProvokingVertexFirstMode();
dynamic_features.has_provoking_vertex_last_mode =
@@ -296,6 +296,9 @@ void PresentManager::RecreateSwapchain(Frame* frame) {
}
void PresentManager::SetImageCount() {
// We cannot have more than 7 images in flight at any given time.
// FRAMES_IN_FLIGHT is 8, and the cache TICKS_TO_DESTROY is 8.
// Mali drivers will give us 6.
image_count = std::min<size_t>(swapchain.GetImageCount(), 7);
}
@@ -919,7 +919,7 @@ private:
return;
}
has_flushed_end_pending = true;
scheduler.MarkTransformFeedbackUsed();
// Refresh buffers state before beginning transform feedback so counters are up-to-date
UpdateBuffers();
if (!has_started || buffers_count == 0) {
// No counter buffers available: begin without counters
@@ -6,7 +6,6 @@
#include <algorithm>
#include <array>
#include <limits>
#include <memory>
#include <mutex>
@@ -204,8 +203,7 @@ RasterizerVulkan::RasterizerVulkan(Core::Frontend::EmuWindow& emu_window_, Tegra
: gpu{gpu_}, device_memory{device_memory_}, device{device_},
memory_allocator{memory_allocator_}, state_tracker{state_tracker_}, scheduler{scheduler_},
staging_pool(device, memory_allocator, scheduler), descriptor_pool(device, scheduler),
guest_descriptor_queue(device, UpdateDescriptorQueue::GUEST_FRAME_PAYLOAD_SIZE),
compute_pass_descriptor_queue(device, UpdateDescriptorQueue::COMPUTE_FRAME_PAYLOAD_SIZE),
guest_descriptor_queue(device), compute_pass_descriptor_queue(device),
blit_image(device, scheduler, state_tracker, descriptor_pool), render_pass_cache(device),
texture_cache_runtime{
device, scheduler, memory_allocator, staging_pool,
@@ -223,23 +221,9 @@ RasterizerVulkan::RasterizerVulkan(Core::Frontend::EmuWindow& emu_window_, Tegra
fence_manager(*this, gpu, texture_cache, buffer_cache, query_cache, device, scheduler),
wfi_event(device.GetLogical().CreateEvent()) {
scheduler.SetQueryCache(query_cache);
memory_allocator.SetReclaimCallback([this](u64 bytes) -> u64 {
u64 freed = staging_pool.ReclaimMemory(bytes);
if (freed < bytes) {
freed += texture_cache.ReclaimMemory(bytes - freed, false);
}
if (freed < bytes) {
freed += buffer_cache.ReclaimMemory(bytes - freed, false);
}
auto& master_semaphore = scheduler.GetMasterSemaphore();
master_semaphore.Refresh();
vk::TickDeletionQueue(master_semaphore.KnownGpuTick());
return freed;
});
}
RasterizerVulkan::~RasterizerVulkan() {
memory_allocator.SetReclaimCallback(nullptr);
scheduler.WaitWorker();
scheduler.Finish();
}
@@ -896,9 +880,6 @@ void RasterizerVulkan::FlushCommands() {
void RasterizerVulkan::TickFrame() {
draw_counter = 0;
auto& master_semaphore = scheduler.GetMasterSemaphore();
master_semaphore.Refresh();
vk::TickDeletionQueue(master_semaphore.KnownGpuTick());
guest_descriptor_queue.TickFrame();
compute_pass_descriptor_queue.TickFrame();
fence_manager.TickFrame();
@@ -1470,10 +1451,7 @@ void RasterizerVulkan::UpdateLineWidth(Tegra::Engines::Maxwell3D::Regs& regs) {
}
const float width =
regs.line_anti_alias_enable ? regs.line_width_smooth : regs.line_width_aliased;
const float clamped_width = device.ClampLineWidth(width);
scheduler.Record([clamped_width](vk::CommandBuffer cmdbuf) {
cmdbuf.SetLineWidth(clamped_width);
});
scheduler.Record([width](vk::CommandBuffer cmdbuf) { cmdbuf.SetLineWidth(width); });
}
void RasterizerVulkan::UpdateCullMode(Tegra::Engines::Maxwell3D::Regs& regs) {
@@ -1570,10 +1548,7 @@ void RasterizerVulkan::UpdateLineStippleEnable(Tegra::Engines::Maxwell3D::Regs&
return;
}
const VkLineRasterizationModeEXT mode =
device.GetLineRasterizationMode(regs.line_anti_alias_enable != 0);
const bool enable = regs.line_stipple_enable != 0 && device.SupportsStippleForMode(mode);
scheduler.Record([enable](vk::CommandBuffer cmdbuf) {
scheduler.Record([enable = regs.line_stipple_enable](vk::CommandBuffer cmdbuf) {
cmdbuf.SetLineStippleEnableEXT(enable);
});
}
@@ -1587,24 +1562,28 @@ void RasterizerVulkan::UpdateLineRasterizationMode(Tegra::Engines::Maxwell3D::Re
}
if (!device.SupportsDynamicState3LineRasterizationMode()) {
static std::once_flag warn_missing_dynamic_state;
std::call_once(warn_missing_dynamic_state, [] {
static std::once_flag warn_missing_rect;
std::call_once(warn_missing_rect, [] {
LOG_WARNING(Render_Vulkan,
"Driver lacks dynamic line rasterization mode; the pipeline static value "
"is used instead");
"Driver lacks rectangular line rasterization support; skipping dynamic "
"line state updates");
});
return;
}
const bool wants_smooth = regs.line_anti_alias_enable != 0;
const VkLineRasterizationModeEXT mode = device.GetLineRasterizationMode(wants_smooth);
if (wants_smooth && mode != VK_LINE_RASTERIZATION_MODE_RECTANGULAR_SMOOTH_EXT) {
static std::once_flag warn_missing_smooth;
std::call_once(warn_missing_smooth, [] {
LOG_WARNING(Render_Vulkan,
"Line anti-aliasing requested but smoothLines feature unavailable; "
"falling back to the closest supported mode");
});
VkLineRasterizationModeEXT mode = VK_LINE_RASTERIZATION_MODE_RECTANGULAR_EXT;
if (wants_smooth) {
if (device.SupportsSmoothLines()) {
mode = VK_LINE_RASTERIZATION_MODE_RECTANGULAR_SMOOTH_EXT;
} else {
static std::once_flag warn_missing_smooth;
std::call_once(warn_missing_smooth, [] {
LOG_WARNING(Render_Vulkan,
"Line anti-aliasing requested but smoothLines feature unavailable; "
"using rectangular rasterization");
});
}
}
scheduler.Record([mode](vk::CommandBuffer cmdbuf) {
cmdbuf.SetLineRasterizationModeEXT(mode);
@@ -1664,7 +1643,12 @@ void RasterizerVulkan::UpdateDepthClampEnable(Tegra::Engines::Maxwell3D::Regs& r
if (!device.SupportsDynamicState3DepthClampEnable()) {
return;
}
const bool is_enabled = IsDepthClampEnabled(regs, device.IsExtDepthClipEnableSupported());
bool is_enabled = !(regs.viewport_clip_control.geometry_clip ==
Maxwell::ViewportClipControl::GeometryClip::Passthrough ||
regs.viewport_clip_control.geometry_clip ==
Maxwell::ViewportClipControl::GeometryClip::FrustumXYZ ||
regs.viewport_clip_control.geometry_clip ==
Maxwell::ViewportClipControl::GeometryClip::FrustumZ);
scheduler.Record(
[is_enabled](vk::CommandBuffer cmdbuf) { cmdbuf.SetDepthClampEnableEXT(is_enabled); });
}
@@ -179,6 +179,7 @@ private:
void UpdateRasterizerDiscardEnable(Tegra::Engines::Maxwell3D::Regs& regs);
void UpdateConservativeRasterizationMode(Tegra::Engines::Maxwell3D::Regs& regs);
void UpdateLineStippleEnable(Tegra::Engines::Maxwell3D::Regs& regs);
void UpdateLineStipple(Tegra::Engines::Maxwell3D::Regs& regs);
void UpdateLineRasterizationMode(Tegra::Engines::Maxwell3D::Regs& regs);
void UpdateDepthBiasEnable(Tegra::Engines::Maxwell3D::Regs& regs);
void UpdateLogicOpEnable(Tegra::Engines::Maxwell3D::Regs& regs);
@@ -47,7 +47,6 @@ Scheduler::Scheduler(const Device& device_, StateTracker& state_tracker_)
master_semaphore{std::make_unique<MasterSemaphore>(device)},
command_pool{std::make_unique<CommandPool>(*master_semaphore, device)} {
vk::SetDeletionTimeline(master_semaphore->CurrentTick());
AcquireNewChunk();
AllocateWorkerCommandBuffer();
worker_thread = std::jthread([this](std::stop_token token) { WorkerThread(token); });
@@ -323,7 +322,6 @@ u64 Scheduler::SubmitExecution(VkSemaphore signal_semaphore, VkSemaphore wait_se
InvalidateState();
const u64 signal_value = master_semaphore->NextTick();
vk::SetDeletionTimeline(master_semaphore->CurrentTick());
RecordWithUploadBuffer([signal_semaphore, wait_semaphore, signal_value,
this](vk::CommandBuffer cmdbuf, vk::CommandBuffer upload_cmdbuf) {
static constexpr VkMemoryBarrier WRITE_BARRIER{
@@ -400,7 +398,7 @@ void Scheduler::EndRenderPass()
Record([num_images = num_renderpass_images,
images = renderpass_images,
ranges = renderpass_image_ranges,
has_transform_feedback = state.uses_transform_feedback](
has_transform_feedback = device.IsExtTransformFeedbackSupported()](
vk::CommandBuffer cmdbuf) {
std::array<VkImageMemoryBarrier, 9> barriers;
for (size_t i = 0; i < num_images; ++i) {
@@ -455,7 +453,6 @@ void Scheduler::EndRenderPass()
});
state.renderpass = VkRenderPass{};
state.uses_transform_feedback = false;
num_renderpass_images = 0;
}
+24 -42
View File
@@ -74,11 +74,6 @@ public:
return state.renderpass != VK_NULL_HANDLE;
}
/// Flags that transform feedback writes have been recorded since the last render pass end.
void MarkTransformFeedbackUsed() noexcept {
state.uses_transform_feedback = true;
}
/// Update the pipeline to the current execution context.
bool UpdateGraphicsPipeline(GraphicsPipeline* pipeline);
@@ -136,16 +131,33 @@ public:
}
master_semaphore->Wait(tick);
}
ApplyFramePacing(target_fps);
}
void WaitSubmitted(u64 tick, double target_fps = 0.0) {
if (tick > 0 && tick < master_semaphore->CurrentTick()) {
master_semaphore->Wait(tick);
if (Settings::values.use_speed_limit.GetValue() && target_fps > 0.0) {
auto now = std::chrono::steady_clock::now();
if (last_target_fps != target_fps) {
frame_interval = std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<double>(1.0 / target_fps));
max_frame_count = static_cast<int>(0.1 * target_fps);
last_target_fps = target_fps;
frame_counter = 0;
start_time = now;
}
frame_counter++;
auto target_time = start_time + frame_interval * frame_counter;
if (target_time >= now) {
auto sleep_time = target_time - now;
if (sleep_time > std::chrono::milliseconds(15)) {
std::this_thread::sleep_for(sleep_time - std::chrono::milliseconds(1));
}
while (std::chrono::steady_clock::now() < target_time) {
std::this_thread::yield();
}
} else if (frame_counter > max_frame_count) {
frame_counter = 0;
start_time = now;
}
}
ApplyFramePacing(target_fps);
}
/// Returns the master timeline semaphore.
[[nodiscard]] MasterSemaphore& GetMasterSemaphore() const noexcept {
return *master_semaphore;
}
@@ -153,35 +165,6 @@ public:
std::mutex submit_mutex;
private:
void ApplyFramePacing(double target_fps) {
if (!Settings::values.use_speed_limit.GetValue() || target_fps <= 0.0) {
return;
}
auto now = std::chrono::steady_clock::now();
if (last_target_fps != target_fps) {
frame_interval = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>(1.0 / target_fps));
max_frame_count = static_cast<int>(0.1 * target_fps);
last_target_fps = target_fps;
frame_counter = 0;
start_time = now;
}
frame_counter++;
auto target_time = start_time + frame_interval * frame_counter;
if (target_time >= now) {
auto sleep_time = target_time - now;
if (sleep_time > std::chrono::milliseconds(15)) {
std::this_thread::sleep_for(sleep_time - std::chrono::milliseconds(1));
}
while (std::chrono::steady_clock::now() < target_time) {
std::this_thread::yield();
}
} else if (frame_counter > max_frame_count) {
frame_counter = 0;
start_time = now;
}
}
class Command {
public:
virtual ~Command() = default;
@@ -272,7 +255,6 @@ private:
bool is_rescaling = false;
bool rescaling_defined = false;
bool needs_state_enable_refresh = false;
bool uses_transform_feedback = false;
};
struct DeferredClear {
@@ -252,62 +252,25 @@ void StagingBufferPool::ReleaseLevel(StagingBuffersCache& cache, size_t log2) {
constexpr size_t deletions_per_tick = 16;
auto& staging = cache[log2];
auto& entries = staging.entries;
if (entries.empty()) {
staging.delete_index = 0;
staging.iterate_index = 0;
return;
}
const size_t old_size = entries.size();
const auto is_deletable = [this](const StagingBuffer& entry) {
return scheduler.IsFree(entry.tick);
};
const size_t begin_offset = (std::min)(staging.delete_index, entries.size());
const size_t end_offset = (std::min)(begin_offset + deletions_per_tick, entries.size());
const size_t begin_offset = staging.delete_index;
const size_t end_offset = (std::min)(begin_offset + deletions_per_tick, old_size);
const auto begin = entries.begin() + begin_offset;
const auto end = entries.begin() + end_offset;
const auto surviving_end = std::remove_if(begin, end, is_deletable);
const size_t removed = static_cast<size_t>(std::distance(surviving_end, end));
entries.erase(surviving_end, end);
entries.erase(std::remove_if(begin, end, is_deletable), end);
staging.delete_index = end_offset - removed;
if (staging.delete_index >= entries.size()) {
const size_t new_size = entries.size();
staging.delete_index += deletions_per_tick;
if (staging.delete_index >= new_size) {
staging.delete_index = 0;
}
if (staging.iterate_index > entries.size()) {
if (staging.iterate_index > new_size) {
staging.iterate_index = 0;
}
}
u64 StagingBufferPool::ReclaimMemory(u64 target_bytes) {
u64 freed = 0;
const auto is_deletable = [this](const StagingBuffer& entry) {
return scheduler.IsFree(entry.tick);
};
const auto reclaim_cache = [&](StagingBuffersCache& cache) {
for (size_t level = NUM_LEVELS; level-- > 0 && freed < target_bytes;) {
auto& staging = cache[level];
auto& entries = staging.entries;
if (entries.empty()) {
continue;
}
const u64 entry_bytes = 1ULL << level;
auto it = entries.begin();
while (it != entries.end() && freed < target_bytes) {
if (is_deletable(*it)) {
it = entries.erase(it);
freed += entry_bytes;
} else {
++it;
}
}
staging.delete_index = 0;
staging.iterate_index = 0;
}
};
reclaim_cache(device_local_cache);
reclaim_cache(upload_cache);
reclaim_cache(download_cache);
return freed;
}
} // namespace Vulkan
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
@@ -45,8 +42,6 @@ public:
void TickFrame();
u64 ReclaimMemory(u64 target_bytes);
private:
struct StreamBufferCommit {
size_t upper_bound;
@@ -9,6 +9,10 @@
#include <limits>
#include <vector>
#ifdef __ANDROID__
#include <android/api-level.h>
#endif
#include "common/logging.h"
#include "common/settings.h"
#include "common/settings_enums.h"
@@ -172,26 +176,34 @@ bool Swapchain::AcquireNextImage() {
break;
}
#ifdef __ANDROID__
scheduler.WaitSubmitted(resource_ticks[image_index]);
#else
const auto wait_with_frame_pacing = [this] {
switch (Settings::values.frame_pacing_mode.GetValue()) {
case Settings::FramePacingMode::Target_Auto:
scheduler.WaitSubmitted(resource_ticks[image_index]);
scheduler.Wait(resource_ticks[image_index]);
break;
case Settings::FramePacingMode::Target_30:
scheduler.WaitSubmitted(resource_ticks[image_index], 30.0);
scheduler.Wait(resource_ticks[image_index], 30.0);
break;
case Settings::FramePacingMode::Target_60:
scheduler.WaitSubmitted(resource_ticks[image_index], 60.0);
scheduler.Wait(resource_ticks[image_index], 60.0);
break;
case Settings::FramePacingMode::Target_90:
scheduler.WaitSubmitted(resource_ticks[image_index], 90.0);
scheduler.Wait(resource_ticks[image_index], 90.0);
break;
case Settings::FramePacingMode::Target_120:
scheduler.WaitSubmitted(resource_ticks[image_index], 120.0);
scheduler.Wait(resource_ticks[image_index], 120.0);
break;
}
};
#ifdef __ANDROID__
if (android_get_device_api_level() >= 30) {
scheduler.Wait(resource_ticks[image_index]);
} else {
wait_with_frame_pacing();
}
#else
wait_with_frame_pacing();
#endif
resource_ticks[image_index] = scheduler.CurrentTick();
@@ -144,6 +144,11 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
info.size.depth == 1;
}
[[nodiscard]] bool WillUseWidenedAstcFormat(const Device& device, const ImageInfo& info) {
return WillUseAcceleratedAstcDecode(device, info) &&
!VideoCore::Surface::IsPixelFormatSRGB(info.format);
}
[[nodiscard]] VkImageCreateInfo MakeImageCreateInfo(const Device& device, const ImageInfo& info,
std::optional<VkFormat> format_override = {}) {
auto format_info =
@@ -207,11 +212,7 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
return device.IsFormatSupported(view_format, VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT,
FormatType::Optimal);
});
const bool storage_allowed_for_samples =
image_ci.samples == VK_SAMPLE_COUNT_1_BIT ||
(device.GetStorageImageSampleCounts() &
static_cast<VkSampleCountFlags>(image_ci.samples)) != 0;
if (has_storage_compatible_view && storage_allowed_for_samples) {
if (has_storage_compatible_view) {
image_ci.usage |= VK_IMAGE_USAGE_STORAGE_BIT;
}
@@ -895,15 +896,6 @@ void BlitScale(Scheduler& scheduler, VkImage src_image, VkImage dst_image, const
0, nullptr, nullptr, write_barriers);
});
}
[[nodiscard]] bool CanBlitNatively(const Device& device, PixelFormat format) {
static constexpr auto OPTIMAL_FORMAT = FormatType::Optimal;
static constexpr VkFormatFeatureFlags BLIT_USAGE =
VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT;
const VkFormat vk_format =
MaxwellToVK::SurfaceFormat(device, OPTIMAL_FORMAT, false, format).format;
return device.IsFormatSupported(vk_format, BLIT_USAGE, OPTIMAL_FORMAT);
}
} // Anonymous namespace
TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& scheduler_,
@@ -1236,19 +1228,27 @@ void TextureCacheRuntime::BlitImage(Framebuffer* dst_framebuffer, ImageView& dst
blit_image_helper.ResolveDepthStencil(dst_framebuffer, src, dst_region, src_region);
return;
}
static constexpr VkImageAspectFlags DEPTH_STENCIL_ASPECTS =
VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
if ((aspect_mask & DEPTH_STENCIL_ASPECTS) != 0 && !CanBlitNatively(device, src.format)) {
if (aspect_mask != DEPTH_STENCIL_ASPECTS) {
UNIMPLEMENTED_MSG("Host cannot blit format {} and no helper path exists for aspect "
"mask 0x{:x}",
src.format, aspect_mask);
if (aspect_mask == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) {
const auto format = src.format;
const auto can_blit_depth_stencil = [this, format] {
switch (format) {
case VideoCore::Surface::PixelFormat::D24_UNORM_S8_UINT:
case VideoCore::Surface::PixelFormat::S8_UINT_D24_UNORM:
return device.IsBlitDepth24Stencil8Supported();
case VideoCore::Surface::PixelFormat::D32_FLOAT_S8_UINT:
return device.IsBlitDepth32Stencil8Supported();
default:
UNREACHABLE();
}
}();
// Use shader-based depth/stencil blits if hardware doesn't support the format
// Note: MSAA resolves (MSAA->single) use vkCmdResolveImage which works fine
if (!can_blit_depth_stencil) {
UNIMPLEMENTED_IF(is_src_msaa || is_dst_msaa);
blit_image_helper.BlitDepthStencil(dst_framebuffer, src, dst_region, src_region,
filter, operation);
return;
}
UNIMPLEMENTED_IF(is_src_msaa || is_dst_msaa);
blit_image_helper.BlitDepthStencil(dst_framebuffer, src, dst_region, src_region, filter,
operation);
return;
}
ASSERT(!(is_dst_msaa && !is_src_msaa));
ASSERT(operation == Fermi2D::Operation::SrcCopy);
@@ -1643,14 +1643,7 @@ void TextureCacheRuntime::CopyImageMSAA(Image& dst, Image& src,
const u32 num_samples = msaa_to_non_msaa ? src.info.num_samples : dst.info.num_samples;
if (dst.AspectMask() != VK_IMAGE_ASPECT_COLOR_BIT ||
VideoCore::Surface::IsPixelFormatInteger(dst.info.format)) {
const u64 key{(static_cast<u64>(dst.AspectMask()) << 32) |
static_cast<u64>(dst.info.format)};
if (unsupported_msaa_resolves.insert(key).second) {
LOG_WARNING(Render_Vulkan,
"MSAA resolve unsupported: format={}, aspect={:#x}, samples {}->{}",
dst.info.format, dst.AspectMask(), src.info.num_samples,
dst.info.num_samples);
}
UNIMPLEMENTED_MSG("Copying images with different samples is not supported.");
return;
}
if (ENABLE_MSAA_RESOLVE_CONSUME && msaa_to_non_msaa && copies.size() == 1 &&
@@ -1760,20 +1753,6 @@ void TextureCacheRuntime::CopyImageMSAA(Image& dst, Image& src,
src.info.format, num_samples, copies, msaa_to_non_msaa);
}
u64 TextureCacheRuntime::CurrentSyncPoint() const noexcept {
return scheduler.CurrentTick();
}
u64 TextureCacheRuntime::CompletedSyncPoint() const {
auto& master_semaphore = scheduler.GetMasterSemaphore();
master_semaphore.Refresh();
return master_semaphore.KnownGpuTick();
}
void TextureCacheRuntime::WaitSyncPoint(u64 sync_point) {
scheduler.Wait(sync_point);
}
u64 TextureCacheRuntime::GetDeviceLocalMemory() const {
return device.GetDeviceLocalMemory();
}
@@ -1782,10 +1761,6 @@ u64 TextureCacheRuntime::GetDeviceMemoryUsage() const {
return device.GetDeviceMemoryUsage();
}
u64 TextureCacheRuntime::GetDeviceAllocationUsage() const {
return device.GetMemoryBudgetInfo().allocation_bytes;
}
bool TextureCacheRuntime::CanReportMemoryUsage() const {
return device.CanReportMemoryUsage();
}
@@ -1795,7 +1770,6 @@ std::optional<size_t> TextureCacheRuntime::GetSamplerHeapBudget() const {
}
void TextureCacheRuntime::TickFrame() {
device.TickAllocatorFrame();
std::erase_if(pending_msaa_images, [this](const auto& pending) {
return scheduler.IsFree(pending.first);
});
@@ -1806,7 +1780,12 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
: VideoCommon::ImageBase(info_, gpu_addr_, cpu_addr_), scheduler{&runtime_.scheduler},
runtime{&runtime_},
original_image(MakeImage(runtime_.device, runtime_.memory_allocator, info,
runtime->ViewFormats(info.format))),
WillUseWidenedAstcFormat(runtime_.device, info)
? std::span<const VkFormat>{}
: runtime->ViewFormats(info.format),
WillUseWidenedAstcFormat(runtime_.device, info)
? std::make_optional(VK_FORMAT_R32G32B32A32_SFLOAT)
: std::nullopt)),
aspect_mask(ImageAspectMask(info.format)) {
if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) {
switch (Settings::values.accelerate_astc.GetValue()) {
@@ -1837,7 +1816,9 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
Settings::values.astc_recompression.GetValue() ==
Settings::AstcRecompression::Uncompressed) {
const auto& device = runtime->device.GetLogical();
const VkFormat storage_format = VK_FORMAT_A8B8G8R8_UNORM_PACK32;
const VkFormat storage_format = WillUseWidenedAstcFormat(runtime->device, info)
? VK_FORMAT_R32G32B32A32_SFLOAT
: VK_FORMAT_A8B8G8R8_UNORM_PACK32;
for (s32 level = 0; level < info.resources.levels; ++level) {
storage_image_views[level] =
MakeStorageView(device, level, *original_image, storage_format);
@@ -1911,11 +1892,9 @@ void Image::UploadMemory(VkBuffer buffer, VkDeviceSize offset,
ScaleDown(true);
}
const bool is_color_upload = (aspect_mask & VK_IMAGE_ASPECT_COLOR_BIT) != 0
const bool wants_msaa_upload = info.num_samples > 1
&& (aspect_mask & VK_IMAGE_ASPECT_COLOR_BIT) != 0
&& !VideoCore::Surface::IsPixelFormatInteger(info.format);
const bool is_depth_upload = (aspect_mask & VK_IMAGE_ASPECT_DEPTH_BIT) != 0;
const bool wants_msaa_upload =
info.num_samples > 1 && (is_color_upload || is_depth_upload);
if (wants_msaa_upload) {
ImageInfo temp_info = info;
@@ -1954,10 +1933,10 @@ void Image::UploadMemory(VkBuffer buffer, VkDeviceSize offset,
image_copies.push_back(image_copy);
}
runtime->TransitionImageLayout(*this);
runtime->blit_image_helper.CopyMSAA(runtime->render_pass_cache, Handle(), info.format,
temp_vk_image, info.format, info.num_samples,
image_copies, false);
initialized = true;
runtime->pending_msaa_images.emplace_back(scheduler->CurrentTick(), std::move(temp_image));
if (is_rescaled) {
@@ -1968,9 +1947,6 @@ void Image::UploadMemory(VkBuffer buffer, VkDeviceSize offset,
if (info.num_samples > 1) {
LOG_WARNING(Render_Vulkan, "MSAA upload not implemented for format {}", info.format);
if (runtime != nullptr) {
runtime->TransitionImageLayout(*this);
}
if (is_rescaled) {
ScaleUp();
}
@@ -2228,7 +2204,9 @@ VkImageView Image::StorageImageView(s32 level) noexcept {
auto format_info =
MaxwellToVK::SurfaceFormat(runtime->device, FormatType::Optimal, true, info.format);
if (WillUseAcceleratedAstcDecode(runtime->device, info)) {
format_info.format = VK_FORMAT_A8B8G8R8_UNORM_PACK32;
format_info.format = WillUseWidenedAstcFormat(runtime->device, info)
? VK_FORMAT_R32G32B32A32_SFLOAT
: VK_FORMAT_A8B8G8R8_UNORM_PACK32;
}
view = MakeStorageView(runtime->device.GetLogical(), level, *(this->*current_image),
format_info.format);
@@ -2404,7 +2382,11 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI
SanitizeDepthStencilSwizzle(swizzle, device->SupportsDepthStencilSwizzleOne());
}
}
uses_widened_astc_format = WillUseWidenedAstcFormat(*device, image.info);
auto format_info = MaxwellToVK::SurfaceFormat(*device, FormatType::Optimal, true, format);
if (uses_widened_astc_format) {
format_info.format = VK_FORMAT_R32G32B32A32_SFLOAT;
}
if (device->ApiVersion() >= VK_API_VERSION_1_3) {
const VkFormatProperties3 properties3 =
device->GetPhysical().GetFormatProperties3(format_info.format);
@@ -2547,12 +2529,14 @@ VkImageView ImageView::StorageView(Shader::TextureType texture_type,
Shader::ImageFormat image_format) {
if (image_handle) {
if (image_format == Shader::ImageFormat::Typeless) {
auto& view{typeless_storage_views[static_cast<size_t>(texture_type)]};
if (!view) {
if (!typeless_storage_view) {
auto info = MaxwellToVK::SurfaceFormat(*device, FormatType::Optimal, true, format);
view = MakeView(info.format, VK_IMAGE_ASPECT_COLOR_BIT, texture_type);
if (uses_widened_astc_format) {
info.format = VK_FORMAT_R32G32B32A32_SFLOAT;
}
typeless_storage_view = MakeView(info.format, VK_IMAGE_ASPECT_COLOR_BIT, texture_type);
}
return *view;
return *typeless_storage_view;
}
const bool is_signed = image_format == Shader::ImageFormat::R8_SINT
|| image_format == Shader::ImageFormat::R16_SINT;
@@ -60,22 +60,10 @@ public:
void TickFrame();
u64 CurrentSyncPoint() const noexcept;
u64 CompletedSyncPoint() const;
void WaitSyncPoint(u64 sync_point);
u64 GetDeviceLocalMemory() const;
u64 GetDeviceMemoryUsage() const;
u64 GetDeviceAllocationUsage() const;
bool CanReportAllocationUsage() const noexcept {
return true;
}
bool CanReportMemoryUsage() const;
std::optional<size_t> GetSamplerHeapBudget() const;
@@ -168,7 +156,6 @@ public:
std::array<vk::Buffer, indexing_slots> buffers{};
std::vector<std::pair<u64, vk::Image>> pending_msaa_images;
ankerl::unordered_dense::map<VkImage, ResolveShadow> resolve_shadows;
ankerl::unordered_dense::set<u64> unsupported_msaa_resolves;
};
class Framebuffer {
@@ -439,7 +426,7 @@ private:
std::array<vk::ImageView, Shader::NUM_TEXTURE_TYPES> image_views;
std::optional<StorageViews> storage_views;
std::array<vk::ImageView, Shader::NUM_TEXTURE_TYPES> typeless_storage_views;
vk::ImageView typeless_storage_view;
vk::ImageView depth_view;
vk::ImageView stencil_view;
vk::ImageView color_view;
@@ -449,6 +436,7 @@ private:
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
u32 buffer_size = 0;
bool uses_widened_astc_format = false;
bool supports_depth_comparison = false;
};
@@ -499,7 +487,6 @@ struct TextureCacheParams {
static constexpr bool HAS_EMULATED_COPIES = false;
static constexpr bool HAS_DEVICE_MEMORY_INFO = true;
static constexpr bool IMPLEMENTS_ASYNC_DOWNLOADS = true;
static constexpr bool HAS_TIMELINE_SYNC_POINTS = true;
using Runtime = Vulkan::TextureCacheRuntime;
using Image = Vulkan::Image;
@@ -16,9 +16,8 @@
namespace Vulkan {
UpdateDescriptorQueue::UpdateDescriptorQueue(const Device& device_, size_t frame_payload_size_)
: device{device_}, frame_payload_size{frame_payload_size_},
payload(frame_payload_size_ * FRAMES_IN_FLIGHT)
UpdateDescriptorQueue::UpdateDescriptorQueue(const Device& device_)
: device{device_}
{
payload_start = payload.data();
payload_cursor = payload.data();
@@ -30,19 +29,19 @@ void UpdateDescriptorQueue::TickFrame() {
if (++frame_index >= FRAMES_IN_FLIGHT) {
frame_index = 0;
}
payload_start = payload.data() + frame_index * frame_payload_size;
payload_start = payload.data() + frame_index * FRAME_PAYLOAD_SIZE;
payload_cursor = payload_start;
}
void UpdateDescriptorQueue::Acquire(Scheduler& scheduler, size_t required_entries) {
static constexpr size_t DEFAULT_REQUIRED_ENTRIES = 0x400;
const size_t reserve = required_entries > 0 ? required_entries : DEFAULT_REQUIRED_ENTRIES;
ASSERT_MSG(reserve < frame_payload_size, "Descriptor reservation {} >= frame capacity {}",
reserve, frame_payload_size);
ASSERT_MSG(reserve < FRAME_PAYLOAD_SIZE, "Descriptor reservation {} >= frame capacity {}",
reserve, FRAME_PAYLOAD_SIZE);
const size_t used = static_cast<size_t>(std::distance(payload_start, payload_cursor));
if (used + reserve >= frame_payload_size) {
if (used + reserve >= FRAME_PAYLOAD_SIZE) {
LOG_WARNING(Render_Vulkan, "Payload overflow (used={}, reserve={}, capacity={})",
used, reserve, frame_payload_size);
used, reserve, FRAME_PAYLOAD_SIZE);
scheduler.WaitWorker();
payload_cursor = payload_start;
}
@@ -6,8 +6,8 @@
#pragma once
#include <array>
#include <variant>
#include <vector>
#include "video_core/vulkan_common/vulkan_wrapper.h"
namespace Vulkan {
@@ -30,12 +30,11 @@ class UpdateDescriptorQueue final {
// This should be plenty for the vast majority of cases. Most desktop platforms only
// provide up to 3 swapchain images.
static constexpr size_t FRAMES_IN_FLIGHT = 8;
static constexpr size_t FRAME_PAYLOAD_SIZE = 0x20000;
static constexpr size_t PAYLOAD_SIZE = FRAME_PAYLOAD_SIZE * FRAMES_IN_FLIGHT;
public:
static constexpr size_t GUEST_FRAME_PAYLOAD_SIZE = 0x80000;
static constexpr size_t COMPUTE_FRAME_PAYLOAD_SIZE = 0x20000;
explicit UpdateDescriptorQueue(const Device& device_, size_t frame_payload_size_);
explicit UpdateDescriptorQueue(const Device& device_);
~UpdateDescriptorQueue();
void TickFrame();
@@ -75,12 +74,11 @@ public:
private:
const Device& device;
const size_t frame_payload_size;
size_t frame_index{0};
DescriptorUpdateEntry* payload_cursor = nullptr;
DescriptorUpdateEntry* payload_start = nullptr;
const DescriptorUpdateEntry* upload_start = nullptr;
std::vector<DescriptorUpdateEntry> payload;
std::array<DescriptorUpdateEntry, PAYLOAD_SIZE> payload;
};
// TODO: should these be separate classes instead?
+1 -18
View File
@@ -284,24 +284,7 @@ std::optional<u64> GenericEnvironment::TryFindSize() {
Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, u32 tic_limit,
bool via_header_index, u32 raw) {
const auto handle{Tegra::Texture::TexturePair(raw, via_header_index)};
if (handle.first > tic_limit) {
LOG_CRITICAL(Shader,
"TIC index out of range: raw=0x{:08x} tic_index={} tsc_index={} tic_limit={} "
"tic_addr=0x{:x} via_header_index={} stage={} program_base=0x{:x} "
"start_address=0x{:x}",
raw, handle.first, handle.second, tic_limit, tic_addr, via_header_index,
static_cast<u32>(stage), program_base, start_address);
ASSERT(handle.first <= tic_limit);
Tegra::Texture::TICEntry fallback{};
fallback.format.Assign(Tegra::Texture::TextureFormat::A8B8G8R8);
fallback.r_type.Assign(Tegra::Texture::ComponentType::UNORM);
fallback.g_type.Assign(Tegra::Texture::ComponentType::UNORM);
fallback.b_type.Assign(Tegra::Texture::ComponentType::UNORM);
fallback.a_type.Assign(Tegra::Texture::ComponentType::UNORM);
fallback.texture_type.Assign(Tegra::Texture::TextureType::Texture2D);
fallback.normalized_coords.Assign(1);
return fallback;
}
ASSERT(handle.first <= tic_limit);
const GPUVAddr descriptor_addr{tic_addr + handle.first * sizeof(Tegra::Texture::TICEntry)};
Tegra::Texture::TICEntry entry;
gpu_memory->ReadBlock(descriptor_addr, &entry, sizeof(entry));
+85 -168
View File
@@ -58,9 +58,23 @@ TextureCache<P>::TextureCache(Runtime& runtime_, Tegra::MaxwellDeviceMemoryManag
void(slot_samplers.insert(runtime, sampler_descriptor));
if constexpr (HAS_DEVICE_MEMORY_INFO) {
memory_budget = runtime.GetDeviceLocalMemory();
const s64 device_local_memory = static_cast<s64>(runtime.GetDeviceLocalMemory());
const s64 min_spacing_expected = device_local_memory - 1_GiB;
const s64 min_spacing_critical = device_local_memory - 512_MiB;
const s64 mem_threshold = (std::min)(device_local_memory, TARGET_THRESHOLD);
const s64 min_vacancy_expected = (6 * mem_threshold) / 10;
const s64 min_vacancy_critical = (2 * mem_threshold) / 10;
expected_memory = static_cast<u64>(
(std::max)((std::min)(device_local_memory - min_vacancy_expected, min_spacing_expected),
DEFAULT_EXPECTED_MEMORY));
critical_memory = static_cast<u64>(
(std::max)((std::min)(device_local_memory - min_vacancy_critical, min_spacing_critical),
DEFAULT_CRITICAL_MEMORY));
minimum_memory = static_cast<u64>((device_local_memory - mem_threshold) / 2);
} else {
memory_budget = FALLBACK_MEMORY_BUDGET;
expected_memory = DEFAULT_EXPECTED_MEMORY + 512_MiB;
critical_memory = DEFAULT_CRITICAL_MEMORY + 1_GiB;
minimum_memory = 0;
}
const bool gpu_unswizzle_enabled = Settings::values.gpu_unswizzle_enabled.GetValue();
@@ -100,154 +114,71 @@ TextureCache<P>::TextureCache(Runtime& runtime_, Tegra::MaxwellDeviceMemoryManag
}
template <class P>
void TextureCache<P>::QueueEvictionDownload(Image& image) {
auto copies = FullDownloadCopies(image.info);
auto staging = runtime.DownloadStagingBuffer(image.unswizzled_size_bytes, true);
image.DownloadMemory(staging, FixSmallVectorADL(copies));
pending_eviction_downloads.push_back(PendingEvictionDownload{
.staging = staging,
.gpu_memory = gpu_memory,
.copies = std::move(copies),
.info = image.info,
.gpu_addr = image.gpu_addr,
.sync_point = runtime.CurrentSyncPoint(),
});
}
template <class P>
void TextureCache<P>::TickEvictionDownloads(u64 completed_sync_point) {
while (!pending_eviction_downloads.empty() &&
pending_eviction_downloads.front().sync_point <= completed_sync_point) {
auto& entry = pending_eviction_downloads.front();
SwizzleImage(*entry.gpu_memory, entry.gpu_addr, entry.info, FixSmallVectorADL(entry.copies),
entry.staging.mapped_span.subspan(entry.staging.offset), swizzle_data_buffer);
runtime.FreeDeferredStagingBuffer(entry.staging);
pending_eviction_downloads.pop_front();
}
}
template <class P>
void TextureCache<P>::FlushEvictionDownloads() {
if (pending_eviction_downloads.empty()) {
return;
}
const u64 last_sync_point = pending_eviction_downloads.back().sync_point;
runtime.WaitSyncPoint(last_sync_point);
TickEvictionDownloads(last_sync_point);
}
template <class P>
u64 TextureCache<P>::ImageSizeBytes(const ImageBase& image) {
u64 tentative_size = (std::max)(image.guest_size_bytes, image.unswizzled_size_bytes);
if ((IsPixelFormatASTC(image.info.format) &&
True(image.flags & ImageFlagBits::AcceleratedUpload)) ||
True(image.flags & ImageFlagBits::Converted)) {
tentative_size = TranscodedAstcSize(tentative_size, image.info.format);
}
u64 size = Common::AlignUp(tentative_size, 1024);
if (image.HasScaled()) {
size += GetScaledImageSizeBytes(image);
}
return size;
}
template <class P>
u64 TextureCache<P>::DeviceUsage(bool force_refresh) {
if (!runtime.CanReportAllocationUsage()) {
return total_used_memory;
}
if (force_refresh || usage_refresh_countdown == 0) {
cached_device_usage = runtime.GetDeviceAllocationUsage();
usage_refresh_countdown = USAGE_REFRESH_INTERVAL;
} else {
--usage_refresh_countdown;
}
return cached_device_usage;
}
template <class P>
u64 TextureCache<P>::ReclaimMemory(u64 target_bytes, bool allow_download) {
if (target_bytes == 0 || in_reclaim) {
return 0;
}
in_reclaim = true;
const u64 drain_point = runtime.CompletedSyncPoint();
TickEvictionDownloads(drain_point);
sentenced_images.Reclaim(drain_point);
sentenced_image_view.Reclaim(drain_point);
sentenced_framebuffers.Reclaim(drain_point);
u64 freed = 0;
const auto evict = [&](ImageId image_id) {
if (freed >= target_bytes) {
void TextureCache<P>::RunGarbageCollector() {
bool high_priority_mode = false;
bool aggressive_mode = false;
u64 ticks_to_destroy = 0;
size_t num_iterations = 0;
const auto Configure = [&](bool allow_aggressive) {
high_priority_mode = total_used_memory >= expected_memory;
aggressive_mode = allow_aggressive && total_used_memory >= critical_memory;
ticks_to_destroy = aggressive_mode ? 10ULL : high_priority_mode ? 25ULL : 50ULL;
num_iterations = aggressive_mode ? 40 : (high_priority_mode ? 20 : 10);
};
const auto Cleanup = [this, &num_iterations, &high_priority_mode, &aggressive_mode](ImageId image_id) {
if (num_iterations == 0) {
return true;
}
--num_iterations;
auto& image = slot_images[image_id];
if (True(image.flags & ImageFlagBits::IsDecoding)) {
return false;
}
const bool must_download =
image.IsSafeDownload() && False(image.flags & ImageFlagBits::BadOverlap);
bool queued_download = false;
if (must_download) {
if constexpr (HAS_TIMELINE_SYNC_POINTS) {
if (!allow_download) {
return false;
}
QueueEvictionDownload(image);
queued_download = true;
} else {
return false;
}
const bool must_download = image.IsSafeDownload() && False(image.flags & ImageFlagBits::BadOverlap);
if ((!aggressive_mode && True(image.flags & ImageFlagBits::CostlyLoad)) || (!high_priority_mode && must_download)) {
return false;
}
if (must_download) {
auto map = runtime.DownloadStagingBuffer(image.unswizzled_size_bytes);
const auto copies = FixSmallVectorADL(FullDownloadCopies(image.info));
image.DownloadMemory(map, copies);
runtime.Finish();
SwizzleImage(*gpu_memory, image.gpu_addr, image.info, copies, map.mapped_span, swizzle_data_buffer);
}
const u64 image_bytes = ImageSizeBytes(image);
if (True(image.flags & ImageFlagBits::Tracked)) {
UntrackImage(image, image_id);
}
UnregisterImage(image_id);
DeleteImage(image_id, !queued_download && image.scale_tick > frame_tick + 5);
freed += image_bytes;
DeleteImage(image_id, image.scale_tick > frame_tick + 5);
if (aggressive_mode && total_used_memory < critical_memory) {
num_iterations >>= 2;
aggressive_mode = false;
} else if (high_priority_mode && total_used_memory < expected_memory) {
num_iterations >>= 1;
high_priority_mode = false;
}
return false;
};
const u64 cold_tick =
frame_tick > RECLAIM_GUARD_FRAMES ? frame_tick - RECLAIM_GUARD_FRAMES : 0;
lru_cache.ForEachItemBelow(cold_tick, evict);
if (freed < target_bytes) {
lru_cache.ForEachItemBelow(frame_tick > 0 ? frame_tick - 1 : 0, evict);
Configure(false);
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
if (total_used_memory >= critical_memory) {
Configure(true);
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
}
const u64 exit_point = runtime.CompletedSyncPoint();
sentenced_images.Reclaim(exit_point);
sentenced_image_view.Reclaim(exit_point);
sentenced_framebuffers.Reclaim(exit_point);
in_reclaim = false;
usage_refresh_countdown = 0;
reclaim_stalled = freed == 0;
return freed;
}
template <class P>
void TextureCache<P>::EnsureHeadroom(bool allow_download) {
if (reclaim_stalled) {
return;
}
const u64 limit = memory_budget > RECLAIM_HEADROOM ? memory_budget - RECLAIM_HEADROOM : 0;
const u64 usage = DeviceUsage(false);
if (usage <= limit) {
return;
}
const u64 target = (limit / 100) * RECLAIM_TARGET_PERCENT;
ReclaimMemory((std::min)(usage - target, total_used_memory), allow_download);
}
template <class P>
void TextureCache<P>::TickFrame() {
usage_refresh_countdown = 0;
reclaim_stalled = false;
EnsureHeadroom(true);
const u64 completed_sync_point = runtime.CompletedSyncPoint();
TickEvictionDownloads(completed_sync_point);
sentenced_images.Reclaim(completed_sync_point);
sentenced_framebuffers.Reclaim(completed_sync_point);
sentenced_image_view.Reclaim(completed_sync_point);
// If we can obtain the memory info, use it instead of the estimate.
if (runtime.CanReportMemoryUsage()) {
total_used_memory = runtime.GetDeviceMemoryUsage();
}
if (total_used_memory > minimum_memory) {
RunGarbageCollector();
}
sentenced_images.Tick();
sentenced_framebuffers.Tick();
sentenced_image_view.Tick();
TickAsyncDecode();
TickAsyncUnswizzle();
@@ -665,7 +596,6 @@ void TextureCache<P>::WriteMemory(DAddr cpu_addr, size_t size) {
template <class P>
void TextureCache<P>::DownloadMemory(DAddr cpu_addr, size_t size) {
FlushEvictionDownloads();
boost::container::small_vector<ImageId, 16> images;
ForEachImageInRegion(cpu_addr, size, [&images](ImageId image_id, ImageBase& image) {
if (!image.IsSafeDownload()) {
@@ -964,7 +894,6 @@ void TextureCache<P>::CommitAsyncFlushes() {
template <class P>
void TextureCache<P>::PopAsyncFlushes() {
TickEvictionDownloads(runtime.CompletedSyncPoint());
if (committed_downloads.empty()) {
return;
}
@@ -1365,9 +1294,8 @@ void TextureCache<P>::InvalidateScale(Image& image) {
}
RemoveImageViewReferences(image_view_ids);
RemoveFramebuffers(image_view_ids);
const u64 sync_point = runtime.CurrentSyncPoint();
for (const ImageViewId image_view_id : image_view_ids) {
sentenced_image_view.Push(std::move(slot_image_views[image_view_id]), sync_point);
sentenced_image_view.Push(std::move(slot_image_views[image_view_id]));
slot_image_views.erase(image_view_id);
}
image.image_view_ids.clear();
@@ -1403,7 +1331,6 @@ void TextureCache<P>::QueueAsyncDecode(Image& image, ImageId image_id) {
LOG_INFO(HW_GPU, "Queuing async texture decode");
image.flags |= ImageFlagBits::IsDecoding;
runtime.TransitionImageLayout(image);
auto decode = std::make_unique<AsyncDecodeContext>();
auto* decode_ptr = decode.get();
decode->image_id = image_id;
@@ -1436,7 +1363,6 @@ void TextureCache<P>::QueueAsyncUnswizzle(Image& image, ImageId image_id) {
}
image.flags |= ImageFlagBits::IsDecoding;
runtime.TransitionImageLayout(image);
unswizzle_queue.push_back({
.image_id = image_id,
@@ -1597,7 +1523,6 @@ ImageId TextureCache<P>::InsertImage(const ImageInfo& info, GPUVAddr gpu_addr,
template <class P>
ImageId TextureCache<P>::JoinImages(const ImageInfo& info, GPUVAddr gpu_addr, DAddr cpu_addr) {
EnsureHeadroom(false);
ImageInfo new_info = info;
const size_t size_bytes = CalculateGuestSizeInBytes(new_info);
const bool broken_views = runtime.HasBrokenTextureViewFormats();
@@ -1706,28 +1631,7 @@ ImageId TextureCache<P>::JoinImages(const ImageInfo& info, GPUVAddr gpu_addr, DA
for (const ImageId overlap_id : join_ignore_textures) {
Image& overlap = slot_images[overlap_id];
if (True(overlap.flags & ImageFlagBits::GpuModified)) {
if (new_image.TryFindBase(overlap.gpu_addr) &&
(!can_rescale || ImageCanRescale(overlap))) {
if (can_rescale) {
ScaleUp(overlap);
} else {
ScaleDown(overlap);
}
join_copies_to_do.emplace_back(JoinCopy{false, overlap_id});
continue;
}
if (overlap.IsSafeDownload() && False(overlap.flags & ImageFlagBits::BadOverlap) &&
gpu_memory->GpuToCpuAddress(overlap.gpu_addr).has_value()) {
QueueEvictionDownload(overlap);
} else {
LOG_WARNING(HW_GPU,
"Dropping GPU modified overlap, contents are not recoverable: "
"gpu_addr=0x{:x} format={} size={}x{}x{} levels={} layers={}",
overlap.gpu_addr, static_cast<int>(overlap.info.format),
overlap.info.size.width, overlap.info.size.height,
overlap.info.size.depth, overlap.info.resources.levels,
overlap.info.resources.layers);
}
UNIMPLEMENTED();
}
if (True(overlap.flags & ImageFlagBits::Tracked)) {
UntrackImage(overlap, overlap_id);
@@ -2281,7 +2185,13 @@ void TextureCache<P>::RegisterImage(ImageId image_id) {
ASSERT_MSG(False(image.flags & ImageFlagBits::Registered),
"Trying to register an already registered image");
image.flags |= ImageFlagBits::Registered;
total_used_memory += ImageSizeBytes(image);
u64 tentative_size = (std::max)(image.guest_size_bytes, image.unswizzled_size_bytes);
if ((IsPixelFormatASTC(image.info.format) &&
True(image.flags & ImageFlagBits::AcceleratedUpload)) ||
True(image.flags & ImageFlagBits::Converted)) {
tentative_size = TranscodedAstcSize(tentative_size, image.info.format);
}
total_used_memory += Common::AlignUp(tentative_size, 1024);
image.lru_index = lru_cache.Insert(image_id, frame_tick);
ForEachGPUPage(image.gpu_addr, image.guest_size_bytes, [this, image_id](u64 page) {
@@ -2444,7 +2354,16 @@ void TextureCache<P>::UntrackImage(ImageBase& image, ImageId image_id) {
template <class P>
void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
ImageBase& image = slot_images[image_id];
total_used_memory -= std::min<u64>(total_used_memory, ImageSizeBytes(image));
if (image.HasScaled()) {
total_used_memory -= GetScaledImageSizeBytes(image);
}
u64 tentative_size = (std::max)(image.guest_size_bytes, image.unswizzled_size_bytes);
if ((IsPixelFormatASTC(image.info.format) &&
True(image.flags & ImageFlagBits::AcceleratedUpload)) ||
True(image.flags & ImageFlagBits::Converted)) {
tentative_size = TranscodedAstcSize(tentative_size, image.info.format);
}
total_used_memory -= Common::AlignUp(tentative_size, 1024);
const GPUVAddr gpu_addr = image.gpu_addr;
const auto alloc_it = image_allocs_table.find(gpu_addr);
if (alloc_it == image_allocs_table.end()) {
@@ -2498,15 +2417,14 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
ASSERT_MSG(num_removed_overlaps == 1, "Invalid number of removed overlapps: {}",
num_removed_overlaps);
}
const u64 sync_point = runtime.CurrentSyncPoint();
for (const ImageViewId image_view_id : image_view_ids) {
if (!immediate_delete) {
sentenced_image_view.Push(std::move(slot_image_views[image_view_id]), sync_point);
sentenced_image_view.Push(std::move(slot_image_views[image_view_id]));
}
slot_image_views.erase(image_view_id);
}
if (!immediate_delete) {
sentenced_images.Push(std::move(slot_images[image_id]), sync_point);
sentenced_images.Push(std::move(slot_images[image_id]));
}
slot_images.erase(image_id);
@@ -2552,8 +2470,7 @@ void TextureCache<P>::RemoveFramebuffers(std::span<const ImageViewId> removed_vi
last_framebuffer_id = {};
last_framebuffer_serial = 0;
}
sentenced_framebuffers.Push(std::move(slot_framebuffers[framebuffer_id]),
runtime.CurrentSyncPoint());
sentenced_framebuffers.Push(std::move(slot_framebuffers[framebuffer_id]));
it = framebuffers.erase(it);
} else {
++it;
@@ -30,7 +30,7 @@
#include "common/thread_worker.h"
#include "video_core/compatible_formats.h"
#include "video_core/control/channel_state_cache.h"
#include "video_core/deferred_destruction_queue.h"
#include "video_core/delayed_destruction_ring.h"
#include "video_core/engines/fermi_2d.h"
#include "video_core/surface.h"
#include "video_core/texture_cache/descriptor_table.h"
@@ -108,20 +108,18 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
static constexpr bool HAS_DEVICE_MEMORY_INFO = P::HAS_DEVICE_MEMORY_INFO;
/// True when the API can do asynchronous texture downloads.
static constexpr bool IMPLEMENTS_ASYNC_DOWNLOADS = P::IMPLEMENTS_ASYNC_DOWNLOADS;
static constexpr bool HAS_TIMELINE_SYNC_POINTS = P::HAS_TIMELINE_SYNC_POINTS;
static constexpr size_t UNSET_CHANNEL{(std::numeric_limits<size_t>::max)()};
#ifdef YUZU_LEGACY
static constexpr u64 RECLAIM_HEADROOM = 384_MiB;
static constexpr s64 TARGET_THRESHOLD = 3_GiB;
#else
static constexpr u64 RECLAIM_HEADROOM = 512_MiB;
static constexpr s64 TARGET_THRESHOLD = 4_GiB;
#endif
static constexpr u64 FALLBACK_MEMORY_BUDGET = 2_GiB;
static constexpr u32 USAGE_REFRESH_INTERVAL = 16;
static constexpr u64 RECLAIM_GUARD_FRAMES = 8;
static constexpr u64 RECLAIM_TARGET_PERCENT = 88;
static constexpr s64 DEFAULT_EXPECTED_MEMORY = 1_GiB + 125_MiB;
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB + 625_MiB;
static constexpr size_t GC_EMERGENCY_COUNTS = 2;
using Runtime = typename P::Runtime;
using Image = typename P::Image;
@@ -156,8 +154,6 @@ public:
/// Notify the cache that a new frame has been queued
void TickFrame();
u64 ReclaimMemory(u64 target_bytes, bool allow_download);
/// Return a constant reference to the given image view id
[[nodiscard]] const ImageView& GetImageView(ImageViewId id) const noexcept;
@@ -297,17 +293,8 @@ private:
void OnGPUASRegister(size_t map_id) final override;
u64 ImageSizeBytes(const ImageBase& image);
u64 DeviceUsage(bool force_refresh);
void EnsureHeadroom(bool allow_download);
void QueueEvictionDownload(Image& image);
void TickEvictionDownloads(u64 completed_sync_point);
void FlushEvictionDownloads();
/// Runs the Garbage Collector.
void RunGarbageCollector();
/// Find or create an image view in the guest descriptor table
ImageViewId VisitImageView(u32 index, bool compute);
@@ -464,11 +451,9 @@ private:
bool has_deleted_images = false;
bool is_rescaling = false;
u64 total_used_memory = 0;
u64 memory_budget = 0;
u64 cached_device_usage = 0;
u32 usage_refresh_countdown = 0;
bool in_reclaim = false;
bool reclaim_stalled = false;
u64 minimum_memory;
u64 expected_memory;
u64 critical_memory;
size_t gpu_unswizzle_maxsize = 0;
size_t swizzle_chunk_size = 0;
u32 swizzle_slices_per_batch = 0;
@@ -506,19 +491,14 @@ private:
};
Common::LeastRecentlyUsedCache<LRUItemParams> lru_cache;
DeferredDestructionQueue<Image> sentenced_images;
DeferredDestructionQueue<ImageView> sentenced_image_view;
DeferredDestructionQueue<Framebuffer> sentenced_framebuffers;
struct PendingEvictionDownload {
AsyncBuffer staging;
Tegra::MemoryManager* gpu_memory;
boost::container::small_vector<VideoCommon::BufferImageCopy, 16> copies;
VideoCommon::ImageInfo info;
GPUVAddr gpu_addr;
u64 sync_point;
};
std::deque<PendingEvictionDownload> pending_eviction_downloads;
#ifdef YUZU_LEGACY
static constexpr size_t TICKS_TO_DESTROY = 6;
#else
static constexpr size_t TICKS_TO_DESTROY = 8;
#endif
DelayedDestructionRing<Image, TICKS_TO_DESTROY> sentenced_images;
DelayedDestructionRing<ImageView, TICKS_TO_DESTROY> sentenced_image_view;
DelayedDestructionRing<Framebuffer, TICKS_TO_DESTROY> sentenced_framebuffers;
ankerl::unordered_dense::map<GPUVAddr, ImageAllocId> image_allocs_table;
@@ -529,8 +509,7 @@ private:
u64 frame_tick = 0;
u64 last_sampler_gc_frame = (std::numeric_limits<u64>::max)();
Common::ThreadWorker texture_decode_worker{1, "TextureDecoder", {},
Common::ThreadPlacement::Background};
Common::ThreadWorker texture_decode_worker{1, "TextureDecoder"};
std::vector<std::unique_ptr<AsyncDecodeContext>> async_decodes;
std::deque<PendingUnswizzle> unswizzle_queue;
+2 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -747,7 +747,7 @@ boost::container::small_vector<ImageCopy, 16> MakeShrinkImageCopies(const ImageI
const bool is_dst_3d = dst.type == ImageType::e3D;
if (is_dst_3d) {
ASSERT(src.type == ImageType::e3D || src.resources.layers == 1);
ASSERT(src.type == ImageType::e3D);
ASSERT(src.resources.levels == 1);
}
const bool both_2d{src.type == ImageType::e2D && dst.type == ImageType::e2D};
+2 -3
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -10,8 +10,7 @@ namespace Tegra::Texture {
Common::ThreadWorker& GetThreadWorkers() {
static Common::ThreadWorker workers{(std::max)(std::thread::hardware_concurrency(), 2U) / 2,
"ImageTranscode", {},
Common::ThreadPlacement::Background};
"ImageTranscode"};
return workers;
}
+5 -57
View File
@@ -5,7 +5,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <bitset>
#include <chrono>
#include <optional>
@@ -733,7 +732,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
.device = *logical,
.preferredLargeHeapBlockSize = is_integrated
? (64u * 1024u * 1024u)
: (128u * 1024u * 1024u),
: (256u * 1024u * 1024u),
.pAllocationCallbacks = nullptr,
.pDeviceMemoryCallbacks = nullptr,
.pHeapSizeLimit = nullptr,
@@ -745,32 +744,12 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
vk::Check(vmaCreateAllocator(&allocator_info, &allocator));
{
const auto& limits = properties.properties.limits;
LOG_INFO(Render_Vulkan, "MSAA sample count support:");
LOG_INFO(Render_Vulkan, " framebufferColorSampleCounts: {:#x}",
limits.framebufferColorSampleCounts);
LOG_INFO(Render_Vulkan, " framebufferDepthSampleCounts: {:#x}",
limits.framebufferDepthSampleCounts);
LOG_INFO(Render_Vulkan, " framebufferStencilSampleCounts: {:#x}",
limits.framebufferStencilSampleCounts);
LOG_INFO(Render_Vulkan, " sampledImageColorSampleCounts: {:#x}",
limits.sampledImageColorSampleCounts);
LOG_INFO(Render_Vulkan, " sampledImageDepthSampleCounts: {:#x}",
limits.sampledImageDepthSampleCounts);
LOG_INFO(Render_Vulkan, " sampledImageIntegerSampleCounts:{:#x}",
limits.sampledImageIntegerSampleCounts);
LOG_INFO(Render_Vulkan, " storageImageSampleCounts: {:#x}",
limits.storageImageSampleCounts);
}
// Initialize GPU logging if enabled
InitializeGPULogging();
}
Device::~Device() {
ShutdownGPULogging();
vk::FlushDeletionQueue();
vmaDestroyAllocator(allocator);
}
@@ -838,8 +817,7 @@ bool Device::ComputeIsOptimalAstcSupported() const {
VK_FORMAT_ASTC_12x10_UNORM_BLOCK, VK_FORMAT_ASTC_12x10_SRGB_BLOCK,
VK_FORMAT_ASTC_12x12_UNORM_BLOCK, VK_FORMAT_ASTC_12x12_SRGB_BLOCK,
};
if (!features.features.textureCompressionASTC_LDR ||
!features.texture_compression_astc_hdr.textureCompressionASTC_HDR) {
if (!features.features.textureCompressionASTC_LDR) {
return false;
}
const auto format_feature_usage{VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
@@ -1231,20 +1209,10 @@ void Device::RemoveUnsuitableExtensions() {
RemoveExtensionFeatureIfUnsuitable(extensions.depth_bias_control, features.depth_bias_control,
VK_EXT_DEPTH_BIAS_CONTROL_EXTENSION_NAME);
// VK_EXT_depth_clamp_zero_one
extensions.depth_clamp_zero_one = features.depth_clamp_zero_one.depthClampZeroOne;
RemoveExtensionFeatureIfUnsuitable(extensions.depth_clamp_zero_one,
features.depth_clamp_zero_one,
VK_EXT_DEPTH_CLAMP_ZERO_ONE_EXTENSION_NAME);
// VK_EXT_depth_clip_control
extensions.depth_clip_control = features.depth_clip_control.depthClipControl;
RemoveExtensionFeatureIfUnsuitable(extensions.depth_clip_control, features.depth_clip_control,
VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME);
// VK_EXT_depth_clip_enable
extensions.depth_clip_enable = features.depth_clip_enable.depthClipEnable;
RemoveExtensionFeatureIfUnsuitable(extensions.depth_clip_enable, features.depth_clip_enable,
VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME);
// VK_EXT_extended_dynamic_state
extensions.extended_dynamic_state = features.extended_dynamic_state.extendedDynamicState;
@@ -1476,23 +1444,6 @@ std::optional<size_t> Device::GetSamplerHeapBudget() const {
return sampler_heap_budget;
}
Device::MemoryBudgetInfo Device::GetMemoryBudgetInfo() const {
std::array<VmaBudget, VK_MAX_MEMORY_HEAPS> budgets{};
vmaGetHeapBudgets(allocator, budgets.data());
MemoryBudgetInfo info{};
for (const size_t heap : valid_heap_memory) {
info.usage += budgets[heap].usage;
info.budget += budgets[heap].budget;
info.block_bytes += budgets[heap].statistics.blockBytes;
info.allocation_bytes += budgets[heap].statistics.allocationBytes;
}
return info;
}
void Device::TickAllocatorFrame() const {
vmaSetCurrentFrameIndex(allocator, ++allocator_frame_index);
}
u64 Device::GetDeviceMemoryUsage() const {
VkPhysicalDeviceMemoryBudgetPropertiesEXT budget;
budget.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT;
@@ -1542,12 +1493,9 @@ void Device::CollectPhysicalMemoryInfo() {
device_access_memory -= reserve_memory;
if (Settings::values.vram_usage_mode.GetValue() != Settings::VramUsageMode::Aggressive) {
// Account for resolution scaling in memory limits
const u64 normal_memory = 6_GiB;
const u64 scaler_memory = 1_GiB * Settings::values.resolution_info.ScaleUp(1);
const u64 baseline = normal_memory + scaler_memory;
const u64 proportional = (device_access_memory / 4) * 3;
device_access_memory =
std::min<u64>(device_access_memory, std::max<u64>(baseline, proportional));
const size_t normal_memory = 6_GiB;
const size_t scaler_memory = 1_GiB * Settings::values.resolution_info.ScaleUp(1);
device_access_memory = std::min<u64>(device_access_memory, normal_memory + scaler_memory);
}
}
}
+2 -61
View File
@@ -54,9 +54,7 @@ VK_DEFINE_HANDLE(VmaAllocator)
FEATURE(EXT, ColorWriteEnable, COLOR_WRITE_ENABLE, color_write_enable) \
FEATURE(EXT, CustomBorderColor, CUSTOM_BORDER_COLOR, custom_border_color) \
FEATURE(EXT, DepthBiasControl, DEPTH_BIAS_CONTROL, depth_bias_control) \
FEATURE(EXT, DepthClampZeroOne, DEPTH_CLAMP_ZERO_ONE, depth_clamp_zero_one) \
FEATURE(EXT, DepthClipControl, DEPTH_CLIP_CONTROL, depth_clip_control) \
FEATURE(EXT, DepthClipEnable, DEPTH_CLIP_ENABLE, depth_clip_enable) \
FEATURE(EXT, ExtendedDynamicState, EXTENDED_DYNAMIC_STATE, extended_dynamic_state) \
FEATURE(EXT, ExtendedDynamicState2, EXTENDED_DYNAMIC_STATE_2, extended_dynamic_state2) \
FEATURE(EXT, ExtendedDynamicState3, EXTENDED_DYNAMIC_STATE_3, extended_dynamic_state3) \
@@ -257,17 +255,6 @@ public:
return allocator;
}
struct MemoryBudgetInfo {
u64 usage;
u64 budget;
u64 block_bytes;
u64 allocation_bytes;
};
MemoryBudgetInfo GetMemoryBudgetInfo() const;
void TickAllocatorFrame() const;
/// Returns the logical device.
const vk::Device& GetLogical() const {
return logical;
@@ -382,7 +369,8 @@ FN_MAX_LIMIT_LIST
}
bool IsOptimalAstcSupported() const {
return is_optimal_astc_supported;
return features.features.textureCompressionASTC_LDR &&
features.texture_compression_astc_hdr.textureCompressionASTC_HDR;
}
/// Returns true if BCn is natively supported.
@@ -614,16 +602,6 @@ FN_MAX_LIMIT_LIST
return extensions.depth_clip_control;
}
/// Returns true if the device supports VK_EXT_depth_clamp_zero_one.
bool IsExtDepthClampZeroOneSupported() const {
return extensions.depth_clamp_zero_one;
}
/// Returns true if the device supports VK_EXT_depth_clip_enable.
bool IsExtDepthClipEnableSupported() const {
return extensions.depth_clip_enable;
}
/// Returns true if the device supports VK_EXT_depth_bias_control.
bool IsExtDepthBiasControlSupported() const {
return extensions.depth_bias_control;
@@ -756,38 +734,6 @@ FN_MAX_LIMIT_LIST
return features.line_rasterization.stippledRectangularLines != VK_FALSE;
}
VkLineRasterizationModeEXT GetLineRasterizationMode(bool wants_smooth) const {
if (wants_smooth && SupportsSmoothLines()) {
return VK_LINE_RASTERIZATION_MODE_RECTANGULAR_SMOOTH_EXT;
}
if (SupportsRectangularLines()) {
return VK_LINE_RASTERIZATION_MODE_RECTANGULAR_EXT;
}
return VK_LINE_RASTERIZATION_MODE_DEFAULT_EXT;
}
bool SupportsStippleForMode(VkLineRasterizationModeEXT mode) const {
switch (mode) {
case VK_LINE_RASTERIZATION_MODE_RECTANGULAR_SMOOTH_EXT:
return features.line_rasterization.stippledSmoothLines != VK_FALSE;
case VK_LINE_RASTERIZATION_MODE_BRESENHAM_EXT:
return features.line_rasterization.stippledBresenhamLines != VK_FALSE;
default:
return features.line_rasterization.stippledRectangularLines != VK_FALSE;
}
}
float ClampLineWidth(float width) const {
if (!features.features.wideLines) {
return 1.0f;
}
const auto& range = properties.properties.limits.lineWidthRange;
if (!(width >= range[0])) {
return range[0];
}
return width > range[1] ? range[1] : width;
}
bool SupportsAlphaToOne() const {
return features.features.alphaToOne != VK_FALSE;
}
@@ -930,10 +876,6 @@ FN_MAX_LIMIT_LIST
u64 GetDeviceMemoryUsage() const;
VkSampleCountFlags GetStorageImageSampleCounts() const {
return properties.properties.limits.storageImageSampleCounts;
}
u32 GetSetsPerPool() const {
return sets_per_pool;
}
@@ -1118,7 +1060,6 @@ private:
private:
VkInstance instance; ///< Vulkan instance.
VmaAllocator allocator; ///< VMA allocator.
mutable u32 allocator_frame_index{};
vk::DeviceDispatch dld; ///< Device function pointers.
vk::PhysicalDevice physical; ///< Physical device.
vk::Device logical; ///< Logical device.
@@ -30,6 +30,26 @@ namespace Vulkan {
// Helpers translating MemoryUsage to flags/usage
[[maybe_unused]] VkMemoryPropertyFlags MemoryUsagePropertyFlags(MemoryUsage usage) {
switch (usage) {
case MemoryUsage::DeviceLocal:
return VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
case MemoryUsage::Upload:
return VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
case MemoryUsage::Download:
return VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT |
VK_MEMORY_PROPERTY_HOST_CACHED_BIT;
case MemoryUsage::Stream:
return VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT |
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
}
ASSERT_MSG(false, "Invalid memory usage={}", usage);
return VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
}
[[nodiscard]] VkMemoryPropertyFlags MemoryUsagePreferredVmaFlags(MemoryUsage usage) {
if (usage == MemoryUsage::Download) {
return VK_MEMORY_PROPERTY_HOST_CACHED_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
@@ -66,11 +86,125 @@ namespace Vulkan {
}
// This avoids calling vkGetBufferMemoryRequirements* directly.
template<typename T>
static VkBuffer GetVkHandleFromBuffer(const T &buf) {
if constexpr (requires { static_cast<VkBuffer>(buf); }) {
return static_cast<VkBuffer>(buf);
} else if constexpr (requires {{ buf.GetHandle() } -> std::convertible_to<VkBuffer>; }) {
return buf.GetHandle();
} else if constexpr (requires {{ buf.Handle() } -> std::convertible_to<VkBuffer>; }) {
return buf.Handle();
} else if constexpr (requires {{ buf.vk_handle() } -> std::convertible_to<VkBuffer>; }) {
return buf.vk_handle();
} else {
static_assert(sizeof(T) == 0, "Cannot extract VkBuffer handle from vk::Buffer");
return VK_NULL_HANDLE;
}
}
} // namespace
//MemoryCommit is now VMA-backed
MemoryCommit::MemoryCommit(VmaAllocator alloc, VmaAllocation a,
const VmaAllocationInfo &info) noexcept
: allocator{alloc}, allocation{a}, memory{info.deviceMemory},
offset{info.offset}, size{info.size}, mapped_ptr{info.pMappedData} {
// Log GPU memory allocation
if (GPU::Logging::IsActive() &&
Settings::values.gpu_log_memory_tracking.GetValue()) {
GPU::Logging::GPULogger::GetInstance().LogMemoryAllocation(
reinterpret_cast<uintptr_t>(memory),
static_cast<u64>(size),
0 // Memory property flags (not easily available from VMA)
);
}
}
MemoryCommit::~MemoryCommit() { Release(); }
MemoryCommit::MemoryCommit(MemoryCommit &&rhs) noexcept
: allocator{std::exchange(rhs.allocator, nullptr)},
allocation{std::exchange(rhs.allocation, nullptr)},
memory{std::exchange(rhs.memory, VK_NULL_HANDLE)},
offset{std::exchange(rhs.offset, 0)},
size{std::exchange(rhs.size, 0)},
mapped_ptr{std::exchange(rhs.mapped_ptr, nullptr)} {}
MemoryCommit &MemoryCommit::operator=(MemoryCommit &&rhs) noexcept {
if (this != &rhs) {
Release();
allocator = std::exchange(rhs.allocator, nullptr);
allocation = std::exchange(rhs.allocation, nullptr);
memory = std::exchange(rhs.memory, VK_NULL_HANDLE);
offset = std::exchange(rhs.offset, 0);
size = std::exchange(rhs.size, 0);
mapped_ptr = std::exchange(rhs.mapped_ptr, nullptr);
}
return *this;
}
std::span<u8> MemoryCommit::Map()
{
if (!allocation) return {};
if (!mapped_ptr) {
if (vmaMapMemory(allocator, allocation, &mapped_ptr) != VK_SUCCESS) return {};
}
const size_t n = static_cast<size_t>(std::min<VkDeviceSize>(size,
(std::numeric_limits<size_t>::max)()));
return std::span<u8>{static_cast<u8 *>(mapped_ptr), n};
}
std::span<const u8> MemoryCommit::Map() const
{
if (!allocation) return {};
if (!mapped_ptr) {
void *p = nullptr;
if (vmaMapMemory(allocator, allocation, &p) != VK_SUCCESS) return {};
const_cast<MemoryCommit *>(this)->mapped_ptr = p;
}
const size_t n = static_cast<size_t>(std::min<VkDeviceSize>(size,
(std::numeric_limits<size_t>::max)()));
return std::span<const u8>{static_cast<const u8 *>(mapped_ptr), n};
}
void MemoryCommit::Unmap()
{
if (allocation && mapped_ptr) {
vmaUnmapMemory(allocator, allocation);
mapped_ptr = nullptr;
}
}
void MemoryCommit::Release() {
if (allocation && allocator) {
// Log GPU memory deallocation
if (GPU::Logging::IsActive() &&
Settings::values.gpu_log_memory_tracking.GetValue() &&
memory != VK_NULL_HANDLE) {
GPU::Logging::GPULogger::GetInstance().LogMemoryDeallocation(
reinterpret_cast<uintptr_t>(memory)
);
}
if (mapped_ptr) {
vmaUnmapMemory(allocator, allocation);
mapped_ptr = nullptr;
}
vmaFreeMemory(allocator, allocation);
}
allocation = nullptr;
allocator = nullptr;
memory = VK_NULL_HANDLE;
offset = 0;
size = 0;
}
MemoryAllocator::MemoryAllocator(const Device &device_)
: device{device_}, allocator{device.GetAllocator()},
properties{device_.GetPhysical().GetMemoryProperties().memoryProperties} {
properties{device_.GetPhysical().GetMemoryProperties().memoryProperties},
buffer_image_granularity{
device_.GetPhysical().GetProperties().limits.bufferImageGranularity} {
// Preserve the previous "RenderDoc small heap" trimming behavior that we had in original vma minus the heap bug
if (device.HasDebuggingToolAttached())
@@ -90,21 +224,6 @@ namespace Vulkan {
MemoryAllocator::~MemoryAllocator() = default;
void MemoryAllocator::SetReclaimCallback(ReclaimCallback callback) {
reclaim_callback = std::move(callback);
vk::SetAllocatorOwnerThread();
}
bool MemoryAllocator::ReclaimAtLeast(u64 hint_bytes) const {
if (!reclaim_callback || in_reclaim) {
return false;
}
in_reclaim = true;
const u64 freed = reclaim_callback(hint_bytes);
in_reclaim = false;
return freed > 0;
}
vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci) const
{
const VmaAllocationCreateInfo alloc_ci = {
@@ -121,26 +240,7 @@ namespace Vulkan {
VkImage handle{};
VmaAllocation allocation{};
VmaAllocationInfo alloc_info{};
DEBUG_ASSERT(vk::OnAllocatorOwnerThread());
VkResult res = vmaCreateImage(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info);
if (res != VK_SUCCESS && ReclaimAtLeast(IMAGE_RECLAIM_HINT)) {
res = vmaCreateImage(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info);
}
if (res != VK_SUCCESS) {
auto relaxed_ci = alloc_ci;
relaxed_ci.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
res = vmaCreateImage(allocator, &ci, &relaxed_ci, &handle, &allocation, &alloc_info);
if (res != VK_SUCCESS) {
relaxed_ci.preferredFlags &= ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
res = vmaCreateImage(allocator, &ci, &relaxed_ci, &handle, &allocation, &alloc_info);
}
}
vk::Check(res);
vk::Check(vmaCreateImage(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info));
// Log GPU memory allocation for images
if (GPU::Logging::IsActive() &&
@@ -177,28 +277,7 @@ namespace Vulkan {
VmaAllocation allocation{};
VkMemoryPropertyFlags property_flags{};
DEBUG_ASSERT(vk::OnAllocatorOwnerThread());
VkResult res = vmaCreateBuffer(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info);
if (res != VK_SUCCESS && ReclaimAtLeast(ci.size)) {
res = vmaCreateBuffer(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info);
}
if (res != VK_SUCCESS) {
auto relaxed_ci = alloc_ci;
relaxed_ci.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
res = vmaCreateBuffer(allocator, &ci, &relaxed_ci, &handle, &allocation, &alloc_info);
if (res != VK_SUCCESS &&
(relaxed_ci.preferredFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) {
relaxed_ci.preferredFlags &= ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
res = vmaCreateBuffer(allocator, &ci, &relaxed_ci, &handle, &allocation,
&alloc_info);
}
}
vk::Check(res);
vk::Check(vmaCreateBuffer(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info));
vmaGetAllocationMemoryProperties(allocator, allocation, &property_flags);
// Log GPU memory allocation for buffers
@@ -220,4 +299,77 @@ namespace Vulkan {
device.GetDispatchLoader());
}
MemoryCommit MemoryAllocator::Commit(const VkMemoryRequirements &reqs, MemoryUsage usage)
{
const auto vma_usage = MemoryUsageVma(usage);
VmaAllocationCreateInfo ci{};
ci.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | MemoryUsageVmaFlags(usage);
ci.usage = vma_usage;
ci.memoryTypeBits = reqs.memoryTypeBits & valid_memory_types;
ci.requiredFlags = 0;
ci.preferredFlags = MemoryUsagePreferredVmaFlags(usage);
VmaAllocation a{};
VmaAllocationInfo info{};
VkResult res = vmaAllocateMemory(allocator, &reqs, &ci, &a, &info);
if (res != VK_SUCCESS) {
// Relax 1: drop budget constraint
auto ci2 = ci;
ci2.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
res = vmaAllocateMemory(allocator, &reqs, &ci2, &a, &info);
// Relax 2: if we preferred DEVICE_LOCAL, drop that preference
if (res != VK_SUCCESS && (ci.preferredFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) {
auto ci3 = ci2;
ci3.preferredFlags &= ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
res = vmaAllocateMemory(allocator, &reqs, &ci3, &a, &info);
}
}
vk::Check(res);
return MemoryCommit(allocator, a, info);
}
MemoryCommit MemoryAllocator::Commit(const vk::Buffer &buffer, MemoryUsage usage) {
// Allocate memory appropriate for this buffer automatically
const auto vma_usage = MemoryUsageVma(usage);
VmaAllocationCreateInfo ci{};
ci.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | MemoryUsageVmaFlags(usage);
ci.usage = vma_usage;
ci.requiredFlags = 0;
ci.preferredFlags = MemoryUsagePreferredVmaFlags(usage);
ci.pool = VK_NULL_HANDLE;
ci.pUserData = nullptr;
ci.priority = 0.0f;
const VkBuffer raw = *buffer;
VmaAllocation a{};
VmaAllocationInfo info{};
// Let VMA infer memory requirements from the buffer
VkResult res = vmaAllocateMemoryForBuffer(allocator, raw, &ci, &a, &info);
if (res != VK_SUCCESS) {
auto ci2 = ci;
ci2.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
res = vmaAllocateMemoryForBuffer(allocator, raw, &ci2, &a, &info);
if (res != VK_SUCCESS && (ci.preferredFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) {
auto ci3 = ci2;
ci3.preferredFlags &= ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
res = vmaAllocateMemoryForBuffer(allocator, raw, &ci3, &a, &info);
}
}
vk::Check(res);
vk::Check(vmaBindBufferMemory2(allocator, a, 0, raw, nullptr));
return MemoryCommit(allocator, a, info);
}
} // namespace Vulkan
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
@@ -6,7 +6,6 @@
#pragma once
#include <functional>
#include <memory>
#include <span>
#include <vector>
@@ -40,6 +39,51 @@ namespace Vulkan {
}
}
/// Ownership handle of a memory commitment (real VMA allocation).
class MemoryCommit {
public:
MemoryCommit() noexcept = default;
MemoryCommit(VmaAllocator allocator, VmaAllocation allocation,
const VmaAllocationInfo &info) noexcept;
~MemoryCommit();
MemoryCommit(const MemoryCommit &) = delete;
MemoryCommit &operator=(const MemoryCommit &) = delete;
MemoryCommit(MemoryCommit &&) noexcept;
MemoryCommit &operator=(MemoryCommit &&) noexcept;
[[nodiscard]] std::span<u8> Map();
[[nodiscard]] std::span<const u8> Map() const;
void Unmap();
explicit operator bool() const noexcept { return allocation != nullptr; }
VkDeviceMemory Memory() const noexcept { return memory; }
VkDeviceSize Offset() const noexcept { return offset; }
VkDeviceSize Size() const noexcept { return size; }
VmaAllocation Allocation() const noexcept { return allocation; }
private:
void Release();
VmaAllocator allocator{}; ///< VMA allocator
VmaAllocation allocation{}; ///< VMA allocation handle
VkDeviceMemory memory{}; ///< Underlying VkDeviceMemory chosen by VMA
VkDeviceSize offset{}; ///< Offset of this allocation inside VkDeviceMemory
VkDeviceSize size{}; ///< Size of the allocation
void *mapped_ptr{}; ///< Optional persistent mapped pointer
};
/// Memory allocator container.
/// Allocates and releases memory allocations on demand.
class MemoryAllocator {
@@ -63,21 +107,36 @@ namespace Vulkan {
vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const;
using ReclaimCallback = std::function<u64(u64)>;
/**
* Commits a memory with the specified requirements.
*
* @param requirements Requirements returned from a Vulkan call.
* @param usage Indicates how the memory will be used.
*
* @returns A memory commit.
*/
MemoryCommit Commit(const VkMemoryRequirements &requirements, MemoryUsage usage);
void SetReclaimCallback(ReclaimCallback callback);
/// Commits memory required by the buffer and binds it (for buffers created outside VMA).
MemoryCommit Commit(const vk::Buffer &buffer, MemoryUsage usage);
private:
bool ReclaimAtLeast(u64 hint_bytes) const;
static constexpr u64 IMAGE_RECLAIM_HINT = 64ULL * 1024 * 1024;
static bool IsAutoUsage(VmaMemoryUsage u) noexcept {
switch (u) {
case VMA_MEMORY_USAGE_AUTO:
case VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE:
case VMA_MEMORY_USAGE_AUTO_PREFER_HOST:
return true;
default:
return false;
}
}
const Device &device; ///< Device handle.
VmaAllocator allocator; ///< VMA allocator.
const VkPhysicalDeviceMemoryProperties properties; ///< Physical device memory properties.
VkDeviceSize buffer_image_granularity; ///< Adjacent buffer/image granularity
u32 valid_memory_types{~0u};
ReclaimCallback reclaim_callback;
mutable bool in_reclaim{false};
};
} // namespace Vulkan
@@ -5,16 +5,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <atomic>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <thread>
#include <utility>
#include <vector>
#include "common/assert.h"
#include "common/common_types.h"
#include "common/logging.h"
#include "video_core/vulkan_common/vk_enum_string_helper.h"
@@ -25,60 +20,6 @@ namespace Vulkan::vk {
namespace {
std::thread::id allocator_owner_thread;
template <typename HandleType>
struct PendingRelease {
VmaAllocator allocator;
HandleType handle;
VmaAllocation allocation;
u64 timeline;
};
std::mutex deletion_mutex;
std::atomic<u64> deletion_timeline{1};
std::vector<PendingRelease<VkImage>> pending_images;
std::vector<PendingRelease<VkBuffer>> pending_buffers;
template <typename HandleType>
void PushPendingRelease(std::vector<PendingRelease<HandleType>>& pending, VmaAllocator allocator,
HandleType handle, VmaAllocation allocation) noexcept {
std::scoped_lock lock{deletion_mutex};
pending.push_back(PendingRelease<HandleType>{
.allocator = allocator,
.handle = handle,
.allocation = allocation,
.timeline = deletion_timeline.load(std::memory_order_acquire),
});
}
template <typename HandleType>
void ExtractReleased(std::vector<PendingRelease<HandleType>>& pending,
std::vector<PendingRelease<HandleType>>& released, u64 completed_value) {
const auto split = std::partition(pending.begin(), pending.end(),
[completed_value](const PendingRelease<HandleType>& entry) {
return entry.timeline > completed_value;
});
released.assign(split, pending.end());
pending.erase(split, pending.end());
}
void DrainDeletionQueue(u64 completed_value) noexcept {
std::vector<PendingRelease<VkImage>> images;
std::vector<PendingRelease<VkBuffer>> buffers;
{
std::scoped_lock lock{deletion_mutex};
ExtractReleased(pending_images, images, completed_value);
ExtractReleased(pending_buffers, buffers, completed_value);
}
for (const auto& entry : images) {
vmaDestroyImage(entry.allocator, entry.handle, entry.allocation);
}
for (const auto& entry : buffers) {
vmaDestroyBuffer(entry.allocator, entry.handle, entry.allocation);
}
}
template <typename Func>
void SortPhysicalDevices(std::vector<VkPhysicalDevice>& devices, const InstanceDispatch& dld,
Func&& func) {
@@ -561,35 +502,13 @@ DebugReportCallback Instance::CreateDebugReportCallback(
return DebugReportCallback(object, handle, *dld);
}
void SetAllocatorOwnerThread() {
allocator_owner_thread = std::this_thread::get_id();
}
bool OnAllocatorOwnerThread() noexcept {
return allocator_owner_thread == std::thread::id{} ||
allocator_owner_thread == std::this_thread::get_id();
}
void SetDeletionTimeline(u64 value) noexcept {
deletion_timeline.store(value, std::memory_order_release);
}
void TickDeletionQueue(u64 completed_value) noexcept {
DEBUG_ASSERT(OnAllocatorOwnerThread());
DrainDeletionQueue(completed_value);
}
void FlushDeletionQueue() noexcept {
DrainDeletionQueue((std::numeric_limits<u64>::max)());
}
void Image::SetObjectNameEXT(const char* name) const {
SetObjectName(dld, owner, handle, VK_OBJECT_TYPE_IMAGE, name);
}
void Image::Release() const noexcept {
if (handle) {
PushPendingRelease(pending_images, allocator, handle, allocation);
vmaDestroyImage(allocator, handle, allocation);
}
}
@@ -611,7 +530,7 @@ void Buffer::SetObjectNameEXT(const char* name) const {
void Buffer::Release() const noexcept {
if (handle) {
PushPendingRelease(pending_buffers, allocator, handle, allocation);
vmaDestroyBuffer(allocator, handle, allocation);
}
}
@@ -131,16 +131,6 @@ private:
VkResult result;
};
void SetAllocatorOwnerThread();
[[nodiscard]] bool OnAllocatorOwnerThread() noexcept;
void SetDeletionTimeline(u64 value) noexcept;
void TickDeletionQueue(u64 completed_value) noexcept;
void FlushDeletionQueue() noexcept;
/// Throws a Vulkan exception if result is not success.
inline void Check(VkResult result) {
if (result != VK_SUCCESS) {

Some files were not shown because too many files have changed in this diff Show More