Compare commits

...

13 Commits

Author SHA1 Message Date
b3eba6a275 ffmpeg downgrade? 2026-01-08 16:42:37 -03:00
c486334c78 lol 2026-01-08 16:20:36 -03:00
7dc30a5e2d salva isso 2026-01-08 16:18:11 -03:00
b16cece0a6 Merge branch 'master' of https://code.ovosimpatico.com/JDLO/eden 2026-01-08 14:33:52 -03:00
19bbd9894e custom auth support 2026-01-08 14:32:59 -03:00
3bb0d29aa1 fix: Update CMake configuration for Apple support and improve legacy online service command handling 2026-01-07 12:25:00 -03:00
cf4f813145 new update? 2026-01-05 23:49:04 -03:00
3290ed80d8 fix macos build 2026-01-02 09:00:48 -03:00
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
xbzk
cfae726289 [video_core] nvn descriptor layout fix (#3206)
Yxzx presumes this:
// The NVN driver buffer (index 0) is known to pack the SSBO address followed by its size.
But in MCI i`ve discovered that there are no sizes, both registers are GPU addresses (hence the 2.8gb allocation, it was an address actually)

Method could be much simpler but for safety i`ve routed both old and new worlds.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3206
Reviewed-by: Caio Oliveira <caiooliveirafarias0@gmail.com>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2025-12-26 04:54:14 +01:00
39 changed files with 1957 additions and 139 deletions

View File

@@ -158,7 +158,7 @@ set(YUZU_QT_MIRROR "" CACHE STRING "What mirror to use for downloading the bundl
option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
set(EXT_DEFAULT OFF)
if (MSVC OR ANDROID)
if (MSVC OR ANDROID OR APPLE)
set(EXT_DEFAULT ON)
endif()
option(YUZU_USE_CPM "Use CPM to fetch system dependencies (fmt, boost, etc) if needed. Externals will still be fetched." ${EXT_DEFAULT})
@@ -425,7 +425,7 @@ if (YUZU_USE_CPM)
endif()
# fmt
AddJsonPackage(fmt)
AddJsonPackage(NAME fmt BUNDLED_PACKAGE ON)
# lz4
AddJsonPackage(lz4)
@@ -530,7 +530,9 @@ if (APPLE)
# Umbrella framework for everything GUI-related
find_library(COCOA_LIBRARY Cocoa REQUIRED)
find_library(IOKIT_LIBRARY IOKit REQUIRED)
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY})
find_library(COREVIDEO_LIBRARY CoreVideo REQUIRED)
find_library(VIDEOTOOLBOX_LIBRARY VideoToolbox REQUIRED)
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY} ${VIDEOTOOLBOX_LIBRARY})
elseif (WIN32)
# Target Windows 10
add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00)
@@ -563,7 +565,7 @@ find_package(VulkanUtilityLibraries)
find_package(SimpleIni)
find_package(SPIRV-Tools)
find_package(sirit)
find_package(gamemode)
find_package(gamemode)
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
find_package(xbyak)

View File

@@ -1,16 +1,16 @@
{
"ffmpeg": {
"repo": "FFmpeg/FFmpeg",
"sha": "5e56937b74",
"hash": "9ab0457dcd6ce6359b5053c1662f57910d332f68ca0cca9d4134d858464840917027374de3d97e0863c3a7daaea2fe4f4cd17d1c6d8e7f740f4ad91e71c2932b",
"tag": "n6.1",
"bundled": true
},
"ffmpeg-ci": {
"ci": true,
"package": "FFmpeg",
"name": "ffmpeg",
"repo": "crueter-ci/FFmpeg",
"version": "8.0.1-5e56937b74",
"repo": "FFmpeg/FFmpeg",
"tag": "n6.1",
"version": "6.1",
"min_version": "4.1"
}
}
}

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

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <array>
@@ -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,14 @@ 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);
// Start Legacy Online Service (UDP port 6000 + HTTP port 8080 for mobile app)
if (!legacy_online) {
legacy_online = std::make_unique<Network::LegacyOnlineService>();
legacy_online->Start();
}
}
void ReinitializeIfNecessary(System& system) {
@@ -293,6 +302,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 +429,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 +478,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);
@@ -517,6 +573,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

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -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.{:04X}", 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.{:04X}", 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

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -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" (hexadecimal)
u64 index = 0;
try {
if (name.size() > 9) {
index = std::stoull(name.substr(9), nullptr, 16); // Base 16 for hex
}
} 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

