@@ -43,8 +43,11 @@ class ESDevice final : public Device
public:
ESDevice(Kernel& ios, const std::string& device_name);

static void InitializeEmulationState();
static void FinalizeEmulationState();

ReturnCode DIVerify(const ES::TMDReader& tmd, const ES::TicketReader& ticket);
bool LaunchTitle(u64 title_id, bool skip_reload = false);
bool LaunchTitle(u64 title_id, HangPPC hang_ppc = HangPPC::No);

void DoState(PointerWrap& p) override;

@@ -343,8 +346,8 @@ class ESDevice final : public Device
ContextArray::iterator FindActiveContext(s32 fd);
ContextArray::iterator FindInactiveContext();

bool LaunchIOS(u64 ios_title_id);
bool LaunchPPCTitle(u64 title_id, bool skip_reload);
bool LaunchIOS(u64 ios_title_id, HangPPC hang_ppc);
bool LaunchPPCTitle(u64 title_id);
bool IsActiveTitlePermittedByTicket(const u8* ticket_view) const;

ReturnCode CheckStreamKeyPermissions(u32 uid, const u8* ticket_view,
@@ -364,8 +367,14 @@ class ESDevice final : public Device
void FinishStaleImport(u64 title_id);
void FinishAllStaleImports();

void FinishInit();

std::string GetContentPath(u64 title_id, const ES::Content& content, Ticks ticks = {}) const;

s32 WriteSystemFile(const std::string& path, const std::vector<u8>& data, Ticks ticks = {});
s32 WriteLaunchFile(const ES::TMDReader& tmd, Ticks ticks = {});
bool BootstrapPPC();

struct OpenedContent
{
bool m_opened = false;
@@ -380,5 +389,6 @@ class ESDevice final : public Device

ContextArray m_contexts;
TitleContext m_title_context{};
std::string m_pending_ppc_boot_content_path;
};
} // namespace IOS::HLE
@@ -151,6 +151,8 @@ struct Ticket
static_assert(sizeof(Ticket) == 0x2A4, "Ticket has the wrong size");
#pragma pack(pop)

constexpr u32 MAX_TMD_SIZE = 0x49e4;

class SignedBlobReader
{
public:
@@ -393,4 +393,47 @@ std::string ESDevice::GetContentPath(const u64 title_id, const ES::Content& cont
}
return fmt::format("{}/{:08x}.app", Common::GetTitleContentPath(title_id), content.id);
}

s32 ESDevice::WriteSystemFile(const std::string& path, const std::vector<u8>& data, Ticks ticks)
{
auto& fs = *m_ios.GetFSDevice();
const std::string tmp_path = "/tmp/" + PathToFileName(path);

auto result = fs.CreateFile(PID_KERNEL, PID_KERNEL, tmp_path, {},
{FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::None}, ticks);
if (result != FS::ResultCode::Success)
{
ERROR_LOG_FMT(IOS_ES, "Failed to create temporary file {}: {}", tmp_path, result);
return FS::ConvertResult(result);
}

const auto fd = fs.Open(PID_KERNEL, PID_KERNEL, tmp_path, FS::Mode::ReadWrite, {}, ticks);
if (fd < 0)
{
ERROR_LOG_FMT(IOS_ES, "Failed to open temporary file {}: {}", tmp_path, fd);
return fd;
}

if (fs.Write(fd, data.data(), u32(data.size()), {}, ticks) != s32(data.size()))
{
ERROR_LOG_FMT(IOS_ES, "Failed to write to temporary file {}", tmp_path);
return ES_EIO;
}

if (const auto ret = fs.Close(fd, ticks); ret != IPC_SUCCESS)
{
ERROR_LOG_FMT(IOS_ES, "Failed to close temporary file {}", tmp_path);
return ret;
}

result = fs.RenameFile(PID_KERNEL, PID_KERNEL, tmp_path, path, ticks);
if (result != FS::ResultCode::Success)
{
ERROR_LOG_FMT(IOS_ES, "Failed to move launch file to final destination ({}): {}", path, result);
return FS::ConvertResult(result);
}

return IPC_SUCCESS;
}

} // namespace IOS::HLE
@@ -610,15 +610,35 @@ IPCReply FSDevice::GetAttribute(const Handle& handle, const IOCtlRequest& reques
return GetFSReply(IPC_SUCCESS, ticks);
}

