Compare commits

..

4 Commits

Author SHA1 Message Date
a71a82d3b7 feat: Add SSL service with OpenSSL, Schannel, and SecureTransport backends, and related internal network and video core components. 2025-12-30 00:55:29 -03:00
bd1d270e97 feat: Introduce new shader environment, OpenGL renderer components, and HLE socket/DNS services with internal network support. 2025-12-30 00:13:48 -03:00
664ff7cc85 2023 Support 2025-12-29 18:47:07 -03:00
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
36 changed files with 984 additions and 152 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,12 @@ struct System::Impl {
stop_event = {};
Network::RestartSocketOperations();
if (legacy_online) {
// Keep legacy_online running for the emulator's lifetime
// legacy_online->Stop();
// legacy_online.reset();
}
if (auto room_member = Network::GetRoomMember().lock()) {
Network::GameInfo game_info{};
room_member->SendGameInfo(game_info);
@@ -516,6 +571,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 {
@@ -55,39 +57,114 @@ std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u
} // Anonymous namespace
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
VirtualDir save_directory_)
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)} {
// Delete all temporary storages
// On hardware, it is expected that temporary storage be empty at first use.
dir->DeleteSubdirectoryRecursive("temp");
}
SaveDataFactory::~SaveDataFactory() = default;
VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
meta.user_id, meta.system_save_data_id);
return dir->CreateDirectoryRelative(save_directory);
}
VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
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, meta.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);
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) {
// User requested removal of auto-copy 0->1 logic.
// We simply don't create/copy '1' if it's missing. We rely on fallback to '0'.
LOG_INFO(Service_FS, "Ryujinx structure detected: '0' exists, '1' missing. Skipping copy (User Request).");
// VfsRawCopyD(dir_0, dir_1); // REMOVED
}
// 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);
}
}
}
};
VirtualDir commit_root = nullptr;
if (dir_1 != nullptr) {
LOG_INFO(Service_FS, "Using subdirectory '1' as Commit Root.");
commit_root = dir_1;
} else if (dir_0 != nullptr) {
LOG_INFO(Service_FS, "Using subdirectory '0' as Commit Root.");
commit_root = dir_0;
} else {
LOG_INFO(Service_FS, "No '0' or '1' subdirectories found. Using parent folder as Commit Root.");
commit_root = out;
}
CleanCache(commit_root);
// Implement SD_Cache.XXXX logic for Cache Storage
if (meta.type == SaveDataType::Cache) {
const std::string sd_cache_name = fmt::format("SD_Cache.{:04}", meta.index);
auto sd_cache_dir = commit_root->GetSubdirectory(sd_cache_name);
if (sd_cache_dir != nullptr) {
LOG_INFO(Service_FS, "Found SD_Cache directory: '{}'", sd_cache_name);
return sd_cache_dir;
} else if (auto_create) {
LOG_INFO(Service_FS, "Auto-creating SD_Cache directory: '{}'", sd_cache_name);
return commit_root->CreateSubdirectory(sd_cache_name);
} else {
LOG_WARNING(Service_FS, "SD_Cache directory '{}' not found in commit root.", sd_cache_name);
// Fallback? Or return nullptr?
// If the user strictly wants SD_Cache, we should probably return nullptr if not found/created.
// But for now, returning the commit_root might be safer for legacy compatibility IF SD_Cache isn't strictly enforced for existing saves?
// User said "SD_Cache now represents which CacheStorage it will be". This implies correct behavior is to use SD_Cache.
// If we return commit_root, it's CacheStorage_0 (conceptually) or just "everything".
// Let's assume we return nullptr if not found, to trigger creation logic or error.
return nullptr;
}
}
return commit_root;
}
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 +173,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 +214,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:
@@ -147,6 +225,40 @@ std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
}
}
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
VirtualDir save_directory_)
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)} {
// Delete all temporary storages
// On hardware, it is expected that temporary storage be empty at first use.
dir->DeleteSubdirectoryRecursive("temp");
}
SaveDataFactory::~SaveDataFactory() = default;
VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
meta.user_id, meta.system_save_data_id);
auto created_dir = dir->CreateDirectoryRelative(save_directory);
// For Cache storage, enforce the new hierarchy: .../1/SD_Cache.XXXX
if (meta.type == SaveDataType::Cache && created_dir != nullptr) {
// Ensure commit directory '1' exists
auto commit_dir = created_dir->GetSubdirectory("1");
if (commit_dir == nullptr) {
commit_dir = created_dir->CreateSubdirectory("1");
}
if (commit_dir != nullptr) {
// Create SD_Cache.XXXX
const std::string sd_cache_name = fmt::format("SD_Cache.{:04}", meta.index);
return commit_dir->CreateSubdirectory(sd_cache_name);
}
}
return created_dir;
}
std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future) {
if (future) {
Common::UUID uuid;

View File

@@ -686,6 +686,16 @@ 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/...`.
// SDMC to NAND sync logic REMOVED as per user request.
// The emulator will no longer attempt to symlink or copy "Nintendo/save" or "SD_Cache.0000"
// from SDMC to the NAND user save directory.
// Users must ensure their save/cache structure is valid within the NAND directory itself
// if that is what they intend to use, or rely on the game creating it.
const auto rw_mode = FileSys::OpenMode::ReadWrite;
auto nand_directory =

View File

@@ -14,7 +14,7 @@ namespace Service::FileSystem {
ISaveDataInfoReader::ISaveDataInfoReader(Core::System& system_,
std::shared_ptr<SaveDataController> save_data_controller_,
FileSys::SaveDataSpaceId space)
FileSys::SaveDataSpaceId space, bool cache_only)
: ServiceFramework{system_, "ISaveDataInfoReader"}, save_data_controller{
save_data_controller_} {
static const FunctionInfo functions[] = {
@@ -22,7 +22,7 @@ ISaveDataInfoReader::ISaveDataInfoReader(Core::System& system_,
};
RegisterHandlers(functions);
FindAllSaves(space);
FindAllSaves(space, cache_only);
}
ISaveDataInfoReader::~ISaveDataInfoReader() = default;
@@ -63,7 +63,7 @@ Result ISaveDataInfoReader::ReadSaveDataInfo(
R_SUCCEED();
}
void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space) {
void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space, bool cache_only) {
FileSys::VirtualDir save_root{};
const auto result = save_data_controller->OpenSaveDataSpace(&save_root, space);
@@ -74,8 +74,12 @@ void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space) {
for (const auto& type : save_root->GetSubdirectories()) {
if (type->GetName() == "save") {
FindNormalSaves(space, type);
} else if (space == FileSys::SaveDataSpaceId::Temporary) {
if (cache_only) {
FindCacheSaves(space, type);
} else {
FindNormalSaves(space, type);
}
} else if (space == FileSys::SaveDataSpaceId::Temporary && !cache_only) {
FindTemporaryStorageSaves(space, type);
}
}
@@ -84,6 +88,11 @@ void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space) {
void ISaveDataInfoReader::FindNormalSaves(FileSys::SaveDataSpaceId space,
const FileSys::VirtualDir& type) {
for (const auto& save_id : type->GetSubdirectories()) {
// Skip cache directory in normal scans
if (save_id->GetName() == "cache") {
continue;
}
for (const auto& user_id : save_id->GetSubdirectories()) {
// Skip non user id subdirectories
if (user_id->GetName().size() != 0x20) {
@@ -132,6 +141,96 @@ void ISaveDataInfoReader::FindNormalSaves(FileSys::SaveDataSpaceId space,
}
}
void ISaveDataInfoReader::FindCacheSaves(FileSys::SaveDataSpaceId space,
const FileSys::VirtualDir& type) {
const auto cache_dir = type->GetSubdirectory("cache");
if (cache_dir == nullptr) {
return;
}
for (const auto& title_id_dir : cache_dir->GetSubdirectories()) {
const auto title_id = stoull_be(title_id_dir->GetName());
// Simple validation: TitleID should be non-zero
if (title_id == 0) {
continue;
}
// Determine commit root (priority to "1", then "0", then self)
auto commit_root = title_id_dir->GetSubdirectory("1");
if (commit_root == nullptr) {
commit_root = title_id_dir->GetSubdirectory("0");
}
// If neither exists, we might fall back to title_id_dir itself if users put SD_Cache directly there,
// but based on SaveDataFactory we expect it inside 0 or 1.
if (commit_root == nullptr) {
// Check if SD_Cache exists directly in title_dir (legacy/fallback)
bool has_sd_cache_root = false;
for (const auto& sub : title_id_dir->GetSubdirectories()) {
if (sub->GetName().find("SD_Cache.") == 0) {
has_sd_cache_root = true;
break;
}
}
if (has_sd_cache_root) {
commit_root = title_id_dir;
} else {
continue; // No valid storage found
}
}
bool found_any = false;
for (const auto& sd_cache_dir : commit_root->GetSubdirectories()) {
const std::string& name = sd_cache_dir->GetName();
if (name.find("SD_Cache.") == 0) {
// Parse index from "SD_Cache.XXXX"
u64 index = 0;
try {
if (name.size() > 9) {
index = std::stoull(name.substr(9));
}
} catch(...) {
continue;
}
info.emplace_back(SaveDataInfo{
0,
space,
FileSys::SaveDataType::Cache,
{}, // padding 0x6
{}, // user_id (empty array match)
0, // save_id
title_id,
sd_cache_dir->GetSize(),
static_cast<u16>(index), // Correct index with cast
FileSys::SaveDataRank::Primary,
{}, // padding 0x25
});
found_any = true;
}
}
// Fallback for legacy "flat" cache if no SD_Cache folders found?
// If the user specific structure IS enforced, maybe we don't fallback.
// But if they have existing cache without the folder, it is effectively index 0.
if (!found_any) {
// Treat the entire commit_root as index 0 (Legacy behavior)
info.emplace_back(SaveDataInfo{
0,
space,
FileSys::SaveDataType::Cache,
{}, // padding 0x6
{}, // user_id (empty array match)
0, // save_id
title_id,
commit_root->GetSize(),
0, // index 0
FileSys::SaveDataRank::Primary,
{}, // padding 0x25
});
}
}
}
void ISaveDataInfoReader::FindTemporaryStorageSaves(FileSys::SaveDataSpaceId space,
const FileSys::VirtualDir& type) {
for (const auto& user_id : type->GetSubdirectories()) {

View File

@@ -16,7 +16,7 @@ class ISaveDataInfoReader final : public ServiceFramework<ISaveDataInfoReader> {
public:
explicit ISaveDataInfoReader(Core::System& system_,
std::shared_ptr<SaveDataController> save_data_controller_,
FileSys::SaveDataSpaceId space);
FileSys::SaveDataSpaceId space, bool cache_only = false);
~ISaveDataInfoReader() override;
struct SaveDataInfo {
@@ -38,8 +38,9 @@ public:
OutArray<SaveDataInfo, BufferAttr_HipcMapAlias> out_entries);
private:
void FindAllSaves(FileSys::SaveDataSpaceId space);
void FindAllSaves(FileSys::SaveDataSpaceId space, bool cache_only);
void FindNormalSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
void FindCacheSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
void FindTemporaryStorageSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
std::shared_ptr<SaveDataController> save_data_controller;

View File

@@ -353,10 +353,10 @@ Result FSP_SRV::OpenSaveDataInfoReaderBySaveDataSpaceId(
Result FSP_SRV::OpenSaveDataInfoReaderOnlyCacheStorage(
OutInterface<ISaveDataInfoReader> out_interface) {
LOG_WARNING(Service_FS, "(STUBBED) called");
LOG_DEBUG(Service_FS, "called");
*out_interface = std::make_shared<ISaveDataInfoReader>(system, save_data_controller,
FileSys::SaveDataSpaceId::Temporary);
FileSys::SaveDataSpaceId::User, true);
R_SUCCEED();
}

View File

@@ -42,8 +42,16 @@ bool SessionRequestManager::HasSessionRequestHandler(const HLERequestContext& co
const auto& message_header = context.GetDomainMessageHeader();
const auto object_id = message_header.object_id;
// Some games send magic numbers in the object_id field, which indicates
// this is not actually a proper domain request
const u32 sfci_magic = Common::MakeMagic('S', 'F', 'C', 'I');
const u32 sfco_magic = Common::MakeMagic('S', 'F', 'C', 'O');
if (object_id == sfci_magic || object_id == sfco_magic) {
// This is not a domain request, treat it as a regular session request
return session_handler != nullptr;
}
if (object_id > DomainHandlerCount()) {
LOG_CRITICAL(IPC, "object_id {} is too big!", object_id);
return false;
}
return !DomainHandler(object_id - 1).expired();
@@ -91,7 +99,18 @@ Result SessionRequestManager::HandleDomainSyncRequest(Kernel::KServerSession* se
// If there is a DomainMessageHeader, then this is CommandType "Request"
const auto& domain_message_header = context.GetDomainMessageHeader();
const u32 object_id{domain_message_header.object_id};
const u32 object_id = domain_message_header.object_id;
// Some games send magic numbers in the object_id field
const u32 sfci_magic = Common::MakeMagic('S', 'F', 'C', 'I');
const u32 sfco_magic = Common::MakeMagic('S', 'F', 'C', 'O');
if (object_id == sfci_magic || object_id == sfco_magic) {
// This is not a domain request, handle as regular session request
LOG_DEBUG(IPC, "Detected magic number 0x{:08X} in object_id, treating as regular session request. Command={}",
object_id, context.GetCommand());
return session_handler->HandleSyncRequest(*server_session, context);
}
switch (domain_message_header.command) {
case IPC::DomainMessageHeader::CommandType::SendMessage:
if (object_id > this->DomainHandlerCount()) {
@@ -200,7 +219,17 @@ void HLERequestContext::ParseCommandBuffer(u32_le* src_cmdbuf, bool incoming) {
// If this is an incoming message, only CommandType "Request" has a domain header
// All outgoing domain messages have the domain header, if only incoming has it
if (incoming || domain_message_header) {
domain_message_header = rp.PopRaw<IPC::DomainMessageHeader>();
// Check if the next value is actually a magic number (SFCI/SFCO)
// Some games send these in the object_id field, indicating this is not a domain request
const u32 possible_object_id = src_cmdbuf[rp.GetCurrentOffset() + 1]; // object_id is second field
const u32 sfci_magic = Common::MakeMagic('S', 'F', 'C', 'I');
const u32 sfco_magic = Common::MakeMagic('S', 'F', 'C', 'O');
if (possible_object_id != sfci_magic && possible_object_id != sfco_magic) {
// This is a proper domain request
domain_message_header = rp.PopRaw<IPC::DomainMessageHeader>();
}
// If it's a magic number, skip reading the domain header entirely
} else {
if (GetManager()->IsDomain()) {
LOG_WARNING(IPC, "Domain request has no DomainMessageHeader!");
@@ -220,9 +249,15 @@ void HLERequestContext::ParseCommandBuffer(u32_le* src_cmdbuf, bool incoming) {
}
if (incoming) {
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'I'));
// Only check magic if we have a valid data payload header
// When domain header is skipped (SFCI in object_id), the structure is different
if (domain_message_header) {
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'I'));
}
} else {
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'O'));
if (domain_message_header) {
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'O'));
}
}
}

View File

@@ -923,7 +923,10 @@ std::pair<s32, Errno> BSD::SendImpl(s32 fd, u32 flags, std::span<const u8> messa
if (!IsFileDescriptorValid(fd)) {
return {-1, Errno::BADF};
}
return Translate(file_descriptors[fd]->socket->Send(message, flags));
LOG_CRITICAL(Service, "SendImpl called: fd={}, size={} bytes, flags={}", fd, message.size(), flags);
auto result = Translate(file_descriptors[fd]->socket->Send(message, flags));
LOG_CRITICAL(Service, "SendImpl result: sent={} bytes, errno={}", result.first, static_cast<int>(result.second));
return result;
}
std::pair<s32, Errno> BSD::SendToImpl(s32 fd, u32 flags, std::span<const u8> message,

View File

@@ -60,12 +60,29 @@ NSD::NSD(Core::System& system_, const char* name) : ServiceFramework{system_, na
RegisterHandlers(functions);
}
static const std::vector<std::pair<std::string, std::string>> redirectionRules = {
{"public-ubiservices.com", "jdlo.ovosimpatico.com"},
{"public-ubiservices.ubi.com", "jdlo.ovosimpatico.com"},
};
static std::string GetRedirectedHost(const std::string& host) {
for (const auto& rule : redirectionRules) {
if (host.find(rule.first) != std::string::npos) {
LOG_INFO(Service, "Redirecting NSD host '{}' to '{}'", host, rule.second);
return rule.second;
}
}
return host;
}
static std::string ResolveImpl(const std::string& fqdn_in) {
// The real implementation makes various substitutions.
// For now we just return the string as-is, which is good enough when not
// connecting to real Nintendo servers.
LOG_WARNING(Service, "(STUBBED) called, fqdn_in={}", fqdn_in);
return fqdn_in;
return GetRedirectedHost(fqdn_in);
}
static Result ResolveCommon(const std::string& fqdn_in, std::array<char, 0x100>& fqdn_out) {

View File

@@ -29,13 +29,13 @@ SFDNSRES::SFDNSRES(Core::System& system_) : ServiceFramework{system_, "sfdnsres"
{4, nullptr, "GetHostStringErrorRequest"},
{5, &SFDNSRES::GetGaiStringErrorRequest, "GetGaiStringErrorRequest"},
{6, &SFDNSRES::GetAddrInfoRequest, "GetAddrInfoRequest"},
{7, nullptr, "GetNameInfoRequest"},
{8, nullptr, "RequestCancelHandleRequest"},
{7, &SFDNSRES::GetNameInfoRequest, "GetNameInfoRequest"},
{8, &SFDNSRES::RequestCancelHandleRequest, "RequestCancelHandleRequest"},
{9, nullptr, "CancelRequest"},
{10, &SFDNSRES::GetHostByNameRequestWithOptions, "GetHostByNameRequestWithOptions"},
{11, nullptr, "GetHostByAddrRequestWithOptions"},
{12, &SFDNSRES::GetAddrInfoRequestWithOptions, "GetAddrInfoRequestWithOptions"},
{13, nullptr, "GetNameInfoRequestWithOptions"},
{13, &SFDNSRES::GetNameInfoRequestWithOptions, "GetNameInfoRequestWithOptions"},
{14, &SFDNSRES::ResolverSetOptionRequest, "ResolverSetOptionRequest"},
{15, nullptr, "ResolverGetOptionRequest"},
};
@@ -66,6 +66,21 @@ static bool IsBlockedHost(const std::string& host) {
[&host](const std::string& domain) { return host.find(domain) != std::string::npos; });
}
static const std::vector<std::pair<std::string, std::string>> redirectionRules = {
{"public-ubiservices.com", "jdlo.ovosimpatico.com"},
{"public-ubiservices.ubi.com", "jdlo.ovosimpatico.com"},
};
static std::string GetRedirectedHost(const std::string& host) {
for (const auto& rule : redirectionRules) {
if (host.find(rule.first) != std::string::npos) {
LOG_INFO(Service, "Redirecting host '{}' to '{}'", host, rule.second);
return rule.second;
}
}
return host;
}
static NetDbError GetAddrInfoErrorToNetDbError(GetAddrInfoError result) {
// These combinations have been verified on console (but are not
// exhaustive).
@@ -163,7 +178,8 @@ static std::pair<u32, GetAddrInfoError> GetHostByNameRequestImpl(HLERequestConte
parameters.use_nsd_resolve, parameters.cancel_handle, parameters.process_id);
const auto host_buffer = ctx.ReadBuffer(0);
const std::string host = Common::StringFromBuffer(host_buffer);
std::string host = Common::StringFromBuffer(host_buffer);
host = GetRedirectedHost(host);
// For now, ignore options, which are in input buffer 1 for GetHostByNameRequestWithOptions.
// Prevent resolution of Nintendo servers
@@ -281,7 +297,8 @@ static std::pair<u32, GetAddrInfoError> GetAddrInfoRequestImpl(HLERequestContext
// before looking up.
const auto host_buffer = ctx.ReadBuffer(0);
const std::string host = Common::StringFromBuffer(host_buffer);
std::string host = Common::StringFromBuffer(host_buffer);
host = GetRedirectedHost(host);
// Prevent resolution of Nintendo servers
if (IsBlockedHost(host)) {
@@ -364,6 +381,120 @@ void SFDNSRES::GetAddrInfoRequestWithOptions(HLERequestContext& ctx) {
});
}
void SFDNSRES::GetNameInfoRequest(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto addr_in = rp.PopRaw<SockAddrIn>();
const u32 flags = rp.Pop<u32>();
const u32 cancel_handle = rp.Pop<u32>();
const u64 process_id = rp.Pop<u64>();
LOG_DEBUG(Service, "called. flags={}, cancel_handle={}, process_id={}", flags, cancel_handle,
process_id);
struct OutputParameters {
u32 data_size;
GetAddrInfoError gai_error;
NetDbError netdb_error;
Errno bsd_errno;
};
static_assert(sizeof(OutputParameters) == 0x10);
const auto res = Network::GetNameInfo(Translate(addr_in));
if (res.second != 0) {
const auto network_error = Network::TranslateGetAddrInfoErrorFromNative(res.second);
const auto service_error = Translate(network_error);
IPC::ResponseBuilder rb{ctx, 6};
rb.Push(ResultSuccess);
rb.PushRaw(OutputParameters{
.data_size = 0,
.gai_error = service_error,
.netdb_error = GetAddrInfoErrorToNetDbError(service_error),
.bsd_errno = GetAddrInfoErrorToErrno(service_error),
});
return;
}
const std::string& host = res.first;
const u32 data_size = static_cast<u32>(host.size() + 1);
ctx.WriteBuffer(host.data(), data_size, 0);
IPC::ResponseBuilder rb{ctx, 6};
rb.Push(ResultSuccess);
rb.PushRaw(OutputParameters{
.data_size = data_size,
.gai_error = GetAddrInfoError::SUCCESS,
.netdb_error = NetDbError::Success,
.bsd_errno = Errno::SUCCESS,
});
}
void SFDNSRES::GetNameInfoRequestWithOptions(HLERequestContext& ctx) {
struct InputParameters {
u32 flags;
u32 interface_index;
u64 process_id;
u32 padding; // 0x14 + 4 = 0x18? No. 0x14 aligned to 8 bytes?
// Wait, sizeof(InputParameters) == 0x14.
};
// Derived from partial snippets:
// u32 flags, u32 interface_index, u64 process_id.
// 4 + 4 + 8 = 16 bytes (0x10).
// The previous prompt had static_assert size 0x14.
// Maybe a u32 padding?
// Let's rely on standard layout.
// I will use manual popping for safety if struct definition is unknown.
IPC::RequestParser rp{ctx};
const auto addr_in = rp.PopRaw<SockAddrIn>();
const u32 flags = rp.Pop<u32>();
const u32 interface_index = rp.Pop<u32>();
const u64 process_id = rp.Pop<u64>();
(void)flags;
(void)interface_index;
(void)process_id;
// If there was padding, it might be implicitly popped or ignored.
struct OutputParameters {
u32 data_size;
GetAddrInfoError gai_error;
NetDbError netdb_error;
Errno bsd_errno;
};
static_assert(sizeof(OutputParameters) == 0x10);
const auto res = Network::GetNameInfo(Translate(addr_in));
if (res.second != 0) {
const auto network_error = Network::TranslateGetAddrInfoErrorFromNative(res.second);
const auto service_error = Translate(network_error);
IPC::ResponseBuilder rb{ctx, 6};
rb.Push(ResultSuccess);
rb.PushRaw(OutputParameters{
.data_size = 0,
.gai_error = service_error,
.netdb_error = GetAddrInfoErrorToNetDbError(service_error),
.bsd_errno = GetAddrInfoErrorToErrno(service_error),
});
return;
}
const std::string& host = res.first;
const u32 data_size = static_cast<u32>(host.size() + 1);
ctx.WriteBuffer(host.data(), data_size, 0);
IPC::ResponseBuilder rb{ctx, 6};
rb.Push(ResultSuccess);
rb.PushRaw(OutputParameters{
.data_size = data_size,
.gai_error = GetAddrInfoError::SUCCESS,
.netdb_error = NetDbError::Success,
.bsd_errno = Errno::SUCCESS,
});
}
void SFDNSRES::ResolverSetOptionRequest(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
@@ -372,4 +503,24 @@ void SFDNSRES::ResolverSetOptionRequest(HLERequestContext& ctx) {
rb.Push(ResultSuccess);
rb.Push<s32>(0); // bsd errno
}
void SFDNSRES::RequestCancelHandleRequest(HLERequestContext& ctx) {
// This is just a stub for now.
// In a real implementation this would likely cancel a pending request represented by the handle.
LOG_WARNING(Service, "(STUBBED) called");
struct InputParameters {
u32 cancel_handle;
};
IPC::RequestParser rp{ctx};
auto input = rp.PopRaw<InputParameters>();
LOG_DEBUG(Service, "cancel_handle={}", input.cancel_handle);
IPC::ResponseBuilder rb{ctx, 4};
rb.Push(ResultSuccess);
rb.Push<u32>(0); // cancel_handle (response seems to echo it or return a new one? usually just an error code or unrelated)
// Actually based on typical patterns, it probably returns an error code (bsd_errno).
rb.Push<s32>(0); // bsd_errno
}
} // namespace Service::Sockets

View File

@@ -22,7 +22,10 @@ private:
void GetHostByNameRequestWithOptions(HLERequestContext& ctx);
void GetAddrInfoRequest(HLERequestContext& ctx);
void GetAddrInfoRequestWithOptions(HLERequestContext& ctx);
void GetNameInfoRequest(HLERequestContext& ctx);
void GetNameInfoRequestWithOptions(HLERequestContext& ctx);
void ResolverSetOptionRequest(HLERequestContext& ctx);
void RequestCancelHandleRequest(HLERequestContext& ctx);
};
} // namespace Service::Sockets

View File

@@ -16,10 +16,12 @@ enum class Errno : u32 {
SUCCESS = 0,
BADF = 9,
AGAIN = 11,
ACCES = 13,
INVAL = 22,
MFILE = 24,
PIPE = 32,
MSGSIZE = 90,
ADDRINUSE = 98,
CONNABORTED = 103,
CONNRESET = 104,
NOTCONN = 107,

View File

@@ -37,6 +37,10 @@ Errno Translate(Network::Errno value) {
return Errno::CONNRESET;
case Network::Errno::INPROGRESS:
return Errno::INPROGRESS;
case Network::Errno::ACCES:
return Errno::ACCES;
case Network::Errno::ADDRINUSE:
return Errno::ADDRINUSE;
default:
UNIMPLEMENTED_MSG("Unimplemented errno={}", value);
return Errno::SUCCESS;
@@ -261,7 +265,9 @@ PollEvents Translate(Network::PollEvents flags) {
Network::SockAddrIn Translate(SockAddrIn value) {
// Note: 6 is incorrect, but can be passed by homebrew (because libnx sets
// sin_len to 6 when deserializing getaddrinfo results).
ASSERT(value.len == 0 || value.len == sizeof(value) || value.len == 6);
if (value.len != 0 && value.len != sizeof(value) && value.len != 6) {
LOG_WARNING(Service, "Unexpected SockAddrIn length={}", value.len);
}
return {
.family = Translate(static_cast<Domain>(value.family)),

View File

@@ -117,15 +117,20 @@ public:
RegisterHandlers(functions);
shared_data->connection_count++;
LOG_CRITICAL(Service_SSL, "ISslConnection created! Total connections: {}", shared_data->connection_count);
}
~ISslConnection() {
shared_data->connection_count--;
if (fd_to_close.has_value()) {
const s32 fd = *fd_to_close;
if (!do_not_close_socket) {
LOG_ERROR(Service_SSL,
"do_not_close_socket was changed after setting socket; is this right?");
if (do_not_close_socket) {
// If we aren't supposed to close the socket, but we have an fd_to_close,
// that means the configuration changed after we took ownership.
// This is weird but we should probably honor the flag.
// However, the original valid logic seemed to imply we duped the socket
// and should close our dup... but let's stick to what the flag says.
LOG_INFO(Service_SSL, "do_not_close_socket is true, skipping close of fd {}", fd);
} else {
auto bsd = system.ServiceManager().GetService<Service::Sockets::BSD>("bsd:u");
if (bsd) {
@@ -269,16 +274,17 @@ private:
}
Result PendingImpl(s32* out_pending) {
LOG_WARNING(Service_SSL, "(STUBBED) called.");
*out_pending = 0;
return ResultSuccess;
ASSERT_OR_EXECUTE(did_handshake, { return ResultInternalError; });
return backend->Pending(out_pending);
}
void SetSocketDescriptor(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const s32 in_fd = rp.Pop<s32>();
LOG_CRITICAL(Service_SSL, "SetSocketDescriptor called with fd={}", in_fd);
s32 out_fd{-1};
const Result res = SetSocketDescriptorImpl(&out_fd, in_fd);
LOG_CRITICAL(Service_SSL, "SetSocketDescriptor result: res={}, out_fd={}", res.raw, out_fd);
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(res);
rb.Push<s32>(out_fd);
@@ -308,7 +314,9 @@ private:
}
void DoHandshake(HLERequestContext& ctx) {
LOG_INFO(Service_SSL, "DoHandshake called, socket={}", socket != nullptr);
const Result res = DoHandshakeImpl();
LOG_INFO(Service_SSL, "DoHandshake result: {}, did_handshake={}", res.raw, did_handshake);
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(res);
}

View File

@@ -38,6 +38,7 @@ public:
virtual Result Read(size_t* out_size, std::span<u8> data) = 0;
virtual Result Write(size_t* out_size, std::span<const u8> data) = 0;
virtual Result GetServerCerts(std::vector<std::vector<u8>>* out_certs) = 0;
virtual Result Pending(s32* out_pending) = 0;
};
Result CreateSSLConnectionBackend(std::unique_ptr<SSLConnectionBackend>* out_backend);

View File

@@ -115,6 +115,22 @@ inline void LoadCaCertStore(SSL_CTX* ctx, const char* ca_cert, std::size_t size)
}
#endif
static const std::vector<std::pair<std::string, std::string>> redirectionRules = {
{"public-ubiservices.com", "jdlo.ovosimpatico.com"},
{"public-ubiservices.ubi.com", "jdlo.ovosimpatico.com"},
};
static std::string GetRedirectedHost(const std::string& host) {
for (const auto& rule : redirectionRules) {
if (host.find(rule.first) != std::string::npos) {
LOG_INFO(Service_SSL, "Redirecting SSL host '{}' to '{}'", host, rule.second);
return rule.second;
}
}
return host;
}
} // namespace
class SSLConnectionBackendOpenSSL final : public SSLConnectionBackend {
@@ -157,7 +173,8 @@ public:
socket = std::move(socket_in);
}
Result SetHostName(const std::string& hostname) override {
Result SetHostName(const std::string& hostname_in) override {
const std::string hostname = GetRedirectedHost(hostname_in);
if (!SSL_set1_host(ssl, hostname.c_str())) { // hostname for verification
LOG_ERROR(Service_SSL, "SSL_set1_host({}) failed", hostname);
return CheckOpenSSLErrors();
@@ -247,6 +264,28 @@ public:
return ResultSuccess;
}
Result Pending(s32* out_pending) override {
if (!ssl) {
return ResultInternalError;
}
int pending = SSL_pending(ssl);
if (pending > 0) {
*out_pending = pending;
return ResultSuccess;
}
Network::PollFD poll_fd{socket.get(), Network::PollEvents::In, Network::PollEvents::In};
std::vector<Network::PollFD> poll_fds{poll_fd};
auto [count, err] = Network::Poll(poll_fds, 0);
if (count > 0 && (poll_fds[0].revents & Network::PollEvents::In) != Network::PollEvents{}) {
*out_pending = 1;
} else {
*out_pending = 0;
}
return ResultSuccess;
}
~SSLConnectionBackendOpenSSL() {
// this is null-tolerant:
SSL_free(ssl);

View File

@@ -489,6 +489,27 @@ public:
return ResultSuccess;
}
Result Pending(s32* out_pending) override {
*out_pending = static_cast<s32>(cleartext_read_buf.size());
if (*out_pending > 0) {
return ResultSuccess;
}
if (!ciphertext_read_buf.empty()) {
*out_pending = 1;
return ResultSuccess;
}
Network::PollFD poll_fd{socket.get(), Network::PollEvents::In, Network::PollEvents::In};
std::vector<Network::PollFD> poll_fds{poll_fd};
auto [count, err] = Network::Poll(poll_fds, 0);
if (count > 0 && (poll_fds[0].revents & Network::PollEvents::In) != Network::PollEvents{}) {
*out_pending = 1;
}
return ResultSuccess;
}
~SSLConnectionBackendSchannel() {
if (handshake_state != HandshakeState::Initial) {
DeleteSecurityContext(&ctxt);

View File

@@ -149,6 +149,26 @@ public:
return ResultSuccess;
}
Result Pending(s32* out_pending) override {
size_t bufferSize = 0;
OSStatus status = SSLGetBufferedReadSize(context, &bufferSize);
if (status == 0 && bufferSize > 0) {
*out_pending = static_cast<s32>(bufferSize);
return ResultSuccess;
}
Network::PollFD poll_fd{socket.get(), Network::PollEvents::In, Network::PollEvents::In};
std::vector<Network::PollFD> poll_fds{poll_fd};
auto [count, err] = Network::Poll(poll_fds, 0);
if (count > 0 && (poll_fds[0].revents & Network::PollEvents::In) != Network::PollEvents{}) {
*out_pending = 1;
} else {
*out_pending = 0;
}
return ResultSuccess;
}
static OSStatus ReadCallback(SSLConnectionRef connection, void* data, size_t* dataLength) {
return ReadOrWriteCallback(connection, data, dataLength, true);
}

View File

@@ -7,7 +7,7 @@
#include "core/internal_network/network_interface.h"
#ifdef _WIN32
#define NOMINMAX
#include <windows.h>
#include <wlanapi.h>
#ifdef _MSC_VER

View File

@@ -0,0 +1,180 @@
// 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() {
#ifdef _WIN32
WSADATA wsa_data;
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) {
LOG_ERROR(Network, "WSAStartup failed with error: {}", WSAGetLastError());
winsock_initialized = false;
} else {
winsock_initialized = true;
}
#endif
}
LegacyOnlineService::~LegacyOnlineService() {
Stop();
#ifdef _WIN32
if (winsock_initialized) {
WSACleanup();
}
#endif
}
void LegacyOnlineService::Start() {
if (is_running) {
return;
}
#ifdef _WIN32
if (!winsock_initialized) {
LOG_ERROR(Network, "Cannot start Legacy Online Service: Winsock not initialized");
return;
}
#endif
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);
// WSAStartup is now handled in the constructor
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) {
#ifdef _WIN32
LOG_ERROR(Network, "Failed to bind to port {}: {}", PORT, WSAGetLastError());
closesocket(static_cast<SOCKET>(socket_fd));
#else
LOG_ERROR(Network, "Failed to bind to port {}: {}", PORT, strerror(errno));
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 is now handled in the destructor
#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,31 @@
// 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
bool winsock_initialized{false};
static constexpr int PORT = 6000;
};
} // namespace Network

View File

@@ -75,6 +75,8 @@ SOCKET GetInterruptSocket() {
return interrupt_socket;
}
} // namespace
sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
sockaddr_in result;
@@ -83,6 +85,7 @@ sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
#endif
switch (static_cast<Domain>(input.family)) {
case Domain::Unspecified:
case Domain::INET:
result.sin_family = AF_INET;
break;
@@ -105,6 +108,8 @@ sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
return addr;
}
namespace {
LINGER MakeLinger(bool enable, u32 linger_value) {
ASSERT(linger_value <= (std::numeric_limits<u_short>::max)());
@@ -124,6 +129,7 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
case 0:
return Errno::SUCCESS;
case WSAEBADF:
case WSAENOTSOCK:
return Errno::BADF;
case WSAEINVAL:
return Errno::INVAL;
@@ -157,6 +163,10 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
return Errno::TIMEDOUT;
case WSAEINPROGRESS:
return Errno::INPROGRESS;
case WSAEACCES:
return Errno::ACCES;
case WSAEADDRINUSE:
return Errno::ADDRINUSE;
default:
UNIMPLEMENTED_MSG("Unimplemented errno={}", e);
return Errno::OTHER;
@@ -212,6 +222,8 @@ SOCKET GetInterruptSocket() {
return interrupt_pipe_fd[0];
}
} // namespace
sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
sockaddr_in result;
@@ -234,6 +246,8 @@ sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
return addr;
}
namespace {
int WSAPoll(WSAPOLLFD* fds, ULONG nfds, int timeout) {
return poll(fds, static_cast<nfds_t>(nfds), timeout);
}
@@ -320,6 +334,8 @@ Errno GetAndLogLastError(CallType call_type = CallType::Other) {
return err;
}
} // namespace
GetAddrInfoError TranslateGetAddrInfoErrorFromNative(int gai_err) {
switch (gai_err) {
case 0:
@@ -373,6 +389,8 @@ GetAddrInfoError TranslateGetAddrInfoErrorFromNative(int gai_err) {
}
}
namespace {
Domain TranslateDomainFromNative(int domain) {
switch (domain) {
case 0:
@@ -607,6 +625,8 @@ Common::Expected<std::vector<AddrInfo>, GetAddrInfoError> GetAddressInfo(
return ret;
}
std::pair<s32, Errno> Poll(std::vector<PollFD>& pollfds, s32 timeout) {
const size_t num = pollfds.size();
@@ -871,8 +891,13 @@ std::pair<s32, Errno> Socket::SendTo(u32 flags, std::span<const u8> message,
}
Errno Socket::Close() {
if (fd == INVALID_SOCKET) {
return Errno::SUCCESS;
}
[[maybe_unused]] const int result = closesocket(fd);
ASSERT(result == 0);
if (result != 0) {
GetAndLogLastError();
}
fd = INVALID_SOCKET;
return Errno::SUCCESS;

View File

@@ -48,6 +48,8 @@ enum class Errno {
TIMEDOUT,
MSGSIZE,
INPROGRESS,
ACCES,
ADDRINUSE,
OTHER,
};
@@ -122,8 +124,33 @@ std::optional<IPv4Address> GetHostIPv4Address();
std::string IPv4AddressToString(IPv4Address ip_addr);
u32 IPv4AddressToInteger(IPv4Address ip_addr);
#ifdef _WIN32
#include <ws2tcpip.h>
#else
#include <netdb.h>
#endif
// named to avoid name collision with Windows macro
Common::Expected<std::vector<AddrInfo>, GetAddrInfoError> GetAddressInfo(
const std::string& host, const std::optional<std::string>& service);
sockaddr TranslateFromSockAddrIn(SockAddrIn input);
inline std::pair<std::string, int> GetNameInfo(const SockAddrIn& addr) {
sockaddr sa = TranslateFromSockAddrIn(addr);
sockaddr_in addr_in{};
std::memcpy(&addr_in, &sa, sizeof(sockaddr_in));
char host[1025]; // NI_MAXHOST
int err = getnameinfo(reinterpret_cast<const sockaddr*>(&addr_in), sizeof(addr_in), host, sizeof(host), nullptr, 0, 0);
if (err != 0) {
return {std::string{}, err};
}
return {std::string(host), 0};
}
GetAddrInfoError TranslateGetAddrInfoErrorFromNative(int err);
} // namespace Network

View File

@@ -446,10 +446,12 @@ void EmitIsHelperInvocation(EmitContext& ctx, IR::Inst& inst) {
void EmitSR_WScaleFactorXY(EmitContext& ctx, IR::Inst& inst) {
LOG_WARNING(Shader, "(STUBBED) called");
ctx.AddU32("{}=0x3c003c00u;", inst);
}
void EmitSR_WScaleFactorZ(EmitContext& ctx, IR::Inst& inst) {
LOG_WARNING(Shader, "(STUBBED) called");
ctx.AddU32("{}=0x3f800000u;", inst);
}
void EmitYDirection(EmitContext& ctx, IR::Inst& inst) {

View File

@@ -58,6 +58,9 @@ std::string FormatFloat(std::string_view value, IR::Type type) {
if (value == "nan") {
return "utof(0x7fc00000)";
}
if (value == "-nan") {
return "utof(0xffc00000)";
}
if (value == "inf") {
return "utof(0x7f800000)";
}

View File

@@ -81,7 +81,8 @@ public:
if constexpr (can_async_check) {
guard.lock();
}
if (Settings::IsGPULevelLow() || (Settings::IsGPULevelMedium() && !should_flush)) {
// if ((Settings::IsGPULevelLow() || Settings::IsGPULevelMedium()) && !should_flush) {
if (false) {
func();
} else {
uncommitted_operations.emplace_back(std::move(func));

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{

View File

@@ -33,6 +33,8 @@ static OGLProgram LinkSeparableProgram(GLuint shader) {
glGetProgramInfoLog(program.handle, log_length, nullptr, log.data());
if (link_status == GL_FALSE) {
LOG_ERROR(Render_OpenGL, "{}", log);
glDeleteProgram(program.handle);
program.handle = 0;
} else {
LOG_WARNING(Render_OpenGL, "{}", log);
}

View File

@@ -116,9 +116,14 @@ void WindowAdaptPass::DrawToFramebuffer(ProgramManager& program_manager, std::li
glBindTextureUnit(0, textures[i]);
glProgramUniformMatrix3x2fv(vert.handle, ModelViewMatrixLocation, 1, GL_FALSE,
matrices[i].data());
glProgramUniform2ui(frag.handle, ScreenSizeLocation,
static_cast<GLuint>(layout.screen.GetWidth()),
static_cast<GLuint>(layout.screen.GetHeight()));
if (frag.handle != 0) {
const GLint screen_size_loc = glGetUniformLocation(frag.handle, "screen_size");
if (screen_size_loc != -1) {
glProgramUniform2ui(frag.handle, screen_size_loc,
static_cast<GLuint>(layout.screen.GetWidth()),
static_cast<GLuint>(layout.screen.GetHeight()));
}
}
glNamedBufferSubData(vertex_buffer.handle, 0, sizeof(vertices[i]), std::data(vertices[i]));
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

View File

@@ -277,7 +277,10 @@ 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)};
ASSERT(handle.first <= tic_limit);
if (handle.first > tic_limit) {
LOG_WARNING(Shader, "Texture ID {} is out of bounds (limit {})", handle.first, tic_limit);
return {};
}
const GPUVAddr descriptor_addr{tic_addr + handle.first * sizeof(Tegra::Texture::TICEntry)};
Tegra::Texture::TICEntry entry;
gpu_memory->ReadBlock(descriptor_addr, &entry, sizeof(entry));