Compare commits

..

1 Commits

Author SHA1 Message Date
b5c86787ab jdlo modifications
Some checks failed
Check Strings / check-strings (push) Has been cancelled
tx-src / sources (push) Has been cancelled
2025-12-26 13:28:47 -03:00
12 changed files with 545 additions and 99 deletions

View File

@@ -22,15 +22,6 @@ Or use Qt Creator (Create Project -> Import Project -> Git Clone).
Android has a completely different build process than other platforms. See its [dedicated page](build/Android.md).
## Cross-compile ARM64
A painless guide for cross compilation (or to test NCE) from a x86_64 system without polluting your main.
- Install QEMU: `sudo pkg install qemu`
- Download Debian 13: `wget https://cdimage.debian.org/debian-cd/current/arm64/iso-cd/debian-13.0.0-arm64-netinst.iso`
- Create a system disk: `qemu-img create -f qcow2 debian-13-arm64-ci.qcow2 30G`
- Run the VM: `qemu-system-aarch64 -M virt -m 2G -cpu max -bios /usr/local/share/qemu/edk2-aarch64-code.fd -drive if=none,file=debian-13.0.0-arm64-netinst.iso,format=raw,id=cdrom -device scsi-cd,drive=cdrom -drive if=none,file=debian-13-arm64-ci.qcow2,id=hd0,format=qcow2 -device virtio-blk-device,drive=hd0 -device virtio-gpu-pci -device usb-ehci -device usb-kbd -device intel-hda -device hda-output -nic user,model=virtio-net-pci`
## Initial Configuration
If the configure phase fails, see the `Troubleshooting` section below. Usually, as long as you followed the dependencies guide, the defaults *should* successfully configure and build.

View File