FS::ResultCode FSDevice::DeleteFile(FS::Uid uid, FS::Gid gid, const std::string& path, Ticks ticks)
{
ticks.Add(IPC_OVERHEAD_TICKS);

const ResultCode result = m_ios.GetFS()->Delete(uid, gid, path);
ticks.Add(GetSuperblockWriteTbTicks(m_ios.GetVersion()));
LogResult(result, "Delete({})", path);
return result;
}

IPCReply FSDevice::DeleteFile(const Handle& handle, const IOCtlRequest& request)
{
if (request.buffer_in_size < 64)
return GetFSReply(ConvertResult(ResultCode::Invalid));

const std::string path = Memory::GetString(request.buffer_in, 64);
const ResultCode result = m_ios.GetFS()->Delete(handle.uid, handle.gid, path);
LogResult(result, "Delete({})", path);
return GetReplyForSuperblockOperation(m_ios.GetVersion(), result);
return MakeIPCReply(
[&](Ticks ticks) { return ConvertResult(DeleteFile(handle.uid, handle.gid, path, ticks)); });
}

FS::ResultCode FSDevice::RenameFile(FS::Uid uid, FS::Gid gid, const std::string& old_path,
const std::string& new_path, Ticks ticks)
{
ticks.Add(IPC_OVERHEAD_TICKS);

const ResultCode result = m_ios.GetFS()->Rename(uid, gid, old_path, new_path);
ticks.Add(GetSuperblockWriteTbTicks(m_ios.GetVersion()));
LogResult(result, "Rename({}, {})", old_path, new_path);
return result;
}

IPCReply FSDevice::RenameFile(const Handle& handle, const IOCtlRequest& request)
@@ -628,21 +648,31 @@ IPCReply FSDevice::RenameFile(const Handle& handle, const IOCtlRequest& request)

const std::string old_path = Memory::GetString(request.buffer_in, 64);
const std::string new_path = Memory::GetString(request.buffer_in + 64, 64);
const ResultCode result = m_ios.GetFS()->Rename(handle.uid, handle.gid, old_path, new_path);
LogResult(result, "Rename({}, {})", old_path, new_path);
return GetReplyForSuperblockOperation(m_ios.GetVersion(), result);
return MakeIPCReply([&](Ticks ticks) {
return ConvertResult(RenameFile(handle.uid, handle.gid, old_path, new_path, ticks));
});
}

FS::ResultCode FSDevice::CreateFile(FS::Uid uid, FS::Gid gid, const std::string& path,
FS::FileAttribute attribute, FS::Modes modes, Ticks ticks)
{
ticks.Add(IPC_OVERHEAD_TICKS);

const ResultCode result = m_ios.GetFS()->CreateFile(uid, gid, path, attribute, modes);
ticks.Add(GetSuperblockWriteTbTicks(m_ios.GetVersion()));
LogResult(result, "CreateFile({})", path);
return result;
}

IPCReply FSDevice::CreateFile(const Handle& handle, const IOCtlRequest& request)
{
const auto params = GetParams<ISFSParams>(request);
if (!params)
return GetFSReply(ConvertResult(params.Error()));

const ResultCode result = m_ios.GetFS()->CreateFile(handle.uid, handle.gid, params->path,
params->attribute, params->modes);
LogResult(result, "CreateFile({})", params->path);
return GetReplyForSuperblockOperation(m_ios.GetVersion(), result);
return MakeIPCReply([&](Ticks ticks) {
return ConvertResult(
CreateFile(handle.uid, handle.gid, params->path, params->attribute, params->modes));
});
}

