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:
LibretroAdmin authored and libretroadmin committed 2026-11-05 13:00:29 +01:00
1 parent 12247f1f50
commit 8c7aa58f6b
57 files changed
-8775

No files matched your search

-4
View File
@@ -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
-2
View File
@@ -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/):
-1
View File
@@ -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 \
-83
View File
@@ -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.
```
-181
View File
@@ -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
-16
View File
@@ -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
-12
View File
@@ -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
-226
View File
@@ -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;
-73
View File
@@ -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
-125
View File
@@ -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;
-351
View File
@@ -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
-12
View File
@@ -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>
-6
View File
@@ -1,6 +0,0 @@
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
IDI_ICON1 ICON "..\\..\\..\\Installer\\Dolphin.ico"
"dolphin" ICON "..\\..\\..\\Installer\\Dolphin.ico"
-382
View File
@@ -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
-12
View File
@@ -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
-170
View File
@@ -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
-12
View File
@@ -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
-69
View File
@@ -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
-198
View File
@@ -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
-12
View File
@@ -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
-23
View File
@@ -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(&notify_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