@@ -66,7 +66,9 @@ NvResult nvhost_nvdec::Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8> in
void nvhost_nvdec::OnOpen(NvCore::SessionId session_id, DeviceFD fd) {
LOG_INFO(Service_NVDRV, "NVDEC video stream started");
system.SetNVDECActive(true);
if (!system.GetNVDECActive()) {
system.SetNVDECActive(true);
}
sessions[fd] = session_id;
host1x.StartDevice(fd, Tegra::Host1x::ChannelType::NvDec, channel_syncpoint);
}

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -22,6 +22,9 @@
#include "core/internal_network/sockets.h"
#include "network/network.h"
#include <common/settings.h>
#include "common/fs/file.h"
#include "common/fs/path_util.h"
#include "common/string_util.h"
using Common::Expected;
using Common::Unexpected;
@@ -315,7 +318,15 @@ void BSD::Fcntl(HLERequestContext& ctx) {
const u32 cmd = rp.Pop<u32>();
const s32 arg = rp.Pop<s32>();
LOG_DEBUG(Service, "called. fd={} cmd={} arg={}", fd, cmd, arg);
// Log with more detail to understand non-blocking configuration
if (cmd == 4) { // SETFL
bool is_nonblock = (arg & 0x800) != 0; // O_NONBLOCK
LOG_INFO(Service, "Fcntl SETFL fd={} arg={} (non-blocking={})", fd, arg, is_nonblock);
} else if (cmd == 3) { // GETFL
LOG_INFO(Service, "Fcntl GETFL fd={}", fd);
} else {
LOG_INFO(Service, "Fcntl fd={} cmd={} arg={}", fd, cmd, arg);
}
const auto [ret, bsd_errno] = FcntlImpl(fd, static_cast<FcntlCmd>(cmd), arg);
@@ -802,33 +813,40 @@ Errno BSD::SetSockOptImpl(s32 fd, u32 level, OptName optname, std::span<const u8
switch (optname) {
case OptName::REUSEADDR:
LOG_INFO(Service, "SetSockOpt fd={} REUSEADDR={}", fd, value);
if (value != 0 && value != 1) {
LOG_WARNING(Service, "Invalid REUSEADDR value: {}", value);
return Errno::INVAL;
}
return Translate(socket->SetReuseAddr(value != 0));
case OptName::KEEPALIVE:
LOG_INFO(Service, "SetSockOpt fd={} KEEPALIVE={}", fd, value);
if (value != 0 && value != 1) {
LOG_WARNING(Service, "Invalid KEEPALIVE value: {}", value);
return Errno::INVAL;
}
return Translate(socket->SetKeepAlive(value != 0));
case OptName::BROADCAST:
LOG_INFO(Service, "SetSockOpt fd={} BROADCAST={}", fd, value);
if (value != 0 && value != 1) {
LOG_WARNING(Service, "Invalid BROADCAST value: {}", value);
return Errno::INVAL;
}
return Translate(socket->SetBroadcast(value != 0));
case OptName::SNDBUF:
LOG_INFO(Service, "SetSockOpt fd={} SNDBUF={}", fd, value);
return Translate(socket->SetSndBuf(value));
case OptName::RCVBUF:
LOG_INFO(Service, "SetSockOpt fd={} RCVBUF={}", fd, value);
return Translate(socket->SetRcvBuf(value));
case OptName::SNDTIMEO:
LOG_INFO(Service, "SetSockOpt fd={} SNDTIMEO={}", fd, value);
return Translate(socket->SetSndTimeo(value));
case OptName::RCVTIMEO:
LOG_INFO(Service, "SetSockOpt fd={} RCVTIMEO={}", fd, value);
return Translate(socket->SetRcvTimeo(value));
case OptName::NOSIGPIPE:
LOG_WARNING(Service, "(STUBBED) setting NOSIGPIPE to {}", value);
LOG_INFO(Service, "SetSockOpt fd={} NOSIGPIPE={}", fd, value);
return Errno::SUCCESS;
default:
LOG_WARNING(Service, "(STUBBED) Unimplemented optname={} (0x{:x}), returning INVAL",
@@ -919,12 +937,134 @@ std::pair<s32, Errno> BSD::RecvFromImpl(s32 fd, u32 flags, std::vector<u8>& mess
return {ret, bsd_errno};
}
std::pair<s32, Errno> BSD::SendImpl(s32 fd, u32 flags, std::span<const u8> message) {
if (!IsFileDescriptorValid(fd)) {
return {-1, Errno::BADF};
std::pair<s32, Errno> BSD::SendImpl(s32 fd, u32 flags, std::span<const u8> message) {
if (!IsFileDescriptorValid(fd)) {
return {-1, Errno::BADF};
}
const size_t original_size = message.size();
// Inspect for Authorization header to inject custom token (HTTP/Localhost support)
const std::string_view data_view(reinterpret_cast<const char*>(message.data()),
message.size());
// Optimized check: only look if it looks like an HTTP request with auth
// We do a case-insensitive search for the specific header pattern
std::string request_str(data_view);
std::string request_lower = Common::ToLower(request_str);
size_t auth_pos = request_lower.find("authorization: switch t=");
if (auth_pos != std::string::npos) {
LOG_INFO(Service,
"BSDJ: Found 'Authorization: switch t=' in SendImpl. Injecting custom auth.");
const auto auth_file_path =
Common::FS::GetEdenPath(Common::FS::EdenPath::EdenDir) / "jdlo_auth.ini";
Common::FS::IOFile auth_file(auth_file_path, Common::FS::FileAccessMode::Read,
Common::FS::FileType::TextFile);
if (auth_file.IsOpen()) {
std::vector<u8> file_content_vec(auth_file.GetSize());
if (auth_file.Read(file_content_vec) == file_content_vec.size()) {
std::string encoded_content(file_content_vec.begin(), file_content_vec.end());
// Simple Base64 Decoder
auto DecodeBase64 = [](std::string_view input) -> std::string {
static const int T[256] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1};
std::string out;
int val = 0, valb = -8;
for (unsigned char c : input) {
if (T[c] == -1)
break;
val = (val << 6) + T[c];
valb += 6;
if (valb >= 0) {
out.push_back(char((val >> valb) & 0xFF));
valb -= 8;
}
}
return out;
};
// Decode the INI content
std::string file_content = DecodeBase64(encoded_content);
// Find TickedId
std::string auth_token;
static constexpr std::string_view KeyName = "TickedId=";
size_t key_pos = file_content.find(KeyName);
if (key_pos != std::string::npos) {
size_t value_start = key_pos + KeyName.size();
size_t value_end = file_content.find_first_of("\r\n", value_start);
if (value_end == std::string::npos) {
value_end = file_content.size();
}
auth_token = file_content.substr(value_start, value_end - value_start);
}
if (!auth_token.empty()) {
// Ensure the token has the correct prefix "uplaypc_v1 t="
if (auth_token.find("uplaypc_v1 t=") == std::string::npos) {
auth_token = "uplaypc_v1 t=" + auth_token;
}
// Find end of the line
size_t end_pos = request_str.find("\r\n", auth_pos);
if (end_pos != std::string::npos) {
bool is_header_start = (auth_pos == 0) || (request_str[auth_pos - 1] == '\n');
if (is_header_start) {
LOG_INFO(Service, "BSDJ: Injecting token (TickedId): {}...",
auth_token.substr(0, 20));
std::string new_header = "Authorization: " + auth_token;
request_str.replace(auth_pos, end_pos - auth_pos, new_header);
// Send the MODIFIED message
std::span<const u8> new_message(
reinterpret_cast<const u8*>(request_str.data()),
request_str.size());
auto result = Translate(
file_descriptors[fd]->socket->Send(new_message, flags));
LOG_CRITICAL(
Service,
"SendImpl (Modified) real_sent={} original_size={} errno={}",
result.first, original_size, static_cast<int>(result.second));
// Mask the return size: If we successfully sent the larger buffer,
// tell the guest we sent exactly what they asked for.
if (result.first > 0 && result.second == Errno::SUCCESS) {
// Return original size to prevent guest confusion
return {static_cast<s32>(original_size), Errno::SUCCESS};
}
return result;
}
}
}
}
} else {
// LOG_WARNING(Service, "jdlo_auth.ini not found");
}
}
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;
}
return Translate(file_descriptors[fd]->socket->Send(message, flags));
}
std::pair<s32, Errno> BSD::SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
std::span<const u8> addr) {

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

@@ -12,6 +12,7 @@
#include <openssl/x509.h>
#include "common/fs/file.h"
#include "common/fs/path_util.h"
#include "common/hex_util.h"
#include "common/string_util.h"
@@ -48,28 +49,32 @@ bool OneTimeInitBIO();
#ifdef YUZU_BUNDLED_OPENSSL
// This is ported from httplib
struct scope_exit {
explicit scope_exit(std::function<void(void)> &&f)
: exit_function(std::move(f)), execute_on_destruction{true} {}
explicit scope_exit(std::function<void(void)> &&f)
: exit_function(std::move(f)), execute_on_destruction{true} {}
scope_exit(scope_exit &&rhs) noexcept
: exit_function(std::move(rhs.exit_function)),
execute_on_destruction{rhs.execute_on_destruction} {
rhs.release();
}
scope_exit(scope_exit &&rhs) noexcept
: exit_function(std::move(rhs.exit_function)),
execute_on_destruction{rhs.execute_on_destruction} {
rhs.release();
}
~scope_exit() {
if (execute_on_destruction) { this->exit_function(); }
}
~scope_exit() {
if (execute_on_destruction) {
this->exit_function();
}
}
void release() { this->execute_on_destruction = false; }
void release() {
this->execute_on_destruction = false;
}
private:
scope_exit(const scope_exit &) = delete;
void operator=(const scope_exit &) = delete;
scope_exit &operator=(scope_exit &&) = delete;
scope_exit(const scope_exit &) = delete;
void operator=(const scope_exit &) = delete;
scope_exit &operator=(scope_exit &&) = delete;
std::function<void(void)> exit_function;
bool execute_on_destruction;
std::function<void(void)> exit_function;
bool execute_on_destruction;
};
inline X509_STORE *CreateCaCertStore(const char *ca_cert,
@@ -115,6 +120,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 +178,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();
@@ -195,6 +217,123 @@ public:
}
Result Write(size_t* out_size, std::span<const u8> data) override {
const size_t original_size = data.size();
const std::string_view data_view(reinterpret_cast<const char*>(data.data()), data.size());
// Log all POST requests for debugging
if (data_view.size() > 5 && data_view.substr(0, 5) == "POST ") {
LOG_INFO(Service_SSL, "Intercepted POST request. Length: {}. Preview: {}", data.size(), data_view.substr(0, std::min(data_view.size(), size_t(200))));
}
std::string request_str(data_view);
std::string request_lower = Common::ToLower(request_str);
// Look for the specific authorization header value we want to replace: "switch t="
// We match "authorization: switch t=" case-insensitively
size_t auth_pos = request_lower.find("authorization: switch t=");
if (auth_pos != std::string::npos) {
LOG_INFO(Service_SSL, "Found 'Authorization: switch t=' header. Injecting custom auth.");
const auto auth_file_path = Common::FS::GetEdenPath(Common::FS::EdenPath::EdenDir) / "jdlo_auth.ini";
Common::FS::IOFile auth_file(auth_file_path, Common::FS::FileAccessMode::Read, Common::FS::FileType::TextFile);
if (auth_file.IsOpen()) {
std::vector<u8> file_content_vec(auth_file.GetSize());
if (auth_file.Read(file_content_vec) == file_content_vec.size()) {
std::string encoded_content(file_content_vec.begin(), file_content_vec.end());
// Simple Base64 Decoder
auto DecodeBase64 = [](std::string_view input) -> std::string {
static const int T[256] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1};
std::string out;
int val = 0, valb = -8;
for (unsigned char c : input) {
if (T[c] == -1)
break;
val = (val << 6) + T[c];
valb += 6;
if (valb >= 0) {
out.push_back(char((val >> valb) & 0xFF));
valb -= 8;
}
}
return out;
};
// Decode the INI content
std::string file_content = DecodeBase64(encoded_content);
// Find TickedId
std::string auth_token;
static constexpr std::string_view KeyName = "TickedId=";
size_t key_pos = file_content.find(KeyName);
if (key_pos != std::string::npos) {
size_t value_start = key_pos + KeyName.size();
size_t value_end = file_content.find_first_of("\r\n", value_start);
if (value_end == std::string::npos) {
value_end = file_content.size();
}
auth_token = file_content.substr(value_start, value_end - value_start);
} else {
// Fallback: If not found, maybe it wasn't base64 or format is different?
// Try using raw content if decode failed to produce readable key?
// Actually, let's just stick to the decoded content.
}
if (!auth_token.empty()) {
// Ensure the token has the correct prefix "uplaypc_v1 t="
if (auth_token.find("uplaypc_v1 t=") == std::string::npos) {
auth_token = "uplaypc_v1 t=" + auth_token;
}
LOG_INFO(Service_SSL,
"Injecting custom Authorization from jdlo_auth.ini (decoded "
"TickedId): {}...",
auth_token.substr(0, 20));
// Find existing Authorization header position case-insensitively
size_t header_pos = request_lower.find("\r\nauthorization: ");
if (header_pos != std::string::npos) {
size_t end_pos = request_str.find("\r\n", header_pos + 2);
if (end_pos != std::string::npos) {
LOG_INFO(Service_SSL, "Replacing existing Authorization header.");
request_str.replace(header_pos, end_pos - header_pos,
"\r\nAuthorization: " + auth_token);
}
} else {
LOG_INFO(Service_SSL, "Appending new Authorization header.");
size_t body_pos = request_str.find("\r\n\r\n");
if (body_pos != std::string::npos) {
request_str.insert(body_pos, "\r\nAuthorization: " + auth_token);
}
}
const int ret = SSL_write_ex(ssl, request_str.data(), request_str.size(), out_size);
if (ret == 1) { // Success
*out_size = original_size;
}
return HandleReturn("SSL_write_ex", out_size, ret);
}
} else {
LOG_ERROR(Service_SSL, "Failed to read jdlo_auth.ini content");
}
} else {
LOG_WARNING(Service_SSL, "jdlo_auth.ini not found at {}",
Common::FS::PathToUTF8String(auth_file_path));
}
}
const int ret = SSL_write_ex(ssl, data.data(), data.size(), out_size);
return HandleReturn("SSL_write_ex", out_size, ret);
}
@@ -247,6 +386,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,701 @@
// 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>
#include <sstream>
#include <regex>
#include <iomanip>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
// For SHA1 and Base64
#include "common/logging/log.h"
namespace Network {
// Simple SHA1 implementation for WebSocket handshake
namespace {
class SHA1 {
public:
SHA1() { reset(); }
void update(const uint8_t* data, size_t len) {
while (len--) {
buffer[buffer_size++] = *data++;
if (buffer_size == 64) {
process_block();
buffer_size = 0;
}
total_bits += 8;
}
}
void update(const std::string& str) {
update(reinterpret_cast<const uint8_t*>(str.data()), str.size());
}
std::array<uint8_t, 20> finalize() {
// Padding
buffer[buffer_size++] = 0x80;
while (buffer_size != 56) {
if (buffer_size == 64) {
process_block();
buffer_size = 0;
}
buffer[buffer_size++] = 0;
}
// Append length in bits
for (int i = 7; i >= 0; --i) {
buffer[buffer_size++] = static_cast<uint8_t>(total_bits >> (i * 8));
}
process_block();
std::array<uint8_t, 20> result;
for (int i = 0; i < 5; ++i) {
result[i*4+0] = static_cast<uint8_t>(h[i] >> 24);
result[i*4+1] = static_cast<uint8_t>(h[i] >> 16);
result[i*4+2] = static_cast<uint8_t>(h[i] >> 8);
result[i*4+3] = static_cast<uint8_t>(h[i]);
}
return result;
}
private:
void reset() {
h[0] = 0x67452301;
h[1] = 0xEFCDAB89;
h[2] = 0x98BADCFE;
h[3] = 0x10325476;
h[4] = 0xC3D2E1F0;
buffer_size = 0;
total_bits = 0;
}
void process_block() {
uint32_t w[80];
for (int i = 0; i < 16; ++i) {
w[i] = (buffer[i*4+0] << 24) | (buffer[i*4+1] << 16) |
(buffer[i*4+2] << 8) | buffer[i*4+3];
}
for (int i = 16; i < 80; ++i) {
uint32_t t = w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16];
w[i] = (t << 1) | (t >> 31);
}
uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
for (int i = 0; i < 80; ++i) {
uint32_t f, k;
if (i < 20) {
f = (b & c) | ((~b) & d);
k = 0x5A827999;
} else if (i < 40) {
f = b ^ c ^ d;
k = 0x6ED9EBA1;
} else if (i < 60) {
f = (b & c) | (b & d) | (c & d);
k = 0x8F1BBCDC;
} else {
f = b ^ c ^ d;
k = 0xCA62C1D6;
}
uint32_t temp = ((a << 5) | (a >> 27)) + f + e + k + w[i];
e = d;
d = c;
c = (b << 30) | (b >> 2);
b = a;
a = temp;
}
h[0] += a;
h[1] += b;
h[2] += c;
h[3] += d;
h[4] += e;
}
uint32_t h[5];
uint8_t buffer[64];
size_t buffer_size;
uint64_t total_bits;
};
std::string base64_encode(const uint8_t* data, size_t len) {
static const char* chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string result;
result.reserve((len + 2) / 3 * 4);
for (size_t i = 0; i < len; i += 3) {
uint32_t n = static_cast<uint32_t>(data[i]) << 16;
if (i + 1 < len) n |= static_cast<uint32_t>(data[i + 1]) << 8;
if (i + 2 < len) n |= static_cast<uint32_t>(data[i + 2]);
result += chars[(n >> 18) & 0x3F];
result += chars[(n >> 12) & 0x3F];
result += (i + 1 < len) ? chars[(n >> 6) & 0x3F] : '=';
result += (i + 2 < len) ? chars[n & 0x3F] : '=';
}
return result;
}
std::string compute_websocket_accept(const std::string& key) {
// WebSocket magic GUID
const std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
std::string combined = key + magic;
SHA1 sha1;
sha1.update(combined);
auto hash = sha1.finalize();
return base64_encode(hash.data(), hash.size());
}
} // anonymous namespace
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;
udp_worker_thread = std::thread(&LegacyOnlineService::UdpServerLoop, this);
http_worker_thread = std::thread(&LegacyOnlineService::HttpServerLoop, this);
}
void LegacyOnlineService::Stop() {
if (!is_running) {
return;
}
is_running = false;
if (udp_socket_fd != ~0ULL) {
#ifdef _WIN32
closesocket(static_cast<SOCKET>(udp_socket_fd));
#else
close(static_cast<int>(udp_socket_fd));
#endif
udp_socket_fd = ~0ULL;
}
if (http_socket_fd != ~0ULL) {
#ifdef _WIN32
closesocket(static_cast<SOCKET>(http_socket_fd));
#else
close(static_cast<int>(http_socket_fd));
#endif
http_socket_fd = ~0ULL;
}
if (udp_worker_thread.joinable()) {
udp_worker_thread.join();
}
if (http_worker_thread.joinable()) {
http_worker_thread.join();
}
}
void LegacyOnlineService::UdpServerLoop() {
LOG_INFO(Network, "Starting Legacy Online UDP Server on port {}", UDP_PORT);
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 UDP socket");
return;
}
udp_socket_fd = static_cast<uintptr_t>(s);
int opt = 1;
#ifdef _WIN32
setsockopt(static_cast<SOCKET>(udp_socket_fd), SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));
#else
setsockopt(static_cast<int>(udp_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(UDP_PORT);
int res = -1;
#ifdef _WIN32
res = bind(static_cast<SOCKET>(udp_socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
#else
res = bind(static_cast<int>(udp_socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
#endif
if (res < 0) {
#ifdef _WIN32
LOG_ERROR(Network, "Failed to bind UDP to port {}: {}", UDP_PORT, WSAGetLastError());
closesocket(static_cast<SOCKET>(udp_socket_fd));
#else
LOG_ERROR(Network, "Failed to bind UDP to port {}: {}", UDP_PORT, strerror(errno));
close(static_cast<int>(udp_socket_fd));
#endif
udp_socket_fd = ~0ULL;
return;
}
LOG_INFO(Network, "Legacy Online UDP Server waiting for messages...");
char buffer[2048];
while (is_running) {
sockaddr_in client_addr{};
#ifdef _WIN32
int client_len = sizeof(client_addr);
int len = recvfrom(static_cast<SOCKET>(udp_socket_fd), buffer, sizeof(buffer), 0, (sockaddr*)&client_addr, &client_len);
#else
socklen_t client_len = sizeof(client_addr);
ssize_t len = recvfrom(static_cast<int>(udp_socket_fd), buffer, sizeof(buffer), 0, (sockaddr*)&client_addr, &client_len);
#endif
if (!is_running) break;
if (len > 0) {
const char* ack_msg = "ACK";
#ifdef _WIN32
sendto(static_cast<SOCKET>(udp_socket_fd), ack_msg, static_cast<int>(strlen(ack_msg)), 0, (sockaddr*)&client_addr, client_len);
#else
sendto(static_cast<int>(udp_socket_fd), ack_msg, strlen(ack_msg), 0, (sockaddr*)&client_addr, client_len);
#endif
} else {
break;
}
}
#ifdef _WIN32
if (udp_socket_fd != ~0ULL) closesocket(static_cast<SOCKET>(udp_socket_fd));
#else
if (udp_socket_fd != ~0ULL) close(static_cast<int>(udp_socket_fd));
#endif
udp_socket_fd = ~0ULL;
LOG_INFO(Network, "Legacy Online UDP Server stopped");
}
void LegacyOnlineService::HttpServerLoop() {
LOG_INFO(Network, "Starting Mobile App HTTP/WebSocket Server on port {}", HTTP_PORT);
auto s = socket(AF_INET, SOCK_STREAM, 0);
#ifdef _WIN32
if (s == INVALID_SOCKET) {
#else
if (s == -1) {
#endif
LOG_ERROR(Network, "Failed to create HTTP socket");
return;
}
http_socket_fd = static_cast<uintptr_t>(s);
int opt = 1;
#ifdef _WIN32
setsockopt(static_cast<SOCKET>(http_socket_fd), SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));
#else
setsockopt(static_cast<int>(http_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(HTTP_PORT);
int res = -1;
#ifdef _WIN32
res = bind(static_cast<SOCKET>(http_socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
#else
res = bind(static_cast<int>(http_socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
#endif
if (res < 0) {
#ifdef _WIN32
LOG_ERROR(Network, "Failed to bind HTTP to port {}: {}", HTTP_PORT, WSAGetLastError());
closesocket(static_cast<SOCKET>(http_socket_fd));
#else
LOG_ERROR(Network, "Failed to bind HTTP to port {}: {}", HTTP_PORT, strerror(errno));
close(static_cast<int>(http_socket_fd));
#endif
http_socket_fd = ~0ULL;
return;
}
#ifdef _WIN32
res = listen(static_cast<SOCKET>(http_socket_fd), 10);
#else
res = listen(static_cast<int>(http_socket_fd), 10);
#endif
if (res < 0) {
#ifdef _WIN32
LOG_ERROR(Network, "Failed to listen on HTTP port {}: {}", HTTP_PORT, WSAGetLastError());
closesocket(static_cast<SOCKET>(http_socket_fd));
#else
LOG_ERROR(Network, "Failed to listen on HTTP port {}: {}", HTTP_PORT, strerror(errno));
close(static_cast<int>(http_socket_fd));
#endif
http_socket_fd = ~0ULL;
return;
}
LOG_INFO(Network, "Mobile App HTTP/WebSocket Server listening on port {}...", HTTP_PORT);
while (is_running) {
sockaddr_in client_addr{};
#ifdef _WIN32
int client_len = sizeof(client_addr);
SOCKET client_fd = accept(static_cast<SOCKET>(http_socket_fd), (sockaddr*)&client_addr, &client_len);
if (client_fd == INVALID_SOCKET) {
#else
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(static_cast<int>(http_socket_fd), (sockaddr*)&client_addr, &client_len);
if (client_fd == -1) {
#endif
if (!is_running) break;
continue;
}
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
LOG_INFO(Network, "HTTP/WebSocket connection from {}:{}", client_ip, ntohs(client_addr.sin_port));
// Read HTTP request
char buffer[4096];
memset(buffer, 0, sizeof(buffer));
#ifdef _WIN32
int bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
#else
ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
#endif
if (bytes_read > 0) {
std::string request(buffer, bytes_read);
LOG_INFO(Network, "Request:\n{}", request);
// Check if this is a WebSocket upgrade request
bool is_websocket = request.find("Upgrade: websocket") != std::string::npos;
if (is_websocket) {
// Extract Sec-WebSocket-Key
std::string ws_key;
std::regex key_regex("Sec-WebSocket-Key: ([^\r\n]+)");
std::smatch match;
if (std::regex_search(request, match, key_regex)) {
ws_key = match[1].str();
}
// Extract Sec-WebSocket-Protocol
std::string ws_protocol;
std::regex protocol_regex("Sec-WebSocket-Protocol: ([^\r\n]+)");
if (std::regex_search(request, match, protocol_regex)) {
ws_protocol = match[1].str();
}
LOG_INFO(Network, "WebSocket upgrade request - Key: {}, Protocol: {}", ws_key, ws_protocol);
// Compute accept key
std::string accept_key = compute_websocket_accept(ws_key);
LOG_INFO(Network, "WebSocket Accept Key: {}", accept_key);
// Build WebSocket handshake response
std::ostringstream ws_response;
ws_response << "HTTP/1.1 101 Switching Protocols\r\n";
ws_response << "Upgrade: websocket\r\n";
ws_response << "Connection: Upgrade\r\n";
ws_response << "Sec-WebSocket-Accept: " << accept_key << "\r\n";
if (!ws_protocol.empty()) {
ws_response << "Sec-WebSocket-Protocol: " << ws_protocol << "\r\n";
}
ws_response << "\r\n";
std::string response_str = ws_response.str();
#ifdef _WIN32
send(client_fd, response_str.c_str(), static_cast<int>(response_str.size()), 0);
#else
send(client_fd, response_str.c_str(), response_str.size(), 0);
#endif
LOG_INFO(Network, "WebSocket handshake completed! Response:\n{}", response_str);
// Now handle WebSocket messages
LOG_INFO(Network, "Mobile app WebSocket connected! Entering message loop...");
while (is_running) {
memset(buffer, 0, sizeof(buffer));
#ifdef _WIN32
bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
#else
bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
#endif
if (bytes_read <= 0) {
LOG_INFO(Network, "WebSocket connection closed");
break;
}
// Parse WebSocket frame
uint8_t* frame = reinterpret_cast<uint8_t*>(buffer);
uint8_t opcode = frame[0] & 0x0F;
bool masked = (frame[1] & 0x80) != 0;
uint64_t payload_len = frame[1] & 0x7F;
size_t header_len = 2;
if (payload_len == 126) {
payload_len = (frame[2] << 8) | frame[3];
header_len = 4;
} else if (payload_len == 127) {
header_len = 10;
payload_len = 0;
for (int i = 0; i < 8; ++i) {
payload_len = (payload_len << 8) | frame[2 + i];
}
}
uint8_t mask_key[4] = {0};
if (masked) {
memcpy(mask_key, frame + header_len, 4);
header_len += 4;
}
// Unmask payload
std::string payload;
for (size_t i = 0; i < payload_len && (header_len + i) < static_cast<size_t>(bytes_read); ++i) {
char c = frame[header_len + i];
if (masked) {
c ^= mask_key[i % 4];
}
payload += c;
}
// LOG_INFO(Network, "WebSocket message (opcode={}): {}", opcode, payload);
// Handle different opcodes
if (opcode == 0x08) {
// Close frame
// LOG_INFO(Network, "WebSocket close frame received");
break;
} else if (opcode == 0x09) {
// Ping - send pong. Theoretically should echo payload, but empty pong is usually fine.
// LOG_INFO(Network, "WebSocket PING received. Responding with PONG.");
uint8_t pong[2] = {0x8A, 0x00};
#ifdef _WIN32
send(client_fd, reinterpret_cast<char*>(pong), 2, 0);
#else
send(client_fd, pong, 2, 0);
#endif
} else if (opcode == 0x0A) {
// Pong (keep-alive response from client)
// LOG_INFO(Network, "WebSocket PONG received from client.");
} else if (opcode == 0x01 || opcode == 0x02) {
// Text or Binary frame - process Just Dance protocol
std::string response;
// Check message type and respond appropriately
// Protocol flow based on JoyDance:
// 1. Phone -> Console: JD_PhoneDataCmdHandshakeHello
// 2. Console -> Phone: JD_PhoneDataCmdHandshakeContinue (with phoneID)
// 3. Phone -> Console: JD_PhoneDataCmdSync (with phoneID)
// 4. Console -> Phone: JD_PhoneDataCmdSyncEnd (with phoneID)
// 5. Connected!
if (payload.find("JD_PhoneDataCmdHandshakeHello") != std::string::npos) {
// Step 2: Respond with HandshakeContinue
// PROTOCOL FIX: No "root" wrapper. Authenticated ID=1 (Int).
// CRITICAL: App sent Freq=0. Providing configuration values.
// CLEANUP: Removed extra status fields to prevent parsing errors.
response = R"({"__class":"JD_PhoneDataCmdHandshakeContinue","phoneID":1,"accelAcquisitionFreqHz":50,"accelAcquisitionLatency":40,"accelMaxRange":8})";
// LOG_INFO(Network, "Sending HandshakeContinue (id=1, cfg=50Hz)");
// Send HandshakeContinue
std::vector<uint8_t> ws_frame;
ws_frame.push_back(0x81);
ws_frame.push_back(static_cast<uint8_t>(response.size()));
for (char c : response) {
ws_frame.push_back(static_cast<uint8_t>(c));
}
#ifdef _WIN32
send(client_fd, reinterpret_cast<char*>(ws_frame.data()), static_cast<int>(ws_frame.size()), 0);
#else
send(client_fd, ws_frame.data(), ws_frame.size(), 0);
#endif
// LOG_INFO(Network, "WebSocket response sent: {}", response);
// Removed proactive commands to verify if app accepts HandshakeContinue and sends Sync
response.clear();
} else if (payload.find("JD_PhoneDataCmdSync") != std::string::npos) {
// Step 4: Respond with SyncEnd (NO ROOT)
// Using phoneID 1 (Int)
response = R"({"__class":"JD_PhoneDataCmdSyncEnd","phoneID":1,"status":"ok"})";
// LOG_INFO(Network, "Sending SyncEnd (id=1, no root) - Connection complete!");
// Send SyncEnd
std::vector<uint8_t> ws_frame;
ws_frame.push_back(0x81);
ws_frame.push_back(static_cast<uint8_t>(response.size()));
for (char c : response) {
ws_frame.push_back(static_cast<uint8_t>(c));
}
#ifdef _WIN32
send(client_fd, reinterpret_cast<char*>(ws_frame.data()), static_cast<int>(ws_frame.size()), 0);
#else
send(client_fd, ws_frame.data(), ws_frame.size(), 0);
#endif
// Step 5: Send Activation Commands immediately to authorize input and accel
// 5a. Enable Input
std::string cmd1 = R"({"__class":"InputSetup_ConsoleCommandData","isEnabled":1,"inputSetup":{"isEnabled":1}})";
std::vector<uint8_t> f1; f1.push_back(0x81); f1.push_back(static_cast<uint8_t>(cmd1.size())); for(char c:cmd1) f1.push_back(static_cast<uint8_t>(c));
#ifdef _WIN32
send(client_fd, reinterpret_cast<char*>(f1.data()), static_cast<int>(f1.size()), 0);
#else
send(client_fd, f1.data(), f1.size(), 0);
#endif
// 5b. Enable Accelerometer
std::string cmd2 = R"({"__class":"JD_EnableAccelValuesSending_ConsoleCommandData","isEnabled":1})";
std::vector<uint8_t> f2; f2.push_back(0x81); f2.push_back(static_cast<uint8_t>(cmd2.size())); for(char c:cmd2) f2.push_back(static_cast<uint8_t>(c));
#ifdef _WIN32
send(client_fd, reinterpret_cast<char*>(f2.data()), static_cast<int>(f2.size()), 0);
#else
send(client_fd, f2.data(), f2.size(), 0);
#endif
// 5c. UI Setup (optional but good for safety)
std::string cmd3 = R"({"__class":"JD_PhoneUiSetupData","isPopup":0,"inputSetup":{"isEnabled":1}})";
std::vector<uint8_t> f3; f3.push_back(0x81); f3.push_back(static_cast<uint8_t>(cmd3.size())); for(char c:cmd3) f3.push_back(static_cast<uint8_t>(c));
#ifdef _WIN32
send(client_fd, reinterpret_cast<char*>(f3.data()), static_cast<int>(f3.size()), 0);
#else
send(client_fd, f3.data(), f3.size(), 0);
#endif
// LOG_INFO(Network, "Sent Activation Commands (Input, Accel, UI)");
response.clear();
} else if (payload.find("JD_PhoneScoringData") != std::string::npos) {
// Accelerometer/scoring data - no response needed
// LOG_DEBUG(Network, "Received phone scoring data");
continue;
} else if (payload.find("JD_Input_PhoneCommandData") != std::string::npos) {
// Button input from phone - no response needed
// LOG_INFO(Network, "Received phone input command");
continue;
} else if (payload.find("JD_Pause_PhoneCommandData") != std::string::npos) {
// Pause command from phone
// LOG_INFO(Network, "Received pause command from phone");
continue;
} else if (payload.find("JD_Custom_PhoneCommandData") != std::string::npos) {
// Custom shortcut command
// LOG_INFO(Network, "Received custom command from phone");
continue;
} else if (payload.find("JD_CancelKeyboard_PhoneCommandData") != std::string::npos) {
// Keyboard cancelled
// LOG_INFO(Network, "Phone cancelled keyboard");
continue;
} else {
// Unknown message - log but don't respond
// LOG_INFO(Network, "Unknown phone message type");
continue;
}
if (!response.empty()) {
// Build WebSocket frame
std::vector<uint8_t> ws_frame;
ws_frame.push_back(0x81); // Text frame, FIN
if (response.size() < 126) {
ws_frame.push_back(static_cast<uint8_t>(response.size()));
} else if (response.size() < 65536) {
ws_frame.push_back(126);
ws_frame.push_back(static_cast<uint8_t>((response.size() >> 8) & 0xFF));
ws_frame.push_back(static_cast<uint8_t>(response.size() & 0xFF));
}
for (char c : response) {
ws_frame.push_back(static_cast<uint8_t>(c));
}
#ifdef _WIN32
send(client_fd, reinterpret_cast<char*>(ws_frame.data()), static_cast<int>(ws_frame.size()), 0);
#else
send(client_fd, ws_frame.data(), ws_frame.size(), 0);
#endif
LOG_INFO(Network, "WebSocket response sent: {}", response);
}
}
}
} else {
// Regular HTTP request
std::string response_body = R"({"status":"ok","message":"Eden Mobile Bridge"})";
std::ostringstream http_response;
http_response << "HTTP/1.1 200 OK\r\n";
http_response << "Content-Type: application/json\r\n";
http_response << "Content-Length: " << response_body.size() << "\r\n";
http_response << "Connection: close\r\n";
http_response << "\r\n";
http_response << response_body;
std::string response_str = http_response.str();
#ifdef _WIN32
send(client_fd, response_str.c_str(), static_cast<int>(response_str.size()), 0);
#else
send(client_fd, response_str.c_str(), response_str.size(), 0);
#endif
}
}
#ifdef _WIN32
closesocket(client_fd);
#else
close(client_fd);
#endif
}
#ifdef _WIN32
if (http_socket_fd != ~0ULL) closesocket(static_cast<SOCKET>(http_socket_fd));
#else
if (http_socket_fd != ~0ULL) close(static_cast<int>(http_socket_fd));
#endif
http_socket_fd = ~0ULL;
LOG_INFO(Network, "Mobile App HTTP/WebSocket Server stopped");
}
} // namespace Network

View File

@@ -0,0 +1,36 @@
// 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 UdpServerLoop();
void HttpServerLoop();
std::atomic_bool is_running{false};
std::thread udp_worker_thread;
std::thread http_worker_thread;
uintptr_t udp_socket_fd{~0ULL};
uintptr_t http_socket_fd{~0ULL};
bool winsock_initialized{false};
static constexpr int UDP_PORT = 6000;
static constexpr int HTTP_PORT = 8080;
};
} // namespace Network

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -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();
@@ -684,6 +704,10 @@ Errno Socket::Initialize(Domain domain, Type type, Protocol protocol) {
fd = socket(TranslateDomainToNative(domain), TranslateTypeToNative(type),
TranslateProtocolToNative(protocol));
if (fd != INVALID_SOCKET) {
// Enable SO_REUSEADDR to allow port reuse after socket close
// This prevents "Address already in use" errors when rebinding
int reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&reuse), sizeof(reuse));
return Errno::SUCCESS;
}
@@ -801,6 +825,35 @@ std::pair<s32, Errno> Socket::Recv(int flags, std::span<u8> message) {
ASSERT(flags == 0);
ASSERT(message.size() < static_cast<size_t>((std::numeric_limits<int>::max)()));
// If socket is blocking, use poll with interrupt socket to avoid infinite blocking
if (!is_non_blocking) {
std::vector<WSAPOLLFD> host_pollfds{
WSAPOLLFD{fd, POLLIN, 0},
WSAPOLLFD{GetInterruptSocket(), POLLIN, 0},
};
// Poll with a longer timeout (30 seconds) to wait for data
const int pollres = WSAPoll(host_pollfds.data(), static_cast<ULONG>(host_pollfds.size()), 30000);
if (host_pollfds[1].revents != 0) {
// Interrupt signaled, return EAGAIN
return {-1, Errno::AGAIN};
}
if (pollres == 0) {
// Timeout - return AGAIN so the game can retry
LOG_DEBUG(Network, "Recv poll timeout after 30 seconds, returning EAGAIN");
return {-1, Errno::AGAIN};
}
if (pollres < 0) {
return {-1, GetAndLogLastError()};
}
// Data is available, proceed with recv
LOG_INFO(Network, "Recv poll detected data available!");
}
const auto result =
recv(fd, reinterpret_cast<char*>(message.data()), static_cast<int>(message.size()), 0);
if (result != SOCKET_ERROR) {
@@ -814,6 +867,35 @@ std::pair<s32, Errno> Socket::RecvFrom(int flags, std::span<u8> message, SockAdd
ASSERT(flags == 0);
ASSERT(message.size() < static_cast<size_t>((std::numeric_limits<int>::max)()));
// If socket is blocking, use poll with interrupt socket to avoid infinite blocking
if (!is_non_blocking) {
std::vector<WSAPOLLFD> host_pollfds{
WSAPOLLFD{fd, POLLIN, 0},
WSAPOLLFD{GetInterruptSocket(), POLLIN, 0},
};
// Poll with a short timeout (5ms) to emulate Switch non-blocking/interruptible behavior
// This prevents the game from freezing when waiting for UDP packets on the main thread
const int pollres = WSAPoll(host_pollfds.data(), static_cast<ULONG>(host_pollfds.size()), 5);
if (host_pollfds[1].revents != 0) {
// Interrupt signaled, return EAGAIN
return {-1, Errno::AGAIN};
}
if (pollres == 0) {
// Timeout - return AGAIN so the game can continue its loop (rendering, input, etc.)
return {-1, Errno::AGAIN};
}
if (pollres < 0) {
return {-1, GetAndLogLastError()};
}
// Data is available, proceed with recvfrom
LOG_INFO(Network, "RecvFrom poll detected data available!");
}
sockaddr_in addr_in{};
socklen_t addrlen = sizeof(addr_in);
socklen_t* const p_addrlen = addr ? &addrlen : nullptr;
@@ -824,6 +906,10 @@ std::pair<s32, Errno> Socket::RecvFrom(int flags, std::span<u8> message, SockAdd
if (result != SOCKET_ERROR) {
if (addr) {
*addr = TranslateToSockAddrIn(addr_in, addrlen);
LOG_INFO(Network, "RecvFrom received {} bytes from {}:{}",
result, IPv4AddressToString(addr->ip), addr->portno);
} else {
LOG_INFO(Network, "RecvFrom received {} bytes", result);
}
return {static_cast<s32>(result), Errno::SUCCESS};
}
@@ -859,20 +945,66 @@ std::pair<s32, Errno> Socket::SendTo(u32 flags, std::span<const u8> message,
if (addr) {
host_addr_in = TranslateFromSockAddrIn(*addr);
to = &host_addr_in;
LOG_INFO(Network, "SendTo sending {} bytes to {}:{}",
message.size(), IPv4AddressToString(addr->ip), addr->portno);
} else {
LOG_INFO(Network, "SendTo sending {} bytes (no addr)", message.size());
}
// Log packet content for debugging mobile app connection
if (message.size() > 0 && message.size() <= 200) {
std::string hex_dump;
std::string ascii_dump;
for (size_t i = 0; i < message.size(); ++i) {
char hex[4];
snprintf(hex, sizeof(hex), "%02X ", message[i]);
hex_dump += hex;
// Build ASCII representation
if (message[i] >= 32 && message[i] < 127) {
ascii_dump += static_cast<char>(message[i]);
} else {
ascii_dump += '.';
}
}
LOG_INFO(Network, "SendTo packet HEX: {}", hex_dump);
LOG_INFO(Network, "SendTo packet ASCII: {}", ascii_dump);
} else if (message.size() > 200) {
// Log first 200 bytes for large packets
std::string hex_dump;
std::string ascii_dump;
for (size_t i = 0; i < 200; ++i) {
char hex[4];
snprintf(hex, sizeof(hex), "%02X ", message[i]);
hex_dump += hex;
if (message[i] >= 32 && message[i] < 127) {
ascii_dump += static_cast<char>(message[i]);
} else {
ascii_dump += '.';
}
}
LOG_INFO(Network, "SendTo packet HEX (first 200): {}", hex_dump);
LOG_INFO(Network, "SendTo packet ASCII (first 200): {}", ascii_dump);
}
const auto result = sendto(fd, reinterpret_cast<const char*>(message.data()),
static_cast<int>(message.size()), 0, to, to_len);
if (result != SOCKET_ERROR) {
LOG_INFO(Network, "SendTo success: sent {} bytes", result);
return {static_cast<s32>(result), Errno::SUCCESS};
}
LOG_ERROR(Network, "SendTo failed!");
return {-1, GetAndLogLastError(CallType::Send)};
}
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

@@ -107,7 +107,7 @@ else()
endif()
find_package(Boost 1.57 REQUIRED)
find_package(fmt 8 CONFIG)
# find_package(fmt 8 CONFIG)
# Pull in externals CMakeLists for libs where available
add_subdirectory(externals)

View File

@@ -97,15 +97,15 @@ public:
MemoryWrite32(vaddr + 4, static_cast<u32>(value >> 32));
}
void InterpreterFallback(u32 pc, size_t num_instructions) override {
void InterpreterFallback(u32 /*pc*/, size_t /*num_instructions*/) override {
UNREACHABLE(); //ASSERT(false && "InterpreterFallback({:08x} && {}) code = {:08x}", pc, num_instructions, *MemoryReadCode(pc));
}
void CallSVC(std::uint32_t swi) override {
void CallSVC(std::uint32_t /*swi*/) override {
UNREACHABLE(); //ASSERT(false && "CallSVC({})", swi);
}
void ExceptionRaised(u32 pc, Dynarmic::A32::Exception /*exception*/) override {
void ExceptionRaised(u32 /*pc*/, Dynarmic::A32::Exception /*exception*/) override {
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:08x}) code = {:08x}", pc, *MemoryReadCode(pc));
}
@@ -190,15 +190,15 @@ public:
return true;
}
void InterpreterFallback(std::uint32_t pc, size_t num_instructions) override {
void InterpreterFallback(std::uint32_t /*pc*/, size_t /*num_instructions*/) override {
UNREACHABLE(); //ASSERT(false && "InterpreterFallback({:016x} && {})", pc, num_instructions);
}
void CallSVC(std::uint32_t swi) override {
void CallSVC(std::uint32_t /*swi*/) override {
UNREACHABLE(); //ASSERT(false && "CallSVC({})", swi);
}
void ExceptionRaised(std::uint32_t pc, Dynarmic::A32::Exception) override {
void ExceptionRaised(std::uint32_t /*pc*/, Dynarmic::A32::Exception) override {
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:016x})", pc);
}

View File

@@ -105,15 +105,15 @@ public:
return true;
}
void InterpreterFallback(u64 pc, size_t num_instructions) override {
void InterpreterFallback(u64 /*pc*/, size_t /*num_instructions*/) override {
UNREACHABLE(); // ASSERT(false&& "InterpreterFallback({:016x} && {})", pc, num_instructions);
}
void CallSVC(std::uint32_t swi) override {
void CallSVC(std::uint32_t /*swi*/) override {
UNREACHABLE(); //ASSERT(false && "CallSVC({})", swi);
}
void ExceptionRaised(u64 pc, Dynarmic::A64::Exception /*exception*/) override {
void ExceptionRaised(u64 /*pc*/, Dynarmic::A64::Exception /*exception*/) override {
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:016x})", pc);
}
@@ -208,15 +208,15 @@ public:
return true;
}
void InterpreterFallback(u64 pc, size_t num_instructions) override {
void InterpreterFallback(u64 /*pc*/, size_t /*num_instructions*/) override {
ASSERT(ignore_invalid_insn && "InterpreterFallback");
}
void CallSVC(std::uint32_t swi) override {
void CallSVC(std::uint32_t /*swi*/) override {
UNREACHABLE(); //ASSERT(false && "CallSVC({})", swi);
}
void ExceptionRaised(u64 pc, Dynarmic::A64::Exception) override {
void ExceptionRaised(u64 /*pc*/, Dynarmic::A64::Exception) override {
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:016x})", pc);
}

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

@@ -1705,21 +1705,26 @@ Binding BufferCache<P>::StorageBufferBinding(GPUVAddr ssbo_addr, u32 cbuf_index,
return NULL_BINDING;
}
// xbzk: New size logic. Fixes MCI.
// If ever the * comment below prove wrong, the 'if' block may be removed.
const auto size = [&]() {
const bool is_nvn_cbuf = cbuf_index == 0;
// The NVN driver buffer (index 0) is known to pack the SSBO address followed by its size.
if (is_nvn_cbuf) {
const u32 ssbo_size = gpu_memory->Read<u32>(ssbo_addr + 8);
if (ssbo_size != 0) {
return ssbo_size;
// * The NVN driver buffer (index 0) is known to pack the SSBO address followed by its size.
const u64 next_qword = gpu_memory->Read<u64>(ssbo_addr + 8);
const u32 upper_32 = static_cast<u32>(next_qword >> 32);
// Hardware-based detection: GPU addresses have non-zero upper bits
if (upper_32 == 0) {
// This is a size field, not a GPU address
return static_cast<u32>(next_qword); // Return lower_32
}
}
// Other titles (notably Doom Eternal) may use STG/LDG on buffer addresses in custom defined
// cbufs, which do not store the sizes adjacent to the addresses, so use the fully
// mapped buffer size for now.
// Fall through: either not NVN cbuf (Doom Eternal & +), or NVN but ssbo_addr+8 is a GPU address (MCI)
const u32 memory_layout_size = static_cast<u32>(gpu_memory->GetMemoryLayoutSize(gpu_addr));
// Cap at 8MB to prevent allocator overflow from misinterpreted addresses
return (std::min)(memory_layout_size, static_cast<u32>(8_MiB));
}();
// Alignment only applies to the offset of the buffer
const u32 alignment = runtime.GetStorageBufferAlignment();
const GPUVAddr aligned_gpu_addr = Common::AlignDown(gpu_addr, alignment);

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

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -8,6 +8,7 @@
#include "common/logging/log.h"
#include "common/scope_exit.h"
#include "common/settings.h"
#include <thread>
#include "core/memory.h"
#include "video_core/host1x/ffmpeg/ffmpeg.h"
#include "video_core/memory_manager.h"

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -7,6 +7,9 @@
#include <array>
#include <tuple>
#include <stdint.h>
#include <thread>
#include <vector>
extern "C" {
#if defined(__GNUC__) || defined(__clang__)
@@ -107,9 +110,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 +135,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:
@@ -191,20 +215,44 @@ void Vic::ReadProgressiveY8__V8U8_N420(const SlotStruct& slot, std::span<const P
const auto alpha{u16(slot.config.planar_alpha.Value())};
for (s32 y = 0; y < in_luma_height; y++) {
const auto src_luma{y * in_luma_stride};
const auto src_chroma{(y / 2) * in_chroma_stride};
const auto dst{y * out_luma_stride};
for (s32 x = 0; x < in_luma_width; x++) {
slot_surface[dst + x].r = u16(luma_buffer[src_luma + x] << 2);
// Chroma samples are duplicated horizontally and vertically.
if(planar) {
slot_surface[dst + x].g = u16(chroma_u_buffer[src_chroma + x / 2] << 2);
slot_surface[dst + x].b = u16(chroma_v_buffer[src_chroma + x / 2] << 2);
const u8* luma_ptr = luma_buffer + y * in_luma_stride;
const u8* chroma_u_ptr = chroma_u_buffer + (y / 2) * in_chroma_stride;
// For planar, V buffer is separate. For NV12, it is not used directly in the same way.
const u8* chroma_v_ptr = planar ? (chroma_v_buffer + (y / 2) * in_chroma_stride) : nullptr;
Pixel* dst_ptr = &slot_surface[y * out_luma_stride];
for (s32 x = 0; x < in_luma_width; x += 2) {
u16 u_val, v_val;
if (planar) {
// YUV420P: U and V are in separate planes.
// 1 UV pair for 2 horizontal pixels.
u_val = u16(chroma_u_ptr[x / 2] << 2);
v_val = u16(chroma_v_ptr[x / 2] << 2);
} else {
slot_surface[dst + x].g = u16(chroma_u_buffer[src_chroma + (x & ~1) + 0] << 2);
slot_surface[dst + x].b = u16(chroma_u_buffer[src_chroma + (x & ~1) + 1] << 2);
// NV12: UV are interleaved in the second plane.
// U is at even byte, V is at odd byte.
// x is even (0, 2, 4...), so x corresponds to the byte offset in the interleaved buffer.
u_val = u16(chroma_u_ptr[x] << 2);
v_val = u16(chroma_u_ptr[x + 1] << 2);
}
slot_surface[dst + x].a = alpha;
// Pixel 1 (Even x)
dst_ptr[0].r = u16(luma_ptr[x] << 2);
dst_ptr[0].g = u_val;
dst_ptr[0].b = v_val;
dst_ptr[0].a = alpha;
// Pixel 2 (Odd x), check boundary
if (x + 1 < in_luma_width) {
dst_ptr[1].r = u16(luma_ptr[x + 1] << 2);
dst_ptr[1].g = u_val;
dst_ptr[1].b = v_val;
dst_ptr[1].a = alpha;
}
dst_ptr += 2;
}
}
}

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));