IPCReply FSDevice::SetFileVersionControl(const Handle& handle, const IOCtlRequest& request)
@@ -35,7 +35,13 @@ class FSDevice : public Device
s32 Write(u64 fd, const u8* data, u32 size, std::optional<u32> ipc_buffer_addr = {},
Ticks ticks = {});
s32 Seek(u64 fd, u32 offset, FS::SeekMode mode, Ticks ticks = {});

FS::Result<FS::FileStatus> GetFileStatus(u64 fd, Ticks ticks = {});
FS::ResultCode RenameFile(FS::Uid uid, FS::Gid gid, const std::string& old_path,
const std::string& new_path, Ticks ticks = {});
FS::ResultCode DeleteFile(FS::Uid uid, FS::Gid gid, const std::string& path, Ticks ticks = {});
FS::ResultCode CreateFile(FS::Uid uid, FS::Gid gid, const std::string& path,
FS::FileAttribute attribute, FS::Modes modes, Ticks ticks = {});

template <typename T>
s32 Read(u64 fd, T* data, size_t count, Ticks ticks = {})
@@ -64,9 +64,10 @@ namespace IOS::HLE
static std::unique_ptr<EmulationKernel> s_ios;

constexpr u64 ENQUEUE_REQUEST_FLAG = 0x100000000ULL;
constexpr u64 ENQUEUE_ACKNOWLEDGEMENT_FLAG = 0x200000000ULL;
static CoreTiming::EventType* s_event_enqueue;
static CoreTiming::EventType* s_event_sdio_notify;
static CoreTiming::EventType* s_event_finish_ppc_bootstrap;
static CoreTiming::EventType* s_event_finish_ios_boot;

constexpr u32 ADDR_MEM1_SIZE = 0x3100;
constexpr u32 ADDR_MEM1_SIM_SIZE = 0x3104;
@@ -137,6 +138,12 @@ static bool SetupMemory(u64 ios_title_id, MemorySetupType setup_type)
return true;
}

// This region is typically used to store constants (e.g. game ID, console type, ...)
// and system information (see below).
constexpr u32 LOW_MEM1_REGION_START = 0;
constexpr u32 LOW_MEM1_REGION_SIZE = 0x3fff;
Memory::Memset(LOW_MEM1_REGION_START, 0, LOW_MEM1_REGION_SIZE);

Memory::Write_U32(target_imv->mem1_physical_size, ADDR_MEM1_SIZE);
Memory::Write_U32(target_imv->mem1_simulated_size, ADDR_MEM1_SIM_SIZE);
Memory::Write_U32(target_imv->mem1_end, ADDR_MEM1_END);
@@ -170,6 +177,28 @@ static bool SetupMemory(u64 ios_title_id, MemorySetupType setup_type)
return true;
}

// On a real console, the Starlet resets the PPC and holds it in reset limbo
// by asserting the PPC's HRESET signal (via HW_RESETS).
// We will simulate that by resetting MSR and putting the PPC into an infinite loop.
// The memory write will not be observable since the PPC is not running any code...
static void ResetAndPausePPC()
{
// This should be cleared when the PPC is released so that the write is not observable.
Memory::Write_U32(0x48000000, 0x00000000); // b 0x0
PowerPC::Reset();
PC = 0;
}

static void ReleasePPC()
{
Memory::Write_U32(0, 0);
// HLE the bootstub that jumps to 0x3400.
// NAND titles start with address translation off at 0x3400 (via the PPC bootstub)
// The state of other CPU registers (like the BAT registers) doesn't matter much
// because the realmode code at 0x3400 initializes everything itself anyway.
PC = 0x3400;
}

void RAMOverrideForIOSMemoryValues(MemorySetupType setup_type)
{
// Don't touch anything if the feature isn't enabled.
@@ -260,9 +289,6 @@ EmulationKernel::EmulationKernel(u64 title_id) : Kernel(title_id)
return;
}

