mirror of
https://github.com/libretro/parallel-gx1.git
synced 2026-11-07 04:26:01 -10:00
Remove UPnP, the unused host input backends, disc conversion and DolphinTool
None of these run in the core: UPnP served NetPlay; the DInput, XInput, WGInput, Win32, Quartz, Xlib, evdev and Pipes input backends are never enabled, since input comes from libretro; and disc conversion, with its multithreaded compressor, was only reached from DolphinTool. The WIA, RVZ and GCZ readers stay.
This commit is contained in:
1 parent
12247f1f50
commit
8c7aa58f6b
57 files changed
-8775
No files matched your search
@@ -86,10 +86,6 @@
|
||||
path = Externals/zstd/zstd
|
||||
url = https://github.com/facebook/zstd.git
|
||||
shallow = true
|
||||
[submodule "Externals/miniupnpc/miniupnp"]
|
||||
path = Externals/miniupnpc/miniupnp
|
||||
url = https://github.com/miniupnp/miniupnp.git
|
||||
shallow = true
|
||||
[submodule "Externals/glslang/glslang"]
|
||||
path = Externals/glslang/glslang
|
||||
url = https://github.com/KhronosGroup/glslang.git
|
||||
|
||||
Vendored
-2
@@ -42,8 +42,6 @@ Dolphin includes or links code of the following third-party software projects:
|
||||
[GPLv2+](http://www.oberhumer.com/opensource/gpl.html)
|
||||
- [mGBA](http://mgba.io)
|
||||
[MPL 2.0](https://github.com/mgba-emu/mgba/blob/master/LICENSE)
|
||||
- [MiniUPnPc](http://miniupnp.free.fr/):
|
||||
[BSD 3-Clause](https://github.com/miniupnp/miniupnp/blob/master/miniupnpc/LICENSE)
|
||||
- [Microsoft Visual C++ Runtime Library](http://www.microsoft.com/en-us/download/details.aspx?id=40784):
|
||||
[System Library if not distributed](https://www.gnu.org/licenses/gpl-faq.html#WindowsRuntimeAndGPL)
|
||||
- [OpenGL Header (MESA)](http://mesa3d.org/):
|
||||
|
||||
Vendored
-1
Submodule Externals/miniupnpc/miniupnp deleted from bf4215a757.
@@ -1166,7 +1166,6 @@ DOLPHIN_SOURCES_CXX += \
|
||||
$(CORE_DIR)/Source/Core/Common/Thread.cpp \
|
||||
$(CORE_DIR)/Source/Core/Common/Timer.cpp \
|
||||
$(CORE_DIR)/Source/Core/Common/TimeUtil.cpp \
|
||||
$(CORE_DIR)/Source/Core/Common/UPnP.cpp \
|
||||
$(CORE_DIR)/Source/Core/Common/Version.cpp \
|
||||
$(CORE_DIR)/Source/Core/Common/GL/GLContext.cpp \
|
||||
$(CORE_DIR)/Source/Core/Common/GL/GLUtil.cpp \
|
||||
|
||||
@@ -111,86 +111,3 @@ Available video backends are "D3D" and "D3D12" (they are only available on Windo
|
||||
There's also "Null", which will not render anything, and
|
||||
"Software Renderer", which uses the CPU for rendering and
|
||||
is intended for debugging purposes only.
|
||||
|
||||
## DolphinTool Usage
|
||||
```
|
||||
usage: dolphin-tool COMMAND -h
|
||||
|
||||
commands supported: [convert, verify, header, extract]
|
||||
```
|
||||
|
||||
```
|
||||
Usage: convert [options]... [FILE]...
|
||||
|
||||
Options:
|
||||
-h, --help show this help message and exit
|
||||
-u USER, --user=USER User folder path, required for temporary processing
|
||||
files.Will be automatically created if this option is
|
||||
not set.
|
||||
-i FILE, --input=FILE
|
||||
Path to disc image FILE.
|
||||
-o FILE, --output=FILE
|
||||
Path to the destination FILE.
|
||||
-f FORMAT, --format=FORMAT
|
||||
Container format to use. Default is RVZ. [iso|gcz|wia|rvz]
|
||||
-s, --scrub Scrub junk data as part of conversion.
|
||||
-b BLOCK_SIZE, --block_size=BLOCK_SIZE
|
||||
Block size for GCZ/WIA/RVZ formats, as an integer.
|
||||
Suggested value for RVZ: 131072 (128 KiB)
|
||||
-c COMPRESSION, --compression=COMPRESSION
|
||||
Compression method to use when converting to WIA/RVZ.
|
||||
Suggested value for RVZ: zstd [none|zstd|bzip|lzma|lzma2]
|
||||
-l COMPRESSION_LEVEL, --compression_level=COMPRESSION_LEVEL
|
||||
Level of compression for the selected method. Ignored
|
||||
if 'none'. Suggested value for zstd: 5
|
||||
```
|
||||
|
||||
```
|
||||
Usage: verify [options]...
|
||||
|
||||
Options:
|
||||
-h, --help show this help message and exit
|
||||
-u USER, --user=USER User folder path, required for temporary processing
|
||||
files.Will be automatically created if this option is
|
||||
not set.
|
||||
-i FILE, --input=FILE
|
||||
Path to disc image FILE.
|
||||
-a ALGORITHM, --algorithm=ALGORITHM
|
||||
Optional. Compute and print the digest using the
|
||||
selected algorithm, then exit. [crc32|md5|sha1|rchash]
|
||||
```
|
||||
|
||||
```
|
||||
Usage: header [options]...
|
||||
|
||||
Options:
|
||||
-h, --help show this help message and exit
|
||||
-i FILE, --input=FILE
|
||||
Path to disc image FILE.
|
||||
-b, --block_size Optional. Print the block size of GCZ/WIA/RVZ formats,
|
||||
then exit.
|
||||
-c, --compression Optional. Print the compression method of GCZ/WIA/RVZ
|
||||
formats, then exit.
|
||||
-l, --compression_level
|
||||
Optional. Print the level of compression for WIA/RVZ
|
||||
formats, then exit.
|
||||
```
|
||||
|
||||
```
|
||||
Usage: extract [options]...
|
||||
|
||||
Options:
|
||||
-h, --help show this help message and exit
|
||||
-i FILE, --input=FILE
|
||||
Path to disc image FILE.
|
||||
-o FOLDER, --output=FOLDER
|
||||
Path to the destination FOLDER.
|
||||
-p PARTITION, --partition=PARTITION
|
||||
Which specific partition you want to extract.
|
||||
-s SINGLE, --single=SINGLE
|
||||
Which specific file/directory you want to extract.
|
||||
-l, --list List all files in volume/partition. Will print the
|
||||
directory/file specified with --single if defined.
|
||||
-q, --quiet Mute all messages except for errors.
|
||||
-g, --gameonly Only extracts the DATA partition.
|
||||
```
|
||||
@@ -1,181 +0,0 @@
|
||||
// Copyright 2017 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#ifdef USE_UPNP
|
||||
|
||||
#include "Common/UPnP.h"
|
||||
|
||||
#include "Common/Logging/Log.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <miniupnpc.h>
|
||||
#include <miniwget.h>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <upnpcommands.h>
|
||||
#include <upnperrors.h>
|
||||
|
||||
static UPNPUrls s_urls;
|
||||
static IGDdatas s_data;
|
||||
static std::array<char, 20> s_our_ip;
|
||||
static u16 s_mapped = 0;
|
||||
static std::thread s_thread;
|
||||
|
||||
// called from ---UPnP--- thread
|
||||
// discovers the IGD
|
||||
static bool InitUPnP()
|
||||
{
|
||||
static bool s_inited = false;
|
||||
static bool s_error = false;
|
||||
|
||||
// Don't init if already inited
|
||||
if (s_inited)
|
||||
return true;
|
||||
|
||||
// Don't init if it failed before
|
||||
if (s_error)
|
||||
return false;
|
||||
|
||||
s_urls = {};
|
||||
s_data = {};
|
||||
|
||||
// Find all UPnP devices
|
||||
int upnperror = 0;
|
||||
std::unique_ptr<UPNPDev, decltype(&freeUPNPDevlist)> devlist(nullptr, freeUPNPDevlist);
|
||||
#if MINIUPNPC_API_VERSION >= 14
|
||||
devlist.reset(upnpDiscover(2000, nullptr, nullptr, 0, 0, 2, &upnperror));
|
||||
#else
|
||||
devlist.reset(upnpDiscover(2000, nullptr, nullptr, 0, 0, &upnperror));
|
||||
#endif
|
||||
if (!devlist)
|
||||
{
|
||||
if (upnperror == UPNPDISCOVER_SUCCESS)
|
||||
{
|
||||
WARN_LOG_FMT(NETPLAY, "No UPnP devices could be found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
WARN_LOG_FMT(NETPLAY, "An error occurred trying to discover UPnP devices: {}",
|
||||
strupnperror(upnperror));
|
||||
}
|
||||
|
||||
s_error = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look for the IGD
|
||||
bool found_valid_igd = false;
|
||||
for (UPNPDev* dev = devlist.get(); dev; dev = dev->pNext)
|
||||
{
|
||||
if (!std::strstr(dev->st, "InternetGatewayDevice"))
|
||||
continue;
|
||||
|
||||
int desc_xml_size = 0;
|
||||
std::unique_ptr<char, decltype(&std::free)> desc_xml(nullptr, std::free);
|
||||
int statusCode = 200;
|
||||
#if MINIUPNPC_API_VERSION >= 16
|
||||
desc_xml.reset(
|
||||
static_cast<char*>(miniwget_getaddr(dev->descURL, &desc_xml_size, s_our_ip.data(),
|
||||
static_cast<int>(s_our_ip.size()), 0, &statusCode)));
|
||||
#else
|
||||
desc_xml.reset(static_cast<char*>(miniwget_getaddr(
|
||||
dev->descURL, &desc_xml_size, s_our_ip.data(), static_cast<int>(s_our_ip.size()), 0)));
|
||||
#endif
|
||||
if (desc_xml && statusCode == 200)
|
||||
{
|
||||
parserootdesc(desc_xml.get(), desc_xml_size, &s_data);
|
||||
GetUPNPUrls(&s_urls, &s_data, dev->descURL, 0);
|
||||
|
||||
found_valid_igd = true;
|
||||
NOTICE_LOG_FMT(NETPLAY, "Got info from IGD at {}.", dev->descURL);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
WARN_LOG_FMT(NETPLAY, "Error getting info from IGD at {}.", dev->descURL);
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_valid_igd)
|
||||
WARN_LOG_FMT(NETPLAY, "Could not find a valid IGD in the discovered UPnP devices.");
|
||||
|
||||
s_inited = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// called from ---UPnP--- thread
|
||||
// Attempt to stop portforwarding.
|
||||
// --
|
||||
// NOTE: It is important that this happens! A few very crappy routers
|
||||
// apparently do not delete UPnP mappings on their own, so if you leave them
|
||||
// hanging, the NVRAM will fill with portmappings, and eventually all UPnP
|
||||
// requests will fail silently, with the only recourse being a factory reset.
|
||||
// --
|
||||
static bool UnmapPort(const u16 port)
|
||||
{
|
||||
std::string port_str = std::to_string(port);
|
||||
UPNP_DeletePortMapping(s_urls.controlURL, s_data.first.servicetype, port_str.c_str(), "UDP",
|
||||
nullptr);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// called from ---UPnP--- thread
|
||||
// Attempt to portforward!
|
||||
static bool MapPort(const char* addr, const u16 port)
|
||||
{
|
||||
if (s_mapped > 0)
|
||||
UnmapPort(s_mapped);
|
||||
|
||||
std::string port_str = std::to_string(port);
|
||||
int result = UPNP_AddPortMapping(
|
||||
s_urls.controlURL, s_data.first.servicetype, port_str.c_str(), port_str.c_str(), addr,
|
||||
(std::string("dolphin-emu UDP on ") + addr).c_str(), "UDP", nullptr, nullptr);
|
||||
|
||||
if (result != 0)
|
||||
return false;
|
||||
|
||||
s_mapped = port;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// UPnP thread: try to map a port
|
||||
static void MapPortThread(const u16 port)
|
||||
{
|
||||
if (InitUPnP() && MapPort(s_our_ip.data(), port))
|
||||
{
|
||||
NOTICE_LOG_FMT(NETPLAY, "Successfully mapped port {} to {}.", port, s_our_ip.data());
|
||||
return;
|
||||
}
|
||||
|
||||
WARN_LOG_FMT(NETPLAY, "Failed to map port {} to {}.", port, s_our_ip.data());
|
||||
}
|
||||
|
||||
// UPnP thread: try to unmap a port
|
||||
static void UnmapPortThread()
|
||||
{
|
||||
if (s_mapped > 0)
|
||||
UnmapPort(s_mapped);
|
||||
}
|
||||
|
||||
void Common::UPnP::TryPortmapping(u16 port)
|
||||
{
|
||||
if (s_thread.joinable())
|
||||
s_thread.join();
|
||||
s_thread = std::thread(&MapPortThread, port);
|
||||
}
|
||||
|
||||
void Common::UPnP::StopPortmapping()
|
||||
{
|
||||
if (s_thread.joinable())
|
||||
s_thread.join();
|
||||
s_thread = std::thread(&UnmapPortThread);
|
||||
s_thread.join();
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright 2017 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_UPNP
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
namespace Common::UPnP
|
||||
{
|
||||
void TryPortmapping(u16 port);
|
||||
void StopPortmapping();
|
||||
} // namespace Common::UPnP
|
||||
|
||||
#endif
|
||||
@@ -195,16 +195,4 @@ private:
|
||||
// Factory function - examines the path to choose the right type of BlobReader, and returns one.
|
||||
std::unique_ptr<BlobReader> CreateBlobReader(const std::string& filename);
|
||||
|
||||
using CompressCB = std::function<bool(const std::string& text, float percent)>;
|
||||
|
||||
bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
|
||||
const std::string& outfile_path, u32 sub_type, int sector_size,
|
||||
const CompressCB& callback);
|
||||
bool ConvertToPlain(BlobReader* infile, const std::string& infile_path,
|
||||
const std::string& outfile_path, const CompressCB& callback);
|
||||
bool ConvertToWIAOrRVZ(BlobReader* infile, const std::string& infile_path,
|
||||
const std::string& outfile_path, bool rvz,
|
||||
WIARVZCompressionType compression_type, int compression_level,
|
||||
int chunk_size, const CompressCB& callback);
|
||||
|
||||
} // namespace DiscIO
|
||||
@@ -26,7 +26,6 @@
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/MsgHandler.h"
|
||||
#include "DiscIO/Blob.h"
|
||||
#include "DiscIO/MultithreadedCompressor.h"
|
||||
#include "DiscIO/Volume.h"
|
||||
|
||||
namespace DiscIO
|
||||
@@ -251,231 +250,6 @@ bool CompressedBlobReader::ValidateBlockPointers() const
|
||||
return invalid_pointers == 0;
|
||||
}
|
||||
|
||||
struct CompressThreadState
|
||||
{
|
||||
CompressThreadState() : z{} {}
|
||||
~CompressThreadState() { deflateEnd(&z); }
|
||||
|
||||
// z_stream will stop working if it changes address, so this object must not be moved
|
||||
CompressThreadState(const CompressThreadState&) = delete;
|
||||
CompressThreadState(CompressThreadState&&) = delete;
|
||||
CompressThreadState& operator=(const CompressThreadState&) = delete;
|
||||
CompressThreadState& operator=(CompressThreadState&&) = delete;
|
||||
|
||||
std::vector<u8> compressed_buffer;
|
||||
z_stream z;
|
||||
};
|
||||
|
||||
struct CompressParameters
|
||||
{
|
||||
std::vector<u8> data{};
|
||||
u32 block_number = 0;
|
||||
u64 inpos = 0;
|
||||
};
|
||||
|
||||
struct OutputParameters
|
||||
{
|
||||
std::vector<u8> data{};
|
||||
u32 block_number = 0;
|
||||
bool compressed = false;
|
||||
u64 inpos = 0;
|
||||
};
|
||||
|
||||
static ConversionResultCode SetUpCompressThreadState(CompressThreadState* state)
|
||||
{
|
||||
return deflateInit(&state->z, 9) == Z_OK ? ConversionResultCode::Success :
|
||||
ConversionResultCode::InternalError;
|
||||
}
|
||||
|
||||
static ConversionResult<OutputParameters> Compress(CompressThreadState* state,
|
||||
CompressParameters parameters, int block_size,
|
||||
std::vector<u32>* hashes, int* num_stored,
|
||||
int* num_compressed)
|
||||
{
|
||||
state->compressed_buffer.resize(block_size);
|
||||
|
||||
int retval = deflateReset(&state->z);
|
||||
state->z.next_in = parameters.data.data();
|
||||
state->z.avail_in = block_size;
|
||||
state->z.next_out = state->compressed_buffer.data();
|
||||
state->z.avail_out = block_size;
|
||||
|
||||
if (retval != Z_OK)
|
||||
{
|
||||
ERROR_LOG_FMT(DISCIO, "Deflate failed");
|
||||
return std::unexpected{ConversionResultCode::InternalError};
|
||||
}
|
||||
|
||||
const int status = deflate(&state->z, Z_FINISH);
|
||||
|
||||
state->compressed_buffer.resize(block_size - state->z.avail_out);
|
||||
|
||||
OutputParameters output_parameters;
|
||||
if ((status != Z_STREAM_END) || (state->z.avail_out < 10))
|
||||
{
|
||||
// let's store uncompressed
|
||||
++*num_stored;
|
||||
output_parameters = OutputParameters{std::move(parameters.data), parameters.block_number, false,
|
||||
parameters.inpos};
|
||||
}
|
||||
else
|
||||
{
|
||||
// let's store compressed
|
||||
++*num_compressed;
|
||||
output_parameters = OutputParameters{std::move(state->compressed_buffer),
|
||||
parameters.block_number, true, parameters.inpos};
|
||||
}
|
||||
|
||||
(*hashes)[parameters.block_number] =
|
||||
Common::HashAdler32(output_parameters.data.data(), output_parameters.data.size());
|
||||
|
||||
return std::move(output_parameters);
|
||||
}
|
||||
|
||||
static ConversionResultCode Output(OutputParameters parameters, File::DirectIOFile* outfile,
|
||||
u64* position, std::vector<u64>* offsets, int progress_monitor,
|
||||
u32 num_blocks, const CompressCB& callback)
|
||||
{
|
||||
u64 offset = *position;
|
||||
if (!parameters.compressed)
|
||||
offset |= uncompressed_flag;
|
||||
(*offsets)[parameters.block_number] = offset;
|
||||
|
||||
*position += parameters.data.size();
|
||||
|
||||
if (!outfile->Write(parameters.data))
|
||||
return ConversionResultCode::WriteFailed;
|
||||
|
||||
if (parameters.block_number % progress_monitor == 0)
|
||||
{
|
||||
const int ratio =
|
||||
parameters.inpos == 0 ? 0 : static_cast<int>(100 * *position / parameters.inpos);
|
||||
|
||||
const std::string text = Common::FmtFormatT("{0} of {1} blocks. Compression ratio {2}%",
|
||||
parameters.block_number, num_blocks, ratio);
|
||||
|
||||
const float completion = static_cast<float>(parameters.block_number) / num_blocks;
|
||||
|
||||
if (!callback(text, completion))
|
||||
return ConversionResultCode::Canceled;
|
||||
}
|
||||
|
||||
return ConversionResultCode::Success;
|
||||
}
|
||||
|
||||
bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
|
||||
const std::string& outfile_path, u32 sub_type, int block_size,
|
||||
const CompressCB& callback)
|
||||
{
|
||||
ASSERT(infile->GetDataSizeType() == DataSizeType::Accurate);
|
||||
|
||||
File::DirectIOFile outfile(outfile_path, File::AccessMode::Write);
|
||||
if (!outfile.IsOpen())
|
||||
{
|
||||
PanicAlertFmtT(
|
||||
"Failed to open the output file \"{0}\".\n"
|
||||
"Check that you have permissions to write the target folder and that the media can "
|
||||
"be written.",
|
||||
outfile_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
callback(Common::GetStringT("Files opened, ready to compress."), 0);
|
||||
|
||||
CompressedBlobHeader header;
|
||||
header.magic_cookie = GCZ_MAGIC;
|
||||
header.sub_type = sub_type;
|
||||
header.block_size = block_size;
|
||||
header.disc_size = infile->GetDataSize();
|
||||
|
||||
// round upwards!
|
||||
header.num_blocks = (u32)((header.disc_size + (block_size - 1)) / block_size);
|
||||
|
||||
std::vector<u64> offsets(header.num_blocks);
|
||||
std::vector<u32> hashes(header.num_blocks);
|
||||
|
||||
// seek past the header (we will write it at the end)
|
||||
outfile.Seek(sizeof(CompressedBlobHeader), File::SeekOrigin::Current);
|
||||
// seek past the offset and hash tables (we will write them at the end)
|
||||
outfile.Seek((sizeof(u64) + sizeof(u32)) * header.num_blocks, File::SeekOrigin::Current);
|
||||
|
||||
// Now we are ready to write compressed data!
|
||||
u64 inpos = 0;
|
||||
u64 position = 0;
|
||||
int num_compressed = 0;
|
||||
int num_stored = 0;
|
||||
int progress_monitor = std::max<int>(1, header.num_blocks / 1000);
|
||||
|
||||
const auto compress = [&](CompressThreadState* state, CompressParameters parameters) {
|
||||
return Compress(state, std::move(parameters), block_size, &hashes, &num_stored,
|
||||
&num_compressed);
|
||||
};
|
||||
|
||||
const auto output = [&](OutputParameters parameters) {
|
||||
return Output(std::move(parameters), &outfile, &position, &offsets, progress_monitor,
|
||||
header.num_blocks, callback);
|
||||
};
|
||||
|
||||
MultithreadedCompressor<CompressThreadState, CompressParameters, OutputParameters> compressor(
|
||||
SetUpCompressThreadState, compress, output);
|
||||
|
||||
std::vector<u8> in_buf(block_size);
|
||||
for (u32 i = 0; i < header.num_blocks; i++)
|
||||
{
|
||||
if (compressor.GetStatus() != ConversionResultCode::Success)
|
||||
break;
|
||||
|
||||
const u64 bytes_to_read = std::min<u64>(block_size, header.disc_size - inpos);
|
||||
|
||||
if (!infile->Read(inpos, bytes_to_read, in_buf.data()))
|
||||
{
|
||||
compressor.SetError(ConversionResultCode::ReadFailed);
|
||||
break;
|
||||
}
|
||||
|
||||
std::fill(in_buf.begin() + bytes_to_read, in_buf.begin() + header.block_size, 0);
|
||||
|
||||
inpos += block_size;
|
||||
|
||||
compressor.CompressAndWrite(CompressParameters{in_buf, i, inpos});
|
||||
}
|
||||
|
||||
compressor.Shutdown();
|
||||
|
||||
header.compressed_data_size = position;
|
||||
|
||||
const ConversionResultCode result = compressor.GetStatus();
|
||||
|
||||
if (result != ConversionResultCode::Success)
|
||||
{
|
||||
// Remove the incomplete output file.
|
||||
outfile.Close();
|
||||
File::Delete(outfile_path);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Okay, go back and fill in headers
|
||||
outfile.Seek(0, File::SeekOrigin::Begin);
|
||||
outfile.Write(Common::AsU8Span(header));
|
||||
outfile.Write(Common::AsU8Span(offsets));
|
||||
outfile.Write(Common::AsU8Span(hashes));
|
||||
|
||||
callback(Common::GetStringT("Done compressing disc image."), 1.0f);
|
||||
}
|
||||
|
||||
if (result == ConversionResultCode::ReadFailed)
|
||||
PanicAlertFmtT("Failed to read from the input file \"{0}\".", infile_path);
|
||||
|
||||
if (result == ConversionResultCode::WriteFailed)
|
||||
{
|
||||
PanicAlertFmtT("Failed to write the output file \"{0}\".\n"
|
||||
"Check that you have enough space available on the target drive.",
|
||||
outfile_path);
|
||||
}
|
||||
|
||||
return result == ConversionResultCode::Success;
|
||||
}
|
||||
|
||||
bool IsGCZBlob(File::DirectIOFile& file)
|
||||
{
|
||||
CompressedBlobHeader header;
|
||||
|
||||
@@ -38,77 +38,4 @@ bool PlainFileReader::Read(u64 offset, u64 nbytes, u8* out_ptr)
|
||||
return m_file.OffsetRead(offset, out_ptr, nbytes);
|
||||
}
|
||||
|
||||
bool ConvertToPlain(BlobReader* infile, const std::string& infile_path,
|
||||
const std::string& outfile_path, const CompressCB& callback)
|
||||
{
|
||||
ASSERT(infile->GetDataSizeType() == DataSizeType::Accurate);
|
||||
|
||||
File::DirectIOFile outfile(outfile_path, File::AccessMode::Write);
|
||||
if (!outfile.IsOpen())
|
||||
{
|
||||
PanicAlertFmtT(
|
||||
"Failed to open the output file \"{0}\".\n"
|
||||
"Check that you have permissions to write the target folder and that the media can "
|
||||
"be written.",
|
||||
outfile_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr size_t DESIRED_BUFFER_SIZE = 0x80000;
|
||||
u64 buffer_size = infile->GetBlockSize();
|
||||
if (buffer_size == 0)
|
||||
{
|
||||
buffer_size = DESIRED_BUFFER_SIZE;
|
||||
}
|
||||
else
|
||||
{
|
||||
while (buffer_size < DESIRED_BUFFER_SIZE)
|
||||
buffer_size *= 2;
|
||||
}
|
||||
|
||||
std::vector<u8> buffer(buffer_size);
|
||||
const u64 num_buffers = (infile->GetDataSize() + buffer_size - 1) / buffer_size;
|
||||
int progress_monitor = std::max<int>(1, num_buffers / 100);
|
||||
bool success = true;
|
||||
|
||||
for (u64 i = 0; i < num_buffers; i++)
|
||||
{
|
||||
if (i % progress_monitor == 0)
|
||||
{
|
||||
const bool was_cancelled =
|
||||
!callback(Common::GetStringT("Unpacking"), (float)i / (float)num_buffers);
|
||||
if (was_cancelled)
|
||||
{
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const u64 inpos = i * buffer_size;
|
||||
const u64 sz = std::min(buffer_size, infile->GetDataSize() - inpos);
|
||||
if (!infile->Read(inpos, sz, buffer.data()))
|
||||
{
|
||||
PanicAlertFmtT("Failed to read from the input file \"{0}\".", infile_path);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
if (!outfile.Write(buffer.data(), sz))
|
||||
{
|
||||
PanicAlertFmtT("Failed to write the output file \"{0}\".\n"
|
||||
"Check that you have enough space available on the target drive.",
|
||||
outfile_path);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
// Remove the incomplete output file.
|
||||
outfile.Close();
|
||||
File::Delete(outfile_path);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
} // namespace DiscIO
|
||||
@@ -1,222 +0,0 @@
|
||||
// Copyright 2020 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/Event.h"
|
||||
|
||||
namespace DiscIO
|
||||
{
|
||||
enum class ConversionResultCode
|
||||
{
|
||||
Success,
|
||||
Canceled,
|
||||
ReadFailed,
|
||||
WriteFailed,
|
||||
InternalError,
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using ConversionResult = std::expected<T, ConversionResultCode>;
|
||||
|
||||
// This class starts a number of compression threads and one output thread.
|
||||
// The set_up_compress_thread_state function is called at the start of each compression thread.
|
||||
// When CompressAndWrite is called, the compress function will be called on one of the
|
||||
// compression threads, and then the output function will be called on the output thread.
|
||||
// The output thread handles data in the order that data was submitted using CompressAndWrite,
|
||||
// but the compression threads are not guaranteed to handle data in a predictable order.
|
||||
// Remember to check GetStatus regularly and cancel if it doesn't return Success,
|
||||
// and call Shutdown when you want to ensure that everything finishes.
|
||||
template <typename CompressThreadState, typename CompressParameters, typename OutputParameters>
|
||||
class MultithreadedCompressor
|
||||
{
|
||||
public:
|
||||
MultithreadedCompressor(
|
||||
std::function<ConversionResultCode(CompressThreadState*)> set_up_compress_thread_state,
|
||||
std::function<ConversionResult<OutputParameters>(CompressThreadState*, CompressParameters)>
|
||||
compress,
|
||||
std::function<ConversionResultCode(OutputParameters)> output)
|
||||
: m_set_up_compress_thread_state(std::move(set_up_compress_thread_state)),
|
||||
m_compress(std::move(compress)), m_output(std::move(output)),
|
||||
m_threads(std::max<unsigned int>(1, std::thread::hardware_concurrency()))
|
||||
{
|
||||
m_compress_threads = std::make_unique<CompressThread[]>(m_threads);
|
||||
|
||||
for (size_t i = 0; i < m_threads; ++i)
|
||||
{
|
||||
m_compress_threads[i].thread =
|
||||
std::thread(std::mem_fn(&MultithreadedCompressor::CompressThreadFunction), this,
|
||||
&m_compress_threads[i]);
|
||||
}
|
||||
|
||||
m_output_thread =
|
||||
std::thread(std::mem_fn(&MultithreadedCompressor::OutputThreadFunction), this);
|
||||
}
|
||||
|
||||
~MultithreadedCompressor()
|
||||
{
|
||||
if (!m_shutting_down.load())
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void CompressAndWrite(CompressParameters parameters)
|
||||
{
|
||||
if (GetStatus() != ConversionResultCode::Success)
|
||||
return;
|
||||
|
||||
CompressThread& compress_thread = m_compress_threads[m_current_index];
|
||||
|
||||
compress_thread.compress_ready_event.Wait();
|
||||
compress_thread.compress_parameters = std::move(parameters);
|
||||
compress_thread.compress_event.Set();
|
||||
|
||||
++m_current_index;
|
||||
if (m_current_index >= m_threads)
|
||||
m_current_index -= m_threads;
|
||||
}
|
||||
|
||||
void SetError(ConversionResultCode result)
|
||||
{
|
||||
ASSERT(result != ConversionResultCode::Success);
|
||||
|
||||
// If we already have an error, don't overwrite it
|
||||
ConversionResultCode expected = ConversionResultCode::Success;
|
||||
m_result.compare_exchange_strong(expected, result);
|
||||
}
|
||||
|
||||
ConversionResultCode GetStatus() const { return m_result.load(); }
|
||||
|
||||
void Shutdown()
|
||||
{
|
||||
for (size_t i = 0; i < m_threads; ++i)
|
||||
m_compress_threads[i].compress_ready_event.Wait();
|
||||
for (size_t i = 0; i < m_threads; ++i)
|
||||
m_compress_threads[i].compress_done_event.Wait();
|
||||
for (size_t i = 0; i < m_threads; ++i)
|
||||
m_compress_threads[i].output_ready_event.Wait();
|
||||
|
||||
m_shutting_down.store(true);
|
||||
|
||||
for (size_t i = 0; i < m_threads; ++i)
|
||||
m_compress_threads[i].compress_event.Set();
|
||||
for (size_t i = 0; i < m_threads; ++i)
|
||||
m_compress_threads[i].output_event.Set();
|
||||
|
||||
for (size_t i = 0; i < m_threads; ++i)
|
||||
m_compress_threads[i].thread.join();
|
||||
|
||||
m_output_thread.join();
|
||||
}
|
||||
|
||||
private:
|
||||
struct CompressThread
|
||||
{
|
||||
std::thread thread;
|
||||
|
||||
Common::Event compress_ready_event;
|
||||
Common::Event compress_event;
|
||||
Common::Event compress_done_event;
|
||||
Common::Event output_ready_event;
|
||||
Common::Event output_event;
|
||||
|
||||
CompressParameters compress_parameters;
|
||||
OutputParameters output_parameters;
|
||||
};
|
||||
|
||||
void CompressThreadFunction(CompressThread* state)
|
||||
{
|
||||
CompressThreadState compress_thread_state;
|
||||
|
||||
ConversionResultCode setup_result = m_set_up_compress_thread_state(&compress_thread_state);
|
||||
if (setup_result != ConversionResultCode::Success)
|
||||
SetError(setup_result);
|
||||
|
||||
state->compress_ready_event.Set();
|
||||
state->compress_done_event.Set();
|
||||
|
||||
while (true)
|
||||
{
|
||||
state->compress_event.Wait();
|
||||
|
||||
if (m_shutting_down.load())
|
||||
return;
|
||||
|
||||
CompressParameters parameters = std::move(state->compress_parameters);
|
||||
|
||||
state->compress_done_event.Reset();
|
||||
state->compress_ready_event.Set();
|
||||
|
||||
ConversionResult<OutputParameters> result =
|
||||
m_compress(&compress_thread_state, std::move(parameters));
|
||||
|
||||
if (result)
|
||||
{
|
||||
state->output_ready_event.Wait();
|
||||
state->output_parameters = std::move(*result);
|
||||
state->output_event.Set();
|
||||
}
|
||||
else
|
||||
{
|
||||
SetError(result.error());
|
||||
}
|
||||
|
||||
state->compress_done_event.Set();
|
||||
}
|
||||
}
|
||||
|
||||
void OutputThreadFunction()
|
||||
{
|
||||
for (size_t i = 0; i < m_threads; ++i)
|
||||
m_compress_threads[i].output_ready_event.Set();
|
||||
|
||||
size_t index = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
CompressThread& compress_thread = m_compress_threads[index];
|
||||
|
||||
compress_thread.output_event.Wait();
|
||||
|
||||
if (m_shutting_down.load())
|
||||
return;
|
||||
|
||||
OutputParameters parameters = std::move(compress_thread.output_parameters);
|
||||
|
||||
compress_thread.output_ready_event.Set();
|
||||
|
||||
const ConversionResultCode result = m_output(std::move(parameters));
|
||||
|
||||
if (result != ConversionResultCode::Success)
|
||||
SetError(result);
|
||||
|
||||
++index;
|
||||
if (index >= m_threads)
|
||||
index -= m_threads;
|
||||
}
|
||||
}
|
||||
|
||||
std::function<ConversionResultCode(CompressThreadState*)> m_set_up_compress_thread_state;
|
||||
std::function<ConversionResult<OutputParameters>(CompressThreadState*, CompressParameters)>
|
||||
m_compress;
|
||||
std::function<ConversionResultCode(OutputParameters)> m_output;
|
||||
|
||||
// We can't use std::vector for this, because Common::Event is not movable
|
||||
std::unique_ptr<CompressThread[]> m_compress_threads;
|
||||
std::thread m_output_thread;
|
||||
|
||||
const size_t m_threads;
|
||||
size_t m_current_index = 0;
|
||||
|
||||
std::atomic<ConversionResultCode> m_result = ConversionResultCode::Success;
|
||||
std::atomic<bool> m_shutting_down = false;
|
||||
};
|
||||
|
||||
} // namespace DiscIO
|
||||
File diff suppressed because it is too large.
Load diff
@@ -7,7 +7,6 @@
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
@@ -16,7 +15,6 @@
|
||||
#include "Common/DirectIOFile.h"
|
||||
#include "Common/Swap.h"
|
||||
#include "DiscIO/Blob.h"
|
||||
#include "DiscIO/MultithreadedCompressor.h"
|
||||
#include "DiscIO/WIACompression.h"
|
||||
#include "DiscIO/WiiEncryptionCache.h"
|
||||
|
||||
@@ -35,8 +33,6 @@ enum class WIARVZCompressionType : u32
|
||||
Zstd = 5,
|
||||
};
|
||||
|
||||
std::pair<int, int> GetAllowedCompressionLevels(WIARVZCompressionType compression_type, bool gui);
|
||||
|
||||
constexpr u32 WIA_MAGIC = 0x01414957; // "WIA\x1" (byteswapped to little endian)
|
||||
constexpr u32 RVZ_MAGIC = 0x015A5652; // "RVZ\x1" (byteswapped to little endian)
|
||||
|
||||
@@ -67,11 +63,6 @@ public:
|
||||
bool SupportsReadWiiDecrypted(u64 offset, u64 size, u64 partition_data_offset) const override;
|
||||
bool ReadWiiDecrypted(u64 offset, u64 size, u8* out_ptr, u64 partition_data_offset) override;
|
||||
|
||||
static ConversionResultCode Convert(BlobReader* infile, const VolumeDisc* infile_volume,
|
||||
File::DirectIOFile* outfile,
|
||||
WIARVZCompressionType compression_type, int compression_level,
|
||||
int chunk_size, CompressCB callback);
|
||||
|
||||
private:
|
||||
using WiiKey = std::array<u8, 16>;
|
||||
|
||||
@@ -247,122 +238,6 @@ private:
|
||||
|
||||
static std::string VersionToString(u32 version);
|
||||
|
||||
struct ReuseID
|
||||
{
|
||||
bool operator==(const ReuseID& other) const
|
||||
{
|
||||
return std::tie(partition_key, data_size, encrypted, value) ==
|
||||
std::tie(other.partition_key, other.data_size, other.encrypted, other.value);
|
||||
}
|
||||
bool operator<(const ReuseID& other) const
|
||||
{
|
||||
return std::tie(partition_key, data_size, encrypted, value) <
|
||||
std::tie(other.partition_key, other.data_size, other.encrypted, other.value);
|
||||
}
|
||||
bool operator>(const ReuseID& other) const
|
||||
{
|
||||
return std::tie(partition_key, data_size, encrypted, value) >
|
||||
std::tie(other.partition_key, other.data_size, other.encrypted, other.value);
|
||||
}
|
||||
|
||||
bool operator>=(const ReuseID& other) const { return !operator<(other); }
|
||||
bool operator<=(const ReuseID& other) const { return !operator>(other); }
|
||||
|
||||
WiiKey partition_key;
|
||||
u64 data_size;
|
||||
bool encrypted;
|
||||
u8 value;
|
||||
};
|
||||
|
||||
struct CompressThreadState
|
||||
{
|
||||
using WiiBlockData = std::array<u8, VolumeWii::BLOCK_DATA_SIZE>;
|
||||
|
||||
std::unique_ptr<Compressor> compressor;
|
||||
|
||||
std::vector<WiiBlockData> decryption_buffer =
|
||||
std::vector<WiiBlockData>(VolumeWii::BLOCKS_PER_GROUP);
|
||||
|
||||
std::vector<VolumeWii::HashBlock> hash_buffer =
|
||||
std::vector<VolumeWii::HashBlock>(VolumeWii::BLOCKS_PER_GROUP);
|
||||
};
|
||||
|
||||
struct CompressParameters
|
||||
{
|
||||
std::vector<u8> data{};
|
||||
const DataEntry* data_entry = nullptr;
|
||||
u64 data_offset = 0;
|
||||
u64 bytes_read = 0;
|
||||
size_t group_index = 0;
|
||||
};
|
||||
|
||||
struct WIAOutputParametersEntry
|
||||
{
|
||||
std::vector<u8> exception_lists;
|
||||
std::vector<u8> main_data;
|
||||
std::optional<ReuseID> reuse_id;
|
||||
std::optional<GroupEntry> reused_group;
|
||||
};
|
||||
|
||||
struct RVZOutputParametersEntry
|
||||
{
|
||||
std::vector<u8> exception_lists;
|
||||
std::vector<u8> main_data;
|
||||
std::optional<ReuseID> reuse_id;
|
||||
std::optional<GroupEntry> reused_group;
|
||||
size_t rvz_packed_size = 0;
|
||||
bool compressed = false;
|
||||
};
|
||||
|
||||
using OutputParametersEntry =
|
||||
std::conditional_t<RVZ, RVZOutputParametersEntry, WIAOutputParametersEntry>;
|
||||
|
||||
struct OutputParameters
|
||||
{
|
||||
std::vector<OutputParametersEntry> entries;
|
||||
u64 bytes_read = 0;
|
||||
size_t group_index = 0;
|
||||
};
|
||||
|
||||
static bool PadTo4(File::DirectIOFile* file, u64* bytes_written);
|
||||
static void AddRawDataEntry(u64 offset, u64 size, int chunk_size, u32* total_groups,
|
||||
std::vector<RawDataEntry>* raw_data_entries,
|
||||
std::vector<DataEntry>* data_entries);
|
||||
static PartitionDataEntry
|
||||
CreatePartitionDataEntry(u64 offset, u64 size, u32 index, int chunk_size, u32* total_groups,
|
||||
const std::vector<PartitionEntry>& partition_entries,
|
||||
std::vector<DataEntry>* data_entries);
|
||||
static ConversionResultCode SetUpDataEntriesForWriting(
|
||||
const VolumeDisc* volume, int chunk_size, u64 iso_size, u32* total_groups,
|
||||
std::vector<PartitionEntry>* partition_entries, std::vector<RawDataEntry>* raw_data_entries,
|
||||
std::vector<DataEntry>* data_entries, std::vector<const FileSystem*>* partition_file_systems);
|
||||
static std::optional<std::vector<u8>> Compress(Compressor* compressor, const u8* data,
|
||||
size_t size);
|
||||
static bool WriteHeader(File::DirectIOFile* file, const u8* data, size_t size, u64 upper_bound,
|
||||
u64* bytes_written, u64* offset_out);
|
||||
|
||||
static void SetUpCompressor(std::unique_ptr<Compressor>* compressor,
|
||||
WIARVZCompressionType compression_type, int compression_level,
|
||||
WIAHeader2* header_2);
|
||||
static bool TryReuse(std::map<ReuseID, GroupEntry>* reusable_groups,
|
||||
std::mutex* reusable_groups_mutex, OutputParametersEntry* entry);
|
||||
static ConversionResult<OutputParameters>
|
||||
ProcessAndCompress(CompressThreadState* state, CompressParameters parameters,
|
||||
const std::vector<PartitionEntry>& partition_entries,
|
||||
const std::vector<DataEntry>& data_entries, const FileSystem* file_system,
|
||||
std::map<ReuseID, GroupEntry>* reusable_groups,
|
||||
std::mutex* reusable_groups_mutex, u64 chunks_per_wii_group,
|
||||
u64 exception_lists_per_chunk, bool compressed_exception_lists,
|
||||
bool compression);
|
||||
static ConversionResultCode Output(std::vector<OutputParametersEntry>* entries,
|
||||
File::DirectIOFile* outfile,
|
||||
std::map<ReuseID, GroupEntry>* reusable_groups,
|
||||
std::mutex* reusable_groups_mutex, GroupEntry* group_entry,
|
||||
u64* bytes_written);
|
||||
static ConversionResultCode RunCallback(size_t groups_written, u64 bytes_read, u64 bytes_written,
|
||||
u32 total_groups, u64 iso_size,
|
||||
const CompressCB& callback);
|
||||
|
||||
bool m_valid;
|
||||
WIARVZCompressionType m_compression_type;
|
||||
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinTool/ConvertCommand.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <OptionParser.h>
|
||||
#include <fmt/ostream.h>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "DiscIO/Blob.h"
|
||||
#include "DiscIO/DiscUtils.h"
|
||||
#include "DiscIO/ScrubbedBlob.h"
|
||||
#include "DiscIO/Volume.h"
|
||||
#include "DiscIO/WIABlob.h"
|
||||
#include "UICommon/UICommon.h"
|
||||
|
||||
namespace DolphinTool
|
||||
{
|
||||
static std::optional<DiscIO::WIARVZCompressionType>
|
||||
ParseCompressionTypeString(const std::string& compression_str)
|
||||
{
|
||||
if (compression_str == "none")
|
||||
return DiscIO::WIARVZCompressionType::None;
|
||||
else if (compression_str == "purge")
|
||||
return DiscIO::WIARVZCompressionType::Purge;
|
||||
else if (compression_str == "bzip2")
|
||||
return DiscIO::WIARVZCompressionType::Bzip2;
|
||||
else if (compression_str == "lzma")
|
||||
return DiscIO::WIARVZCompressionType::LZMA;
|
||||
else if (compression_str == "lzma2")
|
||||
return DiscIO::WIARVZCompressionType::LZMA2;
|
||||
else if (compression_str == "zstd")
|
||||
return DiscIO::WIARVZCompressionType::Zstd;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static std::optional<DiscIO::BlobType> ParseFormatString(const std::string& format_str)
|
||||
{
|
||||
if (format_str == "iso")
|
||||
return DiscIO::BlobType::PLAIN;
|
||||
else if (format_str == "gcz")
|
||||
return DiscIO::BlobType::GCZ;
|
||||
else if (format_str == "wia")
|
||||
return DiscIO::BlobType::WIA;
|
||||
else if (format_str == "rvz")
|
||||
return DiscIO::BlobType::RVZ;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int ConvertCommand(const std::vector<std::string>& args)
|
||||
{
|
||||
optparse::OptionParser parser;
|
||||
|
||||
parser.usage("usage: convert [options]... [FILE]...");
|
||||
|
||||
parser.add_option("-u", "--user")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("User folder path, required for temporary processing files. "
|
||||
"Will be automatically created if this option is not set.")
|
||||
.set_default("");
|
||||
|
||||
parser.add_option("-i", "--input")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Path to disc image FILE.")
|
||||
.metavar("FILE");
|
||||
|
||||
parser.add_option("-o", "--output")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Path to the destination FILE.")
|
||||
.metavar("FILE");
|
||||
|
||||
parser.add_option("-f", "--format")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Container format to use. Default is RVZ. [%choices]")
|
||||
.choices({"iso", "gcz", "wia", "rvz"});
|
||||
|
||||
parser.add_option("-s", "--scrub")
|
||||
.action("store_true")
|
||||
.help("Scrub junk data as part of conversion.");
|
||||
|
||||
parser.add_option("-b", "--block_size")
|
||||
.type("int")
|
||||
.action("store")
|
||||
.help("Block size for GCZ/WIA/RVZ formats, as an integer. Suggested value for RVZ: 131072 "
|
||||
"(128 KiB)");
|
||||
|
||||
parser.add_option("-c", "--compression")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Compression method to use when converting to WIA/RVZ. Suggested value for RVZ: zstd "
|
||||
"[%choices]")
|
||||
.choices({"none", "zstd", "bzip2", "lzma", "lzma2"});
|
||||
|
||||
parser.add_option("-l", "--compression_level")
|
||||
.type("int")
|
||||
.action("store")
|
||||
.help("Level of compression for the selected method. Ignored if 'none'. Suggested value for "
|
||||
"zstd: 5");
|
||||
|
||||
const optparse::Values& options = parser.parse_args(args);
|
||||
|
||||
// Initialize the dolphin user directory, required for temporary processing files
|
||||
// If this is not set, destructive file operations could occur due to path confusion
|
||||
UICommon::SetUserDirectory(options["user"]);
|
||||
UICommon::Init();
|
||||
|
||||
// Validate options
|
||||
|
||||
// --input
|
||||
if (!options.is_set("input"))
|
||||
{
|
||||
fmt::print(std::cerr, "Error: No input set\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string& input_file_path = options["input"];
|
||||
|
||||
// --output
|
||||
if (!options.is_set("output"))
|
||||
{
|
||||
fmt::print(std::cerr, "Error: No output set\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string& output_file_path = options["output"];
|
||||
|
||||
// --format
|
||||
const std::optional<DiscIO::BlobType> format_o = ParseFormatString(options["format"]);
|
||||
if (!format_o.has_value())
|
||||
{
|
||||
fmt::print(std::cerr, "Error: No output format set\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const DiscIO::BlobType format = format_o.value();
|
||||
|
||||
// Open the blob reader
|
||||
std::unique_ptr<DiscIO::BlobReader> blob_reader = DiscIO::CreateBlobReader(input_file_path);
|
||||
if (!blob_reader)
|
||||
{
|
||||
fmt::print(std::cerr, "Error: The input file could not be opened.\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// --scrub
|
||||
const bool scrub = static_cast<bool>(options.get("scrub"));
|
||||
|
||||
// Open the volume
|
||||
const std::unique_ptr<DiscIO::Volume> volume = DiscIO::CreateDisc(input_file_path);
|
||||
if (!volume)
|
||||
{
|
||||
if (scrub)
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Scrubbing is only supported for GC/Wii disc images.\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
fmt::print(std::cerr,
|
||||
"Warning: The input file is not a GC/Wii disc image. Continuing anyway.\n");
|
||||
}
|
||||
|
||||
if (scrub)
|
||||
{
|
||||
if (volume->IsDatelDisc())
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Scrubbing a Datel disc is not supported.\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
blob_reader = DiscIO::ScrubbedBlob::Create(input_file_path);
|
||||
|
||||
if (!blob_reader)
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Unable to process disc image. Try again without --scrub.\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
if (scrub && format == DiscIO::BlobType::RVZ)
|
||||
{
|
||||
fmt::print(std::cerr, "Warning: Scrubbing an RVZ container does not offer significant space "
|
||||
"advantages. Continuing anyway.\n");
|
||||
}
|
||||
|
||||
if (scrub && format == DiscIO::BlobType::PLAIN)
|
||||
{
|
||||
fmt::print(std::cerr, "Warning: Scrubbing does not save space when converting to ISO unless "
|
||||
"using external compression. Continuing anyway.\n");
|
||||
}
|
||||
|
||||
if (!scrub && format == DiscIO::BlobType::GCZ && volume &&
|
||||
volume->GetVolumeType() == DiscIO::Platform::WiiDisc && !volume->IsDatelDisc())
|
||||
{
|
||||
fmt::print(std::cerr, "Warning: Converting Wii disc images to GCZ without scrubbing may not "
|
||||
"offer space advantages over ISO. Continuing anyway.\n");
|
||||
}
|
||||
|
||||
if (volume && volume->IsNKit())
|
||||
{
|
||||
fmt::print(std::cerr,
|
||||
"Warning: Converting an NKit file, output will still be NKit! Continuing anyway.\n");
|
||||
}
|
||||
|
||||
// --block_size
|
||||
std::optional<int> block_size_o;
|
||||
if (options.is_set("block_size"))
|
||||
block_size_o = static_cast<int>(options.get("block_size"));
|
||||
|
||||
if (format == DiscIO::BlobType::GCZ || format == DiscIO::BlobType::WIA ||
|
||||
format == DiscIO::BlobType::RVZ)
|
||||
{
|
||||
if (!block_size_o.has_value())
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Block size must be set for GCZ/RVZ/WIA\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (!DiscIO::IsDiscImageBlockSizeValid(block_size_o.value(), format))
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Block size is not valid for this format\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (block_size_o.value() < DiscIO::PREFERRED_MIN_BLOCK_SIZE ||
|
||||
block_size_o.value() > DiscIO::PREFERRED_MAX_BLOCK_SIZE)
|
||||
{
|
||||
fmt::print(std::cerr,
|
||||
"Warning: Block size is not ideal for performance. Continuing anyway.\n");
|
||||
}
|
||||
|
||||
if (format == DiscIO::BlobType::GCZ && volume &&
|
||||
!DiscIO::IsGCZBlockSizeLegacyCompatible(block_size_o.value(), volume->GetDataSize()))
|
||||
{
|
||||
fmt::print(std::cerr,
|
||||
"Warning: For GCZs to be compatible with Dolphin < 5.0-11893, the file size "
|
||||
"must be an integer multiple of the block size and must not be an integer "
|
||||
"multiple of the block size multiplied by 32. Continuing anyway.\n");
|
||||
}
|
||||
}
|
||||
|
||||
// --compress, --compress_level
|
||||
const std::optional<DiscIO::WIARVZCompressionType> compression_o =
|
||||
ParseCompressionTypeString(options["compression"]);
|
||||
|
||||
std::optional<int> compression_level_o;
|
||||
if (options.is_set("compression_level"))
|
||||
compression_level_o = static_cast<int>(options.get("compression_level"));
|
||||
|
||||
if (format == DiscIO::BlobType::WIA || format == DiscIO::BlobType::RVZ)
|
||||
{
|
||||
if (!compression_o.has_value())
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Compression method must be set for WIA or RVZ\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if ((format == DiscIO::BlobType::WIA &&
|
||||
compression_o.value() == DiscIO::WIARVZCompressionType::Zstd) ||
|
||||
(format == DiscIO::BlobType::RVZ &&
|
||||
compression_o.value() == DiscIO::WIARVZCompressionType::Purge))
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Compression type is not supported for the container format\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (compression_o.value() == DiscIO::WIARVZCompressionType::None)
|
||||
{
|
||||
compression_level_o = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!compression_level_o.has_value())
|
||||
{
|
||||
fmt::print(std::cerr,
|
||||
"Error: Compression level must be set when compression type is not 'none'\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const std::pair<int, int> range =
|
||||
DiscIO::GetAllowedCompressionLevels(compression_o.value(), false);
|
||||
if (compression_level_o.value() < range.first || compression_level_o.value() > range.second)
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Compression level not in acceptable range\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the conversion
|
||||
const auto NOOP_STATUS_CALLBACK = [](const std::string& text, float percent) { return true; };
|
||||
|
||||
bool success = false;
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case DiscIO::BlobType::PLAIN:
|
||||
{
|
||||
success = DiscIO::ConvertToPlain(blob_reader.get(), input_file_path, output_file_path,
|
||||
NOOP_STATUS_CALLBACK);
|
||||
break;
|
||||
}
|
||||
|
||||
case DiscIO::BlobType::GCZ:
|
||||
{
|
||||
u32 sub_type = std::numeric_limits<u32>::max();
|
||||
if (volume)
|
||||
{
|
||||
if (volume->GetVolumeType() == DiscIO::Platform::GameCubeDisc)
|
||||
sub_type = 0;
|
||||
else if (volume->GetVolumeType() == DiscIO::Platform::WiiDisc)
|
||||
sub_type = 1;
|
||||
}
|
||||
success = DiscIO::ConvertToGCZ(blob_reader.get(), input_file_path, output_file_path, sub_type,
|
||||
block_size_o.value(), NOOP_STATUS_CALLBACK);
|
||||
break;
|
||||
}
|
||||
|
||||
case DiscIO::BlobType::WIA:
|
||||
case DiscIO::BlobType::RVZ:
|
||||
{
|
||||
success = DiscIO::ConvertToWIAOrRVZ(blob_reader.get(), input_file_path, output_file_path,
|
||||
format == DiscIO::BlobType::RVZ, compression_o.value(),
|
||||
compression_level_o.value(), block_size_o.value(),
|
||||
NOOP_STATUS_CALLBACK);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
ASSERT(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Conversion failed\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
} // namespace DolphinTool
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace DolphinTool
|
||||
{
|
||||
int ConvertCommand(const std::vector<std::string>& args);
|
||||
} // namespace DolphinTool
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="amd64"
|
||||
name="DolphinTeam.DolphinTool"
|
||||
type="win32"
|
||||
/>
|
||||
<description>Dolphin Tool</description>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="ConvertCommand.cpp" />
|
||||
<ClCompile Include="VerifyCommand.cpp" />
|
||||
<ClCompile Include="HeaderCommand.cpp" />
|
||||
<ClCompile Include="ExtractCommand.cpp" />
|
||||
<ClCompile Include="ToolHeadlessPlatform.cpp" />
|
||||
<ClCompile Include="ToolMain.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ExtractCommand.h" />
|
||||
<ClInclude Include="ConvertCommand.h" />
|
||||
<ClInclude Include="VerifyCommand.h" />
|
||||
<ClInclude Include="HeaderCommand.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Manifest Include="DolphinTool.exe.manifest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Text Include="CMakeLists.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="DolphinTool.rc" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,6 +0,0 @@
|
||||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#include "resource.h"
|
||||
IDI_ICON1 ICON "..\\..\\..\\Installer\\Dolphin.ico"
|
||||
"dolphin" ICON "..\\..\\..\\Installer\\Dolphin.ico"
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinTool/ExtractCommand.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ostream.h>
|
||||
|
||||
#include <OptionParser.h>
|
||||
|
||||
#include "Common/FileUtil.h"
|
||||
|
||||
#include "DiscIO/DiscExtractor.h"
|
||||
#include "DiscIO/DiscUtils.h"
|
||||
#include "DiscIO/Filesystem.h"
|
||||
#include "DiscIO/Volume.h"
|
||||
|
||||
namespace DolphinTool
|
||||
{
|
||||
static void ExtractFile(const DiscIO::Volume& disc_volume, const DiscIO::Partition& partition,
|
||||
const std::string& path, const std::string& out)
|
||||
{
|
||||
const DiscIO::FileSystem* filesystem = disc_volume.GetFileSystem(partition);
|
||||
if (!filesystem)
|
||||
return;
|
||||
|
||||
ExportFile(disc_volume, partition, filesystem->FindFileInfo(path).get(), out);
|
||||
}
|
||||
|
||||
static std::unique_ptr<DiscIO::FileInfo> GetFileInfo(const DiscIO::Volume& disc_volume,
|
||||
const DiscIO::Partition& partition,
|
||||
const std::string& path)
|
||||
{
|
||||
const DiscIO::FileSystem* filesystem = disc_volume.GetFileSystem(partition);
|
||||
if (!filesystem)
|
||||
return nullptr;
|
||||
|
||||
return filesystem->FindFileInfo(path);
|
||||
}
|
||||
|
||||
static bool VolumeSupported(const DiscIO::Volume& disc_volume)
|
||||
{
|
||||
switch (disc_volume.GetVolumeType())
|
||||
{
|
||||
case DiscIO::Platform::WiiWAD:
|
||||
fmt::println(std::cerr, "Error: Wii WADs are not supported.");
|
||||
return false;
|
||||
case DiscIO::Platform::ELFOrDOL:
|
||||
fmt::println(std::cerr,
|
||||
"Error: *.elf or *.dol have no filesystem and are therefore not supported.");
|
||||
return false;
|
||||
case DiscIO::Platform::WiiDisc:
|
||||
case DiscIO::Platform::GameCubeDisc:
|
||||
return true;
|
||||
default:
|
||||
fmt::println(std::cerr, "Error: Unknown volume type.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void ExtractDirectory(const DiscIO::Volume& disc_volume, const DiscIO::Partition& partition,
|
||||
const std::string& path, const std::string& out, bool quiet)
|
||||
{
|
||||
const DiscIO::FileSystem* filesystem = disc_volume.GetFileSystem(partition);
|
||||
if (!filesystem)
|
||||
return;
|
||||
|
||||
const std::unique_ptr<DiscIO::FileInfo> info = filesystem->FindFileInfo(path);
|
||||
u32 size = info->GetTotalChildren();
|
||||
u32 files = 0;
|
||||
ExportDirectory(
|
||||
disc_volume, partition, *info, true, "", out,
|
||||
[&files, &size, &quiet](const std::string& current) {
|
||||
files++;
|
||||
const float progress = static_cast<float>(files) / static_cast<float>(size) * 100;
|
||||
if (!quiet)
|
||||
fmt::println(std::cerr, "Extracting: {} | {}%", current, static_cast<int>(progress));
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
static bool ExtractSystemData(const DiscIO::Volume& disc_volume, const DiscIO::Partition& partition,
|
||||
const std::string& out)
|
||||
{
|
||||
return ExportSystemData(disc_volume, partition, out);
|
||||
}
|
||||
|
||||
static void ExtractPartition(const DiscIO::Volume& disc_volume, const DiscIO::Partition& partition,
|
||||
const std::string& out, bool quiet)
|
||||
{
|
||||
ExtractDirectory(disc_volume, partition, "", out + "/files", quiet);
|
||||
ExtractSystemData(disc_volume, partition, out);
|
||||
}
|
||||
|
||||
static void ListRecursively(const std::string& path, const DiscIO::FileInfo& info,
|
||||
std::string* result_text)
|
||||
{
|
||||
// Don't print the root.
|
||||
if (!path.empty())
|
||||
{
|
||||
const std::string line = fmt::format("{}\n", path);
|
||||
fmt::print("{}", line);
|
||||
result_text->append(line);
|
||||
}
|
||||
for (const DiscIO::FileInfo& child_info : info)
|
||||
{
|
||||
std::string child_path = path + child_info.GetName();
|
||||
if (child_info.IsDirectory())
|
||||
child_path += '/';
|
||||
ListRecursively(child_path, child_info, result_text);
|
||||
}
|
||||
}
|
||||
|
||||
static bool ListPartition(const DiscIO::Volume& disc_volume, const DiscIO::Partition& partition,
|
||||
const std::string& partition_name, const std::string& path,
|
||||
std::string* result_text)
|
||||
{
|
||||
const DiscIO::FileSystem* filesystem = disc_volume.GetFileSystem(partition);
|
||||
if (!filesystem)
|
||||
{
|
||||
fmt::println(std::cerr, "Warning: partition {} has no filesystem.", partition_name);
|
||||
return false;
|
||||
}
|
||||
const std::unique_ptr<DiscIO::FileInfo> info = filesystem->FindFileInfo(path);
|
||||
|
||||
if (!info)
|
||||
{
|
||||
if (!partition_name.empty())
|
||||
{
|
||||
fmt::println(std::cerr, "Warning: {} does not exist in this partition.", path);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Canonicalize user-provided path by reconstructing it using GetPath().
|
||||
ListRecursively(info->GetPath(), *info, result_text);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ListVolume(const DiscIO::Volume& disc_volume, const std::string& path,
|
||||
const std::string& specific_partition_name, bool quiet,
|
||||
std::string* result_text)
|
||||
{
|
||||
if (disc_volume.GetPartitions().empty())
|
||||
{
|
||||
return ListPartition(disc_volume, DiscIO::PARTITION_NONE, specific_partition_name, path,
|
||||
result_text);
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
for (DiscIO::Partition& p : disc_volume.GetPartitions())
|
||||
{
|
||||
const std::optional<u32> partition_type = disc_volume.GetPartitionType(p);
|
||||
if (!partition_type)
|
||||
{
|
||||
fmt::println(std::cerr, "Error: Could not get partition type.");
|
||||
return false;
|
||||
}
|
||||
const std::string partition_name = DiscIO::NameForPartitionType(*partition_type, true);
|
||||
|
||||
if (!specific_partition_name.empty() &&
|
||||
!Common::CaseInsensitiveEquals(partition_name, specific_partition_name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string partition_start =
|
||||
fmt::format("/// PARTITION: {} <{}> ///\n", partition_name, path);
|
||||
fmt::print(std::cout, "{}", partition_start);
|
||||
result_text->append(partition_start);
|
||||
|
||||
success |= ListPartition(disc_volume, p, partition_name, path, result_text);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool HandleExtractPartition(const std::string& output, const std::string& single_file_path,
|
||||
const std::string& partition_name,
|
||||
const DiscIO::Volume& disc_volume,
|
||||
const DiscIO::Partition& partition, bool quiet, bool single)
|
||||
{
|
||||
std::string file;
|
||||
file.append(output).append("/");
|
||||
file.append(partition_name).append("/");
|
||||
if (!single)
|
||||
{
|
||||
ExtractPartition(disc_volume, partition, file, quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto file_info = GetFileInfo(disc_volume, partition, single_file_path);
|
||||
if (file_info != nullptr)
|
||||
{
|
||||
file.append("files/").append(single_file_path);
|
||||
File::CreateFullPath(file);
|
||||
if (file_info->IsDirectory())
|
||||
{
|
||||
file = PathToString(StringToPath(file).remove_filename());
|
||||
ExtractDirectory(disc_volume, partition, single_file_path, file, quiet);
|
||||
}
|
||||
else
|
||||
{
|
||||
ExtractFile(disc_volume, partition, single_file_path, file);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int Extract(const std::vector<std::string>& args)
|
||||
{
|
||||
optparse::OptionParser parser;
|
||||
|
||||
parser.usage("usage: extract [options]...");
|
||||
|
||||
parser.add_option("-i", "--input")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Path to disc image FILE.")
|
||||
.metavar("FILE");
|
||||
parser.add_option("-o", "--output")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Path to the destination FOLDER.")
|
||||
.metavar("FOLDER");
|
||||
parser.add_option("-p", "--partition")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Which specific partition you want to extract.");
|
||||
parser.add_option("-s", "--single")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Which specific file/directory you want to extract.");
|
||||
parser.add_option("-l", "--list")
|
||||
.action("store_true")
|
||||
.help("List all files in volume/partition. Will print the directory/file specified with "
|
||||
"--single if defined.");
|
||||
parser.add_option("-q", "--quiet")
|
||||
.action("store_true")
|
||||
.help("Mute all messages except for errors.");
|
||||
parser.add_option("-g", "--gameonly")
|
||||
.action("store_true")
|
||||
.help("Only extracts the DATA partition.");
|
||||
|
||||
const optparse::Values& options = parser.parse_args(args);
|
||||
|
||||
const bool quiet = options.is_set("quiet");
|
||||
const bool gameonly = options.is_set("gameonly");
|
||||
|
||||
if (!options.is_set("input"))
|
||||
{
|
||||
fmt::println(std::cerr, "Error: No input image set");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string& input_file_path = options["input"];
|
||||
|
||||
const std::string& output_folder_path = options["output"];
|
||||
|
||||
if (!options.is_set("output") && !options.is_set("list"))
|
||||
{
|
||||
fmt::println(std::cerr, "Error: No output folder set");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const std::string& single_file_path = options["single"];
|
||||
std::string specific_partition = options["partition"];
|
||||
|
||||
if (options.is_set("output") && !options.is_set("list"))
|
||||
File::CreateDirs(output_folder_path);
|
||||
|
||||
if (gameonly)
|
||||
specific_partition = std::string("data");
|
||||
|
||||
if (const std::unique_ptr<DiscIO::BlobReader> blob_reader =
|
||||
DiscIO::CreateBlobReader(input_file_path);
|
||||
!blob_reader)
|
||||
{
|
||||
fmt::println(std::cerr, "Error: Unable to open disc image");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const std::unique_ptr<DiscIO::Volume> disc_volume = DiscIO::CreateVolume(input_file_path);
|
||||
|
||||
if (!disc_volume)
|
||||
{
|
||||
fmt::println(std::cerr, "Error: Unable to open volume");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (!VolumeSupported(*disc_volume))
|
||||
return EXIT_FAILURE;
|
||||
|
||||
if (options.is_set("list"))
|
||||
{
|
||||
std::string list_path = options.is_set("single") ? single_file_path : "/";
|
||||
if (quiet && !options.is_set("output"))
|
||||
{
|
||||
fmt::println(std::cerr, "Error: --quiet is set but no output file provided. Please either "
|
||||
"remove the --quiet flag or specify --output");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
std::string text;
|
||||
if (!ListVolume(*disc_volume, list_path, specific_partition, quiet, &text))
|
||||
{
|
||||
fmt::println(std::cerr, "Error: Found nothing to list");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (options.is_set("output"))
|
||||
{
|
||||
File::CreateFullPath(output_folder_path);
|
||||
std::ofstream output_file;
|
||||
output_file.open(output_folder_path);
|
||||
if (!output_file.is_open())
|
||||
{
|
||||
fmt::println(std::cerr, "Error: Unable to open output file");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
output_file << text;
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
bool extracted_one = false;
|
||||
|
||||
if (disc_volume->GetPartitions().empty())
|
||||
{
|
||||
if (options.is_set("partition"))
|
||||
{
|
||||
fmt::println(
|
||||
std::cerr,
|
||||
"Warning: --partition has a value even though this image doesn't have any partitions.");
|
||||
}
|
||||
|
||||
extracted_one = HandleExtractPartition(output_folder_path, single_file_path, "", *disc_volume,
|
||||
DiscIO::PARTITION_NONE, quiet, options.is_set("single"));
|
||||
}
|
||||
else
|
||||
{
|
||||
for (DiscIO::Partition& p : disc_volume->GetPartitions())
|
||||
{
|
||||
if (const std::optional<u32> partition_type = disc_volume->GetPartitionType(p))
|
||||
{
|
||||
const std::string partition_name = DiscIO::NameForPartitionType(*partition_type, true);
|
||||
|
||||
if (!specific_partition.empty() &&
|
||||
!Common::CaseInsensitiveEquals(specific_partition, partition_name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
extracted_one |=
|
||||
HandleExtractPartition(output_folder_path, single_file_path, partition_name,
|
||||
*disc_volume, p, quiet, options.is_set("single"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!extracted_one)
|
||||
{
|
||||
if (options.is_set("single"))
|
||||
fmt::print(std::cerr, "Error: No file/folder was extracted.");
|
||||
else
|
||||
fmt::print(std::cerr, "Error: No partitions were extracted.");
|
||||
if (options.is_set("partition"))
|
||||
fmt::println(std::cerr, " Maybe you misspelled your specified partition?");
|
||||
fmt::println(std::cerr, "\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (!quiet)
|
||||
fmt::println(std::cerr, "Finished Successfully!");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
} // namespace DolphinTool
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace DolphinTool
|
||||
{
|
||||
int Extract(const std::vector<std::string>& args);
|
||||
} // namespace DolphinTool
|
||||
@@ -1,170 +0,0 @@
|
||||
// Copyright 2022 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinTool/HeaderCommand.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <OptionParser.h>
|
||||
#include <fmt/ostream.h>
|
||||
#include <picojson.h>
|
||||
|
||||
#include "DiscIO/Blob.h"
|
||||
#include "DiscIO/Volume.h"
|
||||
|
||||
namespace DolphinTool
|
||||
{
|
||||
int HeaderCommand(const std::vector<std::string>& args)
|
||||
{
|
||||
optparse::OptionParser parser;
|
||||
|
||||
parser.usage("usage: header [options]...");
|
||||
|
||||
parser.add_option("-i", "--input")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Path to disc image FILE.")
|
||||
.metavar("FILE");
|
||||
|
||||
parser.add_option("-j", "--json")
|
||||
.action("store_true")
|
||||
.help("Optional. Print the information as JSON, then exit. Overrides other print options.");
|
||||
|
||||
parser.add_option("-b", "--block_size")
|
||||
.action("store_true")
|
||||
.help("Optional. Print the block size of GCZ/WIA/RVZ formats, then exit.");
|
||||
|
||||
parser.add_option("-c", "--compression")
|
||||
.action("store_true")
|
||||
.help("Optional. Print the compression method of GCZ/WIA/RVZ formats, then exit.");
|
||||
|
||||
parser.add_option("-l", "--compression_level")
|
||||
.action("store_true")
|
||||
.help("Optional. Print the level of compression for WIA/RVZ formats, then exit.");
|
||||
|
||||
const optparse::Values& options = parser.parse_args(args);
|
||||
|
||||
// Validate options
|
||||
const std::string& input_file_path = options["input"];
|
||||
if (input_file_path.empty())
|
||||
{
|
||||
fmt::print(std::cerr, "Error: No input set\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const bool enable_json = options.is_set_by_user("json");
|
||||
const bool enable_block_size = options.is_set_by_user("block_size");
|
||||
const bool enable_compression_method = options.is_set_by_user("compression");
|
||||
const bool enable_compression_level = options.is_set_by_user("compression_level");
|
||||
|
||||
// Open the blob reader
|
||||
const std::unique_ptr<DiscIO::BlobReader> blob_reader = DiscIO::CreateBlobReader(input_file_path);
|
||||
if (!blob_reader)
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Unable to open disc image\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
// Open the volume
|
||||
const std::unique_ptr<DiscIO::Volume> volume = DiscIO::CreateVolume(blob_reader->CopyReader());
|
||||
|
||||
if (enable_json)
|
||||
{
|
||||
auto json = picojson::object();
|
||||
|
||||
// File data
|
||||
if (const u64 block_size = blob_reader->GetBlockSize())
|
||||
json["block_size"] = picojson::value((double)block_size);
|
||||
|
||||
const std::string compression_method = blob_reader->GetCompressionMethod();
|
||||
if (compression_method != "")
|
||||
json["compression_method"] = picojson::value(compression_method);
|
||||
|
||||
if (const std::optional<int> compression_level = blob_reader->GetCompressionLevel())
|
||||
json["compression_level"] = picojson::value((double)compression_level.value());
|
||||
|
||||
// Game data
|
||||
if (volume)
|
||||
{
|
||||
json["internal_name"] = picojson::value(volume->GetInternalName());
|
||||
|
||||
if (const std::optional<u16> revision = volume->GetRevision())
|
||||
json["revision"] = picojson::value((double)revision.value());
|
||||
|
||||
json["game_id"] = picojson::value(volume->GetGameID());
|
||||
|
||||
if (const std::optional<u64> title_id = volume->GetTitleID())
|
||||
json["title_id"] = picojson::value((double)title_id.value());
|
||||
|
||||
json["region"] = picojson::value(DiscIO::GetName(volume->GetRegion(), false));
|
||||
|
||||
json["country"] = picojson::value(DiscIO::GetName(volume->GetCountry(), false));
|
||||
}
|
||||
|
||||
// Print
|
||||
std::cout << picojson::value(json) << '\n';
|
||||
}
|
||||
else if (enable_block_size || enable_compression_method || enable_compression_level)
|
||||
{
|
||||
if (enable_block_size)
|
||||
{
|
||||
const auto block_size = blob_reader->GetBlockSize();
|
||||
if (block_size == 0)
|
||||
fmt::print(std::cout, "N/A\n");
|
||||
else
|
||||
fmt::print(std::cout, "{}\n", block_size);
|
||||
}
|
||||
if (enable_compression_method)
|
||||
{
|
||||
const auto compression_method = blob_reader->GetCompressionMethod();
|
||||
if (compression_method == "")
|
||||
fmt::print(std::cout, "N/A\n");
|
||||
else
|
||||
fmt::print(std::cout, "{}\n", compression_method);
|
||||
}
|
||||
if (enable_compression_level)
|
||||
{
|
||||
const auto compression_level_o = blob_reader->GetCompressionLevel();
|
||||
if (compression_level_o == std::nullopt)
|
||||
fmt::print(std::cout, "N/A\n");
|
||||
else
|
||||
fmt::print(std::cout, "{}\n", compression_level_o.value());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// File data
|
||||
if (const u64 block_size = blob_reader->GetBlockSize())
|
||||
fmt::print(std::cout, "Block Size: {}\n", block_size);
|
||||
|
||||
const std::string compression_method = blob_reader->GetCompressionMethod();
|
||||
if (compression_method != "")
|
||||
fmt::print(std::cout, "Compression Method: {}\n", compression_method);
|
||||
|
||||
if (const std::optional<int> compression_level = blob_reader->GetCompressionLevel())
|
||||
fmt::print(std::cout, "Compression Level: {}\n", compression_level.value());
|
||||
|
||||
// Game data
|
||||
if (volume)
|
||||
{
|
||||
fmt::print(std::cout, "Internal Name: {}\n", volume->GetInternalName());
|
||||
|
||||
if (const std::optional<u16> revision = volume->GetRevision())
|
||||
fmt::print(std::cout, "Revision: {}\n", revision.value());
|
||||
|
||||
fmt::print(std::cout, "Game ID: {}\n", volume->GetGameID());
|
||||
|
||||
if (const std::optional<u64> title_id = volume->GetTitleID())
|
||||
fmt::print(std::cout, "Title ID: {:016x}\n", title_id.value());
|
||||
|
||||
fmt::print(std::cout, "Region: {}\n", DiscIO::GetName(volume->GetRegion(), false));
|
||||
|
||||
fmt::print(std::cout, "Country: {}\n", DiscIO::GetName(volume->GetCountry(), false));
|
||||
}
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
} // namespace DolphinTool
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2022 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace DolphinTool
|
||||
{
|
||||
int HeaderCommand(const std::vector<std::string>& args);
|
||||
} // namespace DolphinTool
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Core/Host.h"
|
||||
|
||||
// Begin stubs needed to satisfy Core dependencies
|
||||
|
||||
std::vector<std::string> Host_GetPreferredLocales()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void Host_PPCSymbolsChanged()
|
||||
{
|
||||
}
|
||||
|
||||
void Host_PPCBreakpointsChanged()
|
||||
{
|
||||
}
|
||||
|
||||
bool Host_UIBlocksControllerState()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void Host_Message(HostMessageID id)
|
||||
{
|
||||
}
|
||||
|
||||
void Host_UpdateTitle(const std::string& title)
|
||||
{
|
||||
}
|
||||
|
||||
void Host_UpdateDisasmDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void Host_JitCacheInvalidation()
|
||||
{
|
||||
}
|
||||
|
||||
void Host_JitProfileDataWiped()
|
||||
{
|
||||
}
|
||||
|
||||
void Host_RequestRenderWindowSize(int width, int height)
|
||||
{
|
||||
}
|
||||
|
||||
bool Host_RendererHasFocus()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Host_RendererHasFullFocus()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Host_RendererIsFullscreen()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Host_TASInputHasFocus()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void Host_YieldToUI()
|
||||
{
|
||||
}
|
||||
|
||||
void Host_TitleChanged()
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<GBAHostInterface> Host_CreateGBAHost(std::weak_ptr<HW::GBA::Core> core)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
// End stubs to satisfy Core dependencies
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/ostream.h>
|
||||
|
||||
#include "DolphinTool/ConvertCommand.h"
|
||||
#include "DolphinTool/ExtractCommand.h"
|
||||
#include "DolphinTool/HeaderCommand.h"
|
||||
#include "DolphinTool/VerifyCommand.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "Common/StringUtil.h"
|
||||
#endif
|
||||
|
||||
static void PrintUsage()
|
||||
{
|
||||
fmt::print(std::cerr, "usage: dolphin-tool COMMAND -h\n"
|
||||
"\n"
|
||||
"commands supported: [convert, verify, header, extract]\n");
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
#define main app_main
|
||||
#endif
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
PrintUsage();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const std::string_view command_str = argv[1];
|
||||
// Take off the program name and command selector before passing arguments down
|
||||
const std::vector<std::string> args(argv + 2, argv + argc);
|
||||
|
||||
if (command_str == "convert")
|
||||
return DolphinTool::ConvertCommand(args);
|
||||
else if (command_str == "verify")
|
||||
return DolphinTool::VerifyCommand(args);
|
||||
else if (command_str == "header")
|
||||
return DolphinTool::HeaderCommand(args);
|
||||
else if (command_str == "extract")
|
||||
return DolphinTool::Extract(args);
|
||||
PrintUsage();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
int wmain(int, wchar_t*[], wchar_t*[])
|
||||
{
|
||||
std::vector<std::string> args = Common::CommandLineToUtf8Argv(GetCommandLineW());
|
||||
const int argc = static_cast<int>(args.size());
|
||||
std::vector<char*> argv(args.size());
|
||||
for (size_t i = 0; i < args.size(); ++i)
|
||||
argv[i] = args[i].data();
|
||||
|
||||
return main(argc, argv.data());
|
||||
}
|
||||
|
||||
#undef main
|
||||
#endif
|
||||
@@ -1,198 +0,0 @@
|
||||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinTool/VerifyCommand.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <OptionParser.h>
|
||||
#include <fmt/ostream.h>
|
||||
|
||||
#include "Core/AchievementManager.h"
|
||||
#include "DiscIO/Volume.h"
|
||||
#include "DiscIO/VolumeVerifier.h"
|
||||
#include "UICommon/UICommon.h"
|
||||
|
||||
namespace DolphinTool
|
||||
{
|
||||
static std::string HashToHexString(const std::vector<u8>& hash)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << std::hex;
|
||||
for (int i = 0; i < static_cast<int>(hash.size()); ++i)
|
||||
{
|
||||
ss << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]);
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
static void PrintFullReport(const DiscIO::VolumeVerifier::Result& result)
|
||||
{
|
||||
if (!result.hashes.crc32.empty())
|
||||
fmt::print(std::cout, "CRC32: {}\n", HashToHexString(result.hashes.crc32));
|
||||
else
|
||||
fmt::print(std::cout, "CRC32 not computed\n");
|
||||
|
||||
if (!result.hashes.md5.empty())
|
||||
fmt::print(std::cout, "MD5: {}\n", HashToHexString(result.hashes.md5));
|
||||
else
|
||||
fmt::print(std::cout, "MD5 not computed\n");
|
||||
|
||||
if (!result.hashes.sha1.empty())
|
||||
fmt::print(std::cout, "SHA1: {}\n", HashToHexString(result.hashes.sha1));
|
||||
else
|
||||
fmt::print(std::cout, "SHA1 not computed\n");
|
||||
|
||||
fmt::print(std::cout, "Problems Found: {}\n", result.problems.empty() ? "No" : "Yes");
|
||||
|
||||
for (const auto& problem : result.problems)
|
||||
{
|
||||
fmt::print(std::cout, "\nSeverity: ");
|
||||
switch (problem.severity)
|
||||
{
|
||||
case DiscIO::VolumeVerifier::Severity::Low:
|
||||
fmt::print(std::cout, "Low");
|
||||
break;
|
||||
case DiscIO::VolumeVerifier::Severity::Medium:
|
||||
fmt::print(std::cout, "Medium");
|
||||
break;
|
||||
case DiscIO::VolumeVerifier::Severity::High:
|
||||
fmt::print(std::cout, "High");
|
||||
break;
|
||||
case DiscIO::VolumeVerifier::Severity::None:
|
||||
fmt::print(std::cout, "None");
|
||||
break;
|
||||
default:
|
||||
ASSERT(false);
|
||||
break;
|
||||
}
|
||||
fmt::print(std::cout, "\nSummary: {}\n\n", problem.text);
|
||||
}
|
||||
}
|
||||
|
||||
int VerifyCommand(const std::vector<std::string>& args)
|
||||
{
|
||||
optparse::OptionParser parser;
|
||||
|
||||
parser.usage("usage: verify [options]...");
|
||||
|
||||
parser.add_option("-u", "--user")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("User folder path, required for temporary processing files. "
|
||||
"Will be automatically created if this option is not set.")
|
||||
.set_default("");
|
||||
|
||||
parser.add_option("-i", "--input")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Path to input file.")
|
||||
.metavar("FILE");
|
||||
|
||||
parser.add_option("-a", "--algorithm")
|
||||
.type("string")
|
||||
.action("store")
|
||||
.help("Optional. Compute and print the digest using the selected algorithm, then exit. "
|
||||
"[%choices]")
|
||||
.choices({"crc32", "md5", "sha1", "rchash"});
|
||||
|
||||
const optparse::Values& options = parser.parse_args(args);
|
||||
|
||||
// Initialize the dolphin user directory, required for temporary processing files
|
||||
// If this is not set, destructive file operations could occur due to path confusion
|
||||
UICommon::SetUserDirectory(options["user"]);
|
||||
UICommon::Init();
|
||||
|
||||
// Validate options
|
||||
if (!options.is_set("input"))
|
||||
{
|
||||
fmt::print(std::cerr, "Error: No input set\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string& input_file_path = options["input"];
|
||||
|
||||
bool rc_hash_calculate = false;
|
||||
std::string rc_hash_result = "0";
|
||||
|
||||
DiscIO::Hashes<bool> hashes_to_calculate{};
|
||||
const bool algorithm_is_set = options.is_set("algorithm");
|
||||
if (!algorithm_is_set)
|
||||
{
|
||||
hashes_to_calculate = DiscIO::VolumeVerifier::GetDefaultHashesToCalculate();
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string& algorithm = options["algorithm"];
|
||||
if (algorithm == "crc32")
|
||||
hashes_to_calculate.crc32 = true;
|
||||
else if (algorithm == "md5")
|
||||
hashes_to_calculate.md5 = true;
|
||||
else if (algorithm == "sha1")
|
||||
hashes_to_calculate.sha1 = true;
|
||||
#ifdef USE_RETRO_ACHIEVEMENTS
|
||||
else if (algorithm == "rchash")
|
||||
rc_hash_calculate = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!hashes_to_calculate.crc32 && !hashes_to_calculate.md5 && !hashes_to_calculate.sha1 &&
|
||||
!rc_hash_calculate)
|
||||
{
|
||||
// optparse should protect from this
|
||||
fmt::print(std::cerr, "Error: No algorithms selected for the operation\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Open the volume
|
||||
const std::unique_ptr<DiscIO::Volume> volume = DiscIO::CreateVolume(input_file_path);
|
||||
if (!volume)
|
||||
{
|
||||
fmt::print(std::cerr, "Error: Unable to open input file\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Verify the volume
|
||||
DiscIO::VolumeVerifier verifier(*volume, false, hashes_to_calculate);
|
||||
verifier.Start();
|
||||
while (verifier.GetBytesProcessed() != verifier.GetTotalBytes())
|
||||
{
|
||||
verifier.Process();
|
||||
}
|
||||
verifier.Finish();
|
||||
const DiscIO::VolumeVerifier::Result& result = verifier.GetResult();
|
||||
|
||||
#ifdef USE_RETRO_ACHIEVEMENTS
|
||||
// Calculate rcheevos hash
|
||||
if (rc_hash_calculate)
|
||||
{
|
||||
rc_hash_result = AchievementManager::CalculateHash(input_file_path);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Print the report
|
||||
if (!algorithm_is_set)
|
||||
{
|
||||
PrintFullReport(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hashes_to_calculate.crc32 && !result.hashes.crc32.empty())
|
||||
fmt::print(std::cout, "{}\n", HashToHexString(result.hashes.crc32));
|
||||
else if (hashes_to_calculate.md5 && !result.hashes.md5.empty())
|
||||
fmt::print(std::cout, "{}\n", HashToHexString(result.hashes.md5));
|
||||
else if (hashes_to_calculate.sha1 && !result.hashes.sha1.empty())
|
||||
fmt::print(std::cout, "{}\n", HashToHexString(result.hashes.sha1));
|
||||
else if (rc_hash_calculate)
|
||||
fmt::print(std::cout, "{}\n", rc_hash_result);
|
||||
else
|
||||
{
|
||||
fmt::print(std::cerr, "Error: No hash computed\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
} // namespace DolphinTool
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace DolphinTool
|
||||
{
|
||||
int VerifyCommand(const std::vector<std::string>& args);
|
||||
} // namespace DolphinTool
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright 2017 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by DolphinNoGui.rc
|
||||
//
|
||||
#ifdef RC_INVOKED
|
||||
#define IDI_ICON1 101
|
||||
#else
|
||||
#define IDI_ICON1 MAKEINTRESOURCE(101)
|
||||
#endif
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
@@ -9,24 +9,9 @@
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Core/HW/WiimoteReal/WiimoteReal.h"
|
||||
|
||||
#ifdef CIFACE_USE_WIN32
|
||||
#include "InputCommon/ControllerInterface/Win32/Win32.h"
|
||||
#endif
|
||||
#ifdef CIFACE_USE_XLIB
|
||||
#include "InputCommon/ControllerInterface/Xlib/XInput2.h"
|
||||
#endif
|
||||
#ifdef CIFACE_USE_OSX
|
||||
#include "InputCommon/ControllerInterface/Quartz/Quartz.h"
|
||||
#endif
|
||||
#ifdef CIFACE_USE_ANDROID
|
||||
#include "InputCommon/ControllerInterface/Android/Android.h"
|
||||
#endif
|
||||
#ifdef CIFACE_USE_EVDEV
|
||||
#include "InputCommon/ControllerInterface/evdev/evdev.h"
|
||||
#endif
|
||||
#ifdef CIFACE_USE_PIPES
|
||||
#include "InputCommon/ControllerInterface/Pipes/Pipes.h"
|
||||
#endif
|
||||
#ifdef CIFACE_USE_DUALSHOCKUDPCLIENT
|
||||
#include "InputCommon/ControllerInterface/DualShockUDPClient/DualShockUDPClient.h"
|
||||
#endif
|
||||
@@ -55,24 +40,9 @@ void ControllerInterface::Initialize(const WindowSystemInfo& wsi)
|
||||
|
||||
m_populating_devices_counter = 1;
|
||||
|
||||
#if defined(CIFACE_USE_WIN32)
|
||||
m_input_backends.emplace_back(ciface::Win32::CreateInputBackend(this));
|
||||
#endif
|
||||
#ifdef CIFACE_USE_XLIB
|
||||
m_input_backends.emplace_back(ciface::XInput2::CreateInputBackend(this));
|
||||
#endif
|
||||
#ifdef CIFACE_USE_OSX
|
||||
m_input_backends.emplace_back(ciface::Quartz::CreateInputBackend(this));
|
||||
#endif
|
||||
#if defined(CIFACE_USE_ANDROID) && !defined(__LIBRETRO__)
|
||||
m_input_backends.emplace_back(ciface::Android::CreateInputBackend(this));
|
||||
#endif
|
||||
#ifdef CIFACE_USE_EVDEV
|
||||
m_input_backends.emplace_back(ciface::evdev::CreateInputBackend(this));
|
||||
#endif
|
||||
#ifdef CIFACE_USE_PIPES
|
||||
m_input_backends.emplace_back(ciface::Pipes::CreateInputBackend(this));
|
||||
#endif
|
||||
#ifdef CIFACE_USE_DUALSHOCKUDPCLIENT
|
||||
m_input_backends.emplace_back(ciface::DualShockUDPClient::CreateInputBackend(this));
|
||||
#endif
|
||||
|
||||
@@ -16,21 +16,6 @@
|
||||
#include "InputCommon/ControllerInterface/InputBackend.h"
|
||||
|
||||
// enable disable sources
|
||||
#if defined(_WIN32) && !defined(__LIBRETRO__)
|
||||
#define CIFACE_USE_WIN32
|
||||
#endif
|
||||
#ifdef HAVE_X11
|
||||
#define CIFACE_USE_XLIB
|
||||
#endif
|
||||
#if defined(__APPLE__) && !defined(__LIBRETRO__)
|
||||
#define CIFACE_USE_OSX
|
||||
#endif
|
||||
#if defined(HAVE_LIBEVDEV) && defined(HAVE_LIBUDEV)
|
||||
#define CIFACE_USE_EVDEV
|
||||
#endif
|
||||
#if defined(USE_PIPES)
|
||||
#define CIFACE_USE_PIPES
|
||||
#endif
|
||||
#define CIFACE_USE_DUALSHOCKUDPCLIENT
|
||||
#if defined(HAVE_HIDAPI)
|
||||
#define CIFACE_USE_STEAMDECK
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
// Copyright 2010 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/DInput/DInput.h"
|
||||
|
||||
#include "Common/HRWrap.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/StringUtil.h"
|
||||
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
#include "InputCommon/ControllerInterface/DInput/DInputJoystick.h"
|
||||
#include "InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.h"
|
||||
|
||||
#pragma comment(lib, "Dinput8.lib")
|
||||
#pragma comment(lib, "dxguid.lib")
|
||||
|
||||
namespace ciface::DInput
|
||||
{
|
||||
static IDirectInput8* s_idi8 = nullptr;
|
||||
|
||||
BOOL CALLBACK DIEnumDeviceObjectsCallback(LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef)
|
||||
{
|
||||
((std::list<DIDEVICEOBJECTINSTANCE>*)pvRef)->push_back(*lpddoi);
|
||||
return DIENUM_CONTINUE;
|
||||
}
|
||||
|
||||
BOOL CALLBACK DIEnumDevicesCallback(LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef)
|
||||
{
|
||||
((std::list<DIDEVICEINSTANCE>*)pvRef)->push_back(*lpddi);
|
||||
return DIENUM_CONTINUE;
|
||||
}
|
||||
|
||||
std::string GetDeviceName(const LPDIRECTINPUTDEVICE8 device)
|
||||
{
|
||||
DIPROPSTRING str = {};
|
||||
str.diph.dwSize = sizeof(str);
|
||||
str.diph.dwHeaderSize = sizeof(str.diph);
|
||||
str.diph.dwHow = DIPH_DEVICE;
|
||||
|
||||
std::string result;
|
||||
HRESULT hr = device->GetProperty(DIPROP_PRODUCTNAME, &str.diph);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
result = StripWhitespace(WStringToUTF8(str.wsz));
|
||||
}
|
||||
else
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "GetProperty(DIPROP_PRODUCTNAME) failed: {}",
|
||||
Common::HRWrap(hr));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Assumes hwnd had not changed from the previous call
|
||||
void PopulateDevices(HWND hwnd)
|
||||
{
|
||||
if (!s_idi8)
|
||||
{
|
||||
HRESULT hr = DirectInput8Create(GetModuleHandle(nullptr), DIRECTINPUT_VERSION,
|
||||
IID_IDirectInput8, (LPVOID*)&s_idi8, nullptr);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "DirectInput8Create failed: {}", Common::HRWrap(hr));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old (invalid) devices. No need to ever remove the KeyboardMouse device.
|
||||
// Note that if we have 2+ DInput controllers, not fully repopulating devices
|
||||
// will mean that a device with index "2" could persist while there is no device with index "0".
|
||||
// This is slightly inconsistent as when we refresh all devices, they will instead reset, and
|
||||
// that happens a lot (for uncontrolled reasons, like starting/stopping the emulation).
|
||||
g_controller_interface.RemoveDevice(
|
||||
[](const auto* dev) { return dev->GetSource() == DINPUT_SOURCE_NAME && !dev->IsValid(); });
|
||||
|
||||
InitKeyboardMouse(s_idi8, hwnd);
|
||||
InitJoystick(s_idi8, hwnd);
|
||||
}
|
||||
|
||||
void ChangeWindow(HWND hwnd)
|
||||
{
|
||||
if (s_idi8) // Has init? Ignore if called before the first PopulateDevices()
|
||||
{
|
||||
// The KeyboardMouse device is marked as virtual device, so we avoid removing it.
|
||||
// We need to force all the DInput joysticks to be destroyed now, or recreation would fail.
|
||||
g_controller_interface.RemoveDevice(
|
||||
[](const auto* dev) {
|
||||
return dev->GetSource() == DINPUT_SOURCE_NAME && !dev->IsVirtualDevice();
|
||||
},
|
||||
true);
|
||||
|
||||
SetKeyboardMouseWindow(hwnd);
|
||||
InitJoystick(s_idi8, hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
void DeInit()
|
||||
{
|
||||
if (s_idi8)
|
||||
{
|
||||
s_idi8->Release();
|
||||
s_idi8 = nullptr;
|
||||
}
|
||||
}
|
||||
} // namespace ciface::DInput
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright 2010 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#define DINPUT_SOURCE_NAME "DInput"
|
||||
|
||||
#include <windows.h>
|
||||
#include <list>
|
||||
#include <string>
|
||||
|
||||
#include "InputCommon/ControllerInterface/DInput/DInput8.h"
|
||||
|
||||
namespace ciface::DInput
|
||||
{
|
||||
// BOOL CALLBACK DIEnumEffectsCallback(LPCDIEFFECTINFO pdei, LPVOID pvRef);
|
||||
BOOL CALLBACK DIEnumDeviceObjectsCallback(LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef);
|
||||
BOOL CALLBACK DIEnumDevicesCallback(LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef);
|
||||
std::string GetDeviceName(const LPDIRECTINPUTDEVICE8 device);
|
||||
|
||||
void PopulateDevices(HWND hwnd);
|
||||
void ChangeWindow(HWND hwnd);
|
||||
void DeInit();
|
||||
} // namespace ciface::DInput
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright 2008 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#define DIRECTINPUT_VERSION 0x0800
|
||||
#include <dinput.h>
|
||||
@@ -1,311 +0,0 @@
|
||||
// Copyright 2010 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/DInput/DInputJoystick.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <type_traits>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/HRWrap.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
#include "InputCommon/ControllerInterface/DInput/DInput.h"
|
||||
#include "InputCommon/ControllerInterface/DInput/XInputFilter.h"
|
||||
|
||||
namespace ciface::DInput
|
||||
{
|
||||
constexpr DWORD DATA_BUFFER_SIZE = 32;
|
||||
|
||||
struct GUIDComparator
|
||||
{
|
||||
bool operator()(const GUID& left, const GUID& right) const
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<GUID>);
|
||||
|
||||
return memcmp(&left, &right, sizeof(left)) < 0;
|
||||
}
|
||||
};
|
||||
|
||||
static std::set<GUID, GUIDComparator> s_guids_in_use;
|
||||
static std::mutex s_guids_mutex;
|
||||
|
||||
void InitJoystick(IDirectInput8* const idi8, HWND hwnd)
|
||||
{
|
||||
std::list<DIDEVICEINSTANCE> joysticks;
|
||||
idi8->EnumDevices(DI8DEVCLASS_GAMECTRL, DIEnumDevicesCallback, (LPVOID)&joysticks,
|
||||
DIEDFL_ATTACHEDONLY);
|
||||
|
||||
std::unordered_set<DWORD> xinput_guids = GetXInputGUIDS();
|
||||
for (DIDEVICEINSTANCE& joystick : joysticks)
|
||||
{
|
||||
// Skip XInput Devices
|
||||
if (xinput_guids.contains(joystick.guidProduct.Data1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip devices we are already using.
|
||||
{
|
||||
std::lock_guard lk(s_guids_mutex);
|
||||
if (s_guids_in_use.contains(joystick.guidInstance))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
LPDIRECTINPUTDEVICE8 js_device;
|
||||
// Don't print any warnings on failure
|
||||
if (SUCCEEDED(idi8->CreateDevice(joystick.guidInstance, &js_device, nullptr)))
|
||||
{
|
||||
if (SUCCEEDED(js_device->SetDataFormat(&c_dfDIJoystick)))
|
||||
{
|
||||
HRESULT hr = js_device->SetCooperativeLevel(GetAncestor(hwnd, GA_ROOT),
|
||||
DISCL_BACKGROUND | DISCL_EXCLUSIVE);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
WARN_LOG_FMT(CONTROLLERINTERFACE,
|
||||
"DInput: Failed to acquire device exclusively. Force feedback will be "
|
||||
"unavailable. {}",
|
||||
Common::HRWrap(hr));
|
||||
// Fall back to non-exclusive mode, with no rumble
|
||||
if (FAILED(
|
||||
js_device->SetCooperativeLevel(nullptr, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)))
|
||||
{
|
||||
js_device->Release();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
auto js = std::make_shared<Joystick>(js_device);
|
||||
// only add if it has some inputs/outputs.
|
||||
// Don't even add it to our static list in case we first created it without a window handle,
|
||||
// failing to get exclusive mode, and then later managed to obtain it, which mean it
|
||||
// could now have some outputs if it didn't before.
|
||||
if (js->Inputs().size() || js->Outputs().size())
|
||||
{
|
||||
if (g_controller_interface.AddDevice(std::move(js)))
|
||||
{
|
||||
std::lock_guard lk(s_guids_mutex);
|
||||
s_guids_in_use.insert(joystick.guidInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
js_device->Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Joystick::Joystick(const LPDIRECTINPUTDEVICE8 device) : m_device(device)
|
||||
{
|
||||
// seems this needs to be done before GetCapabilities
|
||||
// polled or buffered data
|
||||
DIPROPDWORD dipdw;
|
||||
dipdw.diph.dwSize = sizeof(DIPROPDWORD);
|
||||
dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER);
|
||||
dipdw.diph.dwObj = 0;
|
||||
dipdw.diph.dwHow = DIPH_DEVICE;
|
||||
dipdw.dwData = DATA_BUFFER_SIZE;
|
||||
// set the buffer size,
|
||||
// if we can't set the property, we can't use buffered data
|
||||
m_buffered = SUCCEEDED(m_device->SetProperty(DIPROP_BUFFERSIZE, &dipdw.diph));
|
||||
|
||||
// seems this needs to be done after SetProperty of buffer size
|
||||
m_device->Acquire();
|
||||
|
||||
// get joystick caps
|
||||
DIDEVCAPS js_caps;
|
||||
js_caps.dwSize = sizeof(js_caps);
|
||||
if (FAILED(m_device->GetCapabilities(&js_caps)))
|
||||
return;
|
||||
|
||||
// max of 32 buttons and 4 hats / the limit of the data format I am using
|
||||
js_caps.dwButtons = std::min((DWORD)32, js_caps.dwButtons);
|
||||
js_caps.dwPOVs = std::min((DWORD)4, js_caps.dwPOVs);
|
||||
|
||||
// m_must_poll = (js_caps.dwFlags & DIDC_POLLEDDATAFORMAT) != 0;
|
||||
|
||||
// buttons
|
||||
for (u8 i = 0; i != js_caps.dwButtons; ++i)
|
||||
AddInput(new Button(i, m_state_in.rgbButtons[i]));
|
||||
|
||||
// hats
|
||||
for (u8 i = 0; i != js_caps.dwPOVs; ++i)
|
||||
{
|
||||
// each hat gets 4 input instances associated with it, (up down left right)
|
||||
for (u8 d = 0; d != 4; ++d)
|
||||
AddInput(new Hat(i, m_state_in.rgdwPOV[i], d));
|
||||
}
|
||||
|
||||
// get up to 6 axes and 2 sliders
|
||||
DIPROPRANGE range;
|
||||
range.diph.dwSize = sizeof(range);
|
||||
range.diph.dwHeaderSize = sizeof(range.diph);
|
||||
range.diph.dwHow = DIPH_BYOFFSET;
|
||||
// screw EnumObjects, just go through all the axis offsets and try to GetProperty
|
||||
// this should be more foolproof, less code, and probably faster
|
||||
for (unsigned int offset = 0; offset < DIJOFS_BUTTON(0) / sizeof(LONG); ++offset)
|
||||
{
|
||||
range.diph.dwObj = offset * sizeof(LONG);
|
||||
// Try to set a range with 16 bits of precision:
|
||||
range.lMin = std::numeric_limits<s16>::min();
|
||||
range.lMax = std::numeric_limits<s16>::max();
|
||||
m_device->SetProperty(DIPROP_RANGE, &range.diph);
|
||||
// Not all devices support setting DIPROP_RANGE so we must GetProperty right back.
|
||||
// This also checks that the axis is present.
|
||||
// Note: Even though not all devices support setting DIPROP_RANGE, some require it.
|
||||
if (SUCCEEDED(m_device->GetProperty(DIPROP_RANGE, &range.diph)))
|
||||
{
|
||||
const LONG base = (range.lMin + range.lMax) / 2;
|
||||
const LONG& ax = (&m_state_in.lX)[offset];
|
||||
|
||||
// each axis gets a negative and a positive input instance associated with it
|
||||
AddFullAnalogSurfaceInputs(new Axis(offset, ax, base, range.lMin - base),
|
||||
new Axis(offset, ax, base, range.lMax - base));
|
||||
}
|
||||
}
|
||||
|
||||
// Force feedback:
|
||||
std::list<DIDEVICEOBJECTINSTANCE> objects;
|
||||
if (SUCCEEDED(m_device->EnumObjects(DIEnumDeviceObjectsCallback, (LPVOID)&objects, DIDFT_AXIS)))
|
||||
{
|
||||
const int num_ff_axes = std::ranges::count_if(
|
||||
objects, [](const auto& pdidoi) { return (pdidoi.dwFlags & DIDOI_FFACTUATOR) != 0; });
|
||||
InitForceFeedback(m_device, num_ff_axes);
|
||||
}
|
||||
|
||||
// Set hats to center:
|
||||
// "The center position is normally reported as -1" -MSDN
|
||||
std::ranges::fill(m_state_in.rgdwPOV, -1);
|
||||
}
|
||||
|
||||
Joystick::~Joystick()
|
||||
{
|
||||
DIDEVICEINSTANCE info = {};
|
||||
info.dwSize = sizeof(info);
|
||||
if (SUCCEEDED(m_device->GetDeviceInfo(&info)))
|
||||
{
|
||||
std::lock_guard lk(s_guids_mutex);
|
||||
s_guids_in_use.erase(info.guidInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "DInputJoystick: GetDeviceInfo failed.");
|
||||
}
|
||||
|
||||
DeInitForceFeedback();
|
||||
|
||||
m_device->Unacquire();
|
||||
m_device->Release();
|
||||
}
|
||||
|
||||
std::string Joystick::GetName() const
|
||||
{
|
||||
return GetDeviceName(m_device);
|
||||
}
|
||||
|
||||
std::string Joystick::GetSource() const
|
||||
{
|
||||
return DINPUT_SOURCE_NAME;
|
||||
}
|
||||
|
||||
bool Joystick::IsValid() const
|
||||
{
|
||||
return SUCCEEDED(m_device->Acquire());
|
||||
}
|
||||
|
||||
Core::DeviceRemoval Joystick::UpdateInput()
|
||||
{
|
||||
HRESULT hr = 0;
|
||||
|
||||
// just always poll,
|
||||
// MSDN says if this isn't needed it doesn't do anything
|
||||
m_device->Poll();
|
||||
|
||||
if (m_buffered)
|
||||
{
|
||||
DIDEVICEOBJECTDATA evtbuf[DATA_BUFFER_SIZE];
|
||||
DWORD numevents = DATA_BUFFER_SIZE;
|
||||
hr = m_device->GetDeviceData(sizeof(*evtbuf), evtbuf, &numevents, 0);
|
||||
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
for (LPDIDEVICEOBJECTDATA evt = evtbuf; evt != (evtbuf + numevents); ++evt)
|
||||
{
|
||||
// all the buttons are at the end of the data format
|
||||
// they are bytes rather than longs
|
||||
if (evt->dwOfs < DIJOFS_BUTTON(0))
|
||||
*(DWORD*)(((BYTE*)&m_state_in) + evt->dwOfs) = evt->dwData;
|
||||
else
|
||||
((BYTE*)&m_state_in)[evt->dwOfs] = (BYTE)evt->dwData;
|
||||
}
|
||||
|
||||
// seems like this needs to be done maybe...
|
||||
if (DI_BUFFEROVERFLOW == hr)
|
||||
hr = m_device->GetDeviceState(sizeof(m_state_in), &m_state_in);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hr = m_device->GetDeviceState(sizeof(m_state_in), &m_state_in);
|
||||
}
|
||||
|
||||
// try reacquire if input lost
|
||||
if (DIERR_INPUTLOST == hr || DIERR_NOTACQUIRED == hr)
|
||||
m_device->Acquire();
|
||||
|
||||
return Core::DeviceRemoval::Keep;
|
||||
}
|
||||
|
||||
// get name
|
||||
|
||||
std::string Joystick::Button::GetName() const
|
||||
{
|
||||
return fmt::format("Button {}", m_index);
|
||||
}
|
||||
|
||||
std::string Joystick::Axis::GetName() const
|
||||
{
|
||||
const char sign = m_range < 0 ? '-' : '+';
|
||||
if (m_index < 6) // axis
|
||||
return fmt::format("Axis {:c}{}{:c}", 'X' + m_index % 3, m_index > 2 ? "r" : "", sign);
|
||||
else // slider
|
||||
return fmt::format("Slider {}{:c}", m_index - 6, sign);
|
||||
}
|
||||
|
||||
std::string Joystick::Hat::GetName() const
|
||||
{
|
||||
return fmt::format("Hat {} {:c}", m_index, "NESW"[m_direction]);
|
||||
}
|
||||
|
||||
// get / set state
|
||||
|
||||
ControlState Joystick::Axis::GetState() const
|
||||
{
|
||||
return ControlState(m_axis - m_base) / m_range;
|
||||
}
|
||||
|
||||
ControlState Joystick::Button::GetState() const
|
||||
{
|
||||
return ControlState(m_button > 0);
|
||||
}
|
||||
|
||||
ControlState Joystick::Hat::GetState() const
|
||||
{
|
||||
// "Some drivers report the centered position of the POV indicator as 65,535.
|
||||
// Determine whether the indicator is centered as follows" -MSDN
|
||||
const bool is_centered = (0xffff == LOWORD(m_hat));
|
||||
|
||||
if (is_centered)
|
||||
return 0;
|
||||
|
||||
return (std::abs(int(m_hat / 4500 - m_direction * 2 + 8) % 8 - 4) > 2);
|
||||
}
|
||||
} // namespace ciface::DInput
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright 2010 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "InputCommon/ControllerInterface/CoreDevice.h"
|
||||
#include "InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h"
|
||||
|
||||
namespace ciface::DInput
|
||||
{
|
||||
void InitJoystick(IDirectInput8* const idi8, HWND hwnd);
|
||||
|
||||
class Joystick : public ForceFeedback::ForceFeedbackDevice
|
||||
{
|
||||
private:
|
||||
class Button : public Input
|
||||
{
|
||||
public:
|
||||
Button(u8 index, const BYTE& button) : m_button(button), m_index(index) {}
|
||||
std::string GetName() const override;
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const BYTE& m_button;
|
||||
const u8 m_index;
|
||||
};
|
||||
|
||||
class Axis : public Input
|
||||
{
|
||||
public:
|
||||
Axis(u8 index, const LONG& axis, LONG base, LONG range)
|
||||
: m_axis(axis), m_base(base), m_range(range), m_index(index)
|
||||
{
|
||||
}
|
||||
std::string GetName() const override;
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const LONG& m_axis;
|
||||
const LONG m_base, m_range;
|
||||
const u8 m_index;
|
||||
};
|
||||
|
||||
class Hat : public Input
|
||||
{
|
||||
public:
|
||||
Hat(u8 index, const DWORD& hat, u8 direction)
|
||||
: m_hat(hat), m_direction(direction), m_index(index)
|
||||
{
|
||||
}
|
||||
std::string GetName() const override;
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const DWORD& m_hat;
|
||||
const u8 m_index, m_direction;
|
||||
};
|
||||
|
||||
public:
|
||||
Core::DeviceRemoval UpdateInput() override;
|
||||
|
||||
Joystick(const LPDIRECTINPUTDEVICE8 device);
|
||||
~Joystick() override;
|
||||
|
||||
std::string GetName() const override;
|
||||
std::string GetSource() const override;
|
||||
int GetSortPriority() const override { return -3; }
|
||||
|
||||
bool IsValid() const final;
|
||||
|
||||
private:
|
||||
const LPDIRECTINPUTDEVICE8 m_device;
|
||||
|
||||
DIJOYSTATE m_state_in{};
|
||||
|
||||
bool m_buffered;
|
||||
};
|
||||
} // namespace ciface::DInput
|
||||
@@ -1,340 +0,0 @@
|
||||
// Copyright 2010 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "Common/Logging/Log.h"
|
||||
|
||||
#include "Core/Core.h"
|
||||
#include "Core/Host.h"
|
||||
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
#include "InputCommon/ControllerInterface/DInput/DInput.h"
|
||||
|
||||
// (lower would be more sensitive) user can lower sensitivity by setting range
|
||||
// seems decent here ( at 8 ), I don't think anyone would need more sensitive than this
|
||||
// and user can lower it much farther than they would want to with the range
|
||||
#define MOUSE_AXIS_SENSITIVITY 8
|
||||
|
||||
// if input hasn't been received for this many ms, mouse input will be skipped
|
||||
// otherwise it is just some crazy value
|
||||
#define DROP_INPUT_TIME 250
|
||||
|
||||
namespace ciface::DInput
|
||||
{
|
||||
class RelativeMouseAxis final : public Core::Device::RelativeInput
|
||||
{
|
||||
public:
|
||||
std::string GetName() const override
|
||||
{
|
||||
return fmt::format("RelativeMouse {}{}", char('X' + m_index), (m_scale > 0) ? '+' : '-');
|
||||
}
|
||||
|
||||
RelativeMouseAxis(u8 index, bool positive, const RelativeMouseState* state)
|
||||
: m_state(*state), m_index(index), m_scale(positive * 2 - 1)
|
||||
{
|
||||
}
|
||||
|
||||
ControlState GetState() const override
|
||||
{
|
||||
return ControlState(m_state.GetValue().data[m_index] * m_scale);
|
||||
}
|
||||
|
||||
private:
|
||||
const RelativeMouseState& m_state;
|
||||
const u8 m_index;
|
||||
const s8 m_scale;
|
||||
};
|
||||
|
||||
static const struct
|
||||
{
|
||||
const BYTE code;
|
||||
const char* const name;
|
||||
} named_keys[] = {
|
||||
#include "InputCommon/ControllerInterface/DInput/NamedKeys.h" // NOLINT
|
||||
};
|
||||
|
||||
// Prevent duplicate keyboard/mouse devices. Modified by more threads.
|
||||
static bool s_keyboard_mouse_exists;
|
||||
static HWND s_hwnd;
|
||||
|
||||
void InitKeyboardMouse(IDirectInput8* const idi8, HWND hwnd)
|
||||
{
|
||||
if (s_keyboard_mouse_exists)
|
||||
return;
|
||||
|
||||
s_hwnd = hwnd;
|
||||
|
||||
// Mouse and keyboard are a combined device, to allow shift+click and stuff
|
||||
// if that's dumb, I will make a VirtualDevice class that just uses ranges of inputs/outputs from
|
||||
// other devices
|
||||
// so there can be a separated Keyboard and mouse, as well as combined KeyboardMouse
|
||||
|
||||
LPDIRECTINPUTDEVICE8 kb_device = nullptr;
|
||||
LPDIRECTINPUTDEVICE8 mo_device = nullptr;
|
||||
|
||||
// These are "virtual" system devices, so they are always there even if we have no physical
|
||||
// mouse and keyboard plugged into the computer
|
||||
if (SUCCEEDED(idi8->CreateDevice(GUID_SysKeyboard, &kb_device, nullptr)) &&
|
||||
SUCCEEDED(kb_device->SetDataFormat(&c_dfDIKeyboard)) &&
|
||||
SUCCEEDED(kb_device->SetCooperativeLevel(nullptr, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)) &&
|
||||
SUCCEEDED(idi8->CreateDevice(GUID_SysMouse, &mo_device, nullptr)) &&
|
||||
SUCCEEDED(mo_device->SetDataFormat(&c_dfDIMouse2)) &&
|
||||
SUCCEEDED(mo_device->SetCooperativeLevel(nullptr, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)))
|
||||
{
|
||||
g_controller_interface.AddDevice(std::make_shared<KeyboardMouse>(kb_device, mo_device));
|
||||
return;
|
||||
}
|
||||
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "KeyboardMouse device failed to be created");
|
||||
|
||||
if (kb_device)
|
||||
kb_device->Release();
|
||||
if (mo_device)
|
||||
mo_device->Release();
|
||||
}
|
||||
|
||||
void SetKeyboardMouseWindow(HWND hwnd)
|
||||
{
|
||||
s_hwnd = hwnd;
|
||||
}
|
||||
|
||||
KeyboardMouse::~KeyboardMouse()
|
||||
{
|
||||
s_keyboard_mouse_exists = false;
|
||||
|
||||
// Independently of the order in which we do these, if we put a breakpoint on Unacquire() (or in
|
||||
// any place in the call stack before this), when refreshing devices from the UI, on the second
|
||||
// attempt, it will get stuck in an infinite (while) loop inside dinput8.dll. Given that it can't
|
||||
// be otherwise be reproduced (not even with sleeps), we can just ignore the problem.
|
||||
|
||||
// kb
|
||||
m_kb_device->Unacquire();
|
||||
m_kb_device->Release();
|
||||
// mouse
|
||||
m_mo_device->Unacquire();
|
||||
m_mo_device->Release();
|
||||
}
|
||||
|
||||
KeyboardMouse::KeyboardMouse(const LPDIRECTINPUTDEVICE8 kb_device,
|
||||
const LPDIRECTINPUTDEVICE8 mo_device)
|
||||
: m_kb_device(kb_device), m_mo_device(mo_device), m_last_update(GetTickCount()), m_state_in()
|
||||
{
|
||||
s_keyboard_mouse_exists = true;
|
||||
|
||||
if (FAILED(m_kb_device->Acquire()))
|
||||
WARN_LOG_FMT(CONTROLLERINTERFACE, "Keyboard device failed to acquire. We'll retry later");
|
||||
if (FAILED(m_mo_device->Acquire()))
|
||||
WARN_LOG_FMT(CONTROLLERINTERFACE, "Mouse device failed to acquire. We'll retry later");
|
||||
|
||||
// KEYBOARD
|
||||
// add keys
|
||||
for (u8 i = 0; i < sizeof(named_keys) / sizeof(*named_keys); ++i)
|
||||
AddInput(new Key(i, m_state_in.keyboard[named_keys[i].code]));
|
||||
|
||||
// Add combined left/right modifiers with consistent naming across platforms.
|
||||
AddCombinedInput("Alt", {"LMENU", "RMENU"});
|
||||
AddCombinedInput("Shift", {"LSHIFT", "RSHIFT"});
|
||||
AddCombinedInput("Ctrl", {"LCONTROL", "RCONTROL"});
|
||||
|
||||
// MOUSE
|
||||
DIDEVCAPS mouse_caps = {};
|
||||
mouse_caps.dwSize = sizeof(mouse_caps);
|
||||
m_mo_device->GetCapabilities(&mouse_caps);
|
||||
// mouse buttons
|
||||
// We don't use caps here due to reporting inconsistencies
|
||||
for (u8 i = 0; i < std::size(m_state_in.mouse.rgbButtons); ++i)
|
||||
AddInput(new Button(i, m_state_in.mouse.rgbButtons[i]));
|
||||
// mouse axes
|
||||
for (unsigned int i = 0; i < mouse_caps.dwAxes; ++i)
|
||||
{
|
||||
const LONG& ax = (&m_state_in.mouse.lX)[i];
|
||||
|
||||
// each axis gets a negative and a positive input instance associated with it
|
||||
AddInput(new Axis(i, ax, (2 == i) ? -1 : -MOUSE_AXIS_SENSITIVITY));
|
||||
AddInput(new Axis(i, ax, -(2 == i) ? 1 : MOUSE_AXIS_SENSITIVITY));
|
||||
}
|
||||
|
||||
// cursor, with a hax for-loop
|
||||
for (unsigned int i = 0; i < 4; ++i)
|
||||
AddInput(new Cursor(!!(i & 2), (&m_state_in.cursor.x)[i / 2], !!(i & 1)));
|
||||
|
||||
// Raw relative mouse movement.
|
||||
for (unsigned int i = 0; i != mouse_caps.dwAxes; ++i)
|
||||
{
|
||||
AddInput(new RelativeMouseAxis(i, false, &m_state_in.relative_mouse));
|
||||
AddInput(new RelativeMouseAxis(i, true, &m_state_in.relative_mouse));
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardMouse::UpdateCursorInput()
|
||||
{
|
||||
// Get the size of the current window (in my case Rect.top and Rect.left was zero).
|
||||
RECT rect;
|
||||
GetClientRect(s_hwnd, &rect);
|
||||
|
||||
// Width and height are the size of the rendering window. They could be 0
|
||||
const auto win_width = std::max(rect.right - rect.left, 1l);
|
||||
const auto win_height = std::max(rect.bottom - rect.top, 1l);
|
||||
|
||||
POINT point = {};
|
||||
if (g_controller_interface.IsMouseCenteringRequested() &&
|
||||
(Host_RendererHasFocus() || Host_TASInputHasFocus()))
|
||||
{
|
||||
point.x = win_width / 2;
|
||||
point.y = win_height / 2;
|
||||
|
||||
POINT screen_point = point;
|
||||
ClientToScreen(s_hwnd, &screen_point);
|
||||
SetCursorPos(screen_point.x, screen_point.y);
|
||||
g_controller_interface.SetMouseCenteringRequested(false);
|
||||
}
|
||||
else if (Host_TASInputHasFocus())
|
||||
{
|
||||
// When a TAS Input window has focus and "Enable Controller Input" is checked most types of
|
||||
// input should be read normally as if the render window had focus instead. The cursor is an
|
||||
// exception, as otherwise using the mouse to set any control in the TAS Input window will also
|
||||
// update the Wii IR value (or any other input controlled by the cursor).
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
GetCursorPos(&point);
|
||||
|
||||
// Get the cursor position relative to the upper left corner of the current window
|
||||
// (separate or render to main)
|
||||
ScreenToClient(s_hwnd, &point);
|
||||
}
|
||||
|
||||
const auto window_scale = g_controller_interface.GetWindowInputScale();
|
||||
|
||||
// Convert the cursor position to a range from -1 to 1.
|
||||
m_state_in.cursor.x = (ControlState(point.x) / win_width * 2 - 1) * window_scale.x;
|
||||
m_state_in.cursor.y = (ControlState(point.y) / win_height * 2 - 1) * window_scale.y;
|
||||
}
|
||||
|
||||
Core::DeviceRemoval KeyboardMouse::UpdateInput()
|
||||
{
|
||||
UpdateCursorInput();
|
||||
|
||||
DIMOUSESTATE2 tmp_mouse;
|
||||
|
||||
// if mouse position hasn't been updated in a short while, skip a dev state
|
||||
DWORD cur_time = GetTickCount();
|
||||
if (cur_time - m_last_update > DROP_INPUT_TIME)
|
||||
{
|
||||
// set axes to zero
|
||||
m_state_in.mouse = {};
|
||||
m_state_in.relative_mouse = {};
|
||||
|
||||
// skip this input state
|
||||
m_mo_device->GetDeviceState(sizeof(tmp_mouse), &tmp_mouse);
|
||||
}
|
||||
|
||||
m_last_update = cur_time;
|
||||
|
||||
HRESULT mo_hr = m_mo_device->GetDeviceState(sizeof(tmp_mouse), &tmp_mouse);
|
||||
if (DIERR_INPUTLOST == mo_hr || DIERR_NOTACQUIRED == mo_hr)
|
||||
{
|
||||
INFO_LOG_FMT(CONTROLLERINTERFACE, "Mouse device failed to get state");
|
||||
if (FAILED(m_mo_device->Acquire()))
|
||||
INFO_LOG_FMT(CONTROLLERINTERFACE, "Mouse device failed to re-acquire, we'll retry later");
|
||||
}
|
||||
else if (SUCCEEDED(mo_hr))
|
||||
{
|
||||
m_state_in.relative_mouse.Move({tmp_mouse.lX, tmp_mouse.lY, tmp_mouse.lZ});
|
||||
m_state_in.relative_mouse.Update();
|
||||
|
||||
// need to smooth out the axes, otherwise it doesn't work for shit
|
||||
for (unsigned int i = 0; i < 3; ++i)
|
||||
((&m_state_in.mouse.lX)[i] += (&tmp_mouse.lX)[i]) /= 2;
|
||||
|
||||
// copy over the buttons
|
||||
std::copy_n(tmp_mouse.rgbButtons, std::size(tmp_mouse.rgbButtons), m_state_in.mouse.rgbButtons);
|
||||
}
|
||||
|
||||
HRESULT kb_hr = m_kb_device->GetDeviceState(sizeof(m_state_in.keyboard), &m_state_in.keyboard);
|
||||
if (kb_hr == DIERR_INPUTLOST || kb_hr == DIERR_NOTACQUIRED)
|
||||
{
|
||||
INFO_LOG_FMT(CONTROLLERINTERFACE, "Keyboard device failed to get state");
|
||||
if (SUCCEEDED(m_kb_device->Acquire()))
|
||||
m_kb_device->GetDeviceState(sizeof(m_state_in.keyboard), &m_state_in.keyboard);
|
||||
else
|
||||
INFO_LOG_FMT(CONTROLLERINTERFACE, "Keyboard device failed to re-acquire, we'll retry later");
|
||||
}
|
||||
|
||||
return Core::DeviceRemoval::Keep;
|
||||
}
|
||||
|
||||
std::string KeyboardMouse::GetName() const
|
||||
{
|
||||
return "Keyboard Mouse";
|
||||
}
|
||||
|
||||
std::string KeyboardMouse::GetSource() const
|
||||
{
|
||||
return DINPUT_SOURCE_NAME;
|
||||
}
|
||||
|
||||
// Give this device a higher priority to make sure it shows first
|
||||
int KeyboardMouse::GetSortPriority() const
|
||||
{
|
||||
return DEFAULT_DEVICE_SORT_PRIORITY;
|
||||
}
|
||||
|
||||
bool KeyboardMouse::IsVirtualDevice() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// names
|
||||
std::string KeyboardMouse::Key::GetName() const
|
||||
{
|
||||
return named_keys[m_index].name;
|
||||
}
|
||||
|
||||
std::string KeyboardMouse::Button::GetName() const
|
||||
{
|
||||
return std::string("Click ") + char('0' + m_index);
|
||||
}
|
||||
|
||||
std::string KeyboardMouse::Axis::GetName() const
|
||||
{
|
||||
char tmpstr[] = "Axis ..";
|
||||
tmpstr[5] = (char)('X' + m_index);
|
||||
tmpstr[6] = (m_range < 0 ? '-' : '+');
|
||||
return tmpstr;
|
||||
}
|
||||
|
||||
std::string KeyboardMouse::Cursor::GetName() const
|
||||
{
|
||||
char tmpstr[] = "Cursor ..";
|
||||
tmpstr[7] = (char)('X' + m_index);
|
||||
tmpstr[8] = (m_positive ? '+' : '-');
|
||||
return tmpstr;
|
||||
}
|
||||
|
||||
// get/set state
|
||||
ControlState KeyboardMouse::Key::GetState() const
|
||||
{
|
||||
return (m_key != 0);
|
||||
}
|
||||
|
||||
ControlState KeyboardMouse::Button::GetState() const
|
||||
{
|
||||
return (m_button != 0);
|
||||
}
|
||||
|
||||
ControlState KeyboardMouse::Axis::GetState() const
|
||||
{
|
||||
return ControlState(m_axis) / m_range;
|
||||
}
|
||||
|
||||
ControlState KeyboardMouse::Cursor::GetState() const
|
||||
{
|
||||
return m_axis / (m_positive ? 1.0 : -1.0);
|
||||
}
|
||||
} // namespace ciface::DInput
|
||||
@@ -1,116 +0,0 @@
|
||||
// Copyright 2010 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "Common/Matrix.h"
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
#include "InputCommon/ControllerInterface/CoreDevice.h"
|
||||
#include "InputCommon/ControllerInterface/DInput/DInput8.h"
|
||||
|
||||
namespace ciface::DInput
|
||||
{
|
||||
void InitKeyboardMouse(IDirectInput8* const idi8, HWND hwnd);
|
||||
|
||||
using RelativeMouseState = RelativeInputState<Common::TVec3<LONG>>;
|
||||
void SetKeyboardMouseWindow(HWND hwnd);
|
||||
|
||||
class KeyboardMouse : public Core::Device
|
||||
{
|
||||
private:
|
||||
struct State
|
||||
{
|
||||
BYTE keyboard[256]{};
|
||||
|
||||
// Old smoothed relative mouse movement.
|
||||
DIMOUSESTATE2 mouse{};
|
||||
|
||||
// Normalized mouse cursor position.
|
||||
Common::TVec2<ControlState> cursor;
|
||||
|
||||
// Raw relative mouse movement.
|
||||
RelativeMouseState relative_mouse;
|
||||
};
|
||||
|
||||
// Keyboard key
|
||||
class Key : public Input
|
||||
{
|
||||
public:
|
||||
Key(u8 index, const BYTE& key) : m_key(key), m_index(index) {}
|
||||
std::string GetName() const override;
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const BYTE& m_key;
|
||||
const u8 m_index;
|
||||
};
|
||||
|
||||
// Mouse button
|
||||
class Button : public Input
|
||||
{
|
||||
public:
|
||||
Button(u8 index, const BYTE& button) : m_button(button), m_index(index) {}
|
||||
std::string GetName() const override;
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const BYTE& m_button;
|
||||
const u8 m_index;
|
||||
};
|
||||
|
||||
// Mouse movement offset axis. Includes mouse wheel
|
||||
class Axis : public Input
|
||||
{
|
||||
public:
|
||||
Axis(u8 index, const LONG& axis, LONG range) : m_axis(axis), m_range(range), m_index(index) {}
|
||||
std::string GetName() const override;
|
||||
bool IsDetectable() const override { return false; }
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const LONG& m_axis;
|
||||
const LONG m_range;
|
||||
const u8 m_index;
|
||||
};
|
||||
|
||||
// Mouse from window center
|
||||
class Cursor : public Input
|
||||
{
|
||||
public:
|
||||
Cursor(u8 index, const ControlState& axis, const bool positive)
|
||||
: m_axis(axis), m_index(index), m_positive(positive)
|
||||
{
|
||||
}
|
||||
std::string GetName() const override;
|
||||
bool IsDetectable() const override { return false; }
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const ControlState& m_axis;
|
||||
const u8 m_index;
|
||||
const bool m_positive;
|
||||
};
|
||||
|
||||
public:
|
||||
Core::DeviceRemoval UpdateInput() override;
|
||||
|
||||
KeyboardMouse(const LPDIRECTINPUTDEVICE8 kb_device, const LPDIRECTINPUTDEVICE8 mo_device);
|
||||
~KeyboardMouse() override;
|
||||
|
||||
std::string GetName() const override;
|
||||
std::string GetSource() const override;
|
||||
int GetSortPriority() const override;
|
||||
bool IsVirtualDevice() const override;
|
||||
|
||||
private:
|
||||
void UpdateCursorInput();
|
||||
|
||||
const LPDIRECTINPUTDEVICE8 m_kb_device;
|
||||
const LPDIRECTINPUTDEVICE8 m_mo_device;
|
||||
|
||||
DWORD m_last_update;
|
||||
State m_state_in;
|
||||
};
|
||||
} // namespace ciface::DInput
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright 2010 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#ifdef __MINGW32__
|
||||
#define DIK_PREVTRACK 0x98
|
||||
#endif
|
||||
|
||||
{DIK_A, "A"}, {DIK_B, "B"}, {DIK_C, "C"}, {DIK_D, "D"}, {DIK_E, "E"}, {DIK_F, "F"}, {DIK_G, "G"},
|
||||
{DIK_H, "H"}, {DIK_I, "I"}, {DIK_J, "J"}, {DIK_K, "K"}, {DIK_L, "L"}, {DIK_M, "M"},
|
||||
{DIK_N, "N"}, {DIK_O, "O"}, {DIK_P, "P"}, {DIK_Q, "Q"}, {DIK_R, "R"}, {DIK_S, "S"},
|
||||
{DIK_T, "T"}, {DIK_U, "U"}, {DIK_V, "V"}, {DIK_W, "W"}, {DIK_X, "X"}, {DIK_Y, "Y"},
|
||||
{DIK_Z, "Z"}, {DIK_0, "0"}, {DIK_1, "1"}, {DIK_2, "2"}, {DIK_3, "3"}, {DIK_4, "4"},
|
||||
{DIK_5, "5"}, {DIK_6, "6"}, {DIK_7, "7"}, {DIK_8, "8"}, {DIK_9, "9"}, {DIK_UP, "UP"},
|
||||
{DIK_DOWN, "DOWN"}, {DIK_LEFT, "LEFT"}, {DIK_RIGHT, "RIGHT"}, {DIK_ABNT_C1, "ABNT_C1"},
|
||||
{DIK_ABNT_C2, "ABNT_C2"}, {DIK_ADD, "ADD"}, {DIK_APOSTROPHE, "APOSTROPHE"}, {DIK_APPS, "APPS"},
|
||||
{DIK_AT, "AT"}, {DIK_AX, "AX"}, {DIK_BACK, "BACK"}, {DIK_BACKSLASH, "BACKSLASH"},
|
||||
{DIK_CALCULATOR, "CALCULATOR"}, {DIK_CAPITAL, "CAPITAL"}, {DIK_COLON, "COLON"},
|
||||
{DIK_COMMA, "COMMA"}, {DIK_CONVERT, "CONVERT"}, {DIK_DECIMAL, "DECIMAL"},
|
||||
{DIK_DELETE, "DELETE"}, {DIK_DIVIDE, "DIVIDE"}, {DIK_EQUALS, "EQUALS"}, {DIK_ESCAPE, "ESCAPE"},
|
||||
{DIK_F1, "F1"}, {DIK_F2, "F2"}, {DIK_F3, "F3"}, {DIK_F4, "F4"}, {DIK_F5, "F5"}, {DIK_F6, "F6"},
|
||||
{DIK_F7, "F7"}, {DIK_F8, "F8"}, {DIK_F9, "F9"}, {DIK_F10, "F10"}, {DIK_F11, "F11"},
|
||||
{DIK_F12, "F12"}, {DIK_F13, "F13"}, {DIK_F14, "F14"}, {DIK_F15, "F15"}, {DIK_GRAVE, "GRAVE"},
|
||||
{DIK_HOME, "HOME"}, {DIK_END, "END"}, {DIK_INSERT, "INSERT"}, {DIK_KANA, "KANA"},
|
||||
{DIK_KANJI, "KANJI"}, {DIK_MAIL, "MAIL"}, {DIK_MEDIASELECT, "MEDIASELECT"},
|
||||
{DIK_MEDIASTOP, "MEDIASTOP"}, {DIK_MINUS, "MINUS"}, {DIK_MULTIPLY, "MULTIPLY"},
|
||||
{DIK_MUTE, "MUTE"}, {DIK_MYCOMPUTER, "MYCOMPUTER"}, {DIK_NEXTTRACK, "NEXTTRACK"},
|
||||
{DIK_NOCONVERT, "NOCONVERT"}, {DIK_NUMLOCK, "NUMLOCK"}, {DIK_NUMPAD0, "NUMPAD0"},
|
||||
{DIK_NUMPAD1, "NUMPAD1"}, {DIK_NUMPAD2, "NUMPAD2"}, {DIK_NUMPAD3, "NUMPAD3"},
|
||||
{DIK_NUMPAD4, "NUMPAD4"}, {DIK_NUMPAD5, "NUMPAD5"}, {DIK_NUMPAD6, "NUMPAD6"},
|
||||
{DIK_NUMPAD7, "NUMPAD7"}, {DIK_NUMPAD8, "NUMPAD8"}, {DIK_NUMPAD9, "NUMPAD9"},
|
||||
{DIK_NUMPADCOMMA, "NUMPADCOMMA"}, {DIK_NUMPADENTER, "NUMPADENTER"},
|
||||
{DIK_NUMPADEQUALS, "NUMPADEQUALS"}, {DIK_OEM_102, "OEM_102"}, {DIK_PAUSE, "PAUSE"},
|
||||
{DIK_PERIOD, "PERIOD"}, {DIK_PLAYPAUSE, "PLAYPAUSE"}, {DIK_POWER, "POWER"},
|
||||
{DIK_PREVTRACK, "PREVTRACK"}, {DIK_PRIOR, "PRIOR"}, {DIK_NEXT, "NEXT"}, {DIK_RETURN, "RETURN"},
|
||||
{DIK_LBRACKET, "LBRACKET"}, {DIK_RBRACKET, "RBRACKET"}, {DIK_LCONTROL, "LCONTROL"},
|
||||
{DIK_RCONTROL, "RCONTROL"}, {DIK_LMENU, "LMENU"}, {DIK_RMENU, "RMENU"}, {DIK_LSHIFT, "LSHIFT"},
|
||||
{DIK_RSHIFT, "RSHIFT"}, {DIK_LWIN, "LWIN"}, {DIK_RWIN, "RWIN"}, {DIK_SCROLL, "SCROLL"},
|
||||
{DIK_SEMICOLON, "SEMICOLON"}, {DIK_SLASH, "SLASH"}, {DIK_SLEEP, "SLEEP"}, {DIK_SPACE, "SPACE"},
|
||||
{DIK_STOP, "STOP"}, {DIK_SUBTRACT, "SUBTRACT"}, {DIK_SYSRQ, "SYSRQ"}, {DIK_TAB, "TAB"},
|
||||
{DIK_UNDERLINE, "UNDERLINE"}, {DIK_UNLABELED, "UNLABELED"}, {DIK_VOLUMEDOWN, "VOLUMEDOWN"},
|
||||
{DIK_VOLUMEUP, "VOLUMEUP"}, {DIK_WAKE, "WAKE"}, {DIK_WEBBACK, "WEBBACK"},
|
||||
{DIK_WEBFAVORITES, "WEBFAVORITES"}, {DIK_WEBFORWARD, "WEBFORWARD"}, {DIK_WEBHOME, "WEBHOME"},
|
||||
{DIK_WEBREFRESH, "WEBREFRESH"}, {DIK_WEBSEARCH, "WEBSEARCH"}, {DIK_WEBSTOP, "WEBSTOP"},
|
||||
{DIK_YEN, "YEN"},
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright 2014 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/DInput/XInputFilter.h"
|
||||
|
||||
#include <cwchar>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <setupapi.h>
|
||||
|
||||
namespace ciface::DInput
|
||||
{
|
||||
// Code for enumerating hardware devices that use the XINPUT device driver.
|
||||
// The MSDN recommended code suffers from massive performance problems when using language packs,
|
||||
// if the system and user languages differ then WMI Queries become incredibly slow (multiple
|
||||
// seconds). This is more or less equivalent and much faster.
|
||||
std::unordered_set<DWORD> GetXInputGUIDS()
|
||||
{
|
||||
static const GUID s_GUID_devclass_HID = {
|
||||
0x745a17a0, 0x74d3, 0x11d0, {0xb6, 0xfe, 0x00, 0xa0, 0xc9, 0x0f, 0x57, 0xda}};
|
||||
std::unordered_set<DWORD> guids;
|
||||
|
||||
// Enumerate everything under the "Human Interface Devices" tree in the Device Manager
|
||||
// NOTE: Some devices show up multiple times due to sub-devices, we rely on the set to
|
||||
// prevent duplicates.
|
||||
HDEVINFO setup_enum = SetupDiGetClassDevsW(&s_GUID_devclass_HID, nullptr, nullptr, DIGCF_PRESENT);
|
||||
if (setup_enum == INVALID_HANDLE_VALUE)
|
||||
return guids;
|
||||
|
||||
std::vector<wchar_t> buffer(128);
|
||||
SP_DEVINFO_DATA dev_info;
|
||||
dev_info.cbSize = sizeof(SP_DEVINFO_DATA);
|
||||
for (DWORD i = 0; SetupDiEnumDeviceInfo(setup_enum, i, &dev_info); ++i)
|
||||
{
|
||||
// Need to find the size of the data and set the buffer appropriately
|
||||
DWORD buffer_size = 0;
|
||||
while (!SetupDiGetDeviceRegistryPropertyW(setup_enum, &dev_info, SPDRP_HARDWAREID, nullptr,
|
||||
reinterpret_cast<BYTE*>(buffer.data()),
|
||||
static_cast<DWORD>(buffer.size()), &buffer_size))
|
||||
{
|
||||
if (buffer_size > buffer.size())
|
||||
buffer.resize(buffer_size);
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (GetLastError() != ERROR_SUCCESS)
|
||||
continue;
|
||||
|
||||
// HARDWAREID is a REG_MULTI_SZ
|
||||
// There are multiple strings separated by NULs, the list is ended by an empty string.
|
||||
for (std::size_t j = 0; buffer[j]; j += std::wcslen(&buffer[j]) + 1)
|
||||
{
|
||||
// XINPUT devices have "IG_xx" embedded in their IDs which is what we look for.
|
||||
if (!std::wcsstr(&buffer[j], L"IG_"))
|
||||
continue;
|
||||
|
||||
unsigned int vid = 0;
|
||||
unsigned int pid = 0;
|
||||
|
||||
// Extract Vendor and Product IDs for matching against DirectInput's device list.
|
||||
wchar_t* pos = std::wcsstr(&buffer[j], L"VID_");
|
||||
if (!pos || !std::swscanf(pos, L"VID_%4X", &vid))
|
||||
continue;
|
||||
pos = std::wcsstr(&buffer[j], L"PID_");
|
||||
if (!pos || !std::swscanf(pos, L"PID_%4X", &pid))
|
||||
continue;
|
||||
|
||||
guids.insert(MAKELONG(vid, pid));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SetupDiDestroyDeviceInfoList(setup_enum);
|
||||
return guids;
|
||||
}
|
||||
} // namespace ciface::DInput
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2014 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace ciface::DInput
|
||||
{
|
||||
std::unordered_set<DWORD> GetXInputGUIDS();
|
||||
} // namespace ciface::DInput
|
||||
@@ -1,257 +0,0 @@
|
||||
// Copyright 2014 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h"
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
#include "Common/Thread.h"
|
||||
|
||||
namespace ciface::ForceFeedback
|
||||
{
|
||||
// Template instantiation:
|
||||
template class ForceFeedbackDevice::TypedForce<DICONSTANTFORCE>;
|
||||
template class ForceFeedbackDevice::TypedForce<DIRAMPFORCE>;
|
||||
template class ForceFeedbackDevice::TypedForce<DIPERIODIC>;
|
||||
|
||||
struct ForceType
|
||||
{
|
||||
GUID guid;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
static const ForceType force_type_names[] = {
|
||||
{GUID_ConstantForce, "Constant"}, // DICONSTANTFORCE
|
||||
{GUID_RampForce, "Ramp"}, // DIRAMPFORCE
|
||||
{GUID_Square, "Square"}, // DIPERIODIC ...
|
||||
{GUID_Sine, "Sine"},
|
||||
{GUID_Triangle, "Triangle"},
|
||||
{GUID_SawtoothUp, "Sawtooth Up"},
|
||||
{GUID_SawtoothDown, "Sawtooth Down"},
|
||||
//{GUID_Spring, "Spring"}, // DICUSTOMFORCE ... < I think
|
||||
//{GUID_Damper, "Damper"},
|
||||
//{GUID_Inertia, "Inertia"},
|
||||
//{GUID_Friction, "Friction"},
|
||||
};
|
||||
|
||||
void ForceFeedbackDevice::DeInitForceFeedback()
|
||||
{
|
||||
if (!m_run_thread.TestAndClear())
|
||||
return;
|
||||
|
||||
SignalUpdateThread();
|
||||
m_update_thread.join();
|
||||
}
|
||||
|
||||
void ForceFeedbackDevice::ThreadFunc()
|
||||
{
|
||||
Common::SetCurrentThreadName("ForceFeedback update thread");
|
||||
|
||||
while (m_run_thread.IsSet())
|
||||
{
|
||||
m_update_event.Wait();
|
||||
|
||||
for (auto output : Outputs())
|
||||
{
|
||||
auto& force = *static_cast<Force*>(output);
|
||||
force.UpdateOutput();
|
||||
}
|
||||
}
|
||||
|
||||
for (auto output : Outputs())
|
||||
{
|
||||
auto& force = *static_cast<Force*>(output);
|
||||
force.Release();
|
||||
}
|
||||
}
|
||||
|
||||
void ForceFeedbackDevice::SignalUpdateThread()
|
||||
{
|
||||
m_update_event.Set();
|
||||
}
|
||||
|
||||
bool ForceFeedbackDevice::InitForceFeedback(const LPDIRECTINPUTDEVICE8 device, int axis_count)
|
||||
{
|
||||
if (axis_count == 0)
|
||||
return false;
|
||||
|
||||
// We just use the X axis (for wheel left/right).
|
||||
// Gamepads seem to not care which axis you use.
|
||||
// These are temporary for creating the effect:
|
||||
std::array<DWORD, 1> rgdwAxes = {DIJOFS_X};
|
||||
std::array<LONG, 1> rglDirection = {-200};
|
||||
|
||||
DIEFFECT eff{};
|
||||
eff.dwSize = sizeof(eff);
|
||||
eff.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS;
|
||||
eff.dwDuration = DI_SECONDS / 1000 * RUMBLE_LENGTH_MS;
|
||||
eff.dwSamplePeriod = 0;
|
||||
eff.dwGain = DI_FFNOMINALMAX;
|
||||
eff.dwTriggerButton = DIEB_NOTRIGGER;
|
||||
eff.dwTriggerRepeatInterval = 0;
|
||||
eff.cAxes = DWORD(rgdwAxes.size());
|
||||
eff.rgdwAxes = rgdwAxes.data();
|
||||
eff.rglDirection = rglDirection.data();
|
||||
eff.dwStartDelay = 0;
|
||||
|
||||
// Initialize parameters with zero force (their current state).
|
||||
DICONSTANTFORCE diCF{};
|
||||
diCF.lMagnitude = 0;
|
||||
DIRAMPFORCE diRF{};
|
||||
diRF.lStart = diRF.lEnd = 0;
|
||||
DIPERIODIC diPE{};
|
||||
diPE.dwMagnitude = 0;
|
||||
diPE.lOffset = 0;
|
||||
diPE.dwPhase = 0;
|
||||
diPE.dwPeriod = DI_SECONDS / 1000 * RUMBLE_PERIOD_MS;
|
||||
|
||||
for (auto& f : force_type_names)
|
||||
{
|
||||
if (f.guid == GUID_ConstantForce)
|
||||
{
|
||||
eff.cbTypeSpecificParams = sizeof(DICONSTANTFORCE);
|
||||
eff.lpvTypeSpecificParams = &diCF;
|
||||
}
|
||||
else if (f.guid == GUID_RampForce)
|
||||
{
|
||||
eff.cbTypeSpecificParams = sizeof(DIRAMPFORCE);
|
||||
eff.lpvTypeSpecificParams = &diRF;
|
||||
}
|
||||
else
|
||||
{
|
||||
// All other forces need periodic parameters:
|
||||
eff.cbTypeSpecificParams = sizeof(DIPERIODIC);
|
||||
eff.lpvTypeSpecificParams = &diPE;
|
||||
}
|
||||
|
||||
LPDIRECTINPUTEFFECT pEffect;
|
||||
if (SUCCEEDED(device->CreateEffect(f.guid, &eff, &pEffect, nullptr)))
|
||||
{
|
||||
if (f.guid == GUID_ConstantForce)
|
||||
AddOutput(new ForceConstant(this, f.name, pEffect, diCF));
|
||||
else if (f.guid == GUID_RampForce)
|
||||
AddOutput(new ForceRamp(this, f.name, pEffect, diRF));
|
||||
else
|
||||
AddOutput(new ForcePeriodic(this, f.name, pEffect, diPE));
|
||||
}
|
||||
}
|
||||
|
||||
// Disable autocentering:
|
||||
if (Outputs().size())
|
||||
{
|
||||
DIPROPDWORD dipdw;
|
||||
dipdw.diph.dwSize = sizeof(DIPROPDWORD);
|
||||
dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER);
|
||||
dipdw.diph.dwObj = 0;
|
||||
dipdw.diph.dwHow = DIPH_DEVICE;
|
||||
dipdw.dwData = DIPROPAUTOCENTER_OFF;
|
||||
device->SetProperty(DIPROP_AUTOCENTER, &dipdw.diph);
|
||||
|
||||
m_run_thread.Set();
|
||||
m_update_thread = std::thread(&ForceFeedbackDevice::ThreadFunc, this);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename P>
|
||||
void ForceFeedbackDevice::TypedForce<P>::PlayEffect()
|
||||
{
|
||||
DIEFFECT eff{};
|
||||
eff.dwSize = sizeof(eff);
|
||||
eff.cbTypeSpecificParams = sizeof(m_params);
|
||||
eff.lpvTypeSpecificParams = &m_params;
|
||||
|
||||
m_effect->SetParameters(&eff, DIEP_START | DIEP_TYPESPECIFICPARAMS);
|
||||
}
|
||||
|
||||
template <typename P>
|
||||
void ForceFeedbackDevice::TypedForce<P>::StopEffect()
|
||||
{
|
||||
m_effect->Stop();
|
||||
}
|
||||
|
||||
template <>
|
||||
bool ForceFeedbackDevice::ForceConstant::UpdateParameters(int magnitude)
|
||||
{
|
||||
const auto old_magnitude = m_params.lMagnitude;
|
||||
|
||||
m_params.lMagnitude = magnitude;
|
||||
|
||||
return old_magnitude != m_params.lMagnitude;
|
||||
}
|
||||
|
||||
template <>
|
||||
bool ForceFeedbackDevice::ForceRamp::UpdateParameters(int magnitude)
|
||||
{
|
||||
const auto old_magnitude = m_params.lStart;
|
||||
|
||||
// Having the same "start" and "end" here is a bit odd..
|
||||
// But ramp forces don't really make sense for our rumble effects anyways..
|
||||
m_params.lStart = m_params.lEnd = magnitude;
|
||||
|
||||
return old_magnitude != m_params.lStart;
|
||||
}
|
||||
|
||||
template <>
|
||||
bool ForceFeedbackDevice::ForcePeriodic::UpdateParameters(int magnitude)
|
||||
{
|
||||
const auto old_magnitude = m_params.dwMagnitude;
|
||||
|
||||
m_params.dwMagnitude = magnitude;
|
||||
|
||||
return old_magnitude != m_params.dwMagnitude;
|
||||
}
|
||||
|
||||
template <typename P>
|
||||
ForceFeedbackDevice::TypedForce<P>::TypedForce(ForceFeedbackDevice* parent, const char* name,
|
||||
LPDIRECTINPUTEFFECT effect, const P& params)
|
||||
: Force(parent, name, effect), m_params(params)
|
||||
{
|
||||
}
|
||||
|
||||
template <typename P>
|
||||
void ForceFeedbackDevice::TypedForce<P>::UpdateEffect(int magnitude)
|
||||
{
|
||||
if (UpdateParameters(magnitude))
|
||||
{
|
||||
if (magnitude)
|
||||
PlayEffect();
|
||||
else
|
||||
StopEffect();
|
||||
}
|
||||
}
|
||||
|
||||
std::string ForceFeedbackDevice::Force::GetName() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
ForceFeedbackDevice::Force::Force(ForceFeedbackDevice* parent, const char* name,
|
||||
LPDIRECTINPUTEFFECT effect)
|
||||
: m_effect(effect), m_parent(*parent), m_name(name), m_desired_magnitude()
|
||||
{
|
||||
}
|
||||
|
||||
void ForceFeedbackDevice::Force::SetState(ControlState state)
|
||||
{
|
||||
const auto new_val = int(DI_FFNOMINALMAX * state);
|
||||
|
||||
if (m_desired_magnitude.exchange(new_val) != new_val)
|
||||
m_parent.SignalUpdateThread();
|
||||
}
|
||||
|
||||
void ForceFeedbackDevice::Force::UpdateOutput()
|
||||
{
|
||||
UpdateEffect(m_desired_magnitude);
|
||||
}
|
||||
|
||||
void ForceFeedbackDevice::Force::Release()
|
||||
{
|
||||
// This isn't in the destructor because it should happen before the device is released.
|
||||
m_effect->Stop();
|
||||
m_effect->Unload();
|
||||
m_effect->Release();
|
||||
}
|
||||
} // namespace ciface::ForceFeedback
|
||||
@@ -1,80 +0,0 @@
|
||||
// Copyright 2014 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "Common/Event.h"
|
||||
#include "Common/Flag.h"
|
||||
|
||||
#include "InputCommon/ControllerInterface/CoreDevice.h"
|
||||
#include "InputCommon/ControllerInterface/DInput/DInput8.h"
|
||||
|
||||
namespace ciface::ForceFeedback
|
||||
{
|
||||
class ForceFeedbackDevice : public Core::Device
|
||||
{
|
||||
public:
|
||||
bool InitForceFeedback(const LPDIRECTINPUTDEVICE8, int axis_count);
|
||||
void DeInitForceFeedback();
|
||||
|
||||
private:
|
||||
void ThreadFunc();
|
||||
|
||||
class Force : public Output
|
||||
{
|
||||
public:
|
||||
Force(ForceFeedbackDevice* parent, const char* name, LPDIRECTINPUTEFFECT effect);
|
||||
|
||||
void UpdateOutput();
|
||||
void Release();
|
||||
|
||||
void SetState(ControlState state) override;
|
||||
std::string GetName() const override;
|
||||
|
||||
protected:
|
||||
const LPDIRECTINPUTEFFECT m_effect;
|
||||
|
||||
private:
|
||||
virtual void UpdateEffect(int magnitude) = 0;
|
||||
|
||||
ForceFeedbackDevice& m_parent;
|
||||
const char* const m_name;
|
||||
std::atomic<int> m_desired_magnitude;
|
||||
};
|
||||
|
||||
template <typename P>
|
||||
class TypedForce : public Force
|
||||
{
|
||||
public:
|
||||
TypedForce(ForceFeedbackDevice* parent, const char* name, LPDIRECTINPUTEFFECT effect,
|
||||
const P& params);
|
||||
|
||||
private:
|
||||
void UpdateEffect(int magnitude) override;
|
||||
|
||||
// Returns true if parameters changed.
|
||||
bool UpdateParameters(int magnitude);
|
||||
|
||||
void PlayEffect();
|
||||
void StopEffect();
|
||||
|
||||
P m_params = {};
|
||||
};
|
||||
|
||||
void SignalUpdateThread();
|
||||
|
||||
typedef TypedForce<DICONSTANTFORCE> ForceConstant;
|
||||
typedef TypedForce<DIRAMPFORCE> ForceRamp;
|
||||
typedef TypedForce<DIPERIODIC> ForcePeriodic;
|
||||
|
||||
std::thread m_update_thread;
|
||||
Common::Event m_update_event;
|
||||
Common::Flag m_run_thread;
|
||||
};
|
||||
} // namespace ciface::ForceFeedback
|
||||
@@ -1,175 +0,0 @@
|
||||
// Copyright 2015 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/Pipes/Pipes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <fcntl.h>
|
||||
#include <iostream>
|
||||
#include <locale>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
|
||||
namespace ciface::Pipes
|
||||
{
|
||||
static const std::array<std::string, 12> s_button_tokens{
|
||||
{"A", "B", "X", "Y", "Z", "START", "L", "R", "D_UP", "D_DOWN", "D_LEFT", "D_RIGHT"}};
|
||||
|
||||
static const std::array<std::string, 2> s_shoulder_tokens{{"L", "R"}};
|
||||
|
||||
static const std::array<std::string, 2> s_axis_tokens{{"MAIN", "C"}};
|
||||
|
||||
static double StringToDouble(const std::string& text)
|
||||
{
|
||||
std::istringstream is(text);
|
||||
// ignore current locale
|
||||
is.imbue(std::locale::classic());
|
||||
double result;
|
||||
is >> result;
|
||||
return result;
|
||||
}
|
||||
|
||||
class InputBackend final : public ciface::InputBackend
|
||||
{
|
||||
public:
|
||||
using ciface::InputBackend::InputBackend;
|
||||
void PopulateDevices() override;
|
||||
};
|
||||
|
||||
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface)
|
||||
{
|
||||
return std::make_unique<InputBackend>(controller_interface);
|
||||
}
|
||||
|
||||
void InputBackend::PopulateDevices()
|
||||
{
|
||||
// Search the Pipes directory for files that we can open in read-only,
|
||||
// non-blocking mode. The device name is the virtual name of the file.
|
||||
File::FSTEntry fst;
|
||||
std::string dir_path = File::GetUserPath(D_PIPES_IDX);
|
||||
if (!File::Exists(dir_path))
|
||||
return;
|
||||
fst = File::ScanDirectoryTree(dir_path, false);
|
||||
if (!fst.isDirectory)
|
||||
return;
|
||||
for (unsigned int i = 0; i < fst.size; ++i)
|
||||
{
|
||||
const File::FSTEntry& child = fst.children[i];
|
||||
if (child.isDirectory)
|
||||
continue;
|
||||
int fd = open(child.physicalName.c_str(), O_RDONLY | O_NONBLOCK);
|
||||
if (fd < 0)
|
||||
continue;
|
||||
g_controller_interface.AddDevice(std::make_shared<PipeDevice>(fd, child.virtualName));
|
||||
}
|
||||
}
|
||||
|
||||
PipeDevice::PipeDevice(int fd, std::string name) : m_fd(fd), m_name(std::move(name))
|
||||
{
|
||||
for (const auto& tok : s_button_tokens)
|
||||
{
|
||||
PipeInput* btn = new PipeInput("Button " + tok);
|
||||
AddInput(btn);
|
||||
m_buttons[tok] = btn;
|
||||
}
|
||||
for (const auto& tok : s_shoulder_tokens)
|
||||
{
|
||||
AddAxis(tok, 0.0);
|
||||
}
|
||||
for (const auto& tok : s_axis_tokens)
|
||||
{
|
||||
AddAxis(tok + " X", 0.5);
|
||||
AddAxis(tok + " Y", 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
PipeDevice::~PipeDevice()
|
||||
{
|
||||
close(m_fd);
|
||||
}
|
||||
|
||||
Core::DeviceRemoval PipeDevice::UpdateInput()
|
||||
{
|
||||
// Read any pending characters off the pipe. If we hit a newline,
|
||||
// then dequeue a command off the front of m_buf and parse it.
|
||||
char buf[32];
|
||||
ssize_t bytes_read = read(m_fd, buf, sizeof buf);
|
||||
while (bytes_read > 0)
|
||||
{
|
||||
m_buf.append(buf, bytes_read);
|
||||
bytes_read = read(m_fd, buf, sizeof buf);
|
||||
}
|
||||
std::size_t newline = m_buf.find("\n");
|
||||
while (newline != std::string::npos)
|
||||
{
|
||||
std::string command = m_buf.substr(0, newline);
|
||||
ParseCommand(command);
|
||||
m_buf.erase(0, newline + 1);
|
||||
newline = m_buf.find("\n");
|
||||
}
|
||||
return Core::DeviceRemoval::Keep;
|
||||
}
|
||||
|
||||
void PipeDevice::AddAxis(const std::string& name, double value)
|
||||
{
|
||||
// Dolphin uses separate axes for left/right, which complicates things.
|
||||
PipeInput* ax_hi = new PipeInput("Axis " + name + " +");
|
||||
ax_hi->SetState(value);
|
||||
PipeInput* ax_lo = new PipeInput("Axis " + name + " -");
|
||||
ax_lo->SetState(value);
|
||||
m_axes[name + " +"] = ax_hi;
|
||||
m_axes[name + " -"] = ax_lo;
|
||||
AddFullAnalogSurfaceInputs(ax_lo, ax_hi);
|
||||
}
|
||||
|
||||
void PipeDevice::SetAxis(const std::string& entry, double value)
|
||||
{
|
||||
value = std::clamp(value, 0.0, 1.0);
|
||||
double hi = std::max(0.0, value - 0.5) * 2.0;
|
||||
double lo = (0.5 - std::min(0.5, value)) * 2.0;
|
||||
auto search_hi = m_axes.find(entry + " +");
|
||||
if (search_hi != m_axes.end())
|
||||
search_hi->second->SetState(hi);
|
||||
auto search_lo = m_axes.find(entry + " -");
|
||||
if (search_lo != m_axes.end())
|
||||
search_lo->second->SetState(lo);
|
||||
}
|
||||
|
||||
void PipeDevice::ParseCommand(const std::string& command)
|
||||
{
|
||||
const std::vector<std::string> tokens = SplitString(command, ' ');
|
||||
if (tokens.size() < 2 || tokens.size() > 4)
|
||||
return;
|
||||
if (tokens[0] == "PRESS" || tokens[0] == "RELEASE")
|
||||
{
|
||||
auto search = m_buttons.find(tokens[1]);
|
||||
if (search != m_buttons.end())
|
||||
search->second->SetState(tokens[0] == "PRESS" ? 1.0 : 0.0);
|
||||
}
|
||||
else if (tokens[0] == "SET")
|
||||
{
|
||||
if (tokens.size() == 3)
|
||||
{
|
||||
double value = StringToDouble(tokens[2]);
|
||||
SetAxis(tokens[1], (value / 2.0) + 0.5);
|
||||
}
|
||||
else if (tokens.size() == 4)
|
||||
{
|
||||
double x = StringToDouble(tokens[2]);
|
||||
double y = StringToDouble(tokens[3]);
|
||||
SetAxis(tokens[1] + " X", x);
|
||||
SetAxis(tokens[1] + " Y", y);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace ciface::Pipes
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright 2015 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
|
||||
namespace ciface::Pipes
|
||||
{
|
||||
// To create a piped controller input, create a named pipe in the
|
||||
// Pipes directory and write commands out to it. Commands are separated
|
||||
// by a newline character, with spaces separating command tokens.
|
||||
// Command syntax is as follows, where curly brackets are one-of and square
|
||||
// brackets are inclusive numeric ranges. Cases are sensitive. Numeric inputs
|
||||
// are clamped to [0, 1] and otherwise invalid commands are discarded.
|
||||
// {PRESS, RELEASE} {A, B, X, Y, Z, START, L, R, D_UP, D_DOWN, D_LEFT, D_RIGHT}
|
||||
// SET {L, R} [0, 1]
|
||||
// SET {MAIN, C} [0, 1] [0, 1]
|
||||
|
||||
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface);
|
||||
|
||||
class PipeDevice : public Core::Device
|
||||
{
|
||||
public:
|
||||
PipeDevice(int fd, std::string name);
|
||||
~PipeDevice();
|
||||
|
||||
Core::DeviceRemoval UpdateInput() override;
|
||||
std::string GetName() const override { return m_name; }
|
||||
std::string GetSource() const override { return "Pipe"; }
|
||||
|
||||
private:
|
||||
class PipeInput : public Input
|
||||
{
|
||||
public:
|
||||
PipeInput(const std::string& name) : m_name(name), m_state(0.0) {}
|
||||
std::string GetName() const override { return m_name; }
|
||||
ControlState GetState() const override { return m_state; }
|
||||
void SetState(ControlState state) { m_state = state; }
|
||||
|
||||
private:
|
||||
const std::string m_name;
|
||||
ControlState m_state;
|
||||
};
|
||||
|
||||
void AddAxis(const std::string& name, double value);
|
||||
void ParseCommand(const std::string& command);
|
||||
void SetAxis(const std::string& entry, double value);
|
||||
|
||||
const int m_fd;
|
||||
const std::string m_name;
|
||||
std::string m_buf;
|
||||
std::map<std::string, PipeInput*> m_buttons;
|
||||
std::map<std::string, PipeInput*> m_axes;
|
||||
};
|
||||
} // namespace ciface::Pipes
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright 2016 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "InputCommon/ControllerInterface/InputBackend.h"
|
||||
|
||||
namespace ciface::Quartz
|
||||
{
|
||||
std::string GetSourceName();
|
||||
|
||||
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface);
|
||||
} // namespace ciface::Quartz
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright 2016 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/Quartz/Quartz.h"
|
||||
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
#include "InputCommon/ControllerInterface/Quartz/QuartzKeyboardAndMouse.h"
|
||||
|
||||
namespace ciface::Quartz
|
||||
{
|
||||
std::string GetSourceName()
|
||||
{
|
||||
return "Quartz";
|
||||
}
|
||||
|
||||
class InputBackend final : public ciface::InputBackend
|
||||
{
|
||||
public:
|
||||
using ciface::InputBackend::InputBackend;
|
||||
void PopulateDevices() override;
|
||||
void HandleWindowChange() override;
|
||||
};
|
||||
|
||||
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface)
|
||||
{
|
||||
return std::make_unique<InputBackend>(controller_interface);
|
||||
}
|
||||
|
||||
void InputBackend::HandleWindowChange()
|
||||
{
|
||||
const std::string source_name = GetSourceName();
|
||||
GetControllerInterface().RemoveDevice(
|
||||
[&](const auto* dev) { return dev->GetSource() == source_name; }, true);
|
||||
|
||||
PopulateDevices();
|
||||
}
|
||||
|
||||
void InputBackend::PopulateDevices()
|
||||
{
|
||||
const WindowSystemInfo wsi = GetControllerInterface().GetWindowSystemInfo();
|
||||
if (wsi.type != WindowSystemType::MacOS)
|
||||
return;
|
||||
|
||||
GetControllerInterface().AddDevice(std::make_shared<KeyboardAndMouse>(wsi.render_window));
|
||||
}
|
||||
|
||||
} // namespace ciface::Quartz
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright 2016 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QuartzCore/QuartzCore.h>
|
||||
|
||||
#include "Common/Matrix.h"
|
||||
#include "InputCommon/ControllerInterface/CoreDevice.h"
|
||||
|
||||
#ifdef __OBJC__
|
||||
@class DolWindowPositionObserver;
|
||||
#else
|
||||
class DolWindowPositionObserver;
|
||||
#endif
|
||||
|
||||
namespace ciface::Quartz
|
||||
{
|
||||
std::string KeycodeToName(const CGKeyCode keycode);
|
||||
|
||||
class KeyboardAndMouse : public Core::Device
|
||||
{
|
||||
private:
|
||||
class Key : public Input
|
||||
{
|
||||
public:
|
||||
explicit Key(CGKeyCode keycode);
|
||||
std::string GetName() const override;
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
CGKeyCode m_keycode;
|
||||
std::string m_name;
|
||||
};
|
||||
|
||||
class Cursor : public Input
|
||||
{
|
||||
public:
|
||||
Cursor(u8 index, const float& axis, const bool positive)
|
||||
: m_axis(axis), m_index(index), m_positive(positive)
|
||||
{
|
||||
}
|
||||
std::string GetName() const override;
|
||||
bool IsDetectable() const override { return false; }
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const float& m_axis;
|
||||
const u8 m_index;
|
||||
const bool m_positive;
|
||||
};
|
||||
|
||||
class Button : public Input
|
||||
{
|
||||
public:
|
||||
explicit Button(CGMouseButton button) : m_button(button) {}
|
||||
std::string GetName() const override;
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
CGMouseButton m_button;
|
||||
};
|
||||
|
||||
public:
|
||||
Core::DeviceRemoval UpdateInput() override;
|
||||
|
||||
explicit KeyboardAndMouse(void* view);
|
||||
~KeyboardAndMouse() override;
|
||||
|
||||
std::string GetName() const override;
|
||||
std::string GetSource() const override;
|
||||
int GetSortPriority() const override;
|
||||
|
||||
private:
|
||||
void MainThreadInitialization(void* view);
|
||||
|
||||
Common::Vec2 m_cursor;
|
||||
|
||||
DolWindowPositionObserver* m_window_pos_observer;
|
||||
};
|
||||
} // namespace ciface::Quartz
|
||||
@@ -1,325 +0,0 @@
|
||||
// Copyright 2016 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/Quartz/QuartzKeyboardAndMouse.h"
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
|
||||
#include <Carbon/Carbon.h>
|
||||
#include <Cocoa/Cocoa.h>
|
||||
|
||||
#include "Core/Host.h"
|
||||
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
#include "InputCommon/ControllerInterface/Quartz/Quartz.h"
|
||||
|
||||
/// Helper class to get window position data from threads other than the main thread
|
||||
@interface DolWindowPositionObserver : NSObject
|
||||
|
||||
- (instancetype)initWithView:(NSView*)view;
|
||||
@property(readonly) NSRect frame;
|
||||
|
||||
@end
|
||||
|
||||
@implementation DolWindowPositionObserver
|
||||
{
|
||||
NSView* _view;
|
||||
NSWindow* _window;
|
||||
NSRect _frame;
|
||||
std::mutex _mtx;
|
||||
}
|
||||
|
||||
- (NSRect)calcFrame
|
||||
{
|
||||
return [_window convertRectToScreen:[_view frame]];
|
||||
}
|
||||
|
||||
- (instancetype)initWithView:(NSView*)view
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_view = view;
|
||||
_window = [view window];
|
||||
_frame = [self calcFrame];
|
||||
[_window addObserver:self forKeyPath:@"frame" options:0 context:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSRect)frame
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(_mtx);
|
||||
return _frame;
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString*)keyPath
|
||||
ofObject:(id)object
|
||||
change:(NSDictionary<NSKeyValueChangeKey, id>*)change
|
||||
context:(void*)context
|
||||
{
|
||||
if (object == _window)
|
||||
{
|
||||
NSRect new_frame = [self calcFrame];
|
||||
std::lock_guard<std::mutex> guard(_mtx);
|
||||
_frame = new_frame;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[_window removeObserver:self forKeyPath:@"frame"];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace ciface::Quartz
|
||||
{
|
||||
std::string KeycodeToName(const CGKeyCode keycode)
|
||||
{
|
||||
static const std::map<CGKeyCode, std::string> named_keys = {
|
||||
{kVK_ANSI_A, "A"},
|
||||
{kVK_ANSI_B, "B"},
|
||||
{kVK_ANSI_C, "C"},
|
||||
{kVK_ANSI_D, "D"},
|
||||
{kVK_ANSI_E, "E"},
|
||||
{kVK_ANSI_F, "F"},
|
||||
{kVK_ANSI_G, "G"},
|
||||
{kVK_ANSI_H, "H"},
|
||||
{kVK_ANSI_I, "I"},
|
||||
{kVK_ANSI_J, "J"},
|
||||
{kVK_ANSI_K, "K"},
|
||||
{kVK_ANSI_L, "L"},
|
||||
{kVK_ANSI_M, "M"},
|
||||
{kVK_ANSI_N, "N"},
|
||||
{kVK_ANSI_O, "O"},
|
||||
{kVK_ANSI_P, "P"},
|
||||
{kVK_ANSI_Q, "Q"},
|
||||
{kVK_ANSI_R, "R"},
|
||||
{kVK_ANSI_S, "S"},
|
||||
{kVK_ANSI_T, "T"},
|
||||
{kVK_ANSI_U, "U"},
|
||||
{kVK_ANSI_V, "V"},
|
||||
{kVK_ANSI_W, "W"},
|
||||
{kVK_ANSI_X, "X"},
|
||||
{kVK_ANSI_Y, "Y"},
|
||||
{kVK_ANSI_Z, "Z"},
|
||||
{kVK_ANSI_1, "1"},
|
||||
{kVK_ANSI_2, "2"},
|
||||
{kVK_ANSI_3, "3"},
|
||||
{kVK_ANSI_4, "4"},
|
||||
{kVK_ANSI_5, "5"},
|
||||
{kVK_ANSI_6, "6"},
|
||||
{kVK_ANSI_7, "7"},
|
||||
{kVK_ANSI_8, "8"},
|
||||
{kVK_ANSI_9, "9"},
|
||||
{kVK_ANSI_0, "0"},
|
||||
{kVK_Return, "Return"},
|
||||
{kVK_Escape, "Escape"},
|
||||
{kVK_Delete, "Backspace"},
|
||||
{kVK_Tab, "Tab"},
|
||||
{kVK_Space, "Space"},
|
||||
{kVK_ANSI_Minus, "-"},
|
||||
{kVK_ANSI_Equal, "="},
|
||||
{kVK_ANSI_LeftBracket, "["},
|
||||
{kVK_ANSI_RightBracket, "]"},
|
||||
{kVK_ANSI_Backslash, "\\"},
|
||||
{kVK_ANSI_Semicolon, ";"},
|
||||
{kVK_ANSI_Quote, "'"},
|
||||
{kVK_ANSI_Grave, "Tilde"},
|
||||
{kVK_ANSI_Comma, ","},
|
||||
{kVK_ANSI_Period, "."},
|
||||
{kVK_ANSI_Slash, "/"},
|
||||
{kVK_CapsLock, "Caps Lock"},
|
||||
{kVK_F1, "F1"},
|
||||
{kVK_F2, "F2"},
|
||||
{kVK_F3, "F3"},
|
||||
{kVK_F4, "F4"},
|
||||
{kVK_F5, "F5"},
|
||||
{kVK_F6, "F6"},
|
||||
{kVK_F7, "F7"},
|
||||
{kVK_F8, "F8"},
|
||||
{kVK_F9, "F9"},
|
||||
{kVK_F10, "F10"},
|
||||
{kVK_F11, "F11"},
|
||||
{kVK_F12, "F12"},
|
||||
{kVK_Home, "Home"},
|
||||
{kVK_PageUp, "Page Up"},
|
||||
{kVK_ForwardDelete, "Delete"},
|
||||
{kVK_End, "End"},
|
||||
{kVK_PageDown, "Page Down"},
|
||||
{kVK_RightArrow, "Right Arrow"},
|
||||
{kVK_LeftArrow, "Left Arrow"},
|
||||
{kVK_DownArrow, "Down Arrow"},
|
||||
{kVK_UpArrow, "Up Arrow"},
|
||||
{kVK_ANSI_KeypadDivide, "Keypad /"},
|
||||
{kVK_ANSI_KeypadMultiply, "Keypad *"},
|
||||
{kVK_ANSI_KeypadMinus, "Keypad -"},
|
||||
{kVK_ANSI_KeypadPlus, "Keypad +"},
|
||||
{kVK_ANSI_KeypadEnter, "Keypad Enter"},
|
||||
{kVK_ANSI_Keypad1, "Keypad 1"},
|
||||
{kVK_ANSI_Keypad2, "Keypad 2"},
|
||||
{kVK_ANSI_Keypad3, "Keypad 3"},
|
||||
{kVK_ANSI_Keypad4, "Keypad 4"},
|
||||
{kVK_ANSI_Keypad5, "Keypad 5"},
|
||||
{kVK_ANSI_Keypad6, "Keypad 6"},
|
||||
{kVK_ANSI_Keypad7, "Keypad 7"},
|
||||
{kVK_ANSI_Keypad8, "Keypad 8"},
|
||||
{kVK_ANSI_Keypad9, "Keypad 9"},
|
||||
{kVK_ANSI_Keypad0, "Keypad 0"},
|
||||
{kVK_ANSI_KeypadDecimal, "Keypad ."},
|
||||
{kVK_ANSI_KeypadEquals, "Keypad ="},
|
||||
{kVK_Control, "Left Control"},
|
||||
{kVK_Shift, "Left Shift"},
|
||||
{kVK_Option, "Left Alt"},
|
||||
{kVK_Command, "Command"},
|
||||
{kVK_RightControl, "Right Control"},
|
||||
{kVK_RightShift, "Right Shift"},
|
||||
{kVK_RightOption, "Right Alt"},
|
||||
};
|
||||
|
||||
if (named_keys.contains(keycode))
|
||||
return named_keys.at(keycode);
|
||||
else
|
||||
return "Key " + std::to_string(keycode);
|
||||
}
|
||||
|
||||
KeyboardAndMouse::Key::Key(CGKeyCode keycode) : m_keycode(keycode), m_name(KeycodeToName(keycode))
|
||||
{
|
||||
}
|
||||
|
||||
ControlState KeyboardAndMouse::Key::GetState() const
|
||||
{
|
||||
return CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, m_keycode) != 0;
|
||||
}
|
||||
|
||||
std::string KeyboardAndMouse::Key::GetName() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
KeyboardAndMouse::KeyboardAndMouse(void* view)
|
||||
{
|
||||
// All keycodes in <HIToolbox/Events.h> are 0x7e or lower. If you notice
|
||||
// keys that aren't being recognized, bump this number up!
|
||||
for (int keycode = 0; keycode < 0x80; ++keycode)
|
||||
AddInput(new Key(keycode));
|
||||
|
||||
// Add combined left/right modifiers with consistent naming across platforms.
|
||||
AddCombinedInput("Alt", {"Left Alt", "Right Alt"});
|
||||
AddCombinedInput("Shift", {"Left Shift", "Right Shift"});
|
||||
AddCombinedInput("Ctrl", {"Left Control", "Right Control"});
|
||||
|
||||
// PopulateDevices may be called on the Emuthread, so we need to ensure that
|
||||
// these UI APIs are only ever called on the main thread.
|
||||
if ([NSThread isMainThread])
|
||||
MainThreadInitialization(view);
|
||||
else
|
||||
dispatch_sync(dispatch_get_main_queue(), [this, view] { MainThreadInitialization(view); });
|
||||
|
||||
// cursor, with a hax for-loop
|
||||
for (unsigned int i = 0; i < 4; ++i)
|
||||
AddInput(new Cursor(!!(i & 2), (&m_cursor.x)[i / 2], !!(i & 1)));
|
||||
|
||||
AddInput(new Button(kCGMouseButtonLeft));
|
||||
AddInput(new Button(kCGMouseButtonRight));
|
||||
AddInput(new Button(kCGMouseButtonCenter));
|
||||
}
|
||||
|
||||
// Very important that this is here
|
||||
// C++ and ObjC++ have different views of the header, and only ObjC++'s will deallocate properly
|
||||
KeyboardAndMouse::~KeyboardAndMouse() = default;
|
||||
|
||||
void KeyboardAndMouse::MainThreadInitialization(void* view)
|
||||
{
|
||||
NSView* cocoa_view = (__bridge NSView*)view;
|
||||
m_window_pos_observer = [[DolWindowPositionObserver alloc] initWithView:cocoa_view];
|
||||
}
|
||||
|
||||
Core::DeviceRemoval KeyboardAndMouse::UpdateInput()
|
||||
{
|
||||
NSRect bounds = [m_window_pos_observer frame];
|
||||
|
||||
const double window_width = std::max(bounds.size.width, 1.0);
|
||||
const double window_height = std::max(bounds.size.height, 1.0);
|
||||
|
||||
if (g_controller_interface.IsMouseCenteringRequested() &&
|
||||
(Host_RendererHasFocus() || Host_TASInputHasFocus()))
|
||||
{
|
||||
m_cursor.x = 0;
|
||||
m_cursor.y = 0;
|
||||
|
||||
const CGPoint window_center_global_coordinates =
|
||||
CGPointMake(bounds.origin.x + window_width / 2.0, bounds.origin.y + window_height / 2.0);
|
||||
CGWarpMouseCursorPosition(window_center_global_coordinates);
|
||||
// Without this line there is a short but obvious delay after centering the cursor before it can
|
||||
// be moved again
|
||||
CGAssociateMouseAndMouseCursorPosition(true);
|
||||
|
||||
g_controller_interface.SetMouseCenteringRequested(false);
|
||||
}
|
||||
else if (!Host_TASInputHasFocus())
|
||||
{
|
||||
// When a TAS Input window has focus and "Enable Controller Input" is checked most types of
|
||||
// input should be read normally as if the render window had focus instead. The cursor is an
|
||||
// exception, as otherwise using the mouse to set any control in the TAS Input window will also
|
||||
// update the Wii IR value (or any other input controlled by the cursor).
|
||||
|
||||
NSPoint loc = [NSEvent mouseLocation];
|
||||
|
||||
const auto window_scale = g_controller_interface.GetWindowInputScale();
|
||||
|
||||
loc.x -= bounds.origin.x;
|
||||
loc.y -= bounds.origin.y;
|
||||
m_cursor.x = (loc.x / window_width * 2 - 1.0) * window_scale.x;
|
||||
m_cursor.y = (loc.y / window_height * 2 - 1.0) * -window_scale.y;
|
||||
}
|
||||
|
||||
return Core::DeviceRemoval::Keep;
|
||||
}
|
||||
|
||||
std::string KeyboardAndMouse::GetName() const
|
||||
{
|
||||
return "Keyboard & Mouse";
|
||||
}
|
||||
|
||||
std::string KeyboardAndMouse::GetSource() const
|
||||
{
|
||||
return Quartz::GetSourceName();
|
||||
}
|
||||
|
||||
int KeyboardAndMouse::GetSortPriority() const
|
||||
{
|
||||
return DEFAULT_DEVICE_SORT_PRIORITY;
|
||||
}
|
||||
|
||||
ControlState KeyboardAndMouse::Cursor::GetState() const
|
||||
{
|
||||
return std::max(0.0, ControlState(m_axis) / (m_positive ? 1.0 : -1.0));
|
||||
}
|
||||
|
||||
ControlState KeyboardAndMouse::Button::GetState() const
|
||||
{
|
||||
return CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, m_button) != 0;
|
||||
}
|
||||
|
||||
std::string KeyboardAndMouse::Cursor::GetName() const
|
||||
{
|
||||
char tmpstr[] = "Cursor ..";
|
||||
tmpstr[7] = (char)('X' + m_index);
|
||||
tmpstr[8] = (m_positive ? '+' : '-');
|
||||
return tmpstr;
|
||||
}
|
||||
|
||||
std::string KeyboardAndMouse::Button::GetName() const
|
||||
{
|
||||
if (m_button == kCGMouseButtonLeft)
|
||||
return "Left Click";
|
||||
if (m_button == kCGMouseButtonCenter)
|
||||
return "Middle Click";
|
||||
if (m_button == kCGMouseButtonRight)
|
||||
return "Right Click";
|
||||
return std::string("Click ") + char('0' + m_button);
|
||||
}
|
||||
} // namespace ciface::Quartz
|
||||
@@ -1,835 +0,0 @@
|
||||
// Copyright 2020 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/WGInput/WGInput.h"
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
// For CoGetApartmentType
|
||||
#include <objbase.h>
|
||||
#pragma comment(lib, "ole32")
|
||||
|
||||
// NOTE: winrt translates com failures to c++ exceptions, so we must use try/catch in this file to
|
||||
// prevent possible errors from escaping and terminating Dolphin.
|
||||
#ifdef _MSC_VER
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/windows.devices.haptics.h>
|
||||
#include <winrt/windows.devices.power.h>
|
||||
#include <winrt/windows.foundation.collections.h>
|
||||
#include <winrt/windows.gaming.input.h>
|
||||
#include <winrt/windows.system.power.h>
|
||||
#else
|
||||
#ifdef __MINGW32__
|
||||
// for: https://github.com/msys2/MINGW-packages/issues/22160
|
||||
#define ____FIReference_1_boolean_INTERFACE_DEFINED__
|
||||
enum BatteryStatus
|
||||
{
|
||||
BatteryStatus_NotPresent = 0,
|
||||
BatteryStatus_Discharging = 1,
|
||||
BatteryStatus_Idle = 2,
|
||||
BatteryStatus_Charging = 3
|
||||
};
|
||||
namespace ABI
|
||||
{
|
||||
namespace Windows
|
||||
{
|
||||
namespace Storage
|
||||
{
|
||||
enum KnownFoldersAccessStatus
|
||||
{
|
||||
KnownFoldersAccessStatus_Denied = 0,
|
||||
KnownFoldersAccessStatus_Allowed = 1
|
||||
};
|
||||
} // namespace Storage
|
||||
} // namespace Windows
|
||||
} // namespace ABI
|
||||
#include <windows.devices.haptics.h>
|
||||
#include <windows.devices.power.h>
|
||||
#include <windows.foundation.collections.h>
|
||||
#include <windows.gaming.input.forcefeedback.h>
|
||||
#include <windows.gaming.input.h>
|
||||
#include <windows.system.power.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#pragma comment(lib, "windowsapp")
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/HRWrap.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/ScopeGuard.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Common/WorkQueueThread.h"
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
|
||||
namespace WGI = winrt::Windows::Gaming::Input;
|
||||
namespace Haptics = winrt::Windows::Devices::Haptics;
|
||||
using winrt::Windows::Foundation::Collections::IVectorView;
|
||||
|
||||
namespace ciface::WGInput
|
||||
{
|
||||
static constexpr std::string_view SOURCE_NAME = "WGInput";
|
||||
|
||||
// These names correspond to the values of the GameControllerButtonLabel enum.
|
||||
// "None" is not used.
|
||||
// There are some overlapping names assuming no device exposes both
|
||||
// GameControllerButtonLabel::XboxLeftBumper and GameControllerButtonLabel::LeftBumper.
|
||||
// If needed we can prepend "Xbox" to relevant input names on conflict in the future.
|
||||
static constexpr std::array wgi_button_names = {
|
||||
"None", "Back", "Start", "Menu", "View", "Pad N",
|
||||
"Pad S", "Pad W", "Pad E", "Button A", "Button B", "Button X",
|
||||
"Button Y", "Bumper L", "Trigger L", "Thumb L", "Bumper R", "Trigger R",
|
||||
"Thumb R", "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4", "Mode",
|
||||
"Select", "Menu", "View", "Back", "Start", "Options",
|
||||
"Share", "Pad N", "Pad S", "Pad W", "Pad E", "Letter A",
|
||||
"Letter B", "Letter C", "Letter L", "Letter R", "Letter X", "Letter Y",
|
||||
"Letter Z", "Cross", "Circle", "Square", "Triangle", "Bumper L",
|
||||
"Trigger L", "Thumb L", "Left 1", "Left 2", "Left 3", "Bumper R",
|
||||
"Trigger R", "Thumb R", "Right 1", "Right 2", "Right 3", "Paddle 1",
|
||||
"Paddle 2", "Paddle 3", "Paddle 4", "Plus", "Minus", "Down Left Arrow",
|
||||
"Dial L", "Dial R", "Suspension",
|
||||
};
|
||||
|
||||
template <typename T, typename M>
|
||||
struct MemberName
|
||||
{
|
||||
M T::* ptr;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
static constexpr MemberName<WGI::GamepadReading, double> gamepad_trigger_names[] = {
|
||||
{&WGI::GamepadReading::LeftTrigger, "Trigger L"},
|
||||
{&WGI::GamepadReading::RightTrigger, "Trigger R"}};
|
||||
|
||||
static constexpr MemberName<WGI::GamepadReading, double> gamepad_axis_names[] = {
|
||||
{&WGI::GamepadReading::LeftThumbstickX, "Left X"},
|
||||
{&WGI::GamepadReading::LeftThumbstickY, "Left Y"},
|
||||
{&WGI::GamepadReading::RightThumbstickX, "Right X"},
|
||||
{&WGI::GamepadReading::RightThumbstickY, "Right Y"}};
|
||||
|
||||
static constexpr MemberName<WGI::GamepadVibration, double> gamepad_motor_names[] = {
|
||||
{&WGI::GamepadVibration::LeftMotor, "Motor L"},
|
||||
{&WGI::GamepadVibration::RightMotor, "Motor R"},
|
||||
{&WGI::GamepadVibration::LeftTrigger, "Trigger L"},
|
||||
{&WGI::GamepadVibration::RightTrigger, "Trigger R"}};
|
||||
|
||||
class Device : public Core::Device
|
||||
{
|
||||
public:
|
||||
Device(std::string name, WGI::RawGameController raw_controller, WGI::Gamepad gamepad)
|
||||
: m_name(std::move(name)), m_raw_controller(raw_controller), m_gamepad(gamepad)
|
||||
{
|
||||
// Buttons:
|
||||
PopulateButtons();
|
||||
|
||||
// Add inputs for IGamepad interface if available.
|
||||
if (m_gamepad)
|
||||
{
|
||||
// Axes:
|
||||
for (auto& axis : gamepad_axis_names)
|
||||
{
|
||||
AddInput(new NamedAxis(&(m_gamepad_reading.*axis.ptr), 0.0, -1.0, axis.name));
|
||||
AddInput(new NamedAxis(&(m_gamepad_reading.*axis.ptr), 0.0, +1.0, axis.name));
|
||||
}
|
||||
|
||||
// Triggers:
|
||||
for (auto& trigger : gamepad_trigger_names)
|
||||
AddInput(new NamedTrigger(&(m_gamepad_reading.*trigger.ptr), trigger.name));
|
||||
|
||||
// Motors:
|
||||
for (auto& motor : gamepad_motor_names)
|
||||
AddOutput(new NamedMotor(&(m_state_out.*motor.ptr), motor.name, this));
|
||||
}
|
||||
|
||||
// Add IRawGameController's axes if IGamepad is not available.
|
||||
// This may need to change if some devices expose additional axes.
|
||||
// Maybe we can determine which additional axes are not in IGamepad's collection.
|
||||
const bool use_raw_controller_axes = !m_gamepad;
|
||||
|
||||
if (use_raw_controller_axes)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Axes:
|
||||
m_axes.resize(m_raw_controller.AxisCount());
|
||||
|
||||
u32 i = 0;
|
||||
for (auto& axis : m_axes)
|
||||
{
|
||||
AddFullAnalogSurfaceInputs(new IndexedAxis(&axis, 0.5, +0.5, i),
|
||||
new IndexedAxis(&axis, 0.5, -0.5, i));
|
||||
++i;
|
||||
}
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
// If AxisCount failed, m_axes will remain zero-sized; nothing to do.
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Error populating axes");
|
||||
}
|
||||
}
|
||||
|
||||
// Apparently some devices (e.g. DS4) provide the IGameController interface
|
||||
// but expose the dpad only on a switch (IRawGameController interface).
|
||||
// We'll need to add switches regardless of available interfaces.
|
||||
constexpr bool use_raw_controller_switches = true;
|
||||
|
||||
// Switches (Hats):
|
||||
if (use_raw_controller_switches)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_switches.resize(m_raw_controller.SwitchCount());
|
||||
// Accumulate switch kinds first, to ensure no inputs are added if there is any error.
|
||||
std::vector<WGI::GameControllerSwitchKind> switch_kinds;
|
||||
for (u32 i = 0; i < m_switches.size(); i++)
|
||||
switch_kinds.push_back(m_raw_controller.GetSwitchKind(i));
|
||||
|
||||
u32 i = 0;
|
||||
for (auto& swtch : m_switches)
|
||||
{
|
||||
using gcsp = WGI::GameControllerSwitchPosition;
|
||||
|
||||
AddInput(new IndexedSwitch(&swtch, i, gcsp::Up));
|
||||
AddInput(new IndexedSwitch(&swtch, i, gcsp::Down));
|
||||
|
||||
if (switch_kinds[i] != WGI::GameControllerSwitchKind::TwoWay)
|
||||
{
|
||||
// If it's not a "two-way" switch (up/down only) then add the left/right inputs.
|
||||
AddInput(new IndexedSwitch(&swtch, i, gcsp::Left));
|
||||
AddInput(new IndexedSwitch(&swtch, i, gcsp::Right));
|
||||
}
|
||||
|
||||
++i;
|
||||
}
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
// Safe as no inputs will have been added.
|
||||
m_switches.clear();
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Error populating switches");
|
||||
}
|
||||
}
|
||||
|
||||
// Haptics:
|
||||
PopulateHaptics();
|
||||
|
||||
// Battery:
|
||||
if (UpdateBatteryLevel())
|
||||
AddInput(new Battery(&m_battery_level));
|
||||
}
|
||||
|
||||
int GetSortPriority() const override { return -1; }
|
||||
|
||||
const WGI::RawGameController GetRawGameController() const { return m_raw_controller; }
|
||||
|
||||
private:
|
||||
using ButtonValueType = u8;
|
||||
|
||||
class Button : public Input
|
||||
{
|
||||
public:
|
||||
explicit Button(const ButtonValueType* button) : m_button(*button) {}
|
||||
ControlState GetState() const override { return ControlState(m_button != 0); }
|
||||
|
||||
private:
|
||||
const ButtonValueType& m_button;
|
||||
};
|
||||
|
||||
// A button with one of the "labels" that WGI provides.
|
||||
class NamedButton final : public Button
|
||||
{
|
||||
public:
|
||||
NamedButton(const ButtonValueType* button, std::string_view name) : Button(button), m_name(name)
|
||||
{
|
||||
}
|
||||
std::string GetName() const override { return std::string(m_name); }
|
||||
|
||||
private:
|
||||
const std::string_view m_name;
|
||||
};
|
||||
|
||||
// A button with no label so we name it by its index.
|
||||
class IndexedButton final : public Button
|
||||
{
|
||||
public:
|
||||
IndexedButton(const ButtonValueType* button, u32 index) : Button(button), m_index(index) {}
|
||||
std::string GetName() const override { return fmt::format("Button {}", m_index); }
|
||||
|
||||
private:
|
||||
u32 m_index;
|
||||
};
|
||||
|
||||
class Axis : public Input
|
||||
{
|
||||
public:
|
||||
Axis(const double* axis, double base, double range)
|
||||
: m_base(base), m_range(range), m_axis(*axis)
|
||||
{
|
||||
}
|
||||
|
||||
ControlState GetState() const override { return ControlState(m_axis - m_base) / m_range; }
|
||||
|
||||
protected:
|
||||
const double m_base;
|
||||
const double m_range;
|
||||
|
||||
private:
|
||||
const double& m_axis;
|
||||
};
|
||||
|
||||
class NamedAxis final : public Axis
|
||||
{
|
||||
public:
|
||||
NamedAxis(const double* axis, double base, double range, std::string_view name)
|
||||
: Axis(axis, base, range), m_name(name)
|
||||
{
|
||||
}
|
||||
std::string GetName() const override
|
||||
{
|
||||
return fmt::format("{}{}", m_name, m_range < 0 ? '-' : '+');
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string_view m_name;
|
||||
};
|
||||
|
||||
class NamedTrigger final : public Axis
|
||||
{
|
||||
public:
|
||||
NamedTrigger(const double* axis, std::string_view name) : Axis(axis, 0.0, 1.0), m_name(name) {}
|
||||
std::string GetName() const override { return std::string(m_name); }
|
||||
|
||||
private:
|
||||
const std::string_view m_name;
|
||||
};
|
||||
|
||||
class NamedMotor final : public Output
|
||||
{
|
||||
public:
|
||||
NamedMotor(double* motor, std::string_view name, Device* parent)
|
||||
: m_motor(*motor), m_name(name), m_parent(*parent)
|
||||
{
|
||||
}
|
||||
std::string GetName() const override { return std::string(m_name); }
|
||||
void SetState(ControlState state) override
|
||||
{
|
||||
if (m_motor == state)
|
||||
return;
|
||||
|
||||
m_motor = state;
|
||||
m_parent.UpdateMotors();
|
||||
}
|
||||
|
||||
private:
|
||||
double& m_motor;
|
||||
const std::string_view m_name;
|
||||
Device& m_parent;
|
||||
};
|
||||
|
||||
class IndexedAxis final : public Axis
|
||||
{
|
||||
public:
|
||||
IndexedAxis(const double* axis, double base, double range, u32 index)
|
||||
: Axis(axis, base, range), m_index(index)
|
||||
{
|
||||
}
|
||||
std::string GetName() const override
|
||||
{
|
||||
return fmt::format("Axis {}{}", m_index, m_range < 0 ? '-' : '+');
|
||||
}
|
||||
|
||||
private:
|
||||
const u32 m_index;
|
||||
};
|
||||
|
||||
class IndexedSwitch final : public Input
|
||||
{
|
||||
public:
|
||||
IndexedSwitch(const WGI::GameControllerSwitchPosition* swtch, u32 index,
|
||||
WGI::GameControllerSwitchPosition direction)
|
||||
: m_switch(*swtch), m_index(index), m_direction(static_cast<int32_t>(direction))
|
||||
{
|
||||
}
|
||||
std::string GetName() const override
|
||||
{
|
||||
return fmt::format("Switch {} {}", m_index, "NESW"[m_direction / 2]);
|
||||
}
|
||||
ControlState GetState() const override
|
||||
{
|
||||
if (m_switch == WGI::GameControllerSwitchPosition::Center)
|
||||
return 0.0;
|
||||
|
||||
// All of the "in between" states (e.g. Up-Right) are one-off from the four cardinal
|
||||
// directions. This tests that the current switch state value is within 1 of the desired
|
||||
// state.
|
||||
const auto direction_diff = std::abs(static_cast<int32_t>(m_switch) - m_direction);
|
||||
return ControlState(direction_diff <= 1 || direction_diff == 7);
|
||||
}
|
||||
|
||||
private:
|
||||
const WGI::GameControllerSwitchPosition& m_switch;
|
||||
const u32 m_index;
|
||||
const int32_t m_direction;
|
||||
};
|
||||
|
||||
class Battery : public Input
|
||||
{
|
||||
public:
|
||||
Battery(const double* level) : m_level(*level) {}
|
||||
|
||||
bool IsDetectable() const override { return false; }
|
||||
|
||||
ControlState GetState() const override { return m_level; }
|
||||
|
||||
std::string GetName() const override { return "Battery"; }
|
||||
|
||||
private:
|
||||
const double& m_level;
|
||||
};
|
||||
|
||||
class SimpleHaptics : public Output
|
||||
{
|
||||
public:
|
||||
SimpleHaptics(Haptics::SimpleHapticsController haptics,
|
||||
Haptics::SimpleHapticsControllerFeedback feedback, u32 haptics_index)
|
||||
: m_haptics(haptics), m_feedback(feedback), m_haptics_index(haptics_index)
|
||||
{
|
||||
}
|
||||
|
||||
void SetState(ControlState state) override
|
||||
{
|
||||
if (m_current_state == state)
|
||||
return;
|
||||
|
||||
m_current_state = state;
|
||||
|
||||
try
|
||||
{
|
||||
if (state)
|
||||
m_haptics.SendHapticFeedback(m_feedback, state);
|
||||
else
|
||||
m_haptics.StopFeedback();
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
u32 GetHapticsIndex() const { return m_haptics_index; }
|
||||
|
||||
private:
|
||||
Haptics::SimpleHapticsController m_haptics;
|
||||
Haptics::SimpleHapticsControllerFeedback m_feedback;
|
||||
const u32 m_haptics_index;
|
||||
ControlState m_current_state = 0;
|
||||
};
|
||||
|
||||
class NamedFeedback final : public SimpleHaptics
|
||||
{
|
||||
public:
|
||||
NamedFeedback(Haptics::SimpleHapticsController haptics,
|
||||
Haptics::SimpleHapticsControllerFeedback feedback, u32 haptics_index,
|
||||
std::string_view feedback_name)
|
||||
: SimpleHaptics(haptics, feedback, haptics_index), m_feedback_name(feedback_name)
|
||||
{
|
||||
}
|
||||
std::string GetName() const override
|
||||
{
|
||||
return fmt::format("{} {}", m_feedback_name, GetHapticsIndex());
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string_view m_feedback_name;
|
||||
};
|
||||
|
||||
void PopulateButtons()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Using RawGameController for buttons because it gives us a nice array instead of a bitmask.
|
||||
m_buttons.resize(m_raw_controller.ButtonCount());
|
||||
|
||||
u32 i = 0;
|
||||
for (const auto& button : m_buttons)
|
||||
{
|
||||
WGI::GameControllerButtonLabel lbl{WGI::GameControllerButtonLabel::None};
|
||||
try
|
||||
{
|
||||
lbl = m_raw_controller.GetButtonLabel(i);
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
lbl = WGI::GameControllerButtonLabel::None;
|
||||
}
|
||||
|
||||
const int32_t button_name_idx = static_cast<int32_t>(lbl);
|
||||
if (lbl != WGI::GameControllerButtonLabel::None &&
|
||||
button_name_idx < wgi_button_names.size())
|
||||
AddInput(new NamedButton(&button, wgi_button_names[button_name_idx]));
|
||||
else
|
||||
AddInput(new IndexedButton(&button, i));
|
||||
|
||||
++i;
|
||||
}
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
// If ButtonCount failed, m_buttons will be zero-sized.
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Error populating buttons");
|
||||
}
|
||||
}
|
||||
|
||||
void PopulateHaptics()
|
||||
{
|
||||
static const std::map<uint16_t, std::string> waveform_name_map{
|
||||
{Haptics::KnownSimpleHapticsControllerWaveforms::Click(), "Click"},
|
||||
{Haptics::KnownSimpleHapticsControllerWaveforms::BuzzContinuous(), "Buzz"},
|
||||
{Haptics::KnownSimpleHapticsControllerWaveforms::RumbleContinuous(), "Rumble"},
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// SimpleHapticsControllers is available from Windows 1709.
|
||||
u32 haptics_index = 0;
|
||||
for (auto haptics_controller : m_raw_controller.SimpleHapticsControllers())
|
||||
{
|
||||
for (Haptics::SimpleHapticsControllerFeedback feedback :
|
||||
haptics_controller.SupportedFeedback())
|
||||
{
|
||||
const uint16_t waveform = feedback.Waveform();
|
||||
auto waveform_name_it = waveform_name_map.find(waveform);
|
||||
if (waveform_name_it == waveform_name_map.end())
|
||||
{
|
||||
WARN_LOG_FMT(CONTROLLERINTERFACE,
|
||||
"WGInput: Unhandled haptics feedback waveform: 0x{:04x}.", waveform);
|
||||
continue;
|
||||
}
|
||||
AddOutput(new NamedFeedback(haptics_controller, feedback, haptics_index,
|
||||
waveform_name_it->second));
|
||||
}
|
||||
++haptics_index;
|
||||
}
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
// Don't need to cleanup any state. It's OK if some outputs were added.
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Error populating haptics");
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetName() const override { return m_name; }
|
||||
|
||||
std::string GetSource() const override { return std::string(SOURCE_NAME); }
|
||||
|
||||
Core::DeviceRemoval UpdateInput() override
|
||||
{
|
||||
// IRawGameController:
|
||||
static_assert(sizeof(bool) == sizeof(ButtonValueType));
|
||||
// This cludge is needed to workaround GetCurrentReading wanting array_view<bool>, while
|
||||
// using std::vector<bool> would create a bit-packed array, which isn't wanted. So, we keep
|
||||
// vector<u8> and view it as array<bool>.
|
||||
auto buttons =
|
||||
winrt::array_view<bool>(reinterpret_cast<winrt::array_view<bool>::pointer>(&m_buttons[0]),
|
||||
static_cast<winrt::array_view<bool>::size_type>(m_buttons.size()));
|
||||
try
|
||||
{
|
||||
m_raw_controller.GetCurrentReading(buttons, m_switches, m_axes);
|
||||
}
|
||||
catch (winrt::hresult_error error)
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE,
|
||||
"WGInput: IRawGameController::GetCurrentReading failed: {}", error.code());
|
||||
}
|
||||
|
||||
// IGamepad:
|
||||
if (m_gamepad)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_gamepad_reading = m_gamepad.GetCurrentReading();
|
||||
}
|
||||
catch (winrt::hresult_error error)
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: IGamepad::GetCurrentReading failed: {}",
|
||||
error.code());
|
||||
}
|
||||
}
|
||||
|
||||
// IGameControllerBatteryInfo:
|
||||
if (!UpdateBatteryLevel())
|
||||
DEBUG_LOG_FMT(CONTROLLERINTERFACE, "WGInput: UpdateBatteryLevel failed.");
|
||||
|
||||
return Core::DeviceRemoval::Keep;
|
||||
}
|
||||
|
||||
void UpdateMotors()
|
||||
{
|
||||
try
|
||||
{
|
||||
m_gamepad.Vibration(m_state_out);
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdateBatteryLevel()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Workaround for Steam. If Steam's GameOverlayRenderer64.dll is loaded, battery_info is null.
|
||||
auto battery_info = m_raw_controller.try_as<WGI::IGameControllerBatteryInfo>();
|
||||
if (!battery_info)
|
||||
return false;
|
||||
const winrt::Windows::Devices::Power::BatteryReport report =
|
||||
battery_info.TryGetBatteryReport();
|
||||
if (!report)
|
||||
return false;
|
||||
|
||||
// TryGetBatteryReport causes a memleak of 0x58 bytes each time it is called, up to at
|
||||
// least Windows 21H2. A hack to fix the memleak is recorded here for posterity.
|
||||
// auto report_ = *(uintptr_t***)&report;
|
||||
// auto rc = &report_[0x40 / 8][0x30 / 8];
|
||||
// if (*rc == 2)
|
||||
// (*rc)--;
|
||||
|
||||
using winrt::Windows::System::Power::BatteryStatus;
|
||||
const BatteryStatus status = report.Status();
|
||||
switch (status)
|
||||
{
|
||||
case BatteryStatus::NotPresent:
|
||||
m_battery_level = 0;
|
||||
return true;
|
||||
|
||||
case BatteryStatus::Idle:
|
||||
case BatteryStatus::Charging:
|
||||
m_battery_level = BATTERY_INPUT_MAX_VALUE;
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const int32_t full_value = report.FullChargeCapacityInMilliwattHours().GetInt32();
|
||||
const int32_t remaining_value = report.RemainingCapacityInMilliwattHours().GetInt32();
|
||||
m_battery_level = BATTERY_INPUT_MAX_VALUE * remaining_value / full_value;
|
||||
return true;
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string m_name;
|
||||
|
||||
const WGI::RawGameController m_raw_controller;
|
||||
std::vector<ButtonValueType> m_buttons;
|
||||
std::vector<WGI::GameControllerSwitchPosition> m_switches;
|
||||
std::vector<double> m_axes;
|
||||
|
||||
WGI::Gamepad m_gamepad = nullptr;
|
||||
WGI::GamepadReading m_gamepad_reading{};
|
||||
WGI::GamepadVibration m_state_out{};
|
||||
|
||||
ControlState m_battery_level = 0;
|
||||
};
|
||||
|
||||
enum class AddRemoveEventType
|
||||
{
|
||||
AddOrReplace,
|
||||
Remove,
|
||||
};
|
||||
|
||||
struct AddRemoveEvent
|
||||
{
|
||||
AddRemoveEventType type;
|
||||
WGI::RawGameController raw_game_controller;
|
||||
};
|
||||
|
||||
static thread_local bool s_initialized_winrt;
|
||||
static winrt::event_token s_event_added, s_event_removed;
|
||||
static Common::WorkQueueThread<AddRemoveEvent> s_device_add_remove_queue;
|
||||
|
||||
static bool COMIsInitialized()
|
||||
{
|
||||
APTTYPE apt_type{};
|
||||
APTTYPEQUALIFIER apt_qualifier{};
|
||||
return CoGetApartmentType(&apt_type, &apt_qualifier) == S_OK;
|
||||
}
|
||||
|
||||
static void AddDevice(const WGI::RawGameController& raw_game_controller)
|
||||
{
|
||||
// Get user-facing device name if available. Otherwise generate a name from vid/pid.
|
||||
std::string device_name;
|
||||
try
|
||||
{
|
||||
device_name = StripWhitespace(WStringToUTF8(raw_game_controller.DisplayName().c_str()));
|
||||
if (device_name.empty())
|
||||
{
|
||||
const u16 vid = raw_game_controller.HardwareVendorId();
|
||||
const u16 pid = raw_game_controller.HardwareProductId();
|
||||
device_name = fmt::format("Device_{:04x}:{:04x}", vid, pid);
|
||||
INFO_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Empty device name, using {}", device_name);
|
||||
}
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
device_name = "Device";
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Failed to get device name, using {}", device_name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WGI::Gamepad gamepad = WGI::Gamepad::FromGameController(raw_game_controller);
|
||||
// Note that gamepad may be nullptr here. The Device class will deal with this.
|
||||
auto dev = std::make_shared<Device>(std::move(device_name), raw_game_controller, gamepad);
|
||||
|
||||
// Only add if it has some inputs/outputs.
|
||||
if (dev->Inputs().size() || dev->Outputs().size())
|
||||
g_controller_interface.AddDevice(std::move(dev));
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Failed to add device {}", device_name);
|
||||
}
|
||||
}
|
||||
|
||||
static void RemoveDevice(const WGI::RawGameController& raw_game_controller)
|
||||
{
|
||||
g_controller_interface.RemoveDevice([&](const auto* dev) {
|
||||
if (dev->GetSource() != SOURCE_NAME)
|
||||
return false;
|
||||
return static_cast<const Device*>(dev)->GetRawGameController() == raw_game_controller;
|
||||
});
|
||||
}
|
||||
|
||||
#pragma warning(push)
|
||||
// 'winrt::impl::implements_delegate<winrt::Windows::Foundation::EventHandler<winrt::Windows::Gaming::Input::RawGameController>,H>':
|
||||
// class has virtual functions, but its non-trivial destructor is not virtual; instances of this
|
||||
// class may not be destructed correctly
|
||||
// (H is the lambda)
|
||||
#pragma warning(disable : 4265)
|
||||
|
||||
static void HandleAddRemoveEvent(AddRemoveEvent evt)
|
||||
{
|
||||
try
|
||||
{
|
||||
winrt::init_apartment();
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE,
|
||||
"WGInput: Failed to CoInitialize for add/remove controller event: {}",
|
||||
winrt::to_string(ex.message()));
|
||||
return;
|
||||
}
|
||||
Common::ScopeGuard coinit_guard([] { winrt::uninit_apartment(); });
|
||||
|
||||
switch (evt.type)
|
||||
{
|
||||
case AddRemoveEventType::AddOrReplace:
|
||||
RemoveDevice(evt.raw_game_controller);
|
||||
AddDevice(evt.raw_game_controller);
|
||||
break;
|
||||
case AddRemoveEventType::Remove:
|
||||
RemoveDevice(evt.raw_game_controller);
|
||||
break;
|
||||
default:
|
||||
#ifdef __MINGW32__
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Invalid add/remove controller event: {}",
|
||||
static_cast<std::underlying_type_t<decltype(evt.type)>>(evt.type));
|
||||
#else
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Invalid add/remove controller event: {}",
|
||||
std::to_underlying(evt.type));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void Init()
|
||||
{
|
||||
if (!COMIsInitialized())
|
||||
{
|
||||
// NOTE: Devices in g_controller_interface should only be accessed by threads that have had
|
||||
// winrt (com) initialized.
|
||||
winrt::init_apartment();
|
||||
s_initialized_winrt = true;
|
||||
}
|
||||
|
||||
s_device_add_remove_queue.Reset("WGInput Add/Remove Device Thread", HandleAddRemoveEvent);
|
||||
|
||||
try
|
||||
{
|
||||
// These events will be invoked from WGI-managed threadpool.
|
||||
s_event_added = WGI::RawGameController::RawGameControllerAdded(
|
||||
[](auto&&, WGI::RawGameController raw_game_controller) {
|
||||
s_device_add_remove_queue.EmplaceItem(
|
||||
AddRemoveEvent{AddRemoveEventType::AddOrReplace, std::move(raw_game_controller)});
|
||||
});
|
||||
|
||||
s_event_removed = WGI::RawGameController::RawGameControllerRemoved(
|
||||
[](auto&&, WGI::RawGameController raw_game_controller) {
|
||||
s_device_add_remove_queue.EmplaceItem(
|
||||
AddRemoveEvent{AddRemoveEventType::Remove, std::move(raw_game_controller)});
|
||||
});
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: Failed to register event handlers");
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning(pop)
|
||||
|
||||
void DeInit()
|
||||
{
|
||||
s_device_add_remove_queue.Shutdown();
|
||||
|
||||
WGI::RawGameController::RawGameControllerAdded(s_event_added);
|
||||
WGI::RawGameController::RawGameControllerRemoved(s_event_removed);
|
||||
|
||||
if (s_initialized_winrt)
|
||||
{
|
||||
winrt::uninit_apartment();
|
||||
s_initialized_winrt = false;
|
||||
}
|
||||
}
|
||||
|
||||
void PopulateDevices()
|
||||
{
|
||||
// WGI Interfaces to potentially use:
|
||||
// Gamepad: Buttons, 2x Sticks and 2x Triggers, 4x Vibration Motors
|
||||
// RawGameController: Buttons, Switches (Hats), Axes, Haptics
|
||||
// The following are not implemented:
|
||||
// ArcadeStick: Buttons (probably no need to specialize, literally just buttons)
|
||||
// FlightStick: Buttons, HatSwitch, Pitch, Roll, Throttle, Yaw
|
||||
// RacingWheel: Buttons, Clutch, Handbrake, PatternShifterGear, Throttle, Wheel, WheelMotor
|
||||
// UINavigationController: Directions, Scrolling, etc.
|
||||
|
||||
// RawGameController available from Windows 15063.
|
||||
// properties added in 1709: DisplayName, NonRoamableId, SimpleHapticsControllers
|
||||
|
||||
try
|
||||
{
|
||||
for (const WGI::RawGameController& raw_game_controller :
|
||||
WGI::RawGameController::RawGameControllers())
|
||||
{
|
||||
RemoveDevice(raw_game_controller);
|
||||
AddDevice(raw_game_controller);
|
||||
}
|
||||
}
|
||||
catch (winrt::hresult_error)
|
||||
{
|
||||
// Only reach here if RawGameControllers() failed
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "WGInput: PopulateDevices failed");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ciface::WGInput
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2020 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace ciface::WGInput
|
||||
{
|
||||
void Init();
|
||||
void DeInit();
|
||||
void PopulateDevices();
|
||||
|
||||
} // namespace ciface::WGInput
|
||||
@@ -1,128 +0,0 @@
|
||||
// Copyright 2017 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/Win32/Win32.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cfgmgr32.h>
|
||||
// must be before hidclass
|
||||
#include <initguid.h>
|
||||
|
||||
#include <hidclass.h>
|
||||
|
||||
#include "Common/Flag.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "InputCommon/ControllerInterface/DInput/DInput.h"
|
||||
#include "InputCommon/ControllerInterface/WGInput/WGInput.h"
|
||||
#include "InputCommon/ControllerInterface/XInput/XInput.h"
|
||||
|
||||
#pragma comment(lib, "OneCoreUAP.Lib")
|
||||
|
||||
// TODO is this really needed?
|
||||
static Common::Flag s_first_populate_devices_asked;
|
||||
static HCMNOTIFICATION s_notify_handle;
|
||||
|
||||
namespace ciface::Win32
|
||||
{
|
||||
class InputBackend final : public ciface::InputBackend
|
||||
{
|
||||
public:
|
||||
InputBackend(ControllerInterface* controller_interface);
|
||||
~InputBackend() override;
|
||||
|
||||
void PopulateDevices() override;
|
||||
void HandleWindowChange() override;
|
||||
HWND GetHWND();
|
||||
};
|
||||
} // namespace ciface::Win32
|
||||
|
||||
_Pre_satisfies_(EventDataSize >= sizeof(CM_NOTIFY_EVENT_DATA)) static DWORD CALLBACK
|
||||
OnDevicesChanged(_In_ HCMNOTIFICATION hNotify, _In_opt_ PVOID Context,
|
||||
_In_ CM_NOTIFY_ACTION Action,
|
||||
_In_reads_bytes_(EventDataSize) PCM_NOTIFY_EVENT_DATA EventData,
|
||||
_In_ DWORD EventDataSize)
|
||||
{
|
||||
if (Action == CM_NOTIFY_ACTION_DEVICEINTERFACEARRIVAL ||
|
||||
Action == CM_NOTIFY_ACTION_DEVICEINTERFACEREMOVAL)
|
||||
{
|
||||
// Windows automatically sends this message before we ask for it and before we are "ready" to
|
||||
// listen for it.
|
||||
if (s_first_populate_devices_asked.IsSet())
|
||||
{
|
||||
// TODO: we could easily use the message passed alongside this event, which tells
|
||||
// whether a device was added or removed, to avoid removing old, still connected, devices
|
||||
g_controller_interface.PlatformPopulateDevices([&] {
|
||||
ciface::DInput::PopulateDevices(
|
||||
static_cast<ciface::Win32::InputBackend*>(Context)->GetHWND());
|
||||
ciface::XInput::PopulateDevices();
|
||||
});
|
||||
}
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
namespace ciface::Win32
|
||||
{
|
||||
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface)
|
||||
{
|
||||
return std::make_unique<InputBackend>(controller_interface);
|
||||
}
|
||||
|
||||
HWND InputBackend::GetHWND()
|
||||
{
|
||||
return static_cast<HWND>(GetControllerInterface().GetWindowSystemInfo().render_window);
|
||||
}
|
||||
|
||||
InputBackend::InputBackend(ControllerInterface* controller_interface)
|
||||
: ciface::InputBackend(controller_interface)
|
||||
{
|
||||
XInput::Init();
|
||||
WGInput::Init();
|
||||
|
||||
CM_NOTIFY_FILTER notify_filter{.cbSize = sizeof(notify_filter),
|
||||
.FilterType = CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE,
|
||||
.u{.DeviceInterface{.ClassGuid = GUID_DEVINTERFACE_HID}}};
|
||||
const CONFIGRET cfg_rv =
|
||||
CM_Register_Notification(¬ify_filter, this, OnDevicesChanged, &s_notify_handle);
|
||||
if (cfg_rv != CR_SUCCESS)
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "CM_Register_Notification failed: {:x}", cfg_rv);
|
||||
}
|
||||
}
|
||||
|
||||
void InputBackend::PopulateDevices()
|
||||
{
|
||||
g_controller_interface.PlatformPopulateDevices([this] {
|
||||
s_first_populate_devices_asked.Set();
|
||||
ciface::DInput::PopulateDevices(GetHWND());
|
||||
ciface::XInput::PopulateDevices();
|
||||
ciface::WGInput::PopulateDevices();
|
||||
});
|
||||
}
|
||||
|
||||
void InputBackend::HandleWindowChange()
|
||||
{
|
||||
g_controller_interface.PlatformPopulateDevices(
|
||||
[this] { ciface::DInput::ChangeWindow(GetHWND()); });
|
||||
}
|
||||
|
||||
InputBackend::~InputBackend()
|
||||
{
|
||||
s_first_populate_devices_asked.Clear();
|
||||
DInput::DeInit();
|
||||
|
||||
if (s_notify_handle)
|
||||
{
|
||||
const CONFIGRET cfg_rv = CM_Unregister_Notification(s_notify_handle);
|
||||
if (cfg_rv != CR_SUCCESS)
|
||||
{
|
||||
ERROR_LOG_FMT(CONTROLLERINTERFACE, "CM_Unregister_Notification failed: {:x}", cfg_rv);
|
||||
}
|
||||
s_notify_handle = nullptr;
|
||||
}
|
||||
|
||||
XInput::DeInit();
|
||||
WGInput::DeInit();
|
||||
}
|
||||
|
||||
} // namespace ciface::Win32
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright 2017 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "InputCommon/ControllerInterface/InputBackend.h"
|
||||
|
||||
namespace ciface::Win32
|
||||
{
|
||||
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface);
|
||||
} // namespace ciface::Win32
|
||||
@@ -1,321 +0,0 @@
|
||||
// Copyright 2010 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "InputCommon/ControllerInterface/XInput/XInput.h"
|
||||
|
||||
#ifndef XINPUT_GAMEPAD_GUIDE
|
||||
#define XINPUT_GAMEPAD_GUIDE 0x0400
|
||||
#endif
|
||||
|
||||
namespace ciface::XInput
|
||||
{
|
||||
struct ButtonDef
|
||||
{
|
||||
const char* name;
|
||||
WORD bitmask;
|
||||
};
|
||||
static constexpr std::array<ButtonDef, 15> named_buttons{{
|
||||
{"Button A", XINPUT_GAMEPAD_A},
|
||||
{"Button B", XINPUT_GAMEPAD_B},
|
||||
{"Button X", XINPUT_GAMEPAD_X},
|
||||
{"Button Y", XINPUT_GAMEPAD_Y},
|
||||
{"Pad N", XINPUT_GAMEPAD_DPAD_UP},
|
||||
{"Pad S", XINPUT_GAMEPAD_DPAD_DOWN},
|
||||
{"Pad W", XINPUT_GAMEPAD_DPAD_LEFT},
|
||||
{"Pad E", XINPUT_GAMEPAD_DPAD_RIGHT},
|
||||
{"Start", XINPUT_GAMEPAD_START},
|
||||
{"Back", XINPUT_GAMEPAD_BACK},
|
||||
{"Shoulder L", XINPUT_GAMEPAD_LEFT_SHOULDER},
|
||||
{"Shoulder R", XINPUT_GAMEPAD_RIGHT_SHOULDER},
|
||||
{"Guide", XINPUT_GAMEPAD_GUIDE},
|
||||
{"Thumb L", XINPUT_GAMEPAD_LEFT_THUMB},
|
||||
{"Thumb R", XINPUT_GAMEPAD_RIGHT_THUMB},
|
||||
}};
|
||||
|
||||
static constexpr std::array named_triggers{"Trigger L", "Trigger R"};
|
||||
|
||||
static constexpr std::array named_axes{"Left X", "Left Y", "Right X", "Right Y"};
|
||||
|
||||
static constexpr std::array named_motors{"Motor L", "Motor R"};
|
||||
|
||||
class Button final : public Core::Device::Input
|
||||
{
|
||||
public:
|
||||
Button(u8 index, const WORD& buttons) : m_buttons(buttons), m_index(index) {}
|
||||
std::string GetName() const override { return named_buttons[m_index].name; }
|
||||
ControlState GetState() const override
|
||||
{
|
||||
return (m_buttons & named_buttons[m_index].bitmask) > 0;
|
||||
}
|
||||
|
||||
private:
|
||||
const WORD& m_buttons;
|
||||
const u8 m_index;
|
||||
};
|
||||
|
||||
class Axis final : public Core::Device::Input
|
||||
{
|
||||
public:
|
||||
Axis(u8 index, const SHORT& axis, SHORT range) : m_axis(axis), m_range(range), m_index(index) {}
|
||||
std::string GetName() const override
|
||||
{
|
||||
return std::string(named_axes[m_index]) + (m_range < 0 ? '-' : '+');
|
||||
}
|
||||
ControlState GetState() const override { return ControlState(m_axis) / m_range; }
|
||||
|
||||
private:
|
||||
const SHORT& m_axis;
|
||||
const SHORT m_range;
|
||||
const u8 m_index;
|
||||
};
|
||||
|
||||
class Trigger final : public Core::Device::Input
|
||||
{
|
||||
public:
|
||||
Trigger(u8 index, const BYTE& trigger, BYTE range)
|
||||
: m_trigger(trigger), m_range(range), m_index(index)
|
||||
{
|
||||
}
|
||||
std::string GetName() const override { return named_triggers[m_index]; }
|
||||
ControlState GetState() const override { return ControlState(m_trigger) / m_range; }
|
||||
|
||||
private:
|
||||
const BYTE& m_trigger;
|
||||
const BYTE m_range;
|
||||
const u8 m_index;
|
||||
};
|
||||
|
||||
class Motor final : public Core::Device::Output
|
||||
{
|
||||
public:
|
||||
Motor(u8 index, Device* parent, WORD& motor, WORD range)
|
||||
: m_motor(motor), m_range(range), m_index(index), m_parent(parent)
|
||||
{
|
||||
}
|
||||
|
||||
std::string GetName() const override { return named_motors[m_index]; }
|
||||
void SetState(ControlState state) override
|
||||
{
|
||||
const auto old_value = m_motor;
|
||||
m_motor = (WORD)(state * m_range);
|
||||
|
||||
// Only update if the state changed.
|
||||
if (m_motor != old_value)
|
||||
m_parent->UpdateMotors();
|
||||
}
|
||||
|
||||
private:
|
||||
WORD& m_motor;
|
||||
const WORD m_range;
|
||||
const u8 m_index;
|
||||
Device* m_parent;
|
||||
};
|
||||
|
||||
class Battery final : public Core::Device::Input
|
||||
{
|
||||
public:
|
||||
Battery(const ControlState* level) : m_level(*level) {}
|
||||
std::string GetName() const override { return "Battery"; }
|
||||
ControlState GetState() const override { return m_level; }
|
||||
bool IsDetectable() const override { return false; }
|
||||
|
||||
private:
|
||||
const ControlState& m_level;
|
||||
};
|
||||
|
||||
static HMODULE hXInput = nullptr;
|
||||
|
||||
typedef decltype(&XInputGetCapabilities) XInputGetCapabilities_t;
|
||||
typedef decltype(&XInputSetState) XInputSetState_t;
|
||||
typedef decltype(&XInputGetState) XInputGetState_t;
|
||||
typedef decltype(&XInputGetBatteryInformation) XInputGetBatteryInformation_t;
|
||||
|
||||
static XInputGetCapabilities_t PXInputGetCapabilities = nullptr;
|
||||
static XInputSetState_t PXInputSetState = nullptr;
|
||||
static XInputGetState_t PXInputGetState = nullptr;
|
||||
static XInputGetBatteryInformation_t PXInputGetBatteryInformation = nullptr;
|
||||
|
||||
static bool s_have_guide_button = false;
|
||||
|
||||
void Init()
|
||||
{
|
||||
if (!hXInput)
|
||||
{
|
||||
#ifdef __MINGW32__
|
||||
#if(_WIN32_WINNT >= _WIN32_WINNT_WIN8)
|
||||
#define XINPUT_DLL_A "xinput1_4.dll"
|
||||
#define XINPUT_DLL_W L"xinput1_4.dll"
|
||||
#else
|
||||
#define XINPUT_DLL_A "xinput9_1_0.dll"
|
||||
#define XINPUT_DLL_W L"xinput9_1_0.dll"
|
||||
#endif
|
||||
|
||||
#ifdef UNICODE
|
||||
#define XINPUT_DLL XINPUT_DLL_W
|
||||
#else
|
||||
#define XINPUT_DLL XINPUT_DLL_A
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Try for the most recent version we were compiled against (will only work if running on Win8+)
|
||||
hXInput = ::LoadLibrary(XINPUT_DLL);
|
||||
if (!hXInput)
|
||||
{
|
||||
// Drop back to DXSDK June 2010 version. Requires DX June 2010 redist.
|
||||
hXInput = ::LoadLibrary(TEXT("xinput1_3.dll"));
|
||||
if (!hXInput)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
PXInputGetCapabilities =
|
||||
(XInputGetCapabilities_t)::GetProcAddress(hXInput, "XInputGetCapabilities");
|
||||
PXInputSetState = (XInputSetState_t)::GetProcAddress(hXInput, "XInputSetState");
|
||||
PXInputGetBatteryInformation =
|
||||
(XInputGetBatteryInformation_t)::GetProcAddress(hXInput, "XInputGetBatteryInformation");
|
||||
|
||||
// Ordinal 100 is the same as XInputGetState, except it doesn't dummy out the guide
|
||||
// button info. Try loading it and fall back if needed.
|
||||
PXInputGetState = (XInputGetState_t)::GetProcAddress(hXInput, (LPCSTR)100);
|
||||
|
||||
s_have_guide_button = PXInputGetState != nullptr;
|
||||
|
||||
if (!PXInputGetState)
|
||||
PXInputGetState = (XInputGetState_t)::GetProcAddress(hXInput, "XInputGetState");
|
||||
|
||||
if (!PXInputGetCapabilities || !PXInputSetState || !PXInputGetState ||
|
||||
!PXInputGetBatteryInformation)
|
||||
{
|
||||
::FreeLibrary(hXInput);
|
||||
hXInput = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PopulateDevices()
|
||||
{
|
||||
if (!hXInput)
|
||||
return;
|
||||
|
||||
g_controller_interface.RemoveDevice([](const auto* dev) { return dev->GetSource() == "XInput"; });
|
||||
|
||||
XINPUT_CAPABILITIES caps;
|
||||
for (int i = 0; i != 4; ++i)
|
||||
if (ERROR_SUCCESS == PXInputGetCapabilities(i, 0, &caps))
|
||||
g_controller_interface.AddDevice(std::make_shared<Device>(caps, i));
|
||||
}
|
||||
|
||||
void DeInit()
|
||||
{
|
||||
if (hXInput)
|
||||
{
|
||||
::FreeLibrary(hXInput);
|
||||
hXInput = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Device::Device(const XINPUT_CAPABILITIES& caps, u8 index) : m_subtype(caps.SubType), m_index(index)
|
||||
{
|
||||
// XInputGetCaps can be broken on some devices, so we'll just ignore it
|
||||
// and assume all gamepad + vibration capabilities are supported
|
||||
|
||||
// Buttons.
|
||||
for (size_t i = 0; i != size(named_buttons); ++i)
|
||||
{
|
||||
// Only add guide button if we have the 100 ordinal XInputGetState.
|
||||
if (named_buttons[i].bitmask == XINPUT_GAMEPAD_GUIDE && !s_have_guide_button)
|
||||
continue;
|
||||
|
||||
AddInput(new Button(u8(i), m_state_in.Gamepad.wButtons));
|
||||
}
|
||||
|
||||
// Triggers.
|
||||
for (size_t i = 0; i != size(named_triggers); ++i)
|
||||
AddInput(new Trigger(u8(i), (&m_state_in.Gamepad.bLeftTrigger)[i], 255));
|
||||
|
||||
// Axes.
|
||||
for (size_t i = 0; i != size(named_axes); ++i)
|
||||
{
|
||||
const SHORT& ax = (&m_state_in.Gamepad.sThumbLX)[i];
|
||||
|
||||
// Each axis gets a negative and a positive input instance associated with it.
|
||||
AddInput(new Axis(u8(i), ax, -32768));
|
||||
AddInput(new Axis(u8(i), ax, 32767));
|
||||
}
|
||||
|
||||
// Rumble motors.
|
||||
for (size_t i = 0; i != size(named_motors); ++i)
|
||||
AddOutput(new Motor(u8(i), this, (&m_state_out.wLeftMotorSpeed)[i], 65535));
|
||||
|
||||
AddInput(new Battery(&m_battery_level));
|
||||
}
|
||||
|
||||
std::string Device::GetName() const
|
||||
{
|
||||
switch (m_subtype)
|
||||
{
|
||||
case XINPUT_DEVSUBTYPE_GAMEPAD:
|
||||
return "Gamepad";
|
||||
case XINPUT_DEVSUBTYPE_WHEEL:
|
||||
return "Wheel";
|
||||
case XINPUT_DEVSUBTYPE_ARCADE_STICK:
|
||||
return "Arcade Stick";
|
||||
case XINPUT_DEVSUBTYPE_FLIGHT_STICK:
|
||||
return "Flight Stick";
|
||||
case XINPUT_DEVSUBTYPE_DANCE_PAD:
|
||||
return "Dance Pad";
|
||||
case XINPUT_DEVSUBTYPE_GUITAR:
|
||||
return "Guitar";
|
||||
case XINPUT_DEVSUBTYPE_DRUM_KIT:
|
||||
return "Drum Kit";
|
||||
default:
|
||||
return "Device";
|
||||
}
|
||||
}
|
||||
|
||||
std::string Device::GetSource() const
|
||||
{
|
||||
return "XInput";
|
||||
}
|
||||
|
||||
Core::DeviceRemoval Device::UpdateInput()
|
||||
{
|
||||
PXInputGetState(m_index, &m_state_in);
|
||||
|
||||
XINPUT_BATTERY_INFORMATION battery_info = {};
|
||||
if (SUCCEEDED(PXInputGetBatteryInformation(m_index, BATTERY_DEVTYPE_GAMEPAD, &battery_info)))
|
||||
{
|
||||
switch (battery_info.BatteryType)
|
||||
{
|
||||
case BATTERY_TYPE_DISCONNECTED:
|
||||
case BATTERY_TYPE_UNKNOWN:
|
||||
m_battery_level = 0;
|
||||
break;
|
||||
case BATTERY_TYPE_WIRED:
|
||||
m_battery_level = BATTERY_INPUT_MAX_VALUE;
|
||||
break;
|
||||
default:
|
||||
m_battery_level =
|
||||
battery_info.BatteryLevel / ControlState(BATTERY_LEVEL_FULL) * BATTERY_INPUT_MAX_VALUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Core::DeviceRemoval::Keep;
|
||||
}
|
||||
|
||||
void Device::UpdateMotors()
|
||||
{
|
||||
PXInputSetState(m_index, &m_state_out);
|
||||
}
|
||||
|
||||
std::optional<int> Device::GetPreferredId() const
|
||||
{
|
||||
return m_index;
|
||||
}
|
||||
|
||||
} // namespace ciface::XInput
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright 2010 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// XInput suffers a similar issue as XAudio2. Since Win8, it is part of the OS.
|
||||
// However, unlike XAudio2 they have not made the API incompatible - so we just
|
||||
// compile against the latest version and fall back to dynamically loading the
|
||||
// old DLL.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <xinput.h>
|
||||
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
|
||||
#if defined(__MINGW32__) && defined(XINPUT_DEVSUBTYPE_FLIGHT_SICK)
|
||||
#define XINPUT_DEVSUBTYPE_FLIGHT_STICK XINPUT_DEVSUBTYPE_FLIGHT_SICK
|
||||
#endif
|
||||
#ifndef XINPUT_DEVSUBTYPE_FLIGHT_STICK
|
||||
#error You are building this module against the wrong version of DirectX. You probably need to remove DXSDK_DIR from your include path and/or _WIN32_WINNT is wrong.
|
||||
#endif
|
||||
|
||||
namespace ciface::XInput
|
||||
{
|
||||
void Init();
|
||||
void PopulateDevices();
|
||||
void DeInit();
|
||||
|
||||
class Device final : public Core::Device
|
||||
{
|
||||
public:
|
||||
Device(const XINPUT_CAPABILITIES& capabilities, u8 index);
|
||||
|
||||
std::string GetName() const override;
|
||||
std::string GetSource() const override;
|
||||
std::optional<int> GetPreferredId() const override;
|
||||
int GetSortPriority() const override { return -2; }
|
||||
|
||||
Core::DeviceRemoval UpdateInput() override;
|
||||
|
||||
void UpdateMotors();
|
||||
|
||||
private:
|
||||
XINPUT_STATE m_state_in{};
|
||||
XINPUT_VIBRATION m_state_out{};
|
||||
ControlState m_battery_level{};
|
||||
const BYTE m_subtype;
|
||||
const u8 m_index;
|
||||
};
|
||||
} // namespace ciface::XInput
|
||||
@@ -1,523 +0,0 @@
|
||||
// Copyright 2013 Max Eliaser
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/Xlib/XInput2.h"
|
||||
|
||||
#include <X11/XKBlib.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/Logging/Log.h"
|
||||
|
||||
#include "Core/Host.h"
|
||||
|
||||
// This is an input plugin using the XInput 2.0 extension to the X11 protocol,
|
||||
// loosely based on the old XLib plugin. (Has nothing to do with the XInput
|
||||
// API on Windows.)
|
||||
|
||||
// This plugin creates one KeyboardMouse object for each master pointer/
|
||||
// keyboard pair. Each KeyboardMouse object exports four types of controls:
|
||||
// * Mouse button controls: hardcoded at 32 of them, but could be made to
|
||||
// support infinitely many mouse buttons in theory; XInput2 has no limit.
|
||||
// * Mouse cursor controls: one for each cardinal direction. Calculated by
|
||||
// comparing the absolute position of the mouse pointer on screen to the
|
||||
// center of the emulator window.
|
||||
// * Mouse axis controls: one for each cardinal direction. Calculated using
|
||||
// a running average of relative mouse motion on each axis.
|
||||
// * Key controls: these correspond to a limited subset of the keyboard
|
||||
// keys.
|
||||
|
||||
// Mouse axis control tuning. Unlike absolute mouse position, relative mouse
|
||||
// motion data needs to be tweaked and smoothed out a bit to be usable.
|
||||
|
||||
// Mouse axis control output is simply divided by this number. In practice,
|
||||
// that just means you can use a smaller "dead zone" if you bind axis controls
|
||||
// to a joystick. No real need to make this customizable.
|
||||
#define MOUSE_AXIS_SENSITIVITY 8.0f
|
||||
|
||||
// The mouse axis controls use a weighted running average. Each frame, the new
|
||||
// value is the average of the old value and the amount of relative mouse
|
||||
// motion during that frame. The old value is weighted by a ratio of
|
||||
// MOUSE_AXIS_SMOOTHING:1 compared to the new value. Increasing
|
||||
// MOUSE_AXIS_SMOOTHING makes the controls smoother, decreasing it makes them
|
||||
// more responsive. This might be useful as a user-customizable option.
|
||||
#define MOUSE_AXIS_SMOOTHING 1.5f
|
||||
|
||||
// The scroll axis value should decay a lot faster than the mouse axes since
|
||||
// it should ideally register each click of the scroll wheel. Decreasing this
|
||||
// value makes it more likely that a scroll wheel input is registered, but less
|
||||
// likely to differentiate between different inputs, while increasing it will
|
||||
// more cleanly separate each scroll wheel click, but risks dropping some inputs
|
||||
#define SCROLL_AXIS_DECAY 1.1f
|
||||
|
||||
namespace
|
||||
{
|
||||
// We need XInput 2.1 to get raw events on the root window even while another
|
||||
// client has a grab. If we request 2.2 or later, the server will not generate
|
||||
// emulated button presses from touch events, so we want exactly 2.1.
|
||||
constexpr int XINPUT_MAJOR = 2, XINPUT_MINOR = 1;
|
||||
} // namespace
|
||||
|
||||
namespace ciface::XInput2
|
||||
{
|
||||
constexpr std::string_view SOURCE_NAME = "XInput2";
|
||||
|
||||
class InputBackend final : public ciface::InputBackend
|
||||
{
|
||||
public:
|
||||
using ciface::InputBackend::InputBackend;
|
||||
void PopulateDevices() override;
|
||||
void HandleWindowChange() override;
|
||||
};
|
||||
|
||||
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface)
|
||||
{
|
||||
return std::make_unique<InputBackend>(controller_interface);
|
||||
}
|
||||
|
||||
void InputBackend::HandleWindowChange()
|
||||
{
|
||||
GetControllerInterface().RemoveDevice(
|
||||
[](const auto* dev) { return dev->GetSource() == SOURCE_NAME; }, true);
|
||||
|
||||
PopulateDevices();
|
||||
}
|
||||
|
||||
// This function will add zero or more KeyboardMouse objects to devices.
|
||||
void InputBackend::PopulateDevices()
|
||||
{
|
||||
const WindowSystemInfo wsi = GetControllerInterface().GetWindowSystemInfo();
|
||||
if (wsi.type != WindowSystemType::X11)
|
||||
return;
|
||||
|
||||
const auto hwnd = wsi.render_window;
|
||||
|
||||
Display* dpy = XOpenDisplay(nullptr);
|
||||
|
||||
// xi_opcode is important; it will be used to identify XInput events by
|
||||
// the polling loop in UpdateInput.
|
||||
int xi_opcode, event, error;
|
||||
|
||||
// verify that the XInput extension is available
|
||||
if (!XQueryExtension(dpy, "XInputExtension", &xi_opcode, &event, &error))
|
||||
{
|
||||
WARN_LOG_FMT(CONTROLLERINTERFACE, "XInput extension not available (XQueryExtension)");
|
||||
return;
|
||||
}
|
||||
|
||||
int major = XINPUT_MAJOR, minor = XINPUT_MINOR;
|
||||
if (XIQueryVersion(dpy, &major, &minor) != Success || major < XINPUT_MAJOR ||
|
||||
(major == XINPUT_MAJOR && minor < XINPUT_MINOR))
|
||||
{
|
||||
WARN_LOG_FMT(CONTROLLERINTERFACE, "XInput extension not available (XIQueryVersion)");
|
||||
return;
|
||||
}
|
||||
|
||||
// register all master devices with Dolphin
|
||||
|
||||
XIDeviceInfo* all_masters;
|
||||
XIDeviceInfo* current_master;
|
||||
double scroll_increment = 1.0f;
|
||||
int num_masters;
|
||||
|
||||
all_masters = XIQueryDevice(dpy, XIAllMasterDevices, &num_masters);
|
||||
|
||||
for (int i = 0; i < num_masters; i++)
|
||||
{
|
||||
current_master = &all_masters[i];
|
||||
|
||||
if (current_master->use == XIMasterPointer)
|
||||
{
|
||||
// We need to query the master for the scroll wheel's increment, since the increment used
|
||||
// varies depending on what input driver is being used. For example, xf86-libinput uses 120.0.
|
||||
for (int j = 0; j < current_master->num_classes; j++)
|
||||
{
|
||||
if (current_master->classes[j]->type == XIScrollClass)
|
||||
{
|
||||
XIScrollClassInfo* scroll_event =
|
||||
reinterpret_cast<XIScrollClassInfo*>(current_master->classes[j]);
|
||||
scroll_increment = scroll_event->increment;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Since current_master is a master pointer, its attachment must
|
||||
// be a master keyboard.
|
||||
GetControllerInterface().AddDevice(
|
||||
std::make_shared<KeyboardMouse>((Window)hwnd, xi_opcode, current_master->deviceid,
|
||||
current_master->attachment, scroll_increment));
|
||||
}
|
||||
}
|
||||
|
||||
XCloseDisplay(dpy);
|
||||
|
||||
XIFreeDeviceInfo(all_masters);
|
||||
}
|
||||
|
||||
KeyboardMouse::KeyboardMouse(Window window, int opcode, int pointer, int keyboard,
|
||||
double scroll_increment_)
|
||||
: m_window(window), xi_opcode(opcode), pointer_deviceid(pointer), keyboard_deviceid(keyboard),
|
||||
scroll_increment(scroll_increment_)
|
||||
{
|
||||
// The cool thing about each KeyboardMouse object having its own Display
|
||||
// is that each one gets its own separate copy of the X11 event stream,
|
||||
// which it can individually filter to get just the events it's interested
|
||||
// in. So be aware that each KeyboardMouse object actually has its own X11
|
||||
// "context."
|
||||
m_display = XOpenDisplay(nullptr);
|
||||
|
||||
int major = XINPUT_MAJOR, minor = XINPUT_MINOR;
|
||||
XIQueryVersion(m_display, &major, &minor);
|
||||
|
||||
// should always be 1
|
||||
int unused;
|
||||
XIDeviceInfo* const pointer_device = XIQueryDevice(m_display, pointer_deviceid, &unused);
|
||||
name = std::string(pointer_device->name);
|
||||
XIFreeDeviceInfo(pointer_device);
|
||||
|
||||
// Tell core X functions which keyboard is "the" keyboard for this
|
||||
// X connection.
|
||||
XISetClientPointer(m_display, None, pointer_deviceid);
|
||||
|
||||
{
|
||||
unsigned char mask_buf[(XI_LASTEVENT + 7) / 8] = {};
|
||||
XISetMask(mask_buf, XI_RawButtonPress);
|
||||
XISetMask(mask_buf, XI_RawButtonRelease);
|
||||
XISetMask(mask_buf, XI_RawMotion);
|
||||
|
||||
XIEventMask mask;
|
||||
mask.mask = mask_buf;
|
||||
mask.mask_len = sizeof(mask_buf);
|
||||
|
||||
mask.deviceid = pointer_deviceid;
|
||||
XISelectEvents(m_display, DefaultRootWindow(m_display), &mask, 1);
|
||||
}
|
||||
|
||||
{
|
||||
unsigned char mask_buf[(XI_LASTEVENT + 7) / 8] = {};
|
||||
XISetMask(mask_buf, XI_RawKeyPress);
|
||||
XISetMask(mask_buf, XI_RawKeyRelease);
|
||||
|
||||
XIEventMask mask;
|
||||
mask.mask = mask_buf;
|
||||
mask.mask_len = sizeof(mask_buf);
|
||||
mask.deviceid = keyboard_deviceid;
|
||||
XISelectEvents(m_display, DefaultRootWindow(m_display), &mask, 1);
|
||||
}
|
||||
|
||||
// Temporary map to combine keycodes with the same name.
|
||||
std::map<std::string, Key*> keys;
|
||||
|
||||
// Keyboard Keys
|
||||
int min_keycode = 0;
|
||||
int max_keycode = 0;
|
||||
XDisplayKeycodes(m_display, &min_keycode, &max_keycode);
|
||||
for (int keycode = min_keycode; keycode <= max_keycode; ++keycode)
|
||||
{
|
||||
const auto* const keycode_name = Key::GetNameForKeyCode(m_display, keycode);
|
||||
if (keycode_name == nullptr)
|
||||
continue;
|
||||
|
||||
auto& key_input = keys[keycode_name];
|
||||
if (key_input == nullptr)
|
||||
AddInput(key_input = new Key(keycode_name, keycode, m_state.keyboard.data()));
|
||||
else
|
||||
key_input->AddKeyCode(keycode);
|
||||
}
|
||||
|
||||
// Add combined left/right modifiers with consistent naming across platforms.
|
||||
AddCombinedInput("Alt", {"Alt_L", "Alt_R"});
|
||||
AddCombinedInput("Shift", {"Shift_L", "Shift_R"});
|
||||
AddCombinedInput("Ctrl", {"Control_L", "Control_R"});
|
||||
|
||||
// Mouse Buttons
|
||||
for (int i = 0; i < 32; i++)
|
||||
AddInput(new Button(i, &m_state.buttons));
|
||||
|
||||
// Mouse Cursor, X-/+ and Y-/+
|
||||
for (int i = 0; i != 4; ++i)
|
||||
AddInput(new Cursor(!!(i & 2), !!(i & 1), (i & 2) ? &m_state.cursor.y : &m_state.cursor.x));
|
||||
|
||||
// Mouse Axis, X-/+, Y-/+ and Z-/+
|
||||
AddInput(new Axis(0, false, &m_state.axis.x));
|
||||
AddInput(new Axis(0, true, &m_state.axis.x));
|
||||
AddInput(new Axis(1, false, &m_state.axis.y));
|
||||
AddInput(new Axis(1, true, &m_state.axis.y));
|
||||
AddInput(new Axis(2, false, &m_state.axis.z));
|
||||
AddInput(new Axis(2, true, &m_state.axis.z));
|
||||
|
||||
// Relative Mouse, X-/+, Y-/+ and Z-/+
|
||||
AddInput(new RelativeMouse(0, false, &m_state.relative_mouse.x));
|
||||
AddInput(new RelativeMouse(0, true, &m_state.relative_mouse.x));
|
||||
AddInput(new RelativeMouse(1, false, &m_state.relative_mouse.y));
|
||||
AddInput(new RelativeMouse(1, true, &m_state.relative_mouse.y));
|
||||
AddInput(new RelativeMouse(2, false, &m_state.relative_mouse.z));
|
||||
AddInput(new RelativeMouse(2, true, &m_state.relative_mouse.z));
|
||||
}
|
||||
|
||||
KeyboardMouse::~KeyboardMouse()
|
||||
{
|
||||
XCloseDisplay(m_display);
|
||||
}
|
||||
|
||||
// Update the mouse cursor controls
|
||||
void KeyboardMouse::UpdateCursor(bool should_center_mouse)
|
||||
{
|
||||
double root_x, root_y, win_x, win_y;
|
||||
Window root, child;
|
||||
|
||||
XWindowAttributes win_attribs;
|
||||
XGetWindowAttributes(m_display, m_window, &win_attribs);
|
||||
const auto win_width = std::max(win_attribs.width, 1);
|
||||
const auto win_height = std::max(win_attribs.height, 1);
|
||||
|
||||
{
|
||||
XIButtonState button_state;
|
||||
XIModifierState mods;
|
||||
XIGroupState group;
|
||||
|
||||
// Get the absolute position of the mouse pointer and the button state.
|
||||
XIQueryPointer(m_display, pointer_deviceid, m_window, &root, &child, &root_x, &root_y, &win_x,
|
||||
&win_y, &button_state, &mods, &group);
|
||||
|
||||
// X buttons are 1-indexed, so to get 32 button bits we need a larger type
|
||||
// for the shift.
|
||||
u64 buttons_zero_indexed = 0;
|
||||
std::memcpy(&buttons_zero_indexed, button_state.mask,
|
||||
std::min<size_t>(button_state.mask_len, sizeof(m_state.buttons)));
|
||||
m_state.buttons = buttons_zero_indexed >> 1;
|
||||
|
||||
free(button_state.mask);
|
||||
}
|
||||
|
||||
if (should_center_mouse)
|
||||
{
|
||||
win_x = win_width / 2;
|
||||
win_y = win_height / 2;
|
||||
|
||||
XIWarpPointer(m_display, pointer_deviceid, None, m_window, 0.0, 0.0, 0, 0, win_x, win_y);
|
||||
|
||||
g_controller_interface.SetMouseCenteringRequested(false);
|
||||
}
|
||||
|
||||
const auto window_scale = g_controller_interface.GetWindowInputScale();
|
||||
|
||||
// the mouse position as a range from -1 to 1
|
||||
m_state.cursor.x = (win_x / win_width * 2 - 1) * window_scale.x;
|
||||
m_state.cursor.y = (win_y / win_height * 2 - 1) * window_scale.y;
|
||||
}
|
||||
|
||||
Core::DeviceRemoval KeyboardMouse::UpdateInput()
|
||||
{
|
||||
XFlush(m_display);
|
||||
|
||||
// for the axis controls
|
||||
float delta_x = 0.0f, delta_y = 0.0f, delta_z = 0.0f;
|
||||
double delta_delta;
|
||||
bool update_mouse = false, update_keyboard = false;
|
||||
|
||||
// Iterate through the event queue, processing raw pointer motion events and
|
||||
// noting whether the button or key state has changed.
|
||||
XEvent event;
|
||||
while (XPending(m_display))
|
||||
{
|
||||
XNextEvent(m_display, &event);
|
||||
|
||||
if (event.xcookie.type != GenericEvent)
|
||||
continue;
|
||||
if (event.xcookie.extension != xi_opcode)
|
||||
continue;
|
||||
if (!XGetEventData(m_display, &event.xcookie))
|
||||
continue;
|
||||
|
||||
switch (event.xcookie.evtype)
|
||||
{
|
||||
case XI_RawButtonPress:
|
||||
case XI_RawButtonRelease:
|
||||
update_mouse = true;
|
||||
break;
|
||||
case XI_RawKeyPress:
|
||||
case XI_RawKeyRelease:
|
||||
update_keyboard = true;
|
||||
break;
|
||||
case XI_RawMotion:
|
||||
{
|
||||
update_mouse = true;
|
||||
|
||||
XIRawEvent* raw_event = (XIRawEvent*)event.xcookie.data;
|
||||
float values[4] = {};
|
||||
size_t value_idx = 0;
|
||||
|
||||
// We only care about the first 4 axes, which should always be available at minimum
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
if (XIMaskIsSet(raw_event->valuators.mask, i))
|
||||
{
|
||||
values[i] = raw_event->raw_values[value_idx++];
|
||||
}
|
||||
}
|
||||
|
||||
delta_delta = values[0];
|
||||
// test for inf and nan
|
||||
if (delta_delta == delta_delta && 1 + delta_delta != delta_delta)
|
||||
delta_x += delta_delta;
|
||||
|
||||
delta_delta = values[1];
|
||||
// test for inf and nan
|
||||
if (delta_delta == delta_delta && 1 + delta_delta != delta_delta)
|
||||
delta_y += delta_delta;
|
||||
|
||||
// Scroll wheel input gets scaled to be similar to the mouse axes
|
||||
delta_delta = values[3] * 8.0 / scroll_increment;
|
||||
// test for inf and nan
|
||||
if (delta_delta == delta_delta && 1 + delta_delta != delta_delta)
|
||||
delta_z += delta_delta;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XFreeEventData(m_display, &event.xcookie);
|
||||
}
|
||||
|
||||
m_state.relative_mouse.x = delta_x;
|
||||
m_state.relative_mouse.y = delta_y;
|
||||
m_state.relative_mouse.z = delta_z;
|
||||
|
||||
// apply axis smoothing
|
||||
m_state.axis.x *= MOUSE_AXIS_SMOOTHING;
|
||||
m_state.axis.x += delta_x;
|
||||
m_state.axis.x /= MOUSE_AXIS_SMOOTHING + 1.0f;
|
||||
m_state.axis.y *= MOUSE_AXIS_SMOOTHING;
|
||||
m_state.axis.y += delta_y;
|
||||
m_state.axis.y /= MOUSE_AXIS_SMOOTHING + 1.0f;
|
||||
m_state.axis.z += delta_z;
|
||||
m_state.axis.z /= SCROLL_AXIS_DECAY;
|
||||
|
||||
const bool should_center_mouse = g_controller_interface.IsMouseCenteringRequested() &&
|
||||
(Host_RendererHasFocus() || Host_TASInputHasFocus());
|
||||
|
||||
// When a TAS Input window has focus and "Enable Controller Input" is checked most types of
|
||||
// input should be read normally as if the render window had focus instead. The cursor is an
|
||||
// exception, as otherwise using the mouse to set any control in the TAS Input window will also
|
||||
// update the Wii IR value (or any other input controlled by the cursor).
|
||||
const bool should_update_mouse = update_mouse && !Host_TASInputHasFocus();
|
||||
|
||||
if (should_update_mouse || should_center_mouse)
|
||||
UpdateCursor(should_center_mouse);
|
||||
|
||||
if (update_keyboard)
|
||||
XQueryKeymap(m_display, m_state.keyboard.data());
|
||||
|
||||
return Core::DeviceRemoval::Keep;
|
||||
}
|
||||
|
||||
std::string KeyboardMouse::GetName() const
|
||||
{
|
||||
// This is the name string we got from the X server for this master
|
||||
// pointer/keyboard pair.
|
||||
return name;
|
||||
}
|
||||
|
||||
std::string KeyboardMouse::GetSource() const
|
||||
{
|
||||
return std::string(SOURCE_NAME);
|
||||
}
|
||||
|
||||
int KeyboardMouse::GetSortPriority() const
|
||||
{
|
||||
return DEFAULT_DEVICE_SORT_PRIORITY;
|
||||
}
|
||||
|
||||
KeyboardMouse::Key::Key(std::string key_name, KeyCode keycode, const char* keyboard)
|
||||
: m_keyname{std::move(key_name)}, m_keyboard(keyboard), m_keycodes{keycode}
|
||||
{
|
||||
}
|
||||
|
||||
const char* KeyboardMouse::Key::GetNameForKeyCode(Display* const display, KeyCode keycode)
|
||||
{
|
||||
int i = 0;
|
||||
KeySym keysym = 0;
|
||||
do
|
||||
{
|
||||
keysym = XkbKeycodeToKeysym(display, keycode, i, 0);
|
||||
i++;
|
||||
} while (keysym == NoSymbol && i < 8);
|
||||
|
||||
// Convert to upper case for the keyname
|
||||
if (keysym >= 97 && keysym <= 122)
|
||||
keysym -= 32;
|
||||
|
||||
// 0x0110ffff is the top of the unicode character range according
|
||||
// to keysymdef.h although it is probably more than we need.
|
||||
if (keysym == NoSymbol || keysym > 0x0110ffff || XKeysymToString(keysym) == nullptr)
|
||||
return nullptr;
|
||||
|
||||
return XKeysymToString(keysym);
|
||||
}
|
||||
|
||||
ControlState KeyboardMouse::Key::GetState() const
|
||||
{
|
||||
return ControlState(
|
||||
std::ranges::any_of(m_keycodes, std::bind_front(&Key::IsKeyCodePressed, this)));
|
||||
}
|
||||
|
||||
void KeyboardMouse::Key::AddKeyCode(KeyCode keycode)
|
||||
{
|
||||
m_keycodes.emplace_back(keycode);
|
||||
}
|
||||
|
||||
bool KeyboardMouse::Key::IsKeyCodePressed(KeyCode keycode) const
|
||||
{
|
||||
return (m_keyboard[keycode / 8] & (1 << (keycode % 8))) != 0;
|
||||
}
|
||||
|
||||
KeyboardMouse::Button::Button(unsigned int index, u32* buttons) : m_buttons(buttons), m_index(index)
|
||||
{
|
||||
name = fmt::format("Click {}", m_index + 1);
|
||||
}
|
||||
|
||||
ControlState KeyboardMouse::Button::GetState() const
|
||||
{
|
||||
return ((*m_buttons & (1 << m_index)) != 0);
|
||||
}
|
||||
|
||||
KeyboardMouse::Cursor::Cursor(u8 index, bool positive, const float* cursor)
|
||||
: m_cursor(cursor), m_index(index), m_positive(positive)
|
||||
{
|
||||
name = fmt::format("Cursor {}{}", static_cast<char>('X' + m_index), (m_positive ? '+' : '-'));
|
||||
}
|
||||
|
||||
ControlState KeyboardMouse::Cursor::GetState() const
|
||||
{
|
||||
return std::max(0.0f, *m_cursor / (m_positive ? 1.0f : -1.0f));
|
||||
}
|
||||
|
||||
KeyboardMouse::Axis::Axis(u8 index, bool positive, const float* axis)
|
||||
: m_axis(axis), m_index(index), m_positive(positive)
|
||||
{
|
||||
name = fmt::format("Axis {}{}", static_cast<char>('X' + m_index), (m_positive ? '+' : '-'));
|
||||
}
|
||||
|
||||
KeyboardMouse::RelativeMouse::RelativeMouse(u8 index, bool positive, const float* axis)
|
||||
: m_axis(axis), m_index(index), m_positive(positive)
|
||||
{
|
||||
name =
|
||||
fmt::format("RelativeMouse {}{}", static_cast<char>('X' + m_index), (m_positive ? '+' : '-'));
|
||||
}
|
||||
|
||||
ControlState KeyboardMouse::Axis::GetState() const
|
||||
{
|
||||
return std::max(0.0f, *m_axis / (m_positive ? MOUSE_AXIS_SENSITIVITY : -MOUSE_AXIS_SENSITIVITY));
|
||||
}
|
||||
|
||||
ControlState KeyboardMouse::RelativeMouse::GetState() const
|
||||
{
|
||||
return std::max(0.0f, *m_axis / (m_positive ? MOUSE_AXIS_SENSITIVITY : -MOUSE_AXIS_SENSITIVITY));
|
||||
}
|
||||
} // namespace ciface::XInput2
|
||||
@@ -1,142 +0,0 @@
|
||||
// Copyright 2013 Max Eliaser
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// See XInput2.cpp for extensive documentation.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
#include <X11/keysym.h>
|
||||
}
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Matrix.h"
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
#include "InputCommon/ControllerInterface/InputBackend.h"
|
||||
|
||||
namespace ciface::XInput2
|
||||
{
|
||||
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface);
|
||||
|
||||
class KeyboardMouse : public Core::Device
|
||||
{
|
||||
private:
|
||||
struct State
|
||||
{
|
||||
std::array<char, 32> keyboard;
|
||||
u32 buttons;
|
||||
Common::Vec2 cursor;
|
||||
Common::Vec3 axis;
|
||||
Common::Vec3 relative_mouse;
|
||||
};
|
||||
|
||||
class Key : public Input
|
||||
{
|
||||
friend class KeyboardMouse;
|
||||
|
||||
public:
|
||||
Key(std::string name, KeyCode keycode, const char* keyboard);
|
||||
|
||||
std::string GetName() const override { return m_keyname; }
|
||||
ControlState GetState() const override;
|
||||
|
||||
static const char* GetNameForKeyCode(Display* display, KeyCode keycode);
|
||||
|
||||
private:
|
||||
void AddKeyCode(KeyCode);
|
||||
bool IsKeyCodePressed(KeyCode) const;
|
||||
|
||||
const std::string m_keyname;
|
||||
const char* const m_keyboard;
|
||||
std::vector<KeyCode> m_keycodes;
|
||||
};
|
||||
|
||||
class Button : public Input
|
||||
{
|
||||
public:
|
||||
std::string GetName() const override { return name; }
|
||||
Button(unsigned int index, u32* buttons);
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const u32* m_buttons;
|
||||
const unsigned int m_index;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
class Cursor : public Input
|
||||
{
|
||||
public:
|
||||
std::string GetName() const override { return name; }
|
||||
bool IsDetectable() const override { return false; }
|
||||
Cursor(u8 index, bool positive, const float* cursor);
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const float* m_cursor;
|
||||
const u8 m_index;
|
||||
const bool m_positive;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
class Axis : public Input
|
||||
{
|
||||
public:
|
||||
std::string GetName() const override { return name; }
|
||||
bool IsDetectable() const override { return false; }
|
||||
Axis(u8 index, bool positive, const float* axis);
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const float* m_axis;
|
||||
const u8 m_index;
|
||||
const bool m_positive;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
class RelativeMouse : public Input
|
||||
{
|
||||
public:
|
||||
std::string GetName() const override { return name; }
|
||||
bool IsDetectable() const override { return false; }
|
||||
RelativeMouse(u8 index, bool positive, const float* axis);
|
||||
ControlState GetState() const override;
|
||||
|
||||
private:
|
||||
const float* m_axis;
|
||||
const u8 m_index;
|
||||
const bool m_positive;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
private:
|
||||
void UpdateCursor(bool should_center_mouse);
|
||||
|
||||
public:
|
||||
Core::DeviceRemoval UpdateInput() override;
|
||||
|
||||
KeyboardMouse(Window window, int opcode, int pointer_deviceid, int keyboard_deviceid,
|
||||
double scroll_increment);
|
||||
~KeyboardMouse();
|
||||
|
||||
std::string GetName() const override;
|
||||
std::string GetSource() const override;
|
||||
int GetSortPriority() const override;
|
||||
|
||||
private:
|
||||
Window m_window;
|
||||
Display* m_display;
|
||||
State m_state{};
|
||||
const int xi_opcode;
|
||||
const int pointer_deviceid;
|
||||
const int keyboard_deviceid;
|
||||
const double scroll_increment;
|
||||
std::string name;
|
||||
};
|
||||
} // namespace ciface::XInput2
|
||||
@@ -1,878 +0,0 @@
|
||||
// Copyright 2015 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "InputCommon/ControllerInterface/evdev/evdev.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <libudev.h>
|
||||
#include <sys/eventfd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/Flag.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/MathUtil.h"
|
||||
#include "Common/ScopeGuard.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Common/Thread.h"
|
||||
#include "Common/WorkQueueThread.h"
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
|
||||
namespace ciface::evdev
|
||||
{
|
||||
class InputBackend final : public ciface::InputBackend
|
||||
{
|
||||
public:
|
||||
InputBackend(ControllerInterface* controller_interface);
|
||||
~InputBackend();
|
||||
void PopulateDevices() override;
|
||||
|
||||
void RemoveDevnodeObject(const std::string&);
|
||||
|
||||
// Linux has the strange behavior that closing file descriptors of event devices can be
|
||||
// surprisingly slow, in the range of 20-70 milliseconds. For modern systems that have maybe 30
|
||||
// event devices this can quickly add up, leading to visibly slow startup. So we close FDs on a
|
||||
// separate thread *shrug*
|
||||
void CloseDescriptor(int fd) { m_cleanup_thread.Push(fd); }
|
||||
|
||||
private:
|
||||
std::shared_ptr<evdevDevice>
|
||||
FindDeviceWithUniqueIDAndPhysicalLocation(const char* unique_id, const char* physical_location);
|
||||
|
||||
void AddDeviceNode(const char* devnode);
|
||||
|
||||
void StartHotplugThread();
|
||||
void StopHotplugThread();
|
||||
void HotplugThreadFunc();
|
||||
|
||||
std::thread m_hotplug_thread;
|
||||
Common::Flag m_hotplug_thread_running;
|
||||
int m_wakeup_eventfd;
|
||||
|
||||
// There is no easy way to get the device name from only a dev node
|
||||
// during a device removed event, since libevdev can't work on removed devices;
|
||||
// sysfs is not stable, so this is probably the easiest way to get a name for a node.
|
||||
// This can, and will be modified by different thread, possibly concurrently,
|
||||
// as devices can be destroyed by any thread at any time. As of now it's protected
|
||||
// by ControllerInterface::m_devices_population_mutex.
|
||||
std::map<std::string, std::weak_ptr<evdevDevice>> m_devnode_objects;
|
||||
|
||||
Common::WorkQueueThread<int> m_cleanup_thread;
|
||||
};
|
||||
|
||||
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface)
|
||||
{
|
||||
return std::make_unique<InputBackend>(controller_interface);
|
||||
}
|
||||
|
||||
class Input : public Core::Device::Input
|
||||
{
|
||||
public:
|
||||
Input(u16 code, libevdev* dev) : m_code(code), m_dev(dev) {}
|
||||
|
||||
protected:
|
||||
const u16 m_code;
|
||||
libevdev* const m_dev;
|
||||
};
|
||||
|
||||
class Button : public Input
|
||||
{
|
||||
public:
|
||||
Button(u8 index, u16 code, libevdev* dev) : Input(code, dev), m_index(index) {}
|
||||
|
||||
ControlState GetState() const final override
|
||||
{
|
||||
int value = 0;
|
||||
libevdev_fetch_event_value(m_dev, EV_KEY, m_code, &value);
|
||||
return value;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::optional<std::string> GetEventCodeName() const
|
||||
{
|
||||
if (const char* code_name = libevdev_event_code_get_name(EV_KEY, m_code))
|
||||
{
|
||||
const auto name = StripWhitespace(code_name);
|
||||
|
||||
for (auto remove_prefix : {"BTN_", "KEY_"})
|
||||
{
|
||||
if (name.starts_with(remove_prefix))
|
||||
return std::string(name.substr(std::strlen(remove_prefix)));
|
||||
}
|
||||
|
||||
return std::string(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetIndexedName() const { return fmt::format("Button {}", m_index); }
|
||||
|
||||
const u8 m_index;
|
||||
};
|
||||
|
||||
class NumberedButton final : public Button
|
||||
{
|
||||
public:
|
||||
using Button::Button;
|
||||
|
||||
std::string GetName() const override { return GetIndexedName(); }
|
||||
};
|
||||
|
||||
class NamedButton final : public Button
|
||||
{
|
||||
public:
|
||||
using Button::Button;
|
||||
|
||||
bool IsMatchingName(std::string_view name) const final override
|
||||
{
|
||||
// Match either the "START" naming provided by evdev or the "Button 0"-like naming.
|
||||
return name == GetEventCodeName() || name == GetIndexedName();
|
||||
}
|
||||
|
||||
std::string GetName() const override { return GetEventCodeName().value_or(GetIndexedName()); }
|
||||
};
|
||||
|
||||
class NamedButtonWithNoBackwardsCompat final : public Button
|
||||
{
|
||||
public:
|
||||
using Button::Button;
|
||||
|
||||
std::string GetName() const override { return GetEventCodeName().value_or(GetIndexedName()); }
|
||||
};
|
||||
|
||||
class AnalogInput : public Input
|
||||
{
|
||||
public:
|
||||
using Input::Input;
|
||||
|
||||
ControlState GetState() const final override
|
||||
{
|
||||
int value = 0;
|
||||
libevdev_fetch_event_value(m_dev, EV_ABS, m_code, &value);
|
||||
|
||||
return (value - m_base) / m_range;
|
||||
}
|
||||
|
||||
protected:
|
||||
ControlState m_range;
|
||||
int m_base;
|
||||
};
|
||||
|
||||
class Axis : public AnalogInput
|
||||
{
|
||||
public:
|
||||
Axis(u8 index, u16 code, bool upper, libevdev* dev) : AnalogInput(code, dev), m_index(index)
|
||||
{
|
||||
const int min = libevdev_get_abs_minimum(m_dev, m_code);
|
||||
const int max = libevdev_get_abs_maximum(m_dev, m_code);
|
||||
|
||||
m_base = (max + min) / 2;
|
||||
m_range = (upper ? max : min) - m_base;
|
||||
}
|
||||
|
||||
std::string GetName() const override { return GetIndexedName(); }
|
||||
|
||||
protected:
|
||||
std::string GetIndexedName() const
|
||||
{
|
||||
return fmt::format("Axis {}{}", m_index, m_range < 0 ? '-' : '+');
|
||||
}
|
||||
|
||||
private:
|
||||
const u8 m_index;
|
||||
};
|
||||
|
||||
class MotionDataInput final : public AnalogInput
|
||||
{
|
||||
public:
|
||||
MotionDataInput(u16 code, ControlState resolution_scale, libevdev* dev) : AnalogInput(code, dev)
|
||||
{
|
||||
auto* const info = libevdev_get_abs_info(m_dev, m_code);
|
||||
|
||||
// The average of the minimum and maximum value. (neutral value)
|
||||
m_base = (info->maximum + info->minimum) / 2;
|
||||
|
||||
m_range = info->resolution / resolution_scale;
|
||||
}
|
||||
|
||||
std::string GetName() const override
|
||||
{
|
||||
// Unfortunately there doesn't seem to be a "standard" orientation
|
||||
// so we can't use "Accel Up"-like names.
|
||||
constexpr std::array<const char*, 6> motion_data_names = {{
|
||||
"Accel X",
|
||||
"Accel Y",
|
||||
"Accel Z",
|
||||
"Gyro X",
|
||||
"Gyro Y",
|
||||
"Gyro Z",
|
||||
}};
|
||||
|
||||
// Our name array relies on sane axis codes from 0 to 5.
|
||||
static_assert(ABS_X == 0, "evdev axis value sanity check");
|
||||
static_assert(ABS_RX == 3, "evdev axis value sanity check");
|
||||
|
||||
return std::string(motion_data_names[m_code]) + (m_range < 0 ? '-' : '+');
|
||||
}
|
||||
|
||||
bool IsDetectable() const override { return false; }
|
||||
};
|
||||
|
||||
class CursorInput final : public Axis
|
||||
{
|
||||
public:
|
||||
using Axis::Axis;
|
||||
|
||||
std::string GetName() const final override
|
||||
{
|
||||
// "Cursor X-" naming.
|
||||
return std::string("Cursor ") + char('X' + m_code) + (m_range < 0 ? '-' : '+');
|
||||
}
|
||||
|
||||
bool IsDetectable() const override { return false; }
|
||||
};
|
||||
|
||||
std::shared_ptr<evdevDevice>
|
||||
InputBackend::FindDeviceWithUniqueIDAndPhysicalLocation(const char* unique_id,
|
||||
const char* physical_location)
|
||||
{
|
||||
if (!unique_id || !physical_location)
|
||||
return nullptr;
|
||||
|
||||
for (auto& [node, dev] : m_devnode_objects)
|
||||
{
|
||||
if (const auto device = dev.lock())
|
||||
{
|
||||
const auto* dev_uniq = device->GetUniqueID();
|
||||
const auto* dev_phys = device->GetPhysicalLocation();
|
||||
|
||||
if (dev_uniq && dev_phys && std::strcmp(dev_uniq, unique_id) == 0 &&
|
||||
std::strcmp(dev_phys, physical_location) == 0)
|
||||
{
|
||||
return device;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void InputBackend::AddDeviceNode(const char* devnode)
|
||||
{
|
||||
// Unfortunately udev gives us no way to filter out the non event device interfaces.
|
||||
// So we open it and see if it works with evdev ioctls or not.
|
||||
|
||||
// The device file will be read on one of the main threads, so we open in non-blocking mode.
|
||||
const int fd = open(devnode, O_RDWR | O_NONBLOCK);
|
||||
if (fd == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
libevdev* dev = nullptr;
|
||||
if (libevdev_new_from_fd(fd, &dev) != 0)
|
||||
{
|
||||
// This usually fails because the device node isn't an evdev device, such as /dev/input/js0
|
||||
CloseDescriptor(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto uniq = libevdev_get_uniq(dev);
|
||||
const auto phys = libevdev_get_phys(dev);
|
||||
auto evdev_device = FindDeviceWithUniqueIDAndPhysicalLocation(uniq, phys);
|
||||
if (evdev_device)
|
||||
{
|
||||
NOTICE_LOG_FMT(CONTROLLERINTERFACE,
|
||||
"evdev combining devices with unique id: {}, physical location: {}", uniq, phys);
|
||||
|
||||
evdev_device->AddNode(devnode, fd, dev);
|
||||
|
||||
// Remove and re-add device as naming and inputs may have changed.
|
||||
// This will also give it the correct index and invoke device change callbacks.
|
||||
// Make sure to force the device removal immediately (as they are shared ptrs and
|
||||
// they could be kept alive, preventing us from re-creating the device)
|
||||
GetControllerInterface().RemoveDevice(
|
||||
[&evdev_device](const auto* device) {
|
||||
return static_cast<const evdevDevice*>(device) == evdev_device.get();
|
||||
},
|
||||
true);
|
||||
|
||||
GetControllerInterface().AddDevice(evdev_device);
|
||||
}
|
||||
else
|
||||
{
|
||||
evdev_device = std::make_shared<evdevDevice>(this);
|
||||
|
||||
const bool was_interesting = evdev_device->AddNode(devnode, fd, dev);
|
||||
|
||||
if (was_interesting)
|
||||
GetControllerInterface().AddDevice(evdev_device);
|
||||
}
|
||||
|
||||
// If the devices failed to be added to ControllerInterface, it will be added here but then
|
||||
// immediately removed in its destructor due to the shared ptr not having any references left
|
||||
m_devnode_objects.emplace(devnode, std::move(evdev_device));
|
||||
}
|
||||
|
||||
void InputBackend::HotplugThreadFunc()
|
||||
{
|
||||
Common::SetCurrentThreadName("evdev Hotplug Thread");
|
||||
NOTICE_LOG_FMT(CONTROLLERINTERFACE, "evdev hotplug thread started");
|
||||
|
||||
udev* const udev = udev_new();
|
||||
Common::ScopeGuard udev_guard([udev] { udev_unref(udev); });
|
||||
|
||||
ASSERT_MSG(CONTROLLERINTERFACE, udev != nullptr, "Couldn't initialize libudev.");
|
||||
|
||||
// Set up monitoring
|
||||
udev_monitor* const monitor = udev_monitor_new_from_netlink(udev, "udev");
|
||||
Common::ScopeGuard monitor_guard([monitor] { udev_monitor_unref(monitor); });
|
||||
|
||||
udev_monitor_filter_add_match_subsystem_devtype(monitor, "input", nullptr);
|
||||
udev_monitor_enable_receiving(monitor);
|
||||
const int monitor_fd = udev_monitor_get_fd(monitor);
|
||||
|
||||
while (m_hotplug_thread_running.IsSet())
|
||||
{
|
||||
fd_set fds;
|
||||
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(monitor_fd, &fds);
|
||||
FD_SET(m_wakeup_eventfd, &fds);
|
||||
|
||||
const int ret =
|
||||
select(std::max(monitor_fd, m_wakeup_eventfd) + 1, &fds, nullptr, nullptr, nullptr);
|
||||
if (ret < 1 || !FD_ISSET(monitor_fd, &fds))
|
||||
continue;
|
||||
|
||||
udev_device* const dev = udev_monitor_receive_device(monitor);
|
||||
Common::ScopeGuard dev_guard([dev] { udev_device_unref(dev); });
|
||||
|
||||
const char* const action = udev_device_get_action(dev);
|
||||
const char* const devnode = udev_device_get_devnode(dev);
|
||||
if (!devnode)
|
||||
continue;
|
||||
|
||||
// Use GetControllerInterface().PlatformPopulateDevices() to protect access around
|
||||
// m_devnode_objects. Note that even if we get these events at the same time as a
|
||||
// a PopulateDevices() request (e.g. on start up, we might get all the add events
|
||||
// for connected devices), this won't ever cause duplicate devices as AddDeviceNode()
|
||||
// automatically removes the old one if it already existed
|
||||
if (strcmp(action, "remove") == 0)
|
||||
{
|
||||
GetControllerInterface().PlatformPopulateDevices([&devnode, this] {
|
||||
std::shared_ptr<evdevDevice> ptr;
|
||||
|
||||
const auto it = m_devnode_objects.find(devnode);
|
||||
if (it != m_devnode_objects.end())
|
||||
ptr = it->second.lock();
|
||||
|
||||
// If we don't recognize this device, ptr will be null and no device will be removed.
|
||||
|
||||
GetControllerInterface().RemoveDevice([&ptr](const auto* device) {
|
||||
return static_cast<const evdevDevice*>(device) == ptr.get();
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (strcmp(action, "add") == 0)
|
||||
{
|
||||
GetControllerInterface().PlatformPopulateDevices(
|
||||
[&devnode, this] { AddDeviceNode(devnode); });
|
||||
}
|
||||
}
|
||||
NOTICE_LOG_FMT(CONTROLLERINTERFACE, "evdev hotplug thread stopped");
|
||||
}
|
||||
|
||||
void InputBackend::StartHotplugThread()
|
||||
{
|
||||
// Mark the thread as running.
|
||||
if (!m_hotplug_thread_running.TestAndSet())
|
||||
{
|
||||
// It was already running.
|
||||
return;
|
||||
}
|
||||
|
||||
m_wakeup_eventfd = eventfd(0, 0);
|
||||
ASSERT_MSG(CONTROLLERINTERFACE, m_wakeup_eventfd != -1, "Couldn't create eventfd.");
|
||||
m_hotplug_thread = std::thread(&InputBackend::HotplugThreadFunc, this);
|
||||
}
|
||||
|
||||
void InputBackend::StopHotplugThread()
|
||||
{
|
||||
// Tell the hotplug thread to stop.
|
||||
if (!m_hotplug_thread_running.TestAndClear())
|
||||
{
|
||||
// It wasn't running, we're done.
|
||||
return;
|
||||
}
|
||||
|
||||
// Write something to efd so that select() stops blocking.
|
||||
const uint64_t value = 1;
|
||||
static_cast<void>(!write(m_wakeup_eventfd, &value, sizeof(uint64_t)));
|
||||
|
||||
m_hotplug_thread.join();
|
||||
close(m_wakeup_eventfd);
|
||||
}
|
||||
|
||||
InputBackend::InputBackend(ControllerInterface* controller_interface)
|
||||
: ciface::InputBackend(controller_interface), m_cleanup_thread("evdev cleanup", close)
|
||||
{
|
||||
StartHotplugThread();
|
||||
}
|
||||
|
||||
// Only call this when ControllerInterface::m_devices_population_mutex is locked
|
||||
void InputBackend::PopulateDevices()
|
||||
{
|
||||
// Don't run if we are not initialized
|
||||
if (!m_hotplug_thread_running.IsSet())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We use udev to iterate over all /dev/input/event* devices.
|
||||
// Note: the Linux kernel is currently limited to just 32 event devices. If
|
||||
// this ever changes, hopefully udev will take care of this.
|
||||
|
||||
udev* const udev = udev_new();
|
||||
ASSERT_MSG(CONTROLLERINTERFACE, udev != nullptr, "Couldn't initialize libudev.");
|
||||
|
||||
// List all input devices
|
||||
udev_enumerate* const enumerate = udev_enumerate_new(udev);
|
||||
udev_enumerate_add_match_subsystem(enumerate, "input");
|
||||
udev_enumerate_scan_devices(enumerate);
|
||||
udev_list_entry* const devices = udev_enumerate_get_list_entry(enumerate);
|
||||
|
||||
// Iterate over all input devices
|
||||
udev_list_entry* dev_list_entry;
|
||||
udev_list_entry_foreach(dev_list_entry, devices)
|
||||
{
|
||||
const char* path = udev_list_entry_get_name(dev_list_entry);
|
||||
|
||||
udev_device* dev = udev_device_new_from_syspath(udev, path);
|
||||
|
||||
if (const char* devnode = udev_device_get_devnode(dev))
|
||||
AddDeviceNode(devnode);
|
||||
|
||||
udev_device_unref(dev);
|
||||
}
|
||||
udev_enumerate_unref(enumerate);
|
||||
udev_unref(udev);
|
||||
}
|
||||
|
||||
InputBackend::~InputBackend()
|
||||
{
|
||||
StopHotplugThread();
|
||||
}
|
||||
|
||||
bool evdevDevice::AddNode(std::string devnode, int fd, libevdev* dev)
|
||||
{
|
||||
m_nodes.emplace_back(Node{std::move(devnode), fd, dev});
|
||||
|
||||
// Take on the alphabetically first name.
|
||||
const auto potential_new_name = StripWhitespace(libevdev_get_name(dev));
|
||||
if (m_name.empty() || potential_new_name < m_name)
|
||||
m_name = potential_new_name;
|
||||
|
||||
const bool is_motion_device = libevdev_has_property(dev, INPUT_PROP_ACCELEROMETER);
|
||||
const bool is_pointing_device = libevdev_has_property(dev, INPUT_PROP_BUTTONPAD);
|
||||
|
||||
// If a device has BTN_JOYSTICK it probably uses event codes counting up from 0x120
|
||||
// which have very useless and wrong names.
|
||||
const bool has_btn_joystick = libevdev_has_event_code(dev, EV_KEY, BTN_JOYSTICK);
|
||||
const bool has_sensible_button_names = !has_btn_joystick;
|
||||
|
||||
// Buttons (and keyboard keys)
|
||||
int num_buttons = 0;
|
||||
for (int key = 0; key != KEY_CNT; ++key)
|
||||
{
|
||||
if (libevdev_has_event_code(dev, EV_KEY, key))
|
||||
{
|
||||
if (is_pointing_device || is_motion_device)
|
||||
{
|
||||
// This node will probably be combined with another with regular buttons.
|
||||
// We don't want to match "Button 0" names here as it will name clash.
|
||||
AddInput(new NamedButtonWithNoBackwardsCompat(num_buttons, key, dev));
|
||||
}
|
||||
else if (has_sensible_button_names)
|
||||
{
|
||||
AddInput(new NamedButton(num_buttons, key, dev));
|
||||
}
|
||||
else
|
||||
{
|
||||
AddInput(new NumberedButton(num_buttons, key, dev));
|
||||
}
|
||||
|
||||
++num_buttons;
|
||||
}
|
||||
}
|
||||
|
||||
int num_axis = 0;
|
||||
|
||||
if (is_motion_device)
|
||||
{
|
||||
// If INPUT_PROP_ACCELEROMETER is set then X,Y,Z,RX,RY,RZ contain motion data.
|
||||
|
||||
auto add_motion_inputs = [&num_axis, dev, this](int first_code, double scale) {
|
||||
for (int i = 0; i != 3; ++i)
|
||||
{
|
||||
const int code = first_code + i;
|
||||
if (libevdev_has_event_code(dev, EV_ABS, code))
|
||||
{
|
||||
AddInput(new MotionDataInput(code, scale * -1, dev));
|
||||
AddInput(new MotionDataInput(code, scale, dev));
|
||||
|
||||
++num_axis;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// evdev resolution is specified in "g"s and deg/s.
|
||||
// Convert these to m/s/s and rad/s.
|
||||
constexpr ControlState accel_scale = MathUtil::GRAVITY_ACCELERATION;
|
||||
constexpr ControlState gyro_scale = MathUtil::TAU / 360;
|
||||
|
||||
add_motion_inputs(ABS_X, accel_scale);
|
||||
add_motion_inputs(ABS_RX, gyro_scale);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_pointing_device)
|
||||
{
|
||||
auto add_cursor_input = [&num_axis, dev, this](int code) {
|
||||
if (libevdev_has_event_code(dev, EV_ABS, code))
|
||||
{
|
||||
AddInput(new CursorInput(num_axis, code, false, dev));
|
||||
AddInput(new CursorInput(num_axis, code, true, dev));
|
||||
|
||||
++num_axis;
|
||||
}
|
||||
};
|
||||
|
||||
add_cursor_input(ABS_X);
|
||||
add_cursor_input(ABS_Y);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Axes beyond ABS_MISC have strange behavior (for multi-touch) which we do not handle.
|
||||
const int abs_axis_code_count = ABS_MISC;
|
||||
|
||||
// Absolute axis (thumbsticks)
|
||||
for (int axis = 0; axis != abs_axis_code_count; ++axis)
|
||||
{
|
||||
if (libevdev_has_event_code(dev, EV_ABS, axis))
|
||||
{
|
||||
AddFullAnalogSurfaceInputs(new Axis(num_axis, axis, false, dev),
|
||||
new Axis(num_axis, axis, true, dev));
|
||||
++num_axis;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable autocenter
|
||||
if (libevdev_has_event_code(dev, EV_FF, FF_AUTOCENTER))
|
||||
{
|
||||
input_event ie = {};
|
||||
ie.type = EV_FF;
|
||||
ie.code = FF_AUTOCENTER;
|
||||
ie.value = 0;
|
||||
|
||||
static_cast<void>(!write(fd, &ie, sizeof(ie)));
|
||||
}
|
||||
|
||||
// Constant FF effect
|
||||
if (libevdev_has_event_code(dev, EV_FF, FF_CONSTANT))
|
||||
{
|
||||
AddOutput(new ConstantEffect(fd));
|
||||
}
|
||||
|
||||
// Periodic FF effects
|
||||
if (libevdev_has_event_code(dev, EV_FF, FF_PERIODIC))
|
||||
{
|
||||
for (auto wave : {FF_SINE, FF_SQUARE, FF_TRIANGLE, FF_SAW_UP, FF_SAW_DOWN})
|
||||
{
|
||||
if (libevdev_has_event_code(dev, EV_FF, wave))
|
||||
AddOutput(new PeriodicEffect(fd, wave));
|
||||
}
|
||||
}
|
||||
|
||||
// Rumble (i.e. Left/Right) (i.e. Strong/Weak) effect
|
||||
if (libevdev_has_event_code(dev, EV_FF, FF_RUMBLE))
|
||||
{
|
||||
AddOutput(new RumbleEffect(fd, RumbleEffect::Motor::Strong));
|
||||
AddOutput(new RumbleEffect(fd, RumbleEffect::Motor::Weak));
|
||||
}
|
||||
|
||||
// TODO: Add leds as output devices
|
||||
|
||||
// Filter out interesting devices (see description below)
|
||||
return num_axis >= 2 || num_buttons >= 8;
|
||||
|
||||
// On modern linux systems, there are a lot of event devices that aren't controllers.
|
||||
// For example, the PC Speaker is an event device. Webcams sometimes show up as
|
||||
// event devices. The power button is an event device.
|
||||
//
|
||||
// We don't want these showing up in the list of controllers, so we use this
|
||||
// heuristic to filter out anything that doesn't smell like a controller:
|
||||
//
|
||||
// More than two analog axis:
|
||||
// Most controllers have at least one stick. This rule will catch all such
|
||||
// controllers, while ignoring anything with a single axis (like the mouse
|
||||
// scroll-wheel)
|
||||
//
|
||||
// --- OR ---
|
||||
//
|
||||
// More than 8 buttons:
|
||||
// The user might be using a digital only pad such as a NES controller.
|
||||
// This rule caches such controllers, while eliminating any device with
|
||||
// only a few buttons, like the power button. Sometimes laptops have devices
|
||||
// with 5 or 6 special buttons, which is why the threshold is set to 8 to
|
||||
// match a NES controller.
|
||||
//
|
||||
// This heuristic is quite loose. The user may still see weird devices showing up
|
||||
// as controllers, but it hopefully shouldn't filter out anything they actually
|
||||
// want to use.
|
||||
}
|
||||
|
||||
const char* evdevDevice::GetUniqueID() const
|
||||
{
|
||||
if (m_nodes.empty())
|
||||
return nullptr;
|
||||
|
||||
const auto uniq = libevdev_get_uniq(m_nodes.front().device);
|
||||
|
||||
// Some devices (e.g. Mayflash adapter) return an empty string which is not very unique.
|
||||
if (uniq && std::strlen(uniq) == 0)
|
||||
return nullptr;
|
||||
|
||||
return uniq;
|
||||
}
|
||||
|
||||
const char* evdevDevice::GetPhysicalLocation() const
|
||||
{
|
||||
if (m_nodes.empty())
|
||||
return nullptr;
|
||||
|
||||
return libevdev_get_phys(m_nodes.front().device);
|
||||
}
|
||||
|
||||
evdevDevice::evdevDevice(InputBackend* input_backend) : m_input_backend(*input_backend)
|
||||
{
|
||||
}
|
||||
|
||||
evdevDevice::~evdevDevice()
|
||||
{
|
||||
for (auto& node : m_nodes)
|
||||
{
|
||||
m_input_backend.RemoveDevnodeObject(node.devnode);
|
||||
libevdev_free(node.device);
|
||||
m_input_backend.CloseDescriptor(node.fd);
|
||||
}
|
||||
}
|
||||
|
||||
void InputBackend::RemoveDevnodeObject(const std::string& node)
|
||||
{
|
||||
m_devnode_objects.erase(node);
|
||||
}
|
||||
|
||||
Core::DeviceRemoval evdevDevice::UpdateInput()
|
||||
{
|
||||
// Run through all evdev events
|
||||
// libevdev will keep track of the actual controller state internally which can be queried
|
||||
// later with libevdev_fetch_event_value()
|
||||
for (auto& node : m_nodes)
|
||||
{
|
||||
int rc = LIBEVDEV_READ_STATUS_SUCCESS;
|
||||
while (rc >= 0)
|
||||
{
|
||||
input_event ev;
|
||||
if (LIBEVDEV_READ_STATUS_SYNC == rc)
|
||||
rc = libevdev_next_event(node.device, LIBEVDEV_READ_FLAG_SYNC, &ev);
|
||||
else
|
||||
rc = libevdev_next_event(node.device, LIBEVDEV_READ_FLAG_NORMAL, &ev);
|
||||
}
|
||||
}
|
||||
return Core::DeviceRemoval::Keep;
|
||||
}
|
||||
|
||||
bool evdevDevice::IsValid() const
|
||||
{
|
||||
for (auto& node : m_nodes)
|
||||
{
|
||||
const int current_fd = libevdev_get_fd(node.device);
|
||||
|
||||
if (current_fd == -1)
|
||||
return false;
|
||||
|
||||
libevdev* device = nullptr;
|
||||
if (libevdev_new_from_fd(current_fd, &device) != 0)
|
||||
{
|
||||
close(current_fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
libevdev_free(device);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
evdevDevice::Effect::Effect(int fd) : m_fd(fd)
|
||||
{
|
||||
m_effect.id = -1;
|
||||
// Left (for wheels):
|
||||
m_effect.direction = 0x4000;
|
||||
m_effect.replay.length = RUMBLE_LENGTH_MS;
|
||||
|
||||
// FYI: type is set within UpdateParameters.
|
||||
m_effect.type = DISABLED_EFFECT_TYPE;
|
||||
}
|
||||
|
||||
std::string evdevDevice::ConstantEffect::GetName() const
|
||||
{
|
||||
return "Constant";
|
||||
}
|
||||
|
||||
std::string evdevDevice::PeriodicEffect::GetName() const
|
||||
{
|
||||
switch (m_effect.u.periodic.waveform)
|
||||
{
|
||||
case FF_SQUARE:
|
||||
return "Square";
|
||||
case FF_TRIANGLE:
|
||||
return "Triangle";
|
||||
case FF_SINE:
|
||||
return "Sine";
|
||||
case FF_SAW_UP:
|
||||
return "Sawtooth Up";
|
||||
case FF_SAW_DOWN:
|
||||
return "Sawtooth Down";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string evdevDevice::RumbleEffect::GetName() const
|
||||
{
|
||||
return (Motor::Strong == m_motor) ? "Strong" : "Weak";
|
||||
}
|
||||
|
||||
void evdevDevice::Effect::SetState(ControlState state)
|
||||
{
|
||||
if (UpdateParameters(state))
|
||||
{
|
||||
// Update effect if parameters changed.
|
||||
UpdateEffect();
|
||||
}
|
||||
}
|
||||
|
||||
void evdevDevice::Effect::UpdateEffect()
|
||||
{
|
||||
// libevdev doesn't have nice helpers for forcefeedback
|
||||
// we will use the file descriptors directly.
|
||||
|
||||
// Note: m_effect.type is set within UpdateParameters
|
||||
// to determine if effect should be playing or not.
|
||||
if (m_effect.type != DISABLED_EFFECT_TYPE)
|
||||
{
|
||||
if (-1 == m_effect.id)
|
||||
{
|
||||
// If effect was not uploaded (previously stopped)
|
||||
// we upload it and start playback
|
||||
|
||||
ioctl(m_fd, EVIOCSFF, &m_effect);
|
||||
|
||||
input_event play = {};
|
||||
play.type = EV_FF;
|
||||
play.code = m_effect.id;
|
||||
play.value = 1;
|
||||
|
||||
static_cast<void>(!write(m_fd, &play, sizeof(play)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Effect is already playing. Just update parameters.
|
||||
ioctl(m_fd, EVIOCSFF, &m_effect);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop and remove effect.
|
||||
ioctl(m_fd, EVIOCRMFF, m_effect.id);
|
||||
m_effect.id = -1;
|
||||
}
|
||||
}
|
||||
|
||||
evdevDevice::ConstantEffect::ConstantEffect(int fd) : Effect(fd)
|
||||
{
|
||||
m_effect.u.constant = {};
|
||||
}
|
||||
|
||||
evdevDevice::PeriodicEffect::PeriodicEffect(int fd, u16 waveform) : Effect(fd)
|
||||
{
|
||||
m_effect.u.periodic = {};
|
||||
m_effect.u.periodic.waveform = waveform;
|
||||
m_effect.u.periodic.period = RUMBLE_PERIOD_MS;
|
||||
m_effect.u.periodic.offset = 0;
|
||||
m_effect.u.periodic.phase = 0;
|
||||
}
|
||||
|
||||
evdevDevice::RumbleEffect::RumbleEffect(int fd, Motor motor) : Effect(fd), m_motor(motor)
|
||||
{
|
||||
m_effect.u.rumble = {};
|
||||
}
|
||||
|
||||
bool evdevDevice::ConstantEffect::UpdateParameters(ControlState state)
|
||||
{
|
||||
s16& value = m_effect.u.constant.level;
|
||||
const s16 old_value = value;
|
||||
|
||||
constexpr s16 MAX_VALUE = 0x7fff;
|
||||
value = s16(state * MAX_VALUE);
|
||||
|
||||
m_effect.type = value ? FF_CONSTANT : DISABLED_EFFECT_TYPE;
|
||||
return value != old_value;
|
||||
}
|
||||
|
||||
bool evdevDevice::PeriodicEffect::UpdateParameters(ControlState state)
|
||||
{
|
||||
s16& value = m_effect.u.periodic.magnitude;
|
||||
const s16 old_value = value;
|
||||
|
||||
constexpr s16 MAX_VALUE = 0x7fff;
|
||||
value = s16(state * MAX_VALUE);
|
||||
|
||||
m_effect.type = value ? FF_PERIODIC : DISABLED_EFFECT_TYPE;
|
||||
return value != old_value;
|
||||
}
|
||||
|
||||
bool evdevDevice::RumbleEffect::UpdateParameters(ControlState state)
|
||||
{
|
||||
u16& value = (Motor::Strong == m_motor) ? m_effect.u.rumble.strong_magnitude :
|
||||
m_effect.u.rumble.weak_magnitude;
|
||||
const u16 old_value = value;
|
||||
|
||||
constexpr u16 MAX_VALUE = 0xffff;
|
||||
value = u16(state * MAX_VALUE);
|
||||
|
||||
m_effect.type = value ? FF_RUMBLE : DISABLED_EFFECT_TYPE;
|
||||
return value != old_value;
|
||||
}
|
||||
|
||||
evdevDevice::Effect::~Effect()
|
||||
{
|
||||
m_effect.type = DISABLED_EFFECT_TYPE;
|
||||
UpdateEffect();
|
||||
}
|
||||
} // namespace ciface::evdev
|
||||
@@ -1,104 +0,0 @@
|
||||
// Copyright 2015 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <libevdev/libevdev.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
|
||||
namespace ciface::evdev
|
||||
{
|
||||
class InputBackend;
|
||||
|
||||
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface);
|
||||
|
||||
class evdevDevice : public Core::Device
|
||||
{
|
||||
private:
|
||||
class Effect : public Core::Device::Output
|
||||
{
|
||||
public:
|
||||
Effect(int fd);
|
||||
~Effect();
|
||||
void SetState(ControlState state) override;
|
||||
|
||||
protected:
|
||||
virtual bool UpdateParameters(ControlState state) = 0;
|
||||
|
||||
ff_effect m_effect = {};
|
||||
|
||||
static constexpr int DISABLED_EFFECT_TYPE = 0;
|
||||
|
||||
private:
|
||||
void UpdateEffect();
|
||||
|
||||
int const m_fd;
|
||||
};
|
||||
|
||||
class ConstantEffect : public Effect
|
||||
{
|
||||
public:
|
||||
ConstantEffect(int fd);
|
||||
bool UpdateParameters(ControlState state) override;
|
||||
std::string GetName() const override;
|
||||
};
|
||||
|
||||
class PeriodicEffect : public Effect
|
||||
{
|
||||
public:
|
||||
PeriodicEffect(int fd, u16 waveform);
|
||||
bool UpdateParameters(ControlState state) override;
|
||||
std::string GetName() const override;
|
||||
};
|
||||
|
||||
class RumbleEffect : public Effect
|
||||
{
|
||||
public:
|
||||
enum class Motor : u8
|
||||
{
|
||||
Weak,
|
||||
Strong,
|
||||
};
|
||||
|
||||
RumbleEffect(int fd, Motor motor);
|
||||
bool UpdateParameters(ControlState state) override;
|
||||
std::string GetName() const override;
|
||||
|
||||
private:
|
||||
const Motor m_motor;
|
||||
};
|
||||
|
||||
public:
|
||||
Core::DeviceRemoval UpdateInput() override;
|
||||
bool IsValid() const override;
|
||||
|
||||
evdevDevice(InputBackend* input_backend);
|
||||
~evdevDevice();
|
||||
|
||||
// Return true if node was "interesting".
|
||||
bool AddNode(std::string devnode, int fd, libevdev* dev);
|
||||
|
||||
const char* GetUniqueID() const;
|
||||
const char* GetPhysicalLocation() const;
|
||||
|
||||
std::string GetName() const override { return m_name; }
|
||||
std::string GetSource() const override { return "evdev"; }
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
|
||||
struct Node
|
||||
{
|
||||
std::string devnode;
|
||||
int fd;
|
||||
libevdev* device;
|
||||
};
|
||||
|
||||
std::vector<Node> m_nodes;
|
||||
|
||||
InputBackend& m_input_backend;
|
||||
};
|
||||
} // namespace ciface::evdev
|
||||
Reference in new issue
Block a user