@@ -93,11 +93,6 @@ Eden is not currently available as a port on FreeBSD, though it is in the works.
The available OpenSSL port (3.0.17) is out-of-date, and using a bundled static library instead is recommended; to do so, add `-DYUZU_USE_BUNDLED_OPENSSL=ON` to your CMake configure command.
Gamepad/controllers may not work on 15.0, this is due to an outdated SDL not responding well to the new `usbhid(2)` driver. To workaround this simply disable `usbhid(2)` (add the following to `/boot/loader.conf`):
```sh
hw.usb.usbhid.enable="0"
```
## NetBSD
Install `pkgin` if not already `pkg_add pkgin`, see also the general [pkgsrc guide](https://www.netbsd.org/docs/pkgsrc/using.html). For NetBSD 10.1 provide `echo 'PKG_PATH="https://cdn.netbsd.org/pub/pkgsrc/packages/NetBSD/x86_64/10.0_2025Q3/All/"' >/etc/pkg_install.conf`. If `pkgin` is taking too much time consider adding the following to `/etc/rc.conf`:
@@ -201,24 +196,14 @@ windeployqt6 --no-compiler-runtime --no-opengl-sw --no-system-dxc-compiler \
find ./*/ -name "*.dll" | while read -r dll; do deps "$dll"; done
```
## RedoxOS
The package install may randomly hang at times, in which case it has to be restarted. ALWAYS do a `sudo pkg update` or the chances of it hanging will be close to 90%. If "multiple" installs fail at once, try installing 1 by 1 the packages.
When CMake invokes certain file syscalls - it may sometimes cause crashes or corruptions on the (kernel?) address space - so reboot the system if there is a "hang" in CMake.
## Windows
### Windows 7, Windows 8 and Windows 8.1
## Windows 8.1 and below
DirectX 12 is not available - simply copy and paste a random DLL and name it `d3d12.dll`.
Install [Qt6 compatibility libraries](github.com/ANightly/qt6windows7) specifically Qt 6.9.5.
### Windows Vista and below
## RedoxOS
No support for Windows Vista (or below) is present at the moment. Check back later.
The package install may randomly hang at times, in which case it has to be restarted. ALWAYS do a `sudo pkg update` or the chances of it hanging will be close to 90%. If "multiple" installs fail at once, try installing 1 by 1 the packages.
### Windows on ARM
If you're using Snapdragon X or 8CX, use the [the Vulkan translation layer](https://apps.microsoft.com/detail/9nqpsl29bfff?hl=en-us&gl=USE) only if the stock drivers do not work. And of course always keep your system up-to-date.
When CMake invokes certain file syscalls - it may sometimes cause crashes or corruptions on the (kernel?) address space - so reboot the system if there is a "hang" in CMake.

View File

@@ -10,13 +10,7 @@ Simply put, types/classes are named as `PascalCase`, same for methods and functi
Except for Qt MOC where `functionName` is preferred.
Template typenames prefer short names like `T`, `I`, `U`, if a longer name is required either `Iterator` or `perform_action` are fine as well. Do not use names like `SS` as systems like solaris define it for registers, in general do not use any of the following for short names:
- `SS`, `DS`, `GS`, `FS`: Segment registers, defined by Solaris `<ucontext.h>`
- `EAX`, `EBX`, `ECX`, `EDX`, `ESI`, `EDI`, `ESP`, `EBP`, `EIP`: Registers, defined by Solaris.
- `X`: Defined by some utility headers, avoid.
- `_`: Defined by gettext, avoid.
- `N`, `M`, `S`: Preferably don't use this for types, use it for numeric constants.
- `TR`: Used by some weird `<ucontext.h>` whom define the Task Register as a logical register to provide to the user... (Need to remember which OS in specific).
Template typenames prefer short names like `T`, `I`, `U`, if a longer name is required either `Iterator` or `perform_action` are fine as well.
Macros must always be in `SCREAMING_CASE`. Do not use short letter macros as systems like Solaris will conflict with them; a good rule of thumb is >5 characters per macro - i.e `THIS_MACRO_IS_GOOD`, `AND_ALSO_THIS_ONE`.
@@ -24,45 +18,25 @@ Try not using hungarian notation, if you're able.
## Formatting
Formatting is extremelly lax, the general rule of thumb is: Don't add new lines just to increase line count. The less lines we have to look at, the better. This means also packing densely your code while not making it a clusterfuck. Strike a balance of "this is a short and comprehensible piece of code" and "my eyes are actually happy to see this!". Don't just drop the entire thing in a single line and call it "dense code", that's just spaghetti posing as code. In general, be mindful of what other devs need to look at.
Do not put if/while/etc braces after lines:
```c++
// no dont do this
// this is more lines of code for no good reason (why braces need their separate lines?)
// and those take space in someone's screen, cumulatively
if (thing)
{ //<--
{
some(); // ...
} //<-- 2 lines of code for basically "opening" and "closing" an statment
}
// do this
if (thing) { //<-- [...] and with your brain you can deduce it's this piece of code
// that's being closed
if (thing) {
some(); // ...
} //<-- only one line, and it's clearer since you know its closing something [...]
}
// or this, albeit the extra line isn't needed (at your discretion of course)
// or this
if (thing)
some(); // ...
// this is also ok, keeps things in one line and makes it extremely clear
// this is also ok
if (thing) some();
// NOT ok, don't be "clever" and use the comma operator to stash a bunch of statments
// in a single line, doing this will definitely ruin someone's day - just do the thing below
// vvv
if (thing) some(), thing(), a2(a1(), y1(), j1()), do_complex_shit(wa(), wo(), ploo());
// ... and in general don't use the comma operator for "multiple statments", EXCEPT if you think
// that it makes the code more readable (the situation may be rare however)
// Wow so much clearer! Now I can actually see what each statment is meant to do!
if (thing) {
some();
thing();
a2(a1(), y1(), j1());
do_complex_shit(wa(), wo(), ploo());
}
```
Brace rules are lax, if you can get the point across, do it:
@@ -103,21 +77,3 @@ if (device_name.empty()) {
SDL_AudioSpec obtained;
device = SDL_OpenAudioDevice(device_name.empty() ? nullptr : device_name.c_str(), capture, &spec, &obtained, false);
```
A note about operators: Use them sparingly, yes, the language is lax on them, but some usages can be... tripping to say the least.
```c++
a, b, c; //<-- NOT OK multiple statments with comma operator is definitely a recipe for disaster
return c ? a : b; //<-- OK ternaries at end of return statments are clear and fine
return a, b; //<-- NOT OK return will take value of `b` but also evaluate `a`, just use a separate statment
void f(int a[]) //<-- OK? if you intend to use the pointer as an array, otherwise just mark it as *
```
And about templates, use them sparingly, don't just do meta-templating for the sake of it, do it when you actually need it. This isn't a competition to see who can make the most complicated and robust meta-templating system. Just use what works, and preferably stick to the standard libary instead of reinventing the wheel. Additionally:
```c++
// NOT OK This will create (T * N * C * P) versions of the same function. DO. NOT. DO. THIS.
template<typename T, size_t N, size_t C, size_t P> inline void what() const noexcept;
// OK use parameters like a normal person, don't be afraid to use them :)
template<typename T> inline void what(size_t n, size_t c, size_t p) const noexcept;
```

10
docs/CrossCompile.md Normal file
View File

@@ -0,0 +1,10 @@
# Cross Compile
## ARM64
A painless guide for cross compilation (or to test NCE) from a x86_64 system without polluting your main.
- Install QEMU: `sudo pkg install qemu`
- Download Debian 13: `wget https://cdimage.debian.org/debian-cd/current/arm64/iso-cd/debian-13.0.0-arm64-netinst.iso`
- Create a system disk: `qemu-img create -f qcow2 debian-13-arm64-ci.qcow2 30G`
- Run the VM: `qemu-system-aarch64 -M virt -m 2G -cpu max -bios /usr/local/share/qemu/edk2-aarch64-code.fd -drive if=none,file=debian-13.0.0-arm64-netinst.iso,format=raw,id=cdrom -device scsi-cd,drive=cdrom -drive if=none,file=debian-13-arm64-ci.qcow2,id=hd0,format=qcow2 -device virtio-blk-device,drive=hd0 -device virtio-gpu-pci -device usb-ehci -device usb-kbd -device intel-hda -device hda-output -nic user,model=virtio-net-pci`

View File

@@ -294,22 +294,22 @@ sudo pkg install qt6 boost glslang libzip library/lz4 libusb-1 nlohmann-json ope
* Open the `MSYS2 MinGW 64-bit` shell (`mingw64.exe`)
* Download and install all dependencies:
```sh
```
BASE="git make autoconf libtool automake-wrapper jq patch"
MINGW="qt6-base qt6-tools qt6-translations qt6-svg cmake toolchain clang python-pip openssl vulkan-memory-allocator vulkan-devel glslang boost fmt lz4 nlohmann-json zlib zstd enet opus mbedtls libusb unordered_dense openssl SDL2"
# Either x86_64 or clang-aarch64 (Windows on ARM)
MINGW="qt6-base qt6-tools qt6-translations qt6-svg cmake toolchain clang python-pip openssl vulkan-memory-allocator vulkan-devel glslang boost fmt lz4 nlohmann-json zlib zstd enet opus mbedtls libusb unordered_dense"
packages="$BASE"
for pkg in $MINGW; do
packages="$packages mingw-w64-x86_64-$pkg"
#packages="$packages mingw-w64-clang-aarch64-$pkg"
done
pacman -Syuu --needed --noconfirm $packages
```
* Notes:
- Using `qt6-static` is possible but currently untested.
- Other environments are entirely untested, but should theoretically work provided you install all the necessary packages.
- GCC is proven to work better with the MinGW environment. If you choose to use Clang, you *may* be better off using the clang64 environment.
- ...except on ARM64, we recommend using clang instead of GCC for Windows ARM64.
- Add `qt-creator` to the `MINGW` variable to install Qt Creator. You can then create a Start Menu shortcut to the MinGW Qt Creator by running `powershell "\$s=(New-Object -COM WScript.Shell).CreateShortcut('C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Qt Creator.lnk');\$s.TargetPath='C:\\msys64\\mingw64\\bin\\qtcreator.exe';\$s.Save()"` in Git Bash or MSYS2.
* Add MinGW binaries to the PATH if they aren't already:
* `echo 'PATH=/mingw64/bin:$PATH' >> ~/.bashrc`

View File

@@ -17,8 +17,6 @@ add_library(core STATIC
constants.h
core.cpp
core.h
game_settings.cpp
game_settings.h
core_timing.cpp
core_timing.h
cpu_manager.cpp
@@ -45,7 +43,11 @@ add_library(core STATIC
device_memory.cpp
device_memory.h
device_memory_manager.h
device_memory.h
device_memory_manager.h
device_memory_manager.inc
internal_network/legacy_online.cpp
internal_network/legacy_online.h
file_sys/bis_factory.cpp
file_sys/bis_factory.h
file_sys/card_image.cpp

View File

@@ -49,6 +49,7 @@
#include "core/hle/service/services.h"
#include "core/hle/service/set/system_settings_server.h"
#include "core/hle/service/sm/sm.h"
#include "core/internal_network/legacy_online.h"
#include "core/internal_network/network.h"
#include "core/loader/loader.h"
#include "core/memory.h"
@@ -137,6 +138,13 @@ struct System::Impl {
kernel.SetMulticore(is_multicore);
cpu_manager.SetMulticore(is_multicore);
cpu_manager.SetAsyncGpu(is_async_gpu);
cpu_manager.SetMulticore(is_multicore);
cpu_manager.SetAsyncGpu(is_async_gpu);
if (!legacy_online) {
legacy_online = std::make_unique<Network::LegacyOnlineService>();
legacy_online->Start();
}
}
void ReinitializeIfNecessary(System& system) {
@@ -293,6 +301,48 @@ struct System::Impl {
return SystemResultStatus::Success;
}
void LoadOverrides(u64 programId) const {
std::string vendor = gpu_core->Renderer().GetDeviceVendor();
LOG_INFO(Core, "GPU Vendor: {}", vendor);
// Reset all per-game flags
Settings::values.use_squashed_iterated_blend = false;
// Insert PC overrides here
#ifdef ANDROID
// Example on how to set a setting based on the program ID and vendor
if (programId == 0x010028600EBDA000 && vendor == "Mali") { // Mario 3d World
// Settings::values.example = true;
}
// Example array of program IDs
const std::array<u64, 10> example_array = {
//0xprogramId
0x0004000000033400, // Game 1
0x0004000000033500 // Game 2
// And so on
};
for (auto id : example_array) {
if (programId == id) {
// Settings::values.example = true;
break;
}
}
#endif
// Ninja Gaiden Ragebound
constexpr u64 ngr = 0x0100781020710000ULL;
if (programId == ngr) {
LOG_INFO(Core, "Enabling game specifc override: use_squashed_iterated_blend");
Settings::values.use_squashed_iterated_blend = true;
}
}
SystemResultStatus Load(System& system, Frontend::EmuWindow& emu_window,
const std::string& filepath,
Service::AM::FrontendAppletParameters& params) {
@@ -378,8 +428,7 @@ struct System::Impl {
LOG_ERROR(Core, "Failed to find program id for ROM");
}
GameSettings::LoadOverrides(program_id, gpu_core->Renderer());
LoadOverrides(program_id);
if (auto room_member = Network::GetRoomMember().lock()) {
Network::GameInfo game_info;
game_info.name = name;
@@ -428,6 +477,11 @@ struct System::Impl {
stop_event = {};
Network::RestartSocketOperations();
if (legacy_online) {
legacy_online->Stop();
legacy_online.reset();
}
if (auto room_member = Network::GetRoomMember().lock()) {
Network::GameInfo game_info{};
room_member->SendGameInfo(game_info);
@@ -516,6 +570,9 @@ struct System::Impl {
/// Network instance
Network::NetworkInstance network_instance;
/// Legacy Online Service
std::unique_ptr<Network::LegacyOnlineService> legacy_online;
/// Debugger
std::unique_ptr<Core::Debugger> debugger;

View File

@@ -11,6 +11,8 @@
#include "core/core.h"
#include "core/file_sys/savedata_factory.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/sdmc_factory.h"
#include "core/hle/service/filesystem/filesystem.h"
namespace FileSys {
@@ -74,20 +76,97 @@ VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribut
VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
u64 target_program_id = meta.program_id;
// CRITICAL FIX: If the game requests Cache/Temp with ProgramID 0 (generic),
// we MUST redirect it to the actual running TitleID, otherwise it looks in '.../0000000000000000'.
if ((meta.type == SaveDataType::Cache || meta.type == SaveDataType::Temporary) && target_program_id == 0) {
target_program_id = system.GetApplicationProcessProgramID();
LOG_INFO(Service_FS, "Redirecting generic Cache request (ID 0) to active TitleID: {:016X}", target_program_id);
}
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, target_program_id,
meta.user_id, meta.system_save_data_id);
auto out = dir->GetDirectoryRelative(save_directory);
// Fallback for Ryujinx-style cache paths: sdcard/Nintendo/save/{TitleID} (No /cache/ subdir)
// Also include Temporary, as games like Just Dance use Temporary storage for cache-like data (MapBaseCache),
// and Ryujinx stores this in the same simple 'save/{ID}' structure.
// Fallback logic removed. We now enforce standard paths.
// User instructions will direct them to the correct folder.
if (out == nullptr) {
LOG_WARNING(Service_FS, "Cache/Save path NOT FOUND: '{}'. Auto-create={}", save_directory, auto_create);
} else {
LOG_INFO(Service_FS, "Cache/Save path FOUND: '{}'", save_directory);
}
if (out == nullptr && (ShouldSaveDataBeAutomaticallyCreated(space, meta) && auto_create)) {
LOG_INFO(Service_FS, "Auto-creating save directory...");
return Create(space, meta);
}
if (out != nullptr) {
// Some emulators (Ryujinx) or even different firmware versions may rely on the commit
// directories /0 or /1 being present for cache or save data.
// We prioritizing /1 as it usually implies a newer commit if both exist,
// but /0 is what's commonly used by Ryujinx for cache.
// Ryujinx behavior: If 0 exists and 1 does not, copy 0 to 1.
auto dir_0 = out->GetSubdirectory("0");
auto dir_1 = out->GetSubdirectory("1");
if (dir_0) LOG_INFO(Service_FS, "Found subdirectory '0' in save path.");
if (dir_1) LOG_INFO(Service_FS, "Found subdirectory '1' in save path.");
if (dir_0 != nullptr && dir_1 == nullptr) {
LOG_INFO(Service_FS, "Ryujinx structure detected: '0' exists, '1' missing. Copying 0->1 for compatibility.");
dir_1 = out->CreateSubdirectory("1");
if (dir_1 != nullptr) {
// Copy contents from 0 to 1
VfsRawCopyD(dir_0, dir_1);
}
}
// Check for 'Addressables' and 'Addressables2' and delete 'json.cache' if present.
// This is a specific workaround for games (e.g. Just Dance) that freeze if they find old cache metadata.
// We force them to regenerate it.
const auto CleanCache = [](VirtualDir root) {
if (root == nullptr) return;
const char* subdirs[] = {"Addressables", "Addressables2"};
for (const char* subdir_name : subdirs) {
auto subdir = root->GetSubdirectory(subdir_name);
if (subdir != nullptr) {
if (subdir->DeleteFile("json.cache")) {
LOG_INFO(Service_FS, "Deleted stale 'json.cache' in '{}'", subdir_name);
}
}
}
};
if (dir_1 != nullptr) {
LOG_INFO(Service_FS, "Returning subdirectory '1' as Save Root.");
CleanCache(dir_1);
return dir_1;
}
if (dir_0 != nullptr) {
LOG_INFO(Service_FS, "Returning subdirectory '0' as Save Root.");
CleanCache(dir_0);
return dir_0;
}
LOG_INFO(Service_FS, "No '0' or '1' subdirectories found. Returning parent folder as Save Root.");
CleanCache(out);
}
return out;
}
VirtualDir SaveDataFactory::GetSaveDataSpaceDirectory(SaveDataSpaceId space) const {
return dir->GetDirectoryRelative(GetSaveDataSpaceIdPath(space));
const auto path = GetSaveDataSpaceIdPath(space);
// Ensure the directory exists, otherwise FindAllSaves fails.
return GetOrCreateDirectoryRelative(dir, path);
// return dir->GetDirectoryRelative(GetSaveDataSpaceIdPath(space));
}
std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
@@ -96,12 +175,12 @@ std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
return "/system/";
case SaveDataSpaceId::User:
case SaveDataSpaceId::SdUser:
case SaveDataSpaceId::Temporary: // Map into User so we can find the save/ folder
return "/user/";
case SaveDataSpaceId::Temporary:
return "/temp/";
default:
ASSERT_MSG(false, "Unrecognized SaveDataSpaceId: {:02X}", static_cast<u8>(space));
return "/unrecognized/"; ///< To prevent corruption when ignoring asserts.
// ASSERT_MSG(false, "Unrecognized SaveDataSpaceId: {:02X}", static_cast<u8>(space));
LOG_WARNING(Service_FS, "Unrecognized SaveDataSpaceId: {:02X}, defaulting to /user/", static_cast<u8>(space));
return "/user/";
}
}
@@ -137,8 +216,9 @@ std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
title_id);
case SaveDataType::Temporary:
return fmt::format("{}{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
title_id);
// Unified Cache/Temporary Path: Always use save/cache/{TitleID}
// This simplifies user instructions and avoids UUID/Permission issues.
return fmt::format("{}save/cache/{:016X}", out, title_id);
case SaveDataType::Cache:
return fmt::format("{}save/cache/{:016X}", out, title_id);
default:

View File

@@ -686,6 +686,154 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
using EdenPath = Common::FS::EdenPath;
const auto sdmc_dir_path = Common::FS::GetEdenPath(EdenPath::SDMCDir);
const auto sdmc_load_dir_path = sdmc_dir_path / "atmosphere/contents";
// If the NAND user save location doesn't exist but the SDMC contains
// Nintendo/save (common portable save structure), create a host-side
// symlink so the emulator will see those saves under the expected NAND path.
// This helps users who placed saves under `<eden>/user/sdmc/Nintendo/save/...`.
try {
const auto nand_fs_path = Common::FS::GetEdenPath(EdenPath::NANDDir);
const auto sdmc_fs_path = sdmc_dir_path;
const auto nand_user_save_path = nand_fs_path / "user" / "save";
const auto sdmc_nintendo_save_path = sdmc_fs_path / "Nintendo" / "save";
std::error_code ec;
if (!std::filesystem::exists(nand_user_save_path) &&
std::filesystem::exists(sdmc_nintendo_save_path)) {
std::filesystem::create_directory_symlink(sdmc_nintendo_save_path, nand_user_save_path, ec);
if (ec) {
LOG_WARNING(Service_FS, "Could not create symlink {} -> {}: {}",
Common::FS::PathToUTF8String(nand_user_save_path),
Common::FS::PathToUTF8String(sdmc_nintendo_save_path), ec.message());
// Fallback: copy the SDMC saves into the NAND save folder so the emulator can see them
try {
if (!std::filesystem::exists(nand_user_save_path)) {
std::filesystem::create_directories(nand_user_save_path);
}
std::filesystem::copy(sdmc_nintendo_save_path, nand_user_save_path,
std::filesystem::copy_options::recursive |
std::filesystem::copy_options::skip_existing,
ec);
if (ec) {
LOG_WARNING(Service_FS, "Failed to copy SDMC saves to {}: {}",
Common::FS::PathToUTF8String(nand_user_save_path), ec.message());
} else {
LOG_INFO(Service_FS, "Copied SDMC saves to NAND save path: {} -> {}",
Common::FS::PathToUTF8String(sdmc_nintendo_save_path),
Common::FS::PathToUTF8String(nand_user_save_path));
}
} catch (const std::exception& ex) {
LOG_WARNING(Service_FS, "Exception while copying SDMC saves: {}", ex.what());
}
} else {
LOG_INFO(Service_FS, "Linked NAND save path to SDMC saves: {} -> {}",
Common::FS::PathToUTF8String(nand_user_save_path),
Common::FS::PathToUTF8String(sdmc_nintendo_save_path));
}
}
// Also support official-style SD cache directory name used by some Switch setups.
// If users placed saves or cache under `<eden>/user/sdmc/Nintendo/SD_Cache.0000`,
// expose it to the NAND user save path as well so the emulator can find them.
const auto sdmc_nintendo_cache_path = sdmc_fs_path / "Nintendo" / "SD_Cache.0000";
if (!std::filesystem::exists(nand_user_save_path) &&
std::filesystem::exists(sdmc_nintendo_cache_path)) {
std::error_code ec2;
std::filesystem::create_directory_symlink(sdmc_nintendo_cache_path, nand_user_save_path,
ec2);
if (ec2) {
LOG_WARNING(Service_FS,
"Could not create symlink {} -> {}: {}",
Common::FS::PathToUTF8String(nand_user_save_path),
Common::FS::PathToUTF8String(sdmc_nintendo_cache_path), ec2.message());
// Fallback: copy the SDMC cache into the NAND save folder so the emulator can see them
try {
if (!std::filesystem::exists(nand_user_save_path)) {
std::filesystem::create_directories(nand_user_save_path);
}
std::filesystem::copy(sdmc_nintendo_cache_path, nand_user_save_path,
std::filesystem::copy_options::recursive |
std::filesystem::copy_options::skip_existing,
ec2);
if (ec2) {
LOG_WARNING(Service_FS, "Failed to copy SDMC cache to {}: {}",
Common::FS::PathToUTF8String(nand_user_save_path), ec2.message());
} else {
LOG_INFO(Service_FS, "Copied SDMC cache to NAND save path: {} -> {}",
Common::FS::PathToUTF8String(sdmc_nintendo_cache_path),
Common::FS::PathToUTF8String(nand_user_save_path));
}
} catch (const std::exception& ex) {
LOG_WARNING(Service_FS, "Exception while copying SDMC cache: {}", ex.what());
}
} else {
LOG_INFO(Service_FS, "Linked NAND save path to SDMC cache: {} -> {}",
Common::FS::PathToUTF8String(nand_user_save_path),
Common::FS::PathToUTF8String(sdmc_nintendo_cache_path));
}
}
// If the NAND save folder already exists, ensure individual entries from
// SDMC (both `Nintendo/save` and `Nintendo/SD_Cache.0000`) are visible
// inside it: create per-entry symlinks (with copy fallback) for missing
// title/account directories so saves placed in SDMC are reachable.
auto try_merge_sdmc_entries = [&](const std::filesystem::path& sdmc_src) {
try {
if (!std::filesystem::exists(sdmc_src))
return;
std::error_code ec;
if (!std::filesystem::exists(nand_user_save_path))
std::filesystem::create_directories(nand_user_save_path, ec);
for (auto& ent : std::filesystem::directory_iterator(sdmc_src)) {
const auto name = ent.path().filename();
const auto target = nand_user_save_path / name;
if (std::filesystem::exists(target))
continue;
std::error_code ec2;
std::filesystem::create_directory_symlink(ent.path(), target, ec2);
if (ec2) {
LOG_WARNING(Service_FS, "Could not create symlink {} -> {}: {}",
Common::FS::PathToUTF8String(target),
Common::FS::PathToUTF8String(ent.path()), ec2.message());
try {
std::filesystem::copy(ent.path(), target,
std::filesystem::copy_options::recursive |
std::filesystem::copy_options::skip_existing,
ec2);
if (ec2) {
LOG_WARNING(Service_FS, "Failed to copy {} -> {}: {}",
Common::FS::PathToUTF8String(ent.path()),
Common::FS::PathToUTF8String(target), ec2.message());
} else {
LOG_INFO(Service_FS, "Copied SDMC entry to NAND: {} -> {}",
Common::FS::PathToUTF8String(ent.path()),
Common::FS::PathToUTF8String(target));
}
} catch (const std::exception& ex) {
LOG_WARNING(Service_FS, "Exception while copying SDMC entry: {}",
ex.what());
}
} else {
LOG_INFO(Service_FS, "Linked NAND entry to SDMC: {} -> {}",
Common::FS::PathToUTF8String(target),
Common::FS::PathToUTF8String(ent.path()));
}
}
} catch (const std::exception& ex) {
LOG_WARNING(Service_FS, "Exception while merging SDMC entries: {}", ex.what());
}
};
try_merge_sdmc_entries(sdmc_nintendo_save_path);
try_merge_sdmc_entries(sdmc_nintendo_cache_path);
} catch (const std::exception& e) {
LOG_WARNING(Service_FS, "Exception while linking SDMC saves: {}", e.what());
}
const auto rw_mode = FileSys::OpenMode::ReadWrite;
auto nand_directory =

View File

@@ -0,0 +1,164 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "core/internal_network/legacy_online.h"
#include <cstring>
#include <iostream>
#ifdef _WIN32
#include <winsock2.h>
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
#include "common/logging/log.h"
namespace Network {
LegacyOnlineService::LegacyOnlineService() = default;
LegacyOnlineService::~LegacyOnlineService() {
Stop();
}
void LegacyOnlineService::Start() {
if (is_running) {
return;
}
is_running = true;
worker_thread = std::thread(&LegacyOnlineService::ServerLoop, this);
}
void LegacyOnlineService::Stop() {
if (!is_running) {
return;
}
is_running = false;
// Close socket to wake up the thread if it's blocked on recvfrom
if (socket_fd != ~0ULL) {
#ifdef _WIN32
closesocket(static_cast<SOCKET>(socket_fd));
#else
close(static_cast<int>(socket_fd));
#endif
socket_fd = ~0ULL;
}
if (worker_thread.joinable()) {
worker_thread.join();
}
}
void LegacyOnlineService::ServerLoop() {
LOG_INFO(Network, "Starting Legacy Online UDP Server on port {}", PORT);
#ifdef _WIN32
WSADATA wsa_data;
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) {
LOG_ERROR(Network, "WSAStartup failed");
is_running = false;
return;
}
#endif
auto s = socket(AF_INET, SOCK_DGRAM, 0);
#ifdef _WIN32
if (s == INVALID_SOCKET) {
#else
if (s == -1) {
#endif
LOG_ERROR(Network, "Failed to create socket");
is_running = false;
return;
}
socket_fd = static_cast<uintptr_t>(s);
int opt = 1;
#ifdef _WIN32
setsockopt(static_cast<SOCKET>(socket_fd), SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));
#else
setsockopt(static_cast<int>(socket_fd), SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
#endif
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
int res = -1;
#ifdef _WIN32
res = bind(static_cast<SOCKET>(socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
#else
res = bind(static_cast<int>(socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
#endif
if (res < 0) {
LOG_ERROR(Network, "Failed to bind to port {}", PORT);
#ifdef _WIN32
closesocket(static_cast<SOCKET>(socket_fd));
#else
close(static_cast<int>(socket_fd));
#endif
socket_fd = ~0ULL;
is_running = false;
return;
}
LOG_INFO(Network, "Legacy Online Server waiting for messages...");
// Set a timeout for recvfrom so check is_running periodically if not closed via socket
// Alternatively, closing the socket (as done in Stop) will cause recvfrom to return error
char buffer[2048];
while (is_running) {
sockaddr_in client_addr{};
#ifdef _WIN32
int client_len = sizeof(client_addr);
#else
socklen_t client_len = sizeof(client_addr);
#endif
int len = -1;
#ifdef _WIN32
len = recvfrom(static_cast<SOCKET>(socket_fd), buffer, sizeof(buffer), 0, (sockaddr*)&client_addr, &client_len);
#else
len = recvfrom(static_cast<int>(socket_fd), buffer, sizeof(buffer), 0, (sockaddr*)&client_addr, &client_len);
#endif
if (!is_running) break;
if (len > 0) {
// Send ACK
const char* ack_msg = "ACK";
#ifdef _WIN32
sendto(static_cast<SOCKET>(socket_fd), ack_msg, static_cast<int>(strlen(ack_msg)), 0, (sockaddr*)&client_addr, client_len);
#else
sendto(static_cast<int>(socket_fd), ack_msg, strlen(ack_msg), 0, (sockaddr*)&client_addr, client_len);
#endif
} else {
// Error or closed
// If we closed the socket in Stop(), this will likely trigger.
break;
}
}
#ifdef _WIN32
if (socket_fd != ~0ULL) closesocket(static_cast<SOCKET>(socket_fd));
WSACleanup();
#else
if (socket_fd != ~0ULL) close(static_cast<int>(socket_fd));
#endif
socket_fd = ~0ULL;
LOG_INFO(Network, "Legacy Online Server stopped");
}
} // namespace Network

View File

@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <atomic>
#include <cstdint>
#include <memory>
#include <thread>
namespace Network {
class LegacyOnlineService {
public:
LegacyOnlineService();
~LegacyOnlineService();
void Start();
void Stop();
private:
void ServerLoop();
std::atomic_bool is_running{false};
std::thread worker_thread;
uintptr_t socket_fd{~0ULL}; // ~0ULL is approx -1 equivalent for unsigned
static constexpr int PORT = 6000;
};
} // namespace Network

View File

@@ -107,9 +107,19 @@ void Vic::Execute() {
auto output_height{config.output_surface_config.out_surface_height + 1};
output_surface.resize_destructive(output_width * output_height);
// Initialize the surface with the appropriate black pixel
Pixel black_pixel{};
if (config.output_surface_config.out_pixel_format == VideoPixelFormat::Y8__V8U8_N420) {
// Y=0, U=512, V=512 (10-bit), A=0
black_pixel = {0, 512, 512, 0};
} else {
// R=0, G=0, B=0, A=0
black_pixel = {0, 0, 0, 0};
}
std::fill(output_surface.begin(), output_surface.end(), black_pixel);
if (Settings::values.nvdec_emulation.GetValue() == Settings::NvdecEmulation::Off) [[unlikely]] {
// Fill the frame with black, as otherwise they can have random data and be very glitchy.
std::fill(output_surface.begin(), output_surface.end(), Pixel{});
} else {
for (size_t i = 0; i < config.slot_structs.size(); i++) {
auto& slot_config{config.slot_structs[i]};
@@ -122,7 +132,18 @@ void Vic::Execute() {
nvdec_id = frame_queue.VicFindNvdecFdFromOffset(luma_offset);
}
if (auto frame = frame_queue.GetFrame(nvdec_id, luma_offset); frame) {
auto frame = frame_queue.GetFrame(nvdec_id, luma_offset);
if (!frame) {
// We might've failed to find the frame, or the nvdec id is stale/wrong.
// Try to find the nvdec id again.
const s32 new_id = frame_queue.VicFindNvdecFdFromOffset(luma_offset);
if (new_id != -1) {
nvdec_id = new_id;
frame = frame_queue.GetFrame(nvdec_id, luma_offset);
}
}
if (frame) {
if (frame.get()) {
switch (frame->GetPixelFormat()) {
case AV_PIX_FMT_YUV420P:
@@ -170,6 +191,7 @@ void Vic::ReadProgressiveY8__V8U8_N420(const SlotStruct& slot, std::span<const P
}
slot_surface.resize_destructive(out_luma_width * out_luma_height);
std::fill(slot_surface.begin(), slot_surface.end(), Pixel{0, 512, 512, 0});
const auto in_luma_width{(std::min)(frame->GetWidth(), s32(out_luma_width))};
const auto in_luma_height{(std::min)(frame->GetHeight(), s32(out_luma_height))};
@@ -219,6 +241,7 @@ void Vic::ReadInterlacedY8__V8U8_N420(const SlotStruct& slot, std::span<const Pl
const auto out_luma_stride{out_luma_width};
slot_surface.resize_destructive(out_luma_width * out_luma_height);
std::fill(slot_surface.begin(), slot_surface.end(), Pixel{0, 512, 512, 0});
const auto in_luma_width{(std::min)(frame->GetWidth(), s32(out_luma_width))};
[[maybe_unused]] const auto in_luma_height{