// IOS re-inits IPC and sends a dummy ack during its boot process.
EnqueueIPCAcknowledgement(0);

AddCoreDevices();
AddStaticDevices();
}
@@ -317,18 +343,19 @@ u16 Kernel::GetGidForPPC() const
return m_ppc_gid;
}

static std::vector<u8> ReadBootContent(FS::FileSystem* fs, const std::string& path, size_t max_size)
static std::vector<u8> ReadBootContent(FSDevice* fs, const std::string& path, size_t max_size,
Ticks ticks = {})
{
const auto file = fs->OpenFile(0, 0, path, FS::Mode::Read);
if (!file)
const s64 fd = fs->Open(0, 0, path, FS::Mode::Read, {}, ticks);
if (fd < 0)
return {};

const size_t file_size = file->GetStatus()->size;
const size_t file_size = fs->GetFileStatus(fd, ticks)->size;
if (max_size != 0 && file_size > max_size)
return {};

std::vector<u8> buffer(file_size);
if (!file->Read(buffer.data(), buffer.size()))
if (!fs->Read(fd, buffer.data(), buffer.size(), ticks))
return {};
return buffer;
}
@@ -337,23 +364,25 @@ static std::vector<u8> ReadBootContent(FS::FileSystem* fs, const std::string& pa
// Unlike 0x42, IOS will set up some constants in memory before booting the PPC.
bool Kernel::BootstrapPPC(const std::string& boot_content_path)
{
const DolReader dol{ReadBootContent(m_fs.get(), boot_content_path, 0)};
// Seeking and processing overhead is ignored as most time is spent reading from the NAND.
u64 ticks = 0;

const DolReader dol{ReadBootContent(GetFSDevice().get(), boot_content_path, 0, &ticks)};

if (!dol.IsValid())
return false;

if (!SetupMemory(m_title_id, MemorySetupType::Full))
return false;

// Reset the PPC and pause its execution until we're ready.
ResetAndPausePPC();

if (!dol.LoadIntoMemory())
return false;

// NAND titles start with address translation off at 0x3400 (via the PPC bootstub)
// The state of other CPU registers (like the BAT registers) doesn't matter much
// because the realmode code at 0x3400 initializes everything itself anyway.
MSR.Hex = 0;
PC = 0x3400;

INFO_LOG_FMT(IOS, "BootstrapPPC: {}", boot_content_path);
CoreTiming::ScheduleEvent(ticks, s_event_finish_ppc_bootstrap);
return true;
}

@@ -382,21 +411,40 @@ struct ARMBinary final
std::vector<u8> m_bytes;
};

static void FinishIOSBoot(u64 ios_title_id)
{
// Shut down the active IOS first before switching to the new one.
s_ios.reset();
s_ios = std::make_unique<EmulationKernel>(ios_title_id);
}

static constexpr SystemTimers::TimeBaseTick GetIOSBootTicks(u32 version)
{
// Older IOS versions are monolithic so the main ELF is much larger and takes longer to load.
if (version < 28)
return 16'000'000_tbticks;
return 2'600'000_tbticks;
}

