mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-01-26 07:45:33 +01:00
IOS: Implement OH0 (/dev/usb/oh0)
This commit is contained in:
parent
c8a6dc6c23
commit
ee188a1d5a
@ -155,6 +155,8 @@ set(SRCS ActionReplay.cpp
|
|||||||
IOS/STM/STM.cpp
|
IOS/STM/STM.cpp
|
||||||
IOS/USB/Common.cpp
|
IOS/USB/Common.cpp
|
||||||
IOS/USB/Host.cpp
|
IOS/USB/Host.cpp
|
||||||
|
IOS/USB/OH0/OH0.cpp
|
||||||
|
IOS/USB/OH0/OH0Device.cpp
|
||||||
IOS/USB/USBV0.cpp
|
IOS/USB/USBV0.cpp
|
||||||
IOS/USB/USB_KBD.cpp
|
IOS/USB/USB_KBD.cpp
|
||||||
IOS/USB/USB_VEN.cpp
|
IOS/USB/USB_VEN.cpp
|
||||||
|
@ -192,6 +192,8 @@
|
|||||||
<ClCompile Include="IOS\USB\Host.cpp">
|
<ClCompile Include="IOS\USB\Host.cpp">
|
||||||
<DisableSpecificWarnings>4200;%(DisableSpecificWarnings)</DisableSpecificWarnings>
|
<DisableSpecificWarnings>4200;%(DisableSpecificWarnings)</DisableSpecificWarnings>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="IOS\USB\OH0\OH0.cpp" />
|
||||||
|
<ClCompile Include="IOS\USB\OH0\OH0Device.cpp" />
|
||||||
<ClCompile Include="IOS\USB\USBV0.cpp" />
|
<ClCompile Include="IOS\USB\USBV0.cpp" />
|
||||||
<ClCompile Include="IOS\USB\USB_HIDv4.cpp">
|
<ClCompile Include="IOS\USB\USB_HIDv4.cpp">
|
||||||
<!--
|
<!--
|
||||||
@ -427,6 +429,8 @@
|
|||||||
<ClInclude Include="IOS\USB\Common.h" />
|
<ClInclude Include="IOS\USB\Common.h" />
|
||||||
<ClInclude Include="IOS\USB\LibusbDevice.h" />
|
<ClInclude Include="IOS\USB\LibusbDevice.h" />
|
||||||
<ClInclude Include="IOS\USB\Host.h" />
|
<ClInclude Include="IOS\USB\Host.h" />
|
||||||
|
<ClInclude Include="IOS\USB\OH0\OH0.h" />
|
||||||
|
<ClInclude Include="IOS\USB\OH0\OH0Device.h" />
|
||||||
<ClInclude Include="IOS\USB\USBV0.h" />
|
<ClInclude Include="IOS\USB\USBV0.h" />
|
||||||
<ClInclude Include="IOS\USB\USB_HIDv4.h" />
|
<ClInclude Include="IOS\USB\USB_HIDv4.h" />
|
||||||
<ClInclude Include="IOS\USB\USB_KBD.h" />
|
<ClInclude Include="IOS\USB\USB_KBD.h" />
|
||||||
|
@ -779,6 +779,12 @@
|
|||||||
<ClCompile Include="IOS\USB\Host.cpp">
|
<ClCompile Include="IOS\USB\Host.cpp">
|
||||||
<Filter>IOS\USB</Filter>
|
<Filter>IOS\USB</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="IOS\USB\OH0\OH0.cpp">
|
||||||
|
<Filter>IOS\USB</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="IOS\USB\OH0\OH0Device.cpp">
|
||||||
|
<Filter>IOS\USB</Filter>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="IOS\USB\USBV0.cpp">
|
<ClCompile Include="IOS\USB\USBV0.cpp">
|
||||||
<Filter>IOS\USB</Filter>
|
<Filter>IOS\USB</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
@ -1365,6 +1371,12 @@
|
|||||||
<ClInclude Include="IOS\USB\Host.h">
|
<ClInclude Include="IOS\USB\Host.h">
|
||||||
<Filter>IOS\USB</Filter>
|
<Filter>IOS\USB</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="IOS\USB\OH0\OH0.h">
|
||||||
|
<Filter>IOS\USB</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="IOS\USB\OH0\OH0Device.h">
|
||||||
|
<Filter>IOS\USB</Filter>
|
||||||
|
</ClInclude>
|
||||||
<ClInclude Include="IOS\USB\USBV0.h">
|
<ClInclude Include="IOS\USB\USBV0.h">
|
||||||
<Filter>IOS\USB</Filter>
|
<Filter>IOS\USB</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
@ -76,6 +76,16 @@ bool IOCtlVRequest::HasInputVectorWithAddress(const u32 vector_address) const
|
|||||||
[&](const auto& in_vector) { return in_vector.address == vector_address; });
|
[&](const auto& in_vector) { return in_vector.address == vector_address; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IOCtlVRequest::HasNumberOfValidVectors(const size_t in_count, const size_t io_count) const
|
||||||
|
{
|
||||||
|
if (in_vectors.size() != in_count || io_vectors.size() != io_count)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto IsValidVector = [](const auto& vector) { return vector.size == 0 || vector.address != 0; };
|
||||||
|
return std::all_of(in_vectors.begin(), in_vectors.end(), IsValidVector) &&
|
||||||
|
std::all_of(io_vectors.begin(), io_vectors.end(), IsValidVector);
|
||||||
|
}
|
||||||
|
|
||||||
void IOCtlRequest::Log(const std::string& device_name, LogTypes::LOG_TYPE type,
|
void IOCtlRequest::Log(const std::string& device_name, LogTypes::LOG_TYPE type,
|
||||||
LogTypes::LOG_LEVELS verbosity) const
|
LogTypes::LOG_LEVELS verbosity) const
|
||||||
{
|
{
|
||||||
|
@ -42,6 +42,7 @@ enum ReturnCode : s32
|
|||||||
FS_EDIRDEPTH = -116, // Max directory depth exceeded
|
FS_EDIRDEPTH = -116, // Max directory depth exceeded
|
||||||
FS_EBUSY = -118, // Resource busy
|
FS_EBUSY = -118, // Resource busy
|
||||||
IPC_EESEXHAUSTED = -1016, // Max of 2 ES handles exceeded
|
IPC_EESEXHAUSTED = -1016, // Max of 2 ES handles exceeded
|
||||||
|
USB_ECANCELED = -7022, // USB OH0 insertion hook cancelled
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Request
|
struct Request
|
||||||
@ -122,6 +123,7 @@ struct IOCtlVRequest final : Request
|
|||||||
std::vector<IOVector> io_vectors;
|
std::vector<IOVector> io_vectors;
|
||||||
explicit IOCtlVRequest(u32 address);
|
explicit IOCtlVRequest(u32 address);
|
||||||
bool HasInputVectorWithAddress(u32 vector_address) const;
|
bool HasInputVectorWithAddress(u32 vector_address) const;
|
||||||
|
bool HasNumberOfValidVectors(size_t in_count, size_t io_count) const;
|
||||||
void Dump(const std::string& description, LogTypes::LOG_TYPE type = LogTypes::IOS,
|
void Dump(const std::string& description, LogTypes::LOG_TYPE type = LogTypes::IOS,
|
||||||
LogTypes::LOG_LEVELS level = LogTypes::LINFO) const;
|
LogTypes::LOG_LEVELS level = LogTypes::LINFO) const;
|
||||||
void DumpUnknown(const std::string& description, LogTypes::LOG_TYPE type = LogTypes::IOS,
|
void DumpUnknown(const std::string& description, LogTypes::LOG_TYPE type = LogTypes::IOS,
|
||||||
@ -137,6 +139,7 @@ public:
|
|||||||
{
|
{
|
||||||
Static, // Devices which appear in s_device_map.
|
Static, // Devices which appear in s_device_map.
|
||||||
FileIO, // FileIO devices which are created dynamically.
|
FileIO, // FileIO devices which are created dynamically.
|
||||||
|
OH0, // OH0 child devices which are created dynamically.
|
||||||
};
|
};
|
||||||
|
|
||||||
Device(u32 device_id, const std::string& device_name, DeviceType type = DeviceType::Static);
|
Device(u32 device_id, const std::string& device_name, DeviceType type = DeviceType::Static);
|
||||||
|
@ -49,6 +49,8 @@
|
|||||||
#include "Core/IOS/STM/STM.h"
|
#include "Core/IOS/STM/STM.h"
|
||||||
#include "Core/IOS/USB/Bluetooth/BTEmu.h"
|
#include "Core/IOS/USB/Bluetooth/BTEmu.h"
|
||||||
#include "Core/IOS/USB/Bluetooth/BTReal.h"
|
#include "Core/IOS/USB/Bluetooth/BTReal.h"
|
||||||
|
#include "Core/IOS/USB/OH0/OH0.h"
|
||||||
|
#include "Core/IOS/USB/OH0/OH0Device.h"
|
||||||
#include "Core/IOS/USB/USB_KBD.h"
|
#include "Core/IOS/USB/USB_KBD.h"
|
||||||
#include "Core/IOS/USB/USB_VEN.h"
|
#include "Core/IOS/USB/USB_VEN.h"
|
||||||
#include "Core/IOS/WFS/WFSI.h"
|
#include "Core/IOS/WFS/WFSI.h"
|
||||||
@ -516,6 +518,7 @@ void Reinit()
|
|||||||
#else
|
#else
|
||||||
AddDevice<Device::Stub>("/dev/usb/hid");
|
AddDevice<Device::Stub>("/dev/usb/hid");
|
||||||
#endif
|
#endif
|
||||||
|
AddDevice<Device::OH0>("/dev/usb/oh0");
|
||||||
AddDevice<Device::Stub>("/dev/usb/oh1");
|
AddDevice<Device::Stub>("/dev/usb/oh1");
|
||||||
AddDevice<Device::WFSSRV>("/dev/usb/wfssrv");
|
AddDevice<Device::WFSSRV>("/dev/usb/wfssrv");
|
||||||
AddDevice<Device::WFSI>("/dev/wfsi");
|
AddDevice<Device::WFSI>("/dev/wfsi");
|
||||||
@ -657,6 +660,10 @@ void DoState(PointerWrap& p)
|
|||||||
s_fdmap[i] = std::make_shared<Device::FileIO>(i, "");
|
s_fdmap[i] = std::make_shared<Device::FileIO>(i, "");
|
||||||
s_fdmap[i]->DoState(p);
|
s_fdmap[i]->DoState(p);
|
||||||
break;
|
break;
|
||||||
|
case Device::Device::DeviceType::OH0:
|
||||||
|
s_fdmap[i] = std::make_shared<Device::OH0Device>(i, "");
|
||||||
|
s_fdmap[i]->DoState(p);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -723,6 +730,10 @@ static s32 OpenDevice(const OpenRequest& request)
|
|||||||
if (!device)
|
if (!device)
|
||||||
return IPC_EESEXHAUSTED;
|
return IPC_EESEXHAUSTED;
|
||||||
}
|
}
|
||||||
|
else if (request.path.find("/dev/usb/oh0/") == 0 && !GetDeviceByName(request.path))
|
||||||
|
{
|
||||||
|
device = std::make_shared<Device::OH0Device>(new_fd, request.path);
|
||||||
|
}
|
||||||
else if (request.path.find("/dev/") == 0)
|
else if (request.path.find("/dev/") == 0)
|
||||||
{
|
{
|
||||||
device = GetDeviceByName(request.path);
|
device = GetDeviceByName(request.path);
|
||||||
|
356
Source/Core/Core/IOS/USB/OH0/OH0.cpp
Normal file
356
Source/Core/Core/IOS/USB/OH0/OH0.cpp
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
// Copyright 2017 Dolphin Emulator Project
|
||||||
|
// Licensed under GPLv2+
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <istream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "Common/Assert.h"
|
||||||
|
#include "Common/ChunkFile.h"
|
||||||
|
#include "Common/CommonFuncs.h"
|
||||||
|
#include "Common/Logging/Log.h"
|
||||||
|
#include "Core/Core.h"
|
||||||
|
#include "Core/CoreTiming.h"
|
||||||
|
#include "Core/HW/Memmap.h"
|
||||||
|
#include "Core/IOS/USB/Common.h"
|
||||||
|
#include "Core/IOS/USB/OH0/OH0.h"
|
||||||
|
#include "Core/IOS/USB/USBV0.h"
|
||||||
|
|
||||||
|
namespace IOS
|
||||||
|
{
|
||||||
|
namespace HLE
|
||||||
|
{
|
||||||
|
namespace Device
|
||||||
|
{
|
||||||
|
OH0::OH0(u32 device_id, const std::string& device_name) : USBHost(device_id, device_name)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
OH0::~OH0()
|
||||||
|
{
|
||||||
|
StopThreads();
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnCode OH0::Open(const OpenRequest& request)
|
||||||
|
{
|
||||||
|
const u32 ios_major_version = GetVersion();
|
||||||
|
if (ios_major_version == 57 || ios_major_version == 58 || ios_major_version == 59)
|
||||||
|
return IPC_EACCES;
|
||||||
|
return USBHost::Open(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::IOCtl(const IOCtlRequest& request)
|
||||||
|
{
|
||||||
|
request.Log(GetDeviceName(), LogTypes::IOS_USB);
|
||||||
|
switch (request.request)
|
||||||
|
{
|
||||||
|
case USB::IOCTL_USBV0_GETRHDESCA:
|
||||||
|
return GetRhDesca(request);
|
||||||
|
case USB::IOCTL_USBV0_CANCEL_INSERT_HOOK:
|
||||||
|
return CancelInsertionHook(request);
|
||||||
|
default:
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::IOCtlV(const IOCtlVRequest& request)
|
||||||
|
{
|
||||||
|
INFO_LOG(IOS_USB, "/dev/usb/oh0 - IOCtlV %u", request.request);
|
||||||
|
switch (request.request)
|
||||||
|
{
|
||||||
|
case USB::IOCTLV_USBV0_GETDEVLIST:
|
||||||
|
return GetDeviceList(request);
|
||||||
|
case USB::IOCTLV_USBV0_GETRHPORTSTATUS:
|
||||||
|
return GetRhPortStatus(request);
|
||||||
|
case USB::IOCTLV_USBV0_SETRHPORTSTATUS:
|
||||||
|
return SetRhPortStatus(request);
|
||||||
|
case USB::IOCTLV_USBV0_DEVINSERTHOOK:
|
||||||
|
return RegisterInsertionHook(request);
|
||||||
|
case USB::IOCTLV_USBV0_DEVICECLASSCHANGE:
|
||||||
|
return RegisterClassChangeHook(request);
|
||||||
|
case USB::IOCTLV_USBV0_DEVINSERTHOOKID:
|
||||||
|
return RegisterInsertionHookWithID(request);
|
||||||
|
default:
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OH0::DoState(PointerWrap& p)
|
||||||
|
{
|
||||||
|
if (p.GetMode() == PointerWrap::MODE_READ)
|
||||||
|
{
|
||||||
|
Core::DisplayMessage("It is suggested that you unplug and replug all connected USB devices.",
|
||||||
|
5000);
|
||||||
|
Core::DisplayMessage("If USB doesn't work properly, an emulation reset may be needed.", 5000);
|
||||||
|
}
|
||||||
|
p.Do(m_insertion_hooks);
|
||||||
|
p.Do(m_removal_hooks);
|
||||||
|
p.Do(m_opened_devices);
|
||||||
|
USBHost::DoState(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::CancelInsertionHook(const IOCtlRequest& request)
|
||||||
|
{
|
||||||
|
if (!request.buffer_in || request.buffer_in_size != 4)
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
|
||||||
|
// IOS assigns random IDs, but ours are simply the VID + PID (see RegisterInsertionHookWithID)
|
||||||
|
TriggerHook(m_insertion_hooks,
|
||||||
|
{Memory::Read_U16(request.buffer_in), Memory::Read_U16(request.buffer_in + 2)},
|
||||||
|
USB_ECANCELED);
|
||||||
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::GetDeviceList(const IOCtlVRequest& request) const
|
||||||
|
{
|
||||||
|
if (!request.HasNumberOfValidVectors(2, 2))
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
|
||||||
|
const u8 max_entries_count = Memory::Read_U8(request.in_vectors[0].address);
|
||||||
|
if (request.io_vectors[1].size != max_entries_count * sizeof(DeviceEntry))
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
|
||||||
|
const u8 interface_class = Memory::Read_U8(request.in_vectors[1].address);
|
||||||
|
u8 entries_count = 0;
|
||||||
|
std::lock_guard<std::mutex> lk(m_devices_mutex);
|
||||||
|
for (const auto& device : m_devices)
|
||||||
|
{
|
||||||
|
if (entries_count >= max_entries_count)
|
||||||
|
break;
|
||||||
|
if (!device.second->HasClass(interface_class))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
DeviceEntry entry;
|
||||||
|
entry.unknown = 0;
|
||||||
|
entry.vid = Common::swap16(device.second->GetVid());
|
||||||
|
entry.pid = Common::swap16(device.second->GetPid());
|
||||||
|
Memory::CopyToEmu(request.io_vectors[1].address + 8 * entries_count++, &entry, 8);
|
||||||
|
}
|
||||||
|
Memory::Write_U8(entries_count, request.io_vectors[0].address);
|
||||||
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::GetRhDesca(const IOCtlRequest& request) const
|
||||||
|
{
|
||||||
|
if (!request.buffer_out || request.buffer_out_size != 4)
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
|
||||||
|
// Based on a hardware test, this ioctl seems to return a constant value
|
||||||
|
Memory::Write_U32(0x02000302, request.buffer_out);
|
||||||
|
request.Dump(GetDeviceName(), LogTypes::IOS_USB, LogTypes::LWARNING);
|
||||||
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::GetRhPortStatus(const IOCtlVRequest& request) const
|
||||||
|
{
|
||||||
|
if (!request.HasNumberOfValidVectors(1, 1))
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
|
||||||
|
ERROR_LOG(IOS_USB, "Unimplemented IOCtlV: IOCTLV_USBV0_GETRHPORTSTATUS");
|
||||||
|
request.Dump(GetDeviceName(), LogTypes::IOS_USB, LogTypes::LERROR);
|
||||||
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::SetRhPortStatus(const IOCtlVRequest& request)
|
||||||
|
{
|
||||||
|
if (!request.HasNumberOfValidVectors(2, 0))
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
|
||||||
|
ERROR_LOG(IOS_USB, "Unimplemented IOCtlV: IOCTLV_USBV0_SETRHPORTSTATUS");
|
||||||
|
request.Dump(GetDeviceName(), LogTypes::IOS_USB, LogTypes::LERROR);
|
||||||
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::RegisterRemovalHook(const u64 device_id, const IOCtlRequest& request)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock{m_hooks_mutex};
|
||||||
|
// IOS only allows a single device removal hook.
|
||||||
|
if (m_removal_hooks.find(device_id) != m_removal_hooks.end())
|
||||||
|
return GetDefaultReply(IPC_EEXIST);
|
||||||
|
m_removal_hooks.insert({device_id, request.address});
|
||||||
|
return GetNoReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::RegisterInsertionHook(const IOCtlVRequest& request)
|
||||||
|
{
|
||||||
|
if (!request.HasNumberOfValidVectors(2, 0))
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
|
||||||
|
const u16 vid = Memory::Read_U16(request.in_vectors[0].address);
|
||||||
|
const u16 pid = Memory::Read_U16(request.in_vectors[1].address);
|
||||||
|
if (HasDeviceWithVidPid(vid, pid))
|
||||||
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock{m_hooks_mutex};
|
||||||
|
// TODO: figure out whether IOS allows more than one hook.
|
||||||
|
m_insertion_hooks[{vid, pid}] = request.address;
|
||||||
|
return GetNoReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::RegisterInsertionHookWithID(const IOCtlVRequest& request)
|
||||||
|
{
|
||||||
|
if (!request.HasNumberOfValidVectors(3, 1))
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock{m_hooks_mutex};
|
||||||
|
const u16 vid = Memory::Read_U16(request.in_vectors[0].address);
|
||||||
|
const u16 pid = Memory::Read_U16(request.in_vectors[1].address);
|
||||||
|
const bool trigger_only_for_new_device = Memory::Read_U8(request.in_vectors[2].address) == 1;
|
||||||
|
if (!trigger_only_for_new_device && HasDeviceWithVidPid(vid, pid))
|
||||||
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
// TODO: figure out whether IOS allows more than one hook.
|
||||||
|
m_insertion_hooks.insert({{vid, pid}, request.address});
|
||||||
|
// The output vector is overwritten with an ID to use with ioctl 31 for cancelling the hook.
|
||||||
|
Memory::Write_U32(vid << 16 | pid, request.io_vectors[0].address);
|
||||||
|
return GetNoReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::RegisterClassChangeHook(const IOCtlVRequest& request)
|
||||||
|
{
|
||||||
|
if (!request.HasNumberOfValidVectors(1, 0))
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
WARN_LOG(IOS_USB, "Unimplemented IOCtlV: USB::IOCTLV_USBV0_DEVICECLASSCHANGE (no reply)");
|
||||||
|
request.Dump(GetDeviceName(), LogTypes::IOS_USB, LogTypes::LWARNING);
|
||||||
|
return GetNoReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OH0::HasDeviceWithVidPid(const u16 vid, const u16 pid) const
|
||||||
|
{
|
||||||
|
return std::any_of(m_devices.begin(), m_devices.end(), [=](const auto& device) {
|
||||||
|
return device.second->GetVid() == vid && device.second->GetPid() == pid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void OH0::OnDeviceChange(const ChangeEvent event, std::shared_ptr<USB::Device> device)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_devices_mutex);
|
||||||
|
if (event == ChangeEvent::Inserted)
|
||||||
|
TriggerHook(m_insertion_hooks, {device->GetVid(), device->GetPid()}, IPC_SUCCESS);
|
||||||
|
else if (event == ChangeEvent::Removed)
|
||||||
|
TriggerHook(m_removal_hooks, device->GetId(), IPC_SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void OH0::TriggerHook(std::map<T, u32>& hooks, T value, const ReturnCode return_value)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk{m_hooks_mutex};
|
||||||
|
const auto hook = hooks.find(value);
|
||||||
|
if (hook == hooks.end())
|
||||||
|
return;
|
||||||
|
EnqueueReply(Request{hook->second}, return_value, 0, CoreTiming::FromThread::ANY);
|
||||||
|
hooks.erase(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<ReturnCode, u64> OH0::DeviceOpen(const u16 vid, const u16 pid)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_devices_mutex);
|
||||||
|
|
||||||
|
bool has_device_with_vid_pid = false;
|
||||||
|
for (const auto& device : m_devices)
|
||||||
|
{
|
||||||
|
if (device.second->GetVid() != vid || device.second->GetPid() != pid)
|
||||||
|
continue;
|
||||||
|
has_device_with_vid_pid = true;
|
||||||
|
|
||||||
|
if (m_opened_devices.find(device.second->GetId()) != m_opened_devices.cend() ||
|
||||||
|
!device.second->Attach(0))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
m_opened_devices.emplace(device.second->GetId());
|
||||||
|
return {IPC_SUCCESS, device.second->GetId()};
|
||||||
|
}
|
||||||
|
// IOS doesn't allow opening the same device more than once (IPC_EEXIST)
|
||||||
|
return {has_device_with_vid_pid ? IPC_EEXIST : IPC_ENOENT, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
void OH0::DeviceClose(const u64 device_id)
|
||||||
|
{
|
||||||
|
TriggerHook(m_removal_hooks, device_id, IPC_ENOENT);
|
||||||
|
m_opened_devices.erase(device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::DeviceIOCtl(const u64 device_id, const IOCtlRequest& request)
|
||||||
|
{
|
||||||
|
const auto device = GetDeviceById(device_id);
|
||||||
|
if (!device)
|
||||||
|
return GetDefaultReply(IPC_ENOENT);
|
||||||
|
|
||||||
|
switch (request.request)
|
||||||
|
{
|
||||||
|
case USB::IOCTL_USBV0_DEVREMOVALHOOK:
|
||||||
|
return RegisterRemovalHook(device_id, request);
|
||||||
|
case USB::IOCTL_USBV0_SUSPENDDEV:
|
||||||
|
case USB::IOCTL_USBV0_RESUMEDEV:
|
||||||
|
// Unimplemented because libusb doesn't do power management.
|
||||||
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
case USB::IOCTL_USBV0_RESET_DEVICE:
|
||||||
|
TriggerHook(m_removal_hooks, device_id, IPC_SUCCESS);
|
||||||
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
default:
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0::DeviceIOCtlV(const u64 device_id, const IOCtlVRequest& request)
|
||||||
|
{
|
||||||
|
const auto device = GetDeviceById(device_id);
|
||||||
|
if (!device)
|
||||||
|
return GetDefaultReply(IPC_ENOENT);
|
||||||
|
|
||||||
|
switch (request.request)
|
||||||
|
{
|
||||||
|
case USB::IOCTLV_USBV0_CTRLMSG:
|
||||||
|
case USB::IOCTLV_USBV0_BLKMSG:
|
||||||
|
case USB::IOCTLV_USBV0_LBLKMSG:
|
||||||
|
case USB::IOCTLV_USBV0_INTRMSG:
|
||||||
|
case USB::IOCTLV_USBV0_ISOMSG:
|
||||||
|
return HandleTransfer(device, request.request,
|
||||||
|
[&, this]() { return SubmitTransfer(*device, request); });
|
||||||
|
case USB::IOCTLV_USBV0_UNKNOWN_32:
|
||||||
|
request.DumpUnknown(GetDeviceName(), LogTypes::IOS_USB);
|
||||||
|
return GetDefaultReply(IPC_SUCCESS);
|
||||||
|
default:
|
||||||
|
return GetDefaultReply(IPC_EINVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 OH0::SubmitTransfer(USB::Device& device, const IOCtlVRequest& ioctlv)
|
||||||
|
{
|
||||||
|
switch (ioctlv.request)
|
||||||
|
{
|
||||||
|
case USB::IOCTLV_USBV0_CTRLMSG:
|
||||||
|
if (!ioctlv.HasNumberOfValidVectors(6, 1) ||
|
||||||
|
Common::swap16(Memory::Read_U16(ioctlv.in_vectors[4].address)) != ioctlv.io_vectors[0].size)
|
||||||
|
return IPC_EINVAL;
|
||||||
|
return device.SubmitTransfer(std::make_unique<USB::V0CtrlMessage>(ioctlv));
|
||||||
|
|
||||||
|
case USB::IOCTLV_USBV0_BLKMSG:
|
||||||
|
case USB::IOCTLV_USBV0_LBLKMSG:
|
||||||
|
if (!ioctlv.HasNumberOfValidVectors(2, 1) ||
|
||||||
|
Memory::Read_U16(ioctlv.in_vectors[1].address) != ioctlv.io_vectors[0].size)
|
||||||
|
return IPC_EINVAL;
|
||||||
|
return device.SubmitTransfer(
|
||||||
|
std::make_unique<USB::V0BulkMessage>(ioctlv, ioctlv.request == USB::IOCTLV_USBV0_LBLKMSG));
|
||||||
|
|
||||||
|
case USB::IOCTLV_USBV0_INTRMSG:
|
||||||
|
if (!ioctlv.HasNumberOfValidVectors(2, 1) ||
|
||||||
|
Memory::Read_U16(ioctlv.in_vectors[1].address) != ioctlv.io_vectors[0].size)
|
||||||
|
return IPC_EINVAL;
|
||||||
|
return device.SubmitTransfer(std::make_unique<USB::V0IntrMessage>(ioctlv));
|
||||||
|
|
||||||
|
case USB::IOCTLV_USBV0_ISOMSG:
|
||||||
|
if (!ioctlv.HasNumberOfValidVectors(3, 2))
|
||||||
|
return IPC_EINVAL;
|
||||||
|
return device.SubmitTransfer(std::make_unique<USB::V0IsoMessage>(ioctlv));
|
||||||
|
|
||||||
|
default:
|
||||||
|
return IPC_EINVAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace Device
|
||||||
|
} // namespace HLE
|
||||||
|
} // namespace IOS
|
88
Source/Core/Core/IOS/USB/OH0/OH0.h
Normal file
88
Source/Core/Core/IOS/USB/OH0/OH0.h
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright 2017 Dolphin Emulator Project
|
||||||
|
// Licensed under GPLv2+
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
#include "Core/IOS/Device.h"
|
||||||
|
#include "Core/IOS/USB/Host.h"
|
||||||
|
|
||||||
|
class PointerWrap;
|
||||||
|
|
||||||
|
namespace IOS
|
||||||
|
{
|
||||||
|
namespace HLE
|
||||||
|
{
|
||||||
|
namespace USB
|
||||||
|
{
|
||||||
|
struct DeviceInfo
|
||||||
|
{
|
||||||
|
u16 vid;
|
||||||
|
u16 pid;
|
||||||
|
bool operator<(const DeviceInfo& other) const { return vid < other.vid; }
|
||||||
|
};
|
||||||
|
} // namespace USB
|
||||||
|
|
||||||
|
namespace Device
|
||||||
|
{
|
||||||
|
// /dev/usb/oh0
|
||||||
|
class OH0 final : public USBHost
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OH0(u32 device_id, const std::string& device_name);
|
||||||
|
~OH0() override;
|
||||||
|
|
||||||
|
ReturnCode Open(const OpenRequest& request) override;
|
||||||
|
IPCCommandResult IOCtl(const IOCtlRequest& request) override;
|
||||||
|
IPCCommandResult IOCtlV(const IOCtlVRequest& request) override;
|
||||||
|
|
||||||
|
std::pair<ReturnCode, u64> DeviceOpen(u16 vid, u16 pid);
|
||||||
|
void DeviceClose(u64 device_id);
|
||||||
|
IPCCommandResult DeviceIOCtl(u64 device_id, const IOCtlRequest& request);
|
||||||
|
IPCCommandResult DeviceIOCtlV(u64 device_id, const IOCtlVRequest& request);
|
||||||
|
|
||||||
|
void DoState(PointerWrap& p) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
IPCCommandResult CancelInsertionHook(const IOCtlRequest& request);
|
||||||
|
IPCCommandResult GetDeviceList(const IOCtlVRequest& request) const;
|
||||||
|
IPCCommandResult GetRhDesca(const IOCtlRequest& request) const;
|
||||||
|
IPCCommandResult GetRhPortStatus(const IOCtlVRequest& request) const;
|
||||||
|
IPCCommandResult SetRhPortStatus(const IOCtlVRequest& request);
|
||||||
|
IPCCommandResult RegisterRemovalHook(u64 device_id, const IOCtlRequest& request);
|
||||||
|
IPCCommandResult RegisterInsertionHook(const IOCtlVRequest& request);
|
||||||
|
IPCCommandResult RegisterInsertionHookWithID(const IOCtlVRequest& request);
|
||||||
|
IPCCommandResult RegisterClassChangeHook(const IOCtlVRequest& request);
|
||||||
|
s32 SubmitTransfer(USB::Device& device, const IOCtlVRequest& request);
|
||||||
|
|
||||||
|
bool HasDeviceWithVidPid(u16 vid, u16 pid) const;
|
||||||
|
void OnDeviceChange(ChangeEvent event, std::shared_ptr<USB::Device> device) override;
|
||||||
|
template <typename T>
|
||||||
|
void TriggerHook(std::map<T, u32>& hooks, T value, ReturnCode return_value);
|
||||||
|
|
||||||
|
struct DeviceEntry
|
||||||
|
{
|
||||||
|
u32 unknown;
|
||||||
|
u16 vid;
|
||||||
|
u16 pid;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(DeviceEntry) == 8, "sizeof(DeviceEntry) must be 8");
|
||||||
|
|
||||||
|
// Device info (VID, PID) → command address for pending hook requests
|
||||||
|
std::map<USB::DeviceInfo, u32> m_insertion_hooks;
|
||||||
|
std::map<u64, u32> m_removal_hooks;
|
||||||
|
std::set<u64> m_opened_devices;
|
||||||
|
std::mutex m_hooks_mutex;
|
||||||
|
};
|
||||||
|
} // namespace Device
|
||||||
|
} // namespace HLE
|
||||||
|
} // namespace IOS
|
87
Source/Core/Core/IOS/USB/OH0/OH0Device.cpp
Normal file
87
Source/Core/Core/IOS/USB/OH0/OH0Device.cpp
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// Copyright 2017 Dolphin Emulator Project
|
||||||
|
// Licensed under GPLv2+
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Common/ChunkFile.h"
|
||||||
|
#include "Core/IOS/IPC.h"
|
||||||
|
#include "Core/IOS/USB/OH0/OH0.h"
|
||||||
|
#include "Core/IOS/USB/OH0/OH0Device.h"
|
||||||
|
|
||||||
|
namespace IOS
|
||||||
|
{
|
||||||
|
namespace HLE
|
||||||
|
{
|
||||||
|
namespace Device
|
||||||
|
{
|
||||||
|
static void GetVidPidFromDevicePath(const std::string& device_path, u16& vid, u16& pid)
|
||||||
|
{
|
||||||
|
std::stringstream stream{device_path};
|
||||||
|
std::string segment;
|
||||||
|
std::vector<std::string> list;
|
||||||
|
while (std::getline(stream, segment, '/'))
|
||||||
|
if (!segment.empty())
|
||||||
|
list.push_back(segment);
|
||||||
|
|
||||||
|
if (list.size() != 5)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << std::hex << list[3];
|
||||||
|
ss >> vid;
|
||||||
|
ss.clear();
|
||||||
|
ss << std::hex << list[4];
|
||||||
|
ss >> pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
OH0Device::OH0Device(u32 id, const std::string& name) : Device(id, name, DeviceType::OH0)
|
||||||
|
{
|
||||||
|
if (!name.empty())
|
||||||
|
GetVidPidFromDevicePath(name, m_vid, m_pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OH0Device::DoState(PointerWrap& p)
|
||||||
|
{
|
||||||
|
m_oh0 = std::static_pointer_cast<OH0>(GetDeviceByName("/dev/usb/oh0"));
|
||||||
|
p.Do(m_name);
|
||||||
|
p.Do(m_vid);
|
||||||
|
p.Do(m_pid);
|
||||||
|
p.Do(m_device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnCode OH0Device::Open(const OpenRequest& request)
|
||||||
|
{
|
||||||
|
const u32 ios_major_version = GetVersion();
|
||||||
|
if (ios_major_version == 57 || ios_major_version == 58 || ios_major_version == 59)
|
||||||
|
return IPC_ENOENT;
|
||||||
|
|
||||||
|
if (m_vid == 0 && m_pid == 0)
|
||||||
|
return IPC_ENOENT;
|
||||||
|
|
||||||
|
m_oh0 = std::static_pointer_cast<OH0>(GetDeviceByName("/dev/usb/oh0"));
|
||||||
|
|
||||||
|
ReturnCode return_code;
|
||||||
|
std::tie(return_code, m_device_id) = m_oh0->DeviceOpen(m_vid, m_pid);
|
||||||
|
return return_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OH0Device::Close()
|
||||||
|
{
|
||||||
|
m_oh0->DeviceClose(m_device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0Device::IOCtl(const IOCtlRequest& request)
|
||||||
|
{
|
||||||
|
return m_oh0->DeviceIOCtl(m_device_id, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
IPCCommandResult OH0Device::IOCtlV(const IOCtlVRequest& request)
|
||||||
|
{
|
||||||
|
return m_oh0->DeviceIOCtlV(m_device_id, request);
|
||||||
|
}
|
||||||
|
} // namespace Device
|
||||||
|
} // namespace HLE
|
||||||
|
} // namespace IOS
|
41
Source/Core/Core/IOS/USB/OH0/OH0Device.h
Normal file
41
Source/Core/Core/IOS/USB/OH0/OH0Device.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright 2017 Dolphin Emulator Project
|
||||||
|
// Licensed under GPLv2+
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
#include "Core/IOS/Device.h"
|
||||||
|
|
||||||
|
class PointerWrap;
|
||||||
|
|
||||||
|
namespace IOS
|
||||||
|
{
|
||||||
|
namespace HLE
|
||||||
|
{
|
||||||
|
namespace Device
|
||||||
|
{
|
||||||
|
class OH0;
|
||||||
|
class OH0Device final : public Device
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OH0Device(u32 device_id, const std::string& device_name);
|
||||||
|
|
||||||
|
ReturnCode Open(const OpenRequest& request) override;
|
||||||
|
void Close() override;
|
||||||
|
IPCCommandResult IOCtl(const IOCtlRequest& request) override;
|
||||||
|
IPCCommandResult IOCtlV(const IOCtlVRequest& request) override;
|
||||||
|
void DoState(PointerWrap& p) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<OH0> m_oh0;
|
||||||
|
u16 m_vid = 0;
|
||||||
|
u16 m_pid = 0;
|
||||||
|
u64 m_device_id = 0;
|
||||||
|
};
|
||||||
|
} // namespace Device
|
||||||
|
} // namespace HLE
|
||||||
|
} // namespace IOS
|
@ -42,6 +42,17 @@ V0IntrMessage::V0IntrMessage(const IOCtlVRequest& ioctlv)
|
|||||||
endpoint = Memory::Read_U8(ioctlv.in_vectors[0].address);
|
endpoint = Memory::Read_U8(ioctlv.in_vectors[0].address);
|
||||||
length = Memory::Read_U16(ioctlv.in_vectors[1].address);
|
length = Memory::Read_U16(ioctlv.in_vectors[1].address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
V0IsoMessage::V0IsoMessage(const IOCtlVRequest& ioctlv)
|
||||||
|
: IsoMessage(ioctlv, ioctlv.io_vectors[1].address)
|
||||||
|
{
|
||||||
|
endpoint = Memory::Read_U8(ioctlv.in_vectors[0].address);
|
||||||
|
length = Memory::Read_U16(ioctlv.in_vectors[1].address);
|
||||||
|
num_packets = Memory::Read_U8(ioctlv.in_vectors[2].address);
|
||||||
|
packet_sizes_addr = ioctlv.io_vectors[0].address;
|
||||||
|
for (size_t i = 0; i < num_packets; ++i)
|
||||||
|
packet_sizes.push_back(Memory::Read_U16(static_cast<u32>(packet_sizes_addr + i * sizeof(u16))));
|
||||||
|
}
|
||||||
} // namespace USB
|
} // namespace USB
|
||||||
} // namespace HLE
|
} // namespace HLE
|
||||||
} // namespace IOS
|
} // namespace IOS
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
|
|
||||||
#include "Common/CommonTypes.h"
|
#include "Common/CommonTypes.h"
|
||||||
#include "Core/IOS/USB/Common.h"
|
#include "Core/IOS/USB/Common.h"
|
||||||
|
|
||||||
@ -24,6 +22,21 @@ enum V0Requests
|
|||||||
IOCTLV_USBV0_CTRLMSG = 0,
|
IOCTLV_USBV0_CTRLMSG = 0,
|
||||||
IOCTLV_USBV0_BLKMSG = 1,
|
IOCTLV_USBV0_BLKMSG = 1,
|
||||||
IOCTLV_USBV0_INTRMSG = 2,
|
IOCTLV_USBV0_INTRMSG = 2,
|
||||||
|
IOCTL_USBV0_SUSPENDDEV = 5,
|
||||||
|
IOCTL_USBV0_RESUMEDEV = 6,
|
||||||
|
IOCTLV_USBV0_ISOMSG = 9,
|
||||||
|
IOCTLV_USBV0_LBLKMSG = 10,
|
||||||
|
IOCTLV_USBV0_GETDEVLIST = 12,
|
||||||
|
IOCTL_USBV0_GETRHDESCA = 15,
|
||||||
|
IOCTLV_USBV0_GETRHPORTSTATUS = 20,
|
||||||
|
IOCTLV_USBV0_SETRHPORTSTATUS = 25,
|
||||||
|
IOCTL_USBV0_DEVREMOVALHOOK = 26,
|
||||||
|
IOCTLV_USBV0_DEVINSERTHOOK = 27,
|
||||||
|
IOCTLV_USBV0_DEVICECLASSCHANGE = 28,
|
||||||
|
IOCTL_USBV0_RESET_DEVICE = 29,
|
||||||
|
IOCTLV_USBV0_DEVINSERTHOOKID = 30,
|
||||||
|
IOCTL_USBV0_CANCEL_INSERT_HOOK = 31,
|
||||||
|
IOCTLV_USBV0_UNKNOWN_32 = 32,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct V0CtrlMessage final : CtrlMessage
|
struct V0CtrlMessage final : CtrlMessage
|
||||||
@ -40,6 +53,11 @@ struct V0IntrMessage final : IntrMessage
|
|||||||
{
|
{
|
||||||
explicit V0IntrMessage(const IOCtlVRequest& ioctlv);
|
explicit V0IntrMessage(const IOCtlVRequest& ioctlv);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct V0IsoMessage final : IsoMessage
|
||||||
|
{
|
||||||
|
explicit V0IsoMessage(const IOCtlVRequest& ioctlv);
|
||||||
|
};
|
||||||
} // namespace USB
|
} // namespace USB
|
||||||
} // namespace HLE
|
} // namespace HLE
|
||||||
} // namespace IOS
|
} // namespace IOS
|
||||||
|
@ -71,7 +71,7 @@ static Common::Event g_compressAndDumpStateSyncEvent;
|
|||||||
static std::thread g_save_thread;
|
static std::thread g_save_thread;
|
||||||
|
|
||||||
// Don't forget to increase this after doing changes on the savestate system
|
// Don't forget to increase this after doing changes on the savestate system
|
||||||
static const u32 STATE_VERSION = 73; // Last changed in PR 4651
|
static const u32 STATE_VERSION = 74; // Last changed in PR 4408
|
||||||
|
|
||||||
// Maps savestate versions to Dolphin versions.
|
// Maps savestate versions to Dolphin versions.
|
||||||
// Versions after 42 don't need to be added to this list,
|
// Versions after 42 don't need to be added to this list,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user