// Similar to syscall 0x42 (ios_boot); this is used to change the current active IOS.
// IOS writes the new version to 0x3140 before restarting, but it does *not* poke any
// of the other constants to the memory. Warning: this resets the kernel instance.
//
// Passing a boot content path is optional because we do not require IOSes
// to be installed at the moment. If one is passed, the boot binary must exist
// on the NAND, or the call will fail like on a Wii.
bool Kernel::BootIOS(const u64 ios_title_id, const std::string& boot_content_path)
bool Kernel::BootIOS(const u64 ios_title_id, HangPPC hang_ppc, const std::string& boot_content_path)
{
// IOS suspends regular PPC<->ARM IPC before loading a new IOS.
// IPC is not resumed if the boot fails for any reason.
m_ipc_paused = true;

if (!boot_content_path.empty())
{
// Load the ARM binary to memory (if possible).
// Because we do not actually emulate the Starlet, only load the sections that are in MEM1.

ARMBinary binary{ReadBootContent(m_fs.get(), boot_content_path, 0xB00000)};
ARMBinary binary{ReadBootContent(GetFSDevice().get(), boot_content_path, 0xB00000)};
if (!binary.IsValid())
return false;

@@ -405,12 +453,26 @@ bool Kernel::BootIOS(const u64 ios_title_id, const std::string& boot_content_pat
return false;
}

// Shut down the active IOS first before switching to the new one.
s_ios.reset();
s_ios = std::make_unique<EmulationKernel>(ios_title_id);
if (hang_ppc == HangPPC::Yes)
ResetAndPausePPC();

if (Core::IsRunningAndStarted())
CoreTiming::ScheduleEvent(GetIOSBootTicks(GetVersion()), s_event_finish_ios_boot, ios_title_id);
else
FinishIOSBoot(ios_title_id);

return true;
}

void Kernel::InitIPC()
{
if (s_ios == nullptr)
return;

INFO_LOG_FMT(IOS, "IPC initialised.");
GenerateAck(0);
}

void Kernel::AddDevice(std::unique_ptr<Device> device)
{
ASSERT(device->GetDeviceType() == Device::DeviceType::Static);
@@ -658,17 +720,9 @@ void Kernel::EnqueueIPCReply(const Request& request, const s32 return_value, s64
CoreTiming::ScheduleEvent(cycles_in_future, s_event_enqueue, request.address, from);
}

void Kernel::EnqueueIPCAcknowledgement(u32 address, int cycles_in_future)
{
CoreTiming::ScheduleEvent(cycles_in_future, s_event_enqueue,
address | ENQUEUE_ACKNOWLEDGEMENT_FLAG);
}

void Kernel::HandleIPCEvent(u64 userdata)
{
if (userdata & ENQUEUE_ACKNOWLEDGEMENT_FLAG)
m_ack_queue.push_back(static_cast<u32>(userdata));
else if (userdata & ENQUEUE_REQUEST_FLAG)
if (userdata & ENQUEUE_REQUEST_FLAG)
m_request_queue.push_back(static_cast<u32>(userdata));
else
m_reply_queue.push_back(static_cast<u32>(userdata));
@@ -678,7 +732,7 @@ void Kernel::HandleIPCEvent(u64 userdata)

void Kernel::UpdateIPC()
{
if (!IsReady())
if (m_ipc_paused || !IsReady())
return;

if (!m_request_queue.empty())
@@ -698,14 +752,6 @@ void Kernel::UpdateIPC()
m_reply_queue.pop_front();
return;
}

if (!m_ack_queue.empty())
{
GenerateAck(m_ack_queue.front());
WARN_LOG_FMT(IOS, "<<-- Double-ack to IPC Request @ {:#010x}", m_ack_queue.front());
m_ack_queue.pop_front();
return;
}
}

void Kernel::UpdateDevices()
@@ -740,6 +786,7 @@ void Kernel::DoState(PointerWrap& p)
p.Do(m_request_queue);
p.Do(m_reply_queue);
p.Do(m_last_reply_time);
p.Do(m_ipc_paused);
p.Do(m_title_id);
p.Do(m_ppc_uid);
p.Do(m_ppc_gid);
@@ -809,6 +856,13 @@ IOSC& Kernel::GetIOSC()
return m_iosc;
}

static void FinishPPCBootstrap(u64 userdata, s64 cycles_late)
{
ReleasePPC();
SConfig::OnNewTitleLoad();
INFO_LOG_FMT(IOS, "Bootstrapping done.");
}

void Init()
{
s_event_enqueue = CoreTiming::RegisterEvent("IPCEvent", [](u64 userdata, s64) {
@@ -826,6 +880,14 @@ void Init()
device->EventNotify();
});

ESDevice::InitializeEmulationState();

s_event_finish_ppc_bootstrap =
CoreTiming::RegisterEvent("IOSFinishPPCBootstrap", FinishPPCBootstrap);

s_event_finish_ios_boot = CoreTiming::RegisterEvent(
"IOSFinishIOSBoot", [](u64 ios_title_id, s64) { FinishIOSBoot(ios_title_id); });

DIDevice::s_finish_executing_di_command =
CoreTiming::RegisterEvent("FinishDICommand", DIDevice::FinishDICommandCallback);

@@ -842,6 +904,7 @@ void Init()
void Shutdown()
{
s_ios.reset();
ESDevice::FinalizeEmulationState();
}

EmulationKernel* GetIOS()
@@ -97,6 +97,12 @@ enum class MemorySetupType
Full,
};

enum class HangPPC : bool
{
No = false,
Yes = true,
};

void RAMOverrideForIOSMemoryValues(MemorySetupType setup_type);

void WriteReturnValue(s32 value, u32 address);
@@ -132,7 +138,9 @@ class Kernel
u16 GetGidForPPC() const;

bool BootstrapPPC(const std::string& boot_content_path);
bool BootIOS(u64 ios_title_id, const std::string& boot_content_path = "");
bool BootIOS(u64 ios_title_id, HangPPC hang_ppc = HangPPC::No,
const std::string& boot_content_path = {});
void InitIPC();
u32 GetVersion() const;

IOSC& GetIOSC();
@@ -142,7 +150,6 @@ class Kernel

void ExecuteIPCCommand(u32 address);
std::optional<IPCReply> HandleIPCCommand(const Request& request);
void EnqueueIPCAcknowledgement(u32 address, int cycles_in_future = 0);

void AddDevice(std::unique_ptr<Device> device);
void AddCoreDevices();
@@ -165,8 +172,8 @@ class Kernel
using IPCMsgQueue = std::deque<u32>;
IPCMsgQueue m_request_queue; // ppc -> arm
IPCMsgQueue m_reply_queue; // arm -> ppc
IPCMsgQueue m_ack_queue; // arm -> ppc
u64 m_last_reply_time = 0;
bool m_ipc_paused = false;

IOSC m_iosc;
std::shared_ptr<FS::FileSystem> m_fs;
@@ -74,7 +74,7 @@ static Common::Event g_compressAndDumpStateSyncEvent;
static std::thread g_save_thread;

// Don't forget to increase this after doing changes on the savestate system
constexpr u32 STATE_VERSION = 129; // Last changed in PR 9511
constexpr u32 STATE_VERSION = 130; // Last changed in PR 9545

// Maps savestate versions to Dolphin versions.
// Versions after 42 don't need to be added to this list,
@@ -585,7 +585,6 @@ void DirectoryBlobReader::SetPartitionHeader(DirectoryBlobPartition* partition,
constexpr u32 TICKET_OFFSET = 0x0;
constexpr u32 TICKET_SIZE = 0x2a4;
constexpr u32 TMD_OFFSET = 0x2c0;
constexpr u32 MAX_TMD_SIZE = 0x49e4;
constexpr u32 H3_OFFSET = 0x4000;
constexpr u32 H3_SIZE = 0x18000;

@@ -595,7 +594,7 @@ void DirectoryBlobReader::SetPartitionHeader(DirectoryBlobPartition* partition,
partition_address + TICKET_OFFSET, TICKET_SIZE, partition_root + "ticket.bin");

const u64 tmd_size = m_nonpartition_contents.CheckSizeAndAdd(
partition_address + TMD_OFFSET, MAX_TMD_SIZE, partition_root + "tmd.bin");
partition_address + TMD_OFFSET, IOS::ES::MAX_TMD_SIZE, partition_root + "tmd.bin");

const u64 cert_offset = Common::AlignUp(TMD_OFFSET + tmd_size, 0x20ull);
const u64 max_cert_size = H3_OFFSET - cert_offset;