mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-20 21:49:51 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f394212b3 | |||
| dfe32f61e1 | |||
| e3f5e3a3ec | |||
| 0321842569 |
@@ -102,6 +102,7 @@ jobs:
|
||||
mkw_nand_settings_tests \
|
||||
mkw_sc_serial_tests \
|
||||
mkw_input_expr_tests \
|
||||
mkw_bluetooth_controller_tests \
|
||||
mkw_macos_native_compile \
|
||||
mkw_macos_context_abi_tests \
|
||||
mkw_macos_host_context_tests \
|
||||
|
||||
+26
-1
@@ -69,6 +69,7 @@ fuse_ld_override=""
|
||||
native_prebuilt_dir=""
|
||||
sysroot=""
|
||||
setup_version=""
|
||||
x86_cpu_profile=auto
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -95,6 +96,8 @@ Usage: local-build.sh --output-dir DIR [options]
|
||||
standard headers/startup files
|
||||
--setup-version VERSION Version recorded in the runtime log (default: <Version> in
|
||||
Launcher/Directory.Build.props when present)
|
||||
--x86-cpu-profile {auto|v2|v3} x86-64 instruction set to build for (default: auto, v3 when
|
||||
this machine supports it)
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -120,6 +123,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
|
||||
--sysroot) sysroot=$2; shift 2 ;;
|
||||
--setup-version) setup_version=$2; shift 2 ;;
|
||||
--x86-cpu-profile) x86_cpu_profile=$2; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
@@ -135,6 +139,10 @@ case "$profile" in
|
||||
base|retro-rewind|both) ;;
|
||||
*) fail "--profile must be base, retro-rewind, or both" ;;
|
||||
esac
|
||||
case "$x86_cpu_profile" in
|
||||
auto|v2|v3) ;;
|
||||
*) fail "--x86-cpu-profile must be auto, v2, or v3" ;;
|
||||
esac
|
||||
|
||||
builds_retro=0
|
||||
[[ "$profile" == "retro-rewind" || "$profile" == "both" ]] && builds_retro=1
|
||||
@@ -184,6 +192,22 @@ if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
assert_file "$native_prebuilt_dir/native_prebuilt.cmake" "Native prebuilt package"
|
||||
fi
|
||||
|
||||
host_supports_x86_64_v3() {
|
||||
local macros macro
|
||||
macros=$("$cc_bin" -march=native -dM -E -x c /dev/null 2>/dev/null) || return 1
|
||||
for macro in __AVX__ __AVX2__ __BMI__ __BMI2__ __F16C__ __FMA__ __LZCNT__ __MOVBE__; do
|
||||
grep -q "^#define $macro " <<<"$macros" || return 1
|
||||
done
|
||||
}
|
||||
|
||||
if [[ "$x86_cpu_profile" == "auto" ]]; then
|
||||
x86_cpu_profile=v3
|
||||
if [[ "$(uname -m)" == "x86_64" ]] && ! host_supports_x86_64_v3; then
|
||||
x86_cpu_profile=v2
|
||||
echo "MKWCBUILD: This CPU does not support x86-64-v3; building for x86-64-v2"
|
||||
fi
|
||||
fi
|
||||
|
||||
project=$workspace/projects/mkwii/recomp.yml
|
||||
assets=$workspace/Assets
|
||||
generated=$workspace/generated
|
||||
@@ -434,7 +458,8 @@ configure_args=(-S "$workspace/runtime" -B "$build" -G Ninja
|
||||
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin"
|
||||
-DCMAKE_MAKE_PROGRAM="$ninja_bin"
|
||||
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
|
||||
-DMKW_SETUP_VERSION="$setup_version")
|
||||
-DMKW_SETUP_VERSION="$setup_version"
|
||||
-DMKW_X86_CPU_PROFILE="$x86_cpu_profile")
|
||||
# CMAKE_C_COMPILER/CXX_COMPILER stay stable across AppImage runs on their own (they're exactly
|
||||
# what's passed via -D above, and AppRun points --cc/--cxx at a symlink it re-targets at the
|
||||
# current mount every launch - see build-appimage.sh). CMAKE_AR/RANLIB/LINKER/ASM_COMPILER do NOT
|
||||
|
||||
+1
-1
Submodule aurora-main updated: 648e77d7e8...78850a8d30
@@ -63,6 +63,16 @@ target_include_directories(mkw_input_expr_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}
|
||||
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
|
||||
|
||||
# The emulated Bluetooth controller is pure host code.
|
||||
add_executable(mkw_bluetooth_controller_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/bluetooth_controller_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_controller.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_remote_link.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_sdp.cpp")
|
||||
target_include_directories(mkw_bluetooth_controller_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_bluetooth_controller_tests PRIVATE cxx_std_20)
|
||||
add_test(NAME mkw_bluetooth_controller_tests COMMAND mkw_bluetooth_controller_tests)
|
||||
|
||||
# HostContext deliberately keeps the platform-specific context primitive out
|
||||
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
||||
# refactors cannot silently remove its headers, implementation, or link edge.
|
||||
|
||||
@@ -206,7 +206,7 @@ function(mkw_configure_product target)
|
||||
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp bthprops)
|
||||
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
|
||||
@@ -66,7 +66,7 @@ function(mkw_runtime_link_dependencies target scope)
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(${target} ${scope} mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} ${scope} shell32 windowsapp)
|
||||
target_link_libraries(${target} ${scope} shell32 windowsapp bthprops)
|
||||
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty on platforms where dl* is already in libc/libSystem.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "settings_overlay.h"
|
||||
#include "runtime_config.h"
|
||||
#include "gx_worker.h"
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/event.h>
|
||||
@@ -131,7 +132,20 @@ inline void ApplyPendingMkwDynamicAspectSurface() {
|
||||
UpdateMkwDynamicAspectSurface(surfaceWidth, surfaceHeight);
|
||||
}
|
||||
|
||||
void GxWorkerBeginFrame();
|
||||
void GxWorkerEndFrame(AuroraImguiFrame* imgui);
|
||||
|
||||
inline bool BeginAuroraFrame() {
|
||||
if (GxWorker::ShouldDefer()) {
|
||||
// The window half can touch the surface and render targets the worker draws into.
|
||||
GxWorker::Sync();
|
||||
if (!aurora_begin_frame_window()) {
|
||||
return false;
|
||||
}
|
||||
GxWorker::Submit([] { GxWorkerBeginFrame(); });
|
||||
ApplyPendingMkwDynamicAspectSurface();
|
||||
return true;
|
||||
}
|
||||
if (!aurora_begin_frame()) {
|
||||
return false;
|
||||
}
|
||||
@@ -139,6 +153,18 @@ inline bool BeginAuroraFrame() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ends the frame; with the GX worker, the game thread joins it here so the next frame never
|
||||
// overlaps guest memory the worker may still read.
|
||||
inline void EndAuroraFrame() {
|
||||
if (GxWorker::ShouldDefer()) {
|
||||
AuroraImguiFrame* imgui = aurora_end_frame_window();
|
||||
GxWorker::Submit([imgui] { GxWorkerEndFrame(imgui); });
|
||||
GxWorker::Sync();
|
||||
return;
|
||||
}
|
||||
aurora_end_frame();
|
||||
}
|
||||
|
||||
// Poll Aurora events and update cached window/framebuffer dimensions.
|
||||
inline void UpdateAuroraAndProcessEvents() {
|
||||
ProcessAuroraEvents(aurora_update());
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
// HCI and L2CAP fields are little-endian.
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth {
|
||||
|
||||
inline uint16_t Le16(const uint8_t* p) {
|
||||
return static_cast<uint16_t>(p[0] | (p[1] << 8));
|
||||
}
|
||||
|
||||
inline void PutLe16(std::vector<uint8_t>& out, uint16_t v) {
|
||||
out.push_back(static_cast<uint8_t>(v));
|
||||
out.push_back(static_cast<uint8_t>(v >> 8));
|
||||
}
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
// Emulated Bluetooth controller behind the Wii's internal USB module
|
||||
// (/dev/usb/oh1/57e/305). The game's own Broadcom stack (BTE), WUD, WPAD and KPAD
|
||||
// talk HCI to it; each of the four virtual Wii Remotes it hosts is backed by a
|
||||
// physical remote on the PC (see RemoteHost), whose HID reports are passed
|
||||
// through unchanged. Pure host code: no guest memory, so it is unit-testable.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth {
|
||||
|
||||
// Bluetooth device address in HCI (least significant byte first) order.
|
||||
using BdAddr = std::array<uint8_t, 6>;
|
||||
|
||||
inline constexpr size_t kRemoteSlots = 4;
|
||||
|
||||
enum class LinkState : uint8_t {
|
||||
Down, // no baseband connection to the game
|
||||
Connecting, // baseband up, HID channels being opened
|
||||
Ready, // HID control and interrupt channels open
|
||||
};
|
||||
|
||||
// The physical side of the virtual remotes. Called from the thread that drives
|
||||
// the Controller; implementations synchronize with their own I/O threads.
|
||||
class RemoteHost {
|
||||
public:
|
||||
virtual ~RemoteHost() = default;
|
||||
// A physical remote is attached to `slot` and can carry a connection.
|
||||
virtual bool IsAttached(size_t slot) const = 0;
|
||||
// True once each time the remote on `slot` asks to connect (it was just
|
||||
// attached, or a button was pressed while it had no connection).
|
||||
virtual bool ConsumeWake(size_t slot) = 0;
|
||||
// Next input report (report id first, no HID header), false when none.
|
||||
virtual bool PopInputReport(size_t slot, std::vector<uint8_t>& report) = 0;
|
||||
// Output report from the game (report id first, no HID header).
|
||||
virtual void SendOutputReport(size_t slot, const uint8_t* report, size_t size) = 0;
|
||||
virtual void OnLinkStateChanged(size_t slot, LinkState state) = 0;
|
||||
// The game ended a working connection; a real remote powers off here.
|
||||
virtual void OnGuestDisconnect(size_t slot) = 0;
|
||||
// The game is searching for remotes (its 1+2 / reconnect screens).
|
||||
virtual void OnGuestInquiry(bool active) = 0;
|
||||
// "Nintendo RVL-CNT-01" or "Nintendo RVL-CNT-01-TR".
|
||||
virtual const char* DeviceName(size_t slot) const = 0;
|
||||
};
|
||||
|
||||
class RemoteLink;
|
||||
|
||||
class Controller {
|
||||
public:
|
||||
explicit Controller(RemoteHost& host);
|
||||
~Controller();
|
||||
Controller(const Controller&) = delete;
|
||||
Controller& operator=(const Controller&) = delete;
|
||||
|
||||
// The guest opened / closed the USB device.
|
||||
void Open(uint64_t nowUs);
|
||||
void Close();
|
||||
bool IsOpen() const { return m_open; }
|
||||
|
||||
// Raw HCI command packet (opcode first, no H4 byte).
|
||||
void ExecuteCommand(const uint8_t* packet, size_t size, uint64_t nowUs);
|
||||
// Raw ACL packet from the guest (handle first).
|
||||
void ReceiveAcl(const uint8_t* packet, size_t size, uint64_t nowUs);
|
||||
// Advances timers, connection attempts, input forwarding and flow control.
|
||||
void Tick(uint64_t nowUs);
|
||||
|
||||
bool HasEvent() const { return !m_events.empty(); }
|
||||
bool PopEvent(std::vector<uint8_t>& event);
|
||||
// ACL data is only handed out once every queued event has been delivered,
|
||||
// so the guest always sees a connection before data on it.
|
||||
bool PopAcl(std::vector<uint8_t>& packet);
|
||||
size_t AclQueued() const { return m_acl.size(); }
|
||||
|
||||
LinkState SlotLinkState(size_t slot) const;
|
||||
bool PageScanEnabled() const { return (m_scanEnable & 0x02) != 0; }
|
||||
bool InquiryActive() const { return m_inquiry.active; }
|
||||
|
||||
static BdAddr LocalAddress();
|
||||
static BdAddr RemoteAddress(size_t slot);
|
||||
static uint16_t ConnectionHandle(size_t slot);
|
||||
|
||||
// Used by RemoteLink.
|
||||
void QueueEvent(std::vector<uint8_t> event);
|
||||
void QueueAcl(uint16_t handle, const std::vector<uint8_t>& l2capFrame);
|
||||
RemoteHost& Host() { return m_host; }
|
||||
uint32_t PageTimeoutUs() const { return static_cast<uint32_t>(m_pageTimeoutSlots) * 625u; }
|
||||
|
||||
private:
|
||||
struct StoredKey {
|
||||
BdAddr addr{};
|
||||
std::array<uint8_t, 16> key{};
|
||||
};
|
||||
struct Inquiry {
|
||||
bool active = false;
|
||||
uint64_t endUs = 0;
|
||||
uint8_t maxResponses = 0;
|
||||
uint8_t reported = 0;
|
||||
std::array<bool, kRemoteSlots> slotReported{};
|
||||
};
|
||||
struct Reassembly {
|
||||
std::vector<uint8_t> frame;
|
||||
size_t expected = 0;
|
||||
};
|
||||
// A reply the controller sends after a delay (page / name request timeouts).
|
||||
struct PendingEvent {
|
||||
uint64_t dueUs = 0;
|
||||
std::vector<uint8_t> event;
|
||||
};
|
||||
|
||||
void ResetState(bool keepStoredKeys);
|
||||
void CommandComplete(uint16_t opcode, const std::vector<uint8_t>& params);
|
||||
void CommandStatus(uint16_t opcode, uint8_t status);
|
||||
void TickInquiry(uint64_t nowUs);
|
||||
void FinishInquiry();
|
||||
void SendNumberOfCompletedPackets();
|
||||
int SlotForAddress(const BdAddr& addr) const;
|
||||
int SlotForHandle(uint16_t handle) const;
|
||||
const StoredKey* FindStoredKey(const BdAddr& addr) const;
|
||||
|
||||
void HandleLinkControl(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs);
|
||||
void HandleLinkPolicy(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void HandleControllerBaseband(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs);
|
||||
void HandleInformational(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void HandleStatusParameters(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void HandleVendor(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void UnknownCommand(uint16_t opcode);
|
||||
|
||||
RemoteHost& m_host;
|
||||
bool m_open = false;
|
||||
uint8_t m_scanEnable = 0;
|
||||
uint16_t m_pageTimeoutSlots = 0x2000;
|
||||
uint8_t m_inquiryMode = 0;
|
||||
std::array<uint8_t, 3> m_classOfDevice{};
|
||||
std::array<char, 248> m_localName{};
|
||||
Inquiry m_inquiry;
|
||||
bool m_hostInquiryNotified = false;
|
||||
std::vector<StoredKey> m_storedKeys;
|
||||
std::array<std::unique_ptr<RemoteLink>, kRemoteSlots> m_links;
|
||||
std::array<uint16_t, kRemoteSlots> m_completedPackets{};
|
||||
std::array<Reassembly, kRemoteSlots> m_reassembly;
|
||||
std::array<bool, kRemoteSlots> m_authPending{};
|
||||
std::vector<PendingEvent> m_pending;
|
||||
std::deque<std::vector<uint8_t>> m_events;
|
||||
std::deque<std::vector<uint8_t>> m_acl;
|
||||
};
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
// One emulated Wii Remote as seen from the controller: its baseband connection,
|
||||
// L2CAP channels (SDP, HID control, HID interrupt) and the HID transaction layer
|
||||
// that carries the physical remote's reports.
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth {
|
||||
|
||||
class RemoteLink {
|
||||
public:
|
||||
enum class Baseband : uint8_t {
|
||||
Idle,
|
||||
Requesting, // Connection Request sent, waiting for the guest to accept
|
||||
Paging, // the guest paged this remote (Create Connection)
|
||||
Connected,
|
||||
};
|
||||
|
||||
RemoteLink(Controller& controller, size_t slot);
|
||||
|
||||
// Drops every connection without telling the guest (controller reset/close).
|
||||
void Reset();
|
||||
void Tick(uint64_t nowUs);
|
||||
|
||||
// Remote side wants a connection and should be offered to the guest's inquiry.
|
||||
bool Discoverable() const;
|
||||
bool ConnectWanted() const { return m_connectWanted; }
|
||||
|
||||
// Guest accepted / rejected this remote's Connection Request.
|
||||
bool IsRequesting() const { return m_baseband == Baseband::Requesting; }
|
||||
void OnAccepted(uint8_t role);
|
||||
void OnRejected(uint8_t reason, uint64_t nowUs);
|
||||
// Guest paged this remote. Returns false when the remote cannot answer.
|
||||
bool OnPaged(uint64_t nowUs);
|
||||
// Baseband went down. `byGuest` = the guest's HCI Disconnect.
|
||||
void OnDisconnected(bool byGuest, uint64_t nowUs);
|
||||
// The physical remote vanished while connected: tells the guest.
|
||||
void DropFromRemote(uint8_t reason);
|
||||
|
||||
void ReceiveL2cap(const uint8_t* frame, size_t size);
|
||||
|
||||
Baseband BasebandState() const { return m_baseband; }
|
||||
LinkState State() const { return m_state; }
|
||||
|
||||
private:
|
||||
struct Channel {
|
||||
uint16_t psm = 0;
|
||||
uint16_t localCid = 0;
|
||||
uint16_t remoteCid = 0;
|
||||
uint16_t remoteMtu = 672;
|
||||
bool weInitiated = false;
|
||||
bool connected = false; // connection response success seen/sent
|
||||
bool configSent = false; // our Configuration Request is out
|
||||
bool localConfigured = false; // the guest accepted our configuration
|
||||
bool remoteConfigured = false; // we accepted the guest's configuration
|
||||
uint8_t connectIdent = 0;
|
||||
uint8_t configIdent = 0;
|
||||
uint64_t requestUs = 0;
|
||||
std::vector<uint8_t> pendingOptions; // continued Configuration Request
|
||||
bool Open() const { return connected && localConfigured && remoteConfigured; }
|
||||
};
|
||||
|
||||
Channel* FindByLocal(uint16_t cid);
|
||||
Channel* FindByPsm(uint16_t psm);
|
||||
uint16_t AllocateCid() const;
|
||||
uint8_t NextIdent();
|
||||
|
||||
void SendSignal(uint8_t code, uint8_t ident, const std::vector<uint8_t>& data);
|
||||
void SendData(const Channel& channel, const std::vector<uint8_t>& payload);
|
||||
void RequestChannel(uint16_t psm, uint64_t nowUs);
|
||||
void SendConfigRequest(Channel& channel);
|
||||
void HandleSignalling(const uint8_t* data, size_t size);
|
||||
void HandleConnectionRequest(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleConnectionResponse(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleConfigurationRequest(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleConfigurationResponse(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleDisconnectionRequest(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleDisconnectionResponse(const uint8_t* p, size_t n);
|
||||
void HandleChannelData(Channel& channel, const uint8_t* data, size_t size);
|
||||
void HandleHidControl(Channel& channel, const uint8_t* data, size_t size);
|
||||
void UpdateState();
|
||||
void SetState(LinkState state);
|
||||
void ForwardInput();
|
||||
void SendInput(const Channel& interrupt, const std::vector<uint8_t>& report);
|
||||
void ClearConnection();
|
||||
|
||||
Controller& m_controller;
|
||||
size_t m_slot;
|
||||
Baseband m_baseband = Baseband::Idle;
|
||||
LinkState m_state = LinkState::Down;
|
||||
bool m_connectWanted = false;
|
||||
bool m_guestInitiated = false;
|
||||
bool m_everReady = false;
|
||||
uint8_t m_ident = 0;
|
||||
uint64_t m_stateSinceUs = 0;
|
||||
uint64_t m_retryAtUs = 0;
|
||||
uint64_t m_nowUs = 0;
|
||||
std::vector<Channel> m_channels;
|
||||
std::vector<uint8_t> m_report;
|
||||
};
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
// SDP server of an emulated Wii Remote: the HID service record (0x00010000)
|
||||
// and the Device ID record (0x00010001) a real RVL-CNT-01 publishes.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth::Sdp {
|
||||
|
||||
// The Wii Remote's HID report descriptor, as the game also embeds it.
|
||||
const std::vector<uint8_t>& WiiRemoteHidDescriptor();
|
||||
|
||||
// Answers one SDP request PDU. `maxResponse` bounds the PDU returned (the
|
||||
// guest's L2CAP MTU on the SDP channel).
|
||||
std::vector<uint8_t> HandleRequest(const uint8_t* pdu, size_t size, size_t maxResponse);
|
||||
|
||||
} // namespace Bluetooth::Sdp
|
||||
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <new>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
// Dedicated GX thread (Dolphin "dual core"): the game thread records GX HLE calls in order and
|
||||
// the worker executes them. While active, GX work runs on the worker except inside inline scopes,
|
||||
// which run on the game thread after the worker has caught up.
|
||||
namespace GxWorker {
|
||||
|
||||
namespace detail {
|
||||
inline constexpr size_t kAlign = 16;
|
||||
|
||||
extern std::atomic<bool> g_active;
|
||||
extern constinit thread_local bool t_onWorker;
|
||||
extern constinit thread_local int t_inlineDepth;
|
||||
extern bool g_fifoPending;
|
||||
|
||||
void* Reserve(size_t payloadSize, void (*run)(void*));
|
||||
void Commit();
|
||||
void FlushFifo();
|
||||
} // namespace detail
|
||||
|
||||
// Starts the worker unless [video] gx_worker or MKW_GX_WORKER=0 disables it. Call once Aurora is up.
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
inline bool Active() noexcept { return detail::g_active.load(std::memory_order_relaxed); }
|
||||
inline bool OnWorker() noexcept { return detail::t_onWorker; }
|
||||
// True when a GX entry point on this thread must be recorded instead of executed.
|
||||
inline bool ShouldDefer() noexcept {
|
||||
return Active() && !detail::t_onWorker && detail::t_inlineDepth == 0;
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
void Submit(F&& fn) {
|
||||
using Fn = std::decay_t<F>;
|
||||
static_assert(alignof(Fn) <= detail::kAlign, "over-aligned GX command");
|
||||
if (detail::g_fifoPending) {
|
||||
detail::FlushFifo();
|
||||
}
|
||||
void* storage = detail::Reserve(sizeof(Fn), [](void* p) {
|
||||
struct Destroy {
|
||||
Fn* f;
|
||||
~Destroy() { f->~Fn(); }
|
||||
} guard{static_cast<Fn*>(p)};
|
||||
(*guard.f)();
|
||||
});
|
||||
new (storage) Fn(std::forward<F>(fn));
|
||||
detail::Commit();
|
||||
}
|
||||
|
||||
// Records fn when deferring, otherwise runs it now. Read call-time guest memory before calling.
|
||||
template <typename F>
|
||||
void Run(F&& fn) {
|
||||
if (ShouldDefer()) {
|
||||
Submit(std::forward<F>(fn));
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
|
||||
// Gather-pipe writes, coalesced into one record per run of consecutive writes and replayed
|
||||
// with their original sizes.
|
||||
void FifoWrite(uint32_t value, uint32_t sizeBytes);
|
||||
void FifoBurst(const uint8_t* data, uint32_t sizeBytes);
|
||||
|
||||
// Called about every millisecond while the game thread waits in Sync.
|
||||
void SetWaitCallback(void (*callback)());
|
||||
|
||||
// Blocks until every recorded command has executed.
|
||||
void Sync();
|
||||
|
||||
// GX entry points run inline on the game thread while inside (after a Sync), for work whose
|
||||
// results the guest needs immediately.
|
||||
void BeginInline();
|
||||
void EndInline();
|
||||
|
||||
class InlineScope {
|
||||
public:
|
||||
InlineScope() { BeginInline(); }
|
||||
~InlineScope() { EndInline(); }
|
||||
InlineScope(const InlineScope&) = delete;
|
||||
InlineScope& operator=(const InlineScope&) = delete;
|
||||
};
|
||||
|
||||
} // namespace GxWorker
|
||||
|
||||
// Records the enclosing call for the GX worker and returns, or falls through when it should run
|
||||
// here (worker thread, inline scope, worker disabled).
|
||||
#define GX_DEFER_CALL(fn, ...) \
|
||||
do { \
|
||||
if (GxWorker::ShouldDefer()) { \
|
||||
GxWorker::Submit([=] { fn(__VA_ARGS__); }); \
|
||||
return; \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#define GX_DEFER_CALL0(fn) \
|
||||
do { \
|
||||
if (GxWorker::ShouldDefer()) { \
|
||||
GxWorker::Submit([] { fn(); }); \
|
||||
return; \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
// Runs the rest of the enclosing function on this thread once the worker has caught up.
|
||||
#define GX_SYNC_INLINE() GxWorker::InlineScope gxInlineScope_
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
// IOS side of the emulated Bluetooth module (/dev/usb/oh1/57e/305): routes the
|
||||
// guest's USB control / bulk / interrupt transfers to Bluetooth::Controller and
|
||||
// completes the held event and ACL reads from the alarm pump.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct CpuContext;
|
||||
|
||||
// Returns the fd for the Bluetooth device path, 0 when `path` is not ours.
|
||||
int32_t Bluetooth_HLE_OpenDevice(const char* path);
|
||||
bool Bluetooth_HLE_IsFd(uint32_t fd);
|
||||
// Cancels the held reads (their callbacks are queued) and closes the device.
|
||||
int32_t Bluetooth_HLE_Close(uint32_t fd);
|
||||
// True when the request completed now with `result`; false when it is held
|
||||
// until data arrives (its callback is queued later by the pump).
|
||||
bool Bluetooth_HLE_IoctlvAsync(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr,
|
||||
uint32_t callback, uint32_t callbackArg, int32_t& result);
|
||||
int32_t Bluetooth_HLE_Ioctlv(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr);
|
||||
// Runs the controller and completes held reads. Emulation thread only.
|
||||
bool Bluetooth_HLE_ProcessCompletions(CpuContext* cpu);
|
||||
@@ -41,42 +41,3 @@ inline GuestStatus Encode(const Fields& fields)
|
||||
}
|
||||
|
||||
} // namespace PadStatusContract
|
||||
|
||||
namespace WpadContract {
|
||||
|
||||
inline constexpr std::size_t kChannelCount = 4;
|
||||
inline constexpr int32_t kStatusDisabled = 0;
|
||||
inline constexpr int32_t kStatusReady = 3;
|
||||
inline constexpr int32_t kErrorNoController = -1;
|
||||
inline constexpr int32_t kErrorNotReady = -2;
|
||||
inline constexpr int32_t kErrorBadChannel = -6;
|
||||
inline constexpr int32_t kExtensionCore = 0;
|
||||
|
||||
class State {
|
||||
public:
|
||||
void Initialize() { m_initialized = true; }
|
||||
bool IsInitialized() const { return m_initialized; }
|
||||
int32_t GetLibraryStatus() const { return m_initialized ? kStatusReady : kStatusDisabled; }
|
||||
|
||||
int32_t GetDataFormat(uint32_t chan) const
|
||||
{
|
||||
if (chan >= kChannelCount) {
|
||||
return kErrorBadChannel;
|
||||
}
|
||||
return m_initialized ? 0 : kErrorNotReady;
|
||||
}
|
||||
|
||||
int32_t SetDataFormat(uint32_t chan, int32_t format) const
|
||||
{
|
||||
(void)format;
|
||||
if (chan >= kChannelCount) {
|
||||
return kErrorBadChannel;
|
||||
}
|
||||
return m_initialized ? kErrorNoController : kErrorNotReady;
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
} // namespace WpadContract
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// HW_GPIOB_OUT, the GPIO outputs the PPC owns. WPAD sets bit 8 to power the
|
||||
// sensor bar; there is nothing behind it on a PC, so the register only latches.
|
||||
inline constexpr uint32_t kHollywoodGpioBOut = 0xCD0000C0u;
|
||||
|
||||
bool HollywoodGpio_Read32(uint32_t address, uint32_t& value);
|
||||
bool HollywoodGpio_Write32(uint32_t address, uint32_t value);
|
||||
@@ -44,6 +44,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<uint32_t> frameInterpolationFps;
|
||||
std::optional<bool> metalFxSpatialUpscaling;
|
||||
std::optional<bool> skipUnreadyPipelines;
|
||||
std::optional<bool> gxWorker;
|
||||
std::optional<bool> disableCopyFilter;
|
||||
std::optional<bool> textureReplacements;
|
||||
std::optional<bool> textureDumps;
|
||||
@@ -57,26 +58,16 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> audioMuted;
|
||||
std::optional<bool> audioMixWorker;
|
||||
std::optional<bool> attenuateMusicWhenMediaPlays;
|
||||
// Real Wii Remotes (with or without Nunchuk / Classic Controller) and Wii U Pro
|
||||
// Controllers paired over Bluetooth, driven by SDL's HIDAPI Wii driver. The driver
|
||||
// is opt-in on SDL's side, so this decides whether the runtime turns it on.
|
||||
// Real Wii Remotes passed through to the game's own Bluetooth stack. Off hands
|
||||
// Nintendo controllers (e.g. the Wii U Pro Controller) to SDL as plain gamepads.
|
||||
std::optional<bool> wiiRemotes;
|
||||
// Keep re-enumerating Bluetooth HID devices while no Wii controller is connected
|
||||
// (Dolphin's "continuous scanning"), so a remote that dropped or was switched on
|
||||
// after launch shows up without restarting.
|
||||
std::optional<bool> wiiContinuousScan;
|
||||
// Accelerometer zero-point correction for the Bluetooth Wii Remote, in g and in
|
||||
// SDL's sensor frame (x right, y out of the button face, z towards the user).
|
||||
// SDL's Wii driver falls back to a nominal zero point when its read of the
|
||||
// remote's calibration block times out (common over Bluetooth), so this is
|
||||
// measured in the overlay with the remote at rest. Stored per remote under
|
||||
// [controller.wii_accel_offsets]; the old shared
|
||||
// wii_accel_offset_* keys are only detected, to report that they are ignored.
|
||||
std::map<std::string, std::array<double, 3>> wiiAccelOffsets;
|
||||
bool hasLegacyWiiAccelOffset = false;
|
||||
// Debugging aid: append every KPAD sample of the Bluetooth remote (raw and
|
||||
// corrected accelerometer, buttons) to wii_accel_trace.csv next to Config.toml.
|
||||
std::optional<bool> wiiAccelTrace;
|
||||
// Look for remotes in pairing mode (1+2 / SYNC) while none is connected.
|
||||
std::optional<bool> wiiAutoPair;
|
||||
// The console's Wii Remote settings (SYSCONF BT.BAR, BT.SENS, BT.SPKV, BT.MOT).
|
||||
std::optional<bool> wiiSensorBarAbove;
|
||||
std::optional<int32_t> wiiIrSensitivity;
|
||||
std::optional<int32_t> wiiSpeakerVolume;
|
||||
std::optional<bool> wiiRumble;
|
||||
std::optional<bool> networkEnabled;
|
||||
std::optional<bool> discordPresenceEnabled;
|
||||
// The application ID of the WiiCompiled Discord application. This is only
|
||||
@@ -285,6 +276,8 @@ inline void EnsureConfigFile() {
|
||||
"display_mode = \"windowed\"\n"
|
||||
"graphics_api = \"auto\"\n"
|
||||
"skip_unready_pipelines = true\n"
|
||||
"# Runs GX command processing on its own thread. Read once, at startup.\n"
|
||||
"gx_worker = true\n"
|
||||
"disable_copy_filter = true\n"
|
||||
"show_fps = true\n"
|
||||
"# Dolphin-style custom textures. When enabled, the renderer indexes\n"
|
||||
@@ -382,28 +375,6 @@ inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roo
|
||||
}
|
||||
|
||||
// Reads every supported setting out of a parsed Config.toml document.
|
||||
// [x, y, z] of finite numbers (integers allowed), else nullopt.
|
||||
inline std::optional<std::array<double, 3>> ParseAccelOffset(const toml::value& value) {
|
||||
if (!value.is_array() || value.as_array().size() != 3) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::array<double, 3> offset{};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
const toml::value& element = value.as_array()[i];
|
||||
if (element.is_floating()) {
|
||||
offset[i] = element.as_floating();
|
||||
} else if (element.is_integer()) {
|
||||
offset[i] = static_cast<double>(element.as_integer());
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!std::isfinite(offset[i])) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
RuntimeUserConfig config;
|
||||
|
||||
@@ -427,13 +398,6 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.controllerExpressions[key] = value.as_string();
|
||||
}
|
||||
}
|
||||
if (section->contains("wii_accel_offsets") && section->at("wii_accel_offsets").is_table()) {
|
||||
for (const auto& [key, value] : section->at("wii_accel_offsets").as_table()) {
|
||||
if (const auto offset = ParseAccelOffset(value)) {
|
||||
config.wiiAccelOffsets[key] = *offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
||||
@@ -471,6 +435,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.metalFxSpatialUpscaling =
|
||||
FindConfigValue<bool>(document, "video", "metalfx_spatial_upscaling");
|
||||
config.skipUnreadyPipelines = FindConfigValue<bool>(document, "video", "skip_unready_pipelines");
|
||||
config.gxWorker = FindConfigValue<bool>(document, "video", "gx_worker");
|
||||
config.disableCopyFilter = FindConfigValue<bool>(document, "video", "disable_copy_filter");
|
||||
config.showFps = FindConfigValue<bool>(document, "video", "show_fps");
|
||||
config.textureReplacements = FindConfigValue<bool>(document, "video", "texture_replacements");
|
||||
@@ -494,11 +459,17 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.attenuateMusicWhenMediaPlays =
|
||||
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
|
||||
config.wiiRemotes = FindConfigValue<bool>(document, "controller", "wii_remotes");
|
||||
config.wiiContinuousScan = FindConfigValue<bool>(document, "controller", "wii_continuous_scan");
|
||||
for (const char* key : {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"}) {
|
||||
config.hasLegacyWiiAccelOffset |= FindConfigValue<double>(document, "controller", key).value_or(0.0) != 0.0;
|
||||
config.wiiAutoPair = FindConfigValue<bool>(document, "controller", "wii_auto_pair");
|
||||
if (const auto bar = FindConfigValue<std::string>(document, "controller", "wii_sensor_bar")) {
|
||||
if (*bar == "above" || *bar == "below") config.wiiSensorBarAbove = *bar == "above";
|
||||
}
|
||||
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
|
||||
if (const auto value = FindConfigInt(document, "controller", "wii_ir_sensitivity"); value && *value >= 1 && *value <= 5) {
|
||||
config.wiiIrSensitivity = *value;
|
||||
}
|
||||
if (const auto value = FindConfigInt(document, "controller", "wii_speaker_volume"); value && *value >= 0 && *value <= 127) {
|
||||
config.wiiSpeakerVolume = *value;
|
||||
}
|
||||
config.wiiRumble = FindConfigValue<bool>(document, "controller", "wii_rumble");
|
||||
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
|
||||
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
|
||||
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
|
||||
@@ -844,56 +815,70 @@ inline bool AudioMixWorkerEnabled(bool fallback = true) {
|
||||
return Get().audioMixWorker.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool GxWorkerEnabled(bool fallback = true) {
|
||||
return Get().gxWorker.value_or(fallback);
|
||||
}
|
||||
|
||||
// Whether background music should duck automatically for other media playback.
|
||||
inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) {
|
||||
return Get().attenuateMusicWhenMediaPlays.value_or(fallback);
|
||||
}
|
||||
|
||||
// Bluetooth Wii Remotes / Wii U Pro Controllers. Read once before SDL's joystick
|
||||
// subsystem comes up, so a change only takes effect on the next launch.
|
||||
inline bool WiiRemotesEnabled(bool fallback = true) {
|
||||
// Bluetooth Wii Remotes. Read once before SDL's joystick subsystem comes up, so a
|
||||
// change only takes effect on the next launch.
|
||||
inline bool WiiRemotesEnabled(bool fallback = false) {
|
||||
return Get().wiiRemotes.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the Bluetooth Wii Remote driver switch.
|
||||
inline bool SetWiiRemotesEnabled(bool value) {
|
||||
Mutable().wiiRemotes = value;
|
||||
return WriteSetting("controller", "wii_remotes", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Whether to keep rescanning Bluetooth while no Wii controller is connected.
|
||||
inline bool WiiContinuousScanEnabled(bool fallback = false) {
|
||||
return Get().wiiContinuousScan.value_or(fallback);
|
||||
inline bool WiiAutoPairEnabled(bool fallback = true) {
|
||||
return Get().wiiAutoPair.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the continuous scanning switch.
|
||||
inline bool SetWiiContinuousScanEnabled(bool value) {
|
||||
Mutable().wiiContinuousScan = value;
|
||||
return WriteSetting("controller", "wii_continuous_scan", value ? "true" : "false");
|
||||
inline bool SetWiiAutoPairEnabled(bool value) {
|
||||
Mutable().wiiAutoPair = value;
|
||||
return WriteSetting("controller", "wii_auto_pair", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Stored correction of the remote identified by `remoteKey`, if it has one.
|
||||
inline std::optional<std::array<double, 3>> WiiAccelOffsetFor(const std::string& remoteKey) {
|
||||
const auto it = Get().wiiAccelOffsets.find(remoteKey);
|
||||
if (it == Get().wiiAccelOffsets.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it->second;
|
||||
// Defaults are the console's.
|
||||
inline bool WiiSensorBarAbove(bool fallback = false) {
|
||||
return Get().wiiSensorBarAbove.value_or(fallback);
|
||||
}
|
||||
|
||||
// Whether to write the per-frame accelerometer trace (off unless asked for).
|
||||
inline bool WiiAccelTraceEnabled(bool fallback = false) {
|
||||
return Get().wiiAccelTrace.value_or(fallback);
|
||||
inline bool SetWiiSensorBarAbove(bool value) {
|
||||
Mutable().wiiSensorBarAbove = value;
|
||||
return WriteSetting("controller", "wii_sensor_bar", value ? "\"above\"" : "\"below\"");
|
||||
}
|
||||
|
||||
// Persists the accelerometer correction of one remote. `remoteKey` must be a bare TOML key.
|
||||
inline bool SetWiiAccelOffsetFor(const std::string& remoteKey, const std::array<double, 3>& offset) {
|
||||
Mutable().wiiAccelOffsets[remoteKey] = offset;
|
||||
// Always float literals, so a whole-number offset does not come back as a TOML integer.
|
||||
std::ostringstream formatted;
|
||||
formatted << std::fixed << std::setprecision(4) << '[' << offset[0] << ", " << offset[1] << ", " << offset[2]
|
||||
<< ']';
|
||||
return WriteSetting("controller.wii_accel_offsets", remoteKey, formatted.str());
|
||||
inline int32_t WiiIrSensitivity(int32_t fallback = 3) {
|
||||
return Get().wiiIrSensitivity.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetWiiIrSensitivity(int32_t value) {
|
||||
Mutable().wiiIrSensitivity = value;
|
||||
return WriteSetting("controller", "wii_ir_sensitivity", std::to_string(value));
|
||||
}
|
||||
|
||||
inline int32_t WiiSpeakerVolume(int32_t fallback = 0x59) {
|
||||
return Get().wiiSpeakerVolume.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetWiiSpeakerVolume(int32_t value) {
|
||||
Mutable().wiiSpeakerVolume = value;
|
||||
return WriteSetting("controller", "wii_speaker_volume", std::to_string(value));
|
||||
}
|
||||
|
||||
inline bool WiiRumbleEnabled(bool fallback = true) {
|
||||
return Get().wiiRumble.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetWiiRumbleEnabled(bool value) {
|
||||
Mutable().wiiRumble = value;
|
||||
return WriteSetting("controller", "wii_rumble", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Target frame rate for frame interpolation, or 0 to disable it.
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
struct PADStatus;
|
||||
|
||||
// Real Wii Remotes paired over Bluetooth.
|
||||
//
|
||||
// SDL 3 ships a HIDAPI driver for them (Wii Remote alone, with Nunchuk, with a
|
||||
// Classic Controller, and the Wii U Pro Controller) that exposes each as a
|
||||
// regular SDL gamepad; it is off by default on SDL's side and ConfigureSdlHints
|
||||
// turns it on. A remote, alone or with a Nunchuk or Classic Controller, is
|
||||
// handed to the game as a real Wii Remote with that extension: the KPAD/WPAD
|
||||
// HLE builds a KPADStatus (and the WPADCLStatus behind KPADGetUnifiedWpadStatus)
|
||||
// from it every frame (see ReadKpadSample), so the game's own code does wheelies,
|
||||
// tricks, Wii Wheel steering and the Classic Controller layout, and plugging an
|
||||
// extension in or out mid-game switches control scheme like on the console. Only
|
||||
// the Wii U Pro Controller, which has no Wii-era equivalent, goes through
|
||||
// aurora's PAD layer as a GameCube pad.
|
||||
namespace WiiRemoteInput {
|
||||
|
||||
enum class Kind : uint8_t {
|
||||
NotWii,
|
||||
Remote, // Wii Remote with no extension
|
||||
RemoteWithNunchuk,
|
||||
RemoteWithClassic,
|
||||
WiiUPro,
|
||||
};
|
||||
|
||||
// Must run before SDL's joystick subsystem is initialized (aurora does that
|
||||
// inside aurora_initialize); SDL only consults the hint on its first device scan.
|
||||
void ConfigureSdlHints(bool enabled);
|
||||
|
||||
// Classifies a gamepad by the name SDL's Wii driver reports for it.
|
||||
Kind KindForName(const char* gamepadName);
|
||||
// Classification of the SDL gamepad currently assigned to a game port.
|
||||
Kind KindForPort(uint32_t port);
|
||||
const char* KindLabel(Kind kind);
|
||||
|
||||
// One frame of a Wii Remote in the units KPAD uses.
|
||||
struct KpadSample {
|
||||
uint32_t hold = 0; // WPAD button bits (WPAD_BUTTON_*), Nunchuk C/Z included
|
||||
float acc[3] = {}; // remote accelerometer in g, KPAD frame (rest: y = -1)
|
||||
bool hasNunchuk = false;
|
||||
float stick[2] = {}; // Nunchuk stick, -1..1, +y up
|
||||
float nunchukAcc[3] = {};
|
||||
bool hasClassic = false;
|
||||
uint32_t clHold = 0; // WPAD_CL_BUTTON_* bits
|
||||
float clLStick[2] = {}; // Classic sticks, -1..1, +y up
|
||||
float clRStick[2] = {};
|
||||
int16_t clLStickRaw[2] = {}; // as WPADCLStatus reports them: -512..511, centre 0, +y up
|
||||
int16_t clRStickRaw[2] = {};
|
||||
uint8_t clTriggerL = 0; // 0..255; SDL only exposes the digital click
|
||||
uint8_t clTriggerR = 0;
|
||||
};
|
||||
|
||||
// What the game should see on `chan`: the controller SDL has there right now,
|
||||
// or, for a few seconds after a remote vanished, the kind it had. SDL's driver
|
||||
// destroys and re-creates the joystick when an extension is plugged in or out,
|
||||
// and the console never disconnects for that, so the gap is papered over with
|
||||
// neutral input instead of a "communications interrupted" prompt.
|
||||
Kind EffectiveKind(uint32_t chan);
|
||||
// True when the game reads `chan` through KPAD: a Wii Remote alone, with a
|
||||
// Nunchuk or with a Classic Controller (live or within the swap grace period).
|
||||
bool IsRemoteChannel(uint32_t chan);
|
||||
// Reads the current state of the remote on `chan`; false when IsRemoteChannel
|
||||
// is false. During the swap grace period the sample is neutral.
|
||||
bool ReadKpadSample(uint32_t chan, KpadSample& sample);
|
||||
|
||||
// Remote accelerometer for the overlay readout: the SDL sample in g after the
|
||||
// zero-point correction (x right across the face, y out of the button face, z
|
||||
// towards the user) and the KPAD vector built from it. False when the port has
|
||||
// no remote or SDL has not delivered a sample yet.
|
||||
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
|
||||
|
||||
// Residual zero-point trim on top of SDL's factory-calibrated decode, stored per
|
||||
// physical remote under [controller.wii_accel_offsets]. Expects the remote lying
|
||||
// still, buttons up; Poll() averages about a second and stores it for that remote.
|
||||
void StartAccelCalibration(uint32_t chan);
|
||||
// Forgets the stored correction of the remote on `chan`.
|
||||
void ClearAccelCalibration(uint32_t chan);
|
||||
bool IsAccelCalibrating(uint32_t chan);
|
||||
// 0..1 while a calibration is collecting samples.
|
||||
float AccelCalibrationProgress();
|
||||
// Correction in effect for the remote on `chan` (zeros when uncalibrated), or
|
||||
// nullopt when the port has no remote.
|
||||
std::optional<std::array<double, 3>> ReadAccelOffset(uint32_t chan);
|
||||
// Outcome of the last calibration attempt on `chan` for the overlay, or nullptr.
|
||||
const char* AccelCalibrationMessage(uint32_t chan);
|
||||
|
||||
// Reports "no controller" on the GameCube side for every port served through
|
||||
// KPAD, so the game never sees the same remote twice. Runs after aurora's PADRead.
|
||||
void HideRemotesFromPad(PADStatus* statuses, uint32_t count);
|
||||
|
||||
// Dolphin-style continuous scanning. SDL's HIDAPI Wii driver drops a remote on
|
||||
// a failed Bluetooth read or an extension change and never re-adds it on its
|
||||
// own. Poll() runs once per PADRead and, while no Wii controller is present,
|
||||
// periodically forces SDL to re-enumerate HIDAPI by toggling the driver hint.
|
||||
void Poll();
|
||||
// Forces one re-enumeration right now (settings overlay "Rescan now").
|
||||
void RescanNow();
|
||||
// True while Poll() is looking for a remote (no Wii controller connected).
|
||||
bool IsScanning();
|
||||
// True where looking means periodic rescans; elsewhere Poll() waits for hotplug.
|
||||
bool PeriodicRescanEnabled();
|
||||
// Rescans issued since a Wii controller was last seen.
|
||||
uint32_t ScanCount();
|
||||
|
||||
} // namespace WiiRemoteInput
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
// The Wii's extension controller encryption: the game writes a random key to
|
||||
// the extension, which then XORs and offsets every byte it reports.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace Wiimote::ExtCrypto {
|
||||
|
||||
struct Tables {
|
||||
std::array<uint8_t, 8> ft{};
|
||||
std::array<uint8_t, 8> sb{};
|
||||
void Encrypt(uint8_t* data, uint32_t address, size_t length) const;
|
||||
void Decrypt(uint8_t* data, uint32_t address, size_t length) const;
|
||||
};
|
||||
|
||||
// Tables for the key the game wrote to registers 0x40..0x4F, or nullopt when it
|
||||
// does not come from the first-party key schedule.
|
||||
std::optional<Tables> FromKey(const std::array<uint8_t, 16>& keyData);
|
||||
|
||||
} // namespace Wiimote::ExtCrypto
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
// Follows the game's conversation with a remote's extension (type, calibration,
|
||||
// encryption key) so its data can be put at rest while the overlay owns input.
|
||||
|
||||
#include "wiimote_ext_crypto.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace Wiimote {
|
||||
|
||||
class ExtensionState {
|
||||
public:
|
||||
void ObserveOutput(const uint8_t* report, size_t size);
|
||||
void ObserveInput(const uint8_t* report, size_t size);
|
||||
// Centres the sticks and releases the buttons of the extension data in a
|
||||
// data report, leaving its motion data alone.
|
||||
void Neutralize(std::vector<uint8_t>& report) const;
|
||||
|
||||
private:
|
||||
enum class Type : uint8_t { Unknown, Nunchuk, Classic };
|
||||
|
||||
void Reset();
|
||||
uint8_t Calibration(size_t index, uint8_t fallback) const;
|
||||
|
||||
Type m_type = Type::Unknown;
|
||||
uint8_t m_format = 0;
|
||||
bool m_encrypted = false;
|
||||
std::array<uint8_t, 16> m_key{};
|
||||
std::optional<ExtCrypto::Tables> m_tables;
|
||||
std::array<uint8_t, 16> m_calibration{};
|
||||
bool m_haveCalibration = false;
|
||||
uint32_t m_readAddress = 0;
|
||||
};
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
// Physical Wii Remotes on the PC. Each one is attached to one of the four
|
||||
// virtual remotes of the emulated Bluetooth controller and its HID reports are
|
||||
// passed through unchanged, so the game's own WPAD/KPAD drive it (IR, speaker,
|
||||
// rumble, extensions, battery).
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace Wiimote {
|
||||
|
||||
struct SlotInfo {
|
||||
bool attached = false;
|
||||
// The game disconnected it and the platform could not power it off; any
|
||||
// button wakes it up again.
|
||||
bool asleep = false;
|
||||
Bluetooth::LinkState link = Bluetooth::LinkState::Down;
|
||||
std::string model; // "Wii Remote" / "Wii Remote Plus"
|
||||
std::string extension; // empty when none
|
||||
int battery = -1; // 0..4 like the game's battery bars, -1 unknown
|
||||
};
|
||||
|
||||
struct PairingInfo {
|
||||
bool supported = false; // this platform can discover and pair remotes itself
|
||||
bool searching = false;
|
||||
std::string status; // last pairing outcome, for the overlay
|
||||
};
|
||||
|
||||
// Must run before SDL's joystick subsystem starts (aurora_initialize).
|
||||
void ConfigureSdlHints(bool enabled);
|
||||
// Starts device scanning when Wii Remotes are enabled.
|
||||
void Initialize();
|
||||
// Powers the remotes off, like a console shutting down, and stops the threads.
|
||||
void Shutdown();
|
||||
|
||||
Bluetooth::RemoteHost& Host();
|
||||
|
||||
SlotInfo Slot(size_t slot);
|
||||
PairingInfo Pairing();
|
||||
// The console's SYNC button: look for remotes in pairing mode for a while.
|
||||
void StartPairing();
|
||||
// Ends the game's connection to the remote on `slot` and powers it off.
|
||||
void Disconnect(size_t slot);
|
||||
// Setup problems worth showing (kernel driver, missing permissions), else empty.
|
||||
std::string PlatformNotice();
|
||||
// Connected remotes waiting for a free player slot.
|
||||
size_t WaitingRemotes();
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
// Host Bluetooth services the Wii Remote manager needs from the OS: finding and
|
||||
// pairing remotes in pairing mode, naming a HID device, and dropping a remote's
|
||||
// connection (which powers it off, as a console does).
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
struct SDL_hid_device_info;
|
||||
|
||||
namespace Wiimote::Platform {
|
||||
|
||||
struct Capabilities {
|
||||
bool pairing = false;
|
||||
bool powerOff = false;
|
||||
};
|
||||
|
||||
Capabilities Query();
|
||||
|
||||
// One search for remotes in pairing mode (red SYNC or 1+2); pairs and connects
|
||||
// each one found. Blocks for a few seconds. Returns the number connected.
|
||||
int DiscoverAndPair(const std::atomic<bool>& cancel, std::string& status);
|
||||
|
||||
// Disconnects the remote; it switches itself off. False when not possible.
|
||||
bool PowerOff(const std::string& address);
|
||||
|
||||
// Bluetooth address of a HID device as 12 lowercase hex digits, or empty.
|
||||
std::string AddressFor(const SDL_hid_device_info& info);
|
||||
|
||||
// Bluetooth name ("Nintendo RVL-CNT-01-TR", ...) or the HID product string.
|
||||
std::string NameFor(const std::string& address, const SDL_hid_device_info& info);
|
||||
|
||||
// False only when the OS knows the remote is not connected right now.
|
||||
bool MaybeConnected(const std::string& address);
|
||||
|
||||
// False when the OS driver would fight over the device (Linux hid-wiimote).
|
||||
bool Usable(const SDL_hid_device_info& info);
|
||||
|
||||
// Setup problems worth showing in the overlay, else empty.
|
||||
std::string Notice();
|
||||
|
||||
} // namespace Wiimote::Platform
|
||||
@@ -29,6 +29,8 @@ constexpr uint32_t kAIDmaCallbackAddr = 0x80386480u;
|
||||
// Max completed 3 ms DMA blocks delivered per tick. Draining several at once catches up
|
||||
// backlog from a long frame without letting a large stall spiral into an unbounded loop.
|
||||
constexpr int kMaxBlocksPerTick = 4;
|
||||
// Backlog (in blocks) tolerated before a catch-up block waits on an in-flight mix.
|
||||
constexpr double kMaxDeferredBlocks = 2.0;
|
||||
|
||||
struct AIDmaState {
|
||||
std::mutex mutex;
|
||||
@@ -407,11 +409,15 @@ void Audio_HLE_Tick(CpuContext* ctx, uint32_t deltaMicros)
|
||||
|
||||
int blocksCompleted = 0;
|
||||
while (true) {
|
||||
// A block due while the previous mix is still running would block on it; leave it for the
|
||||
// next service point unless the backlog is getting long.
|
||||
const bool deferWhileMixing = AxDspHle::MixWorkerBusy();
|
||||
// Claim the block in one critical section; sample-then-consume separately gains
|
||||
// nothing since the callback below (the only reentrancy point) runs mutex-released.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_ai.mutex);
|
||||
if (g_ai.accumulatorSeconds < blockDuration) {
|
||||
if (g_ai.accumulatorSeconds < blockDuration ||
|
||||
(deferWhileMixing && g_ai.accumulatorSeconds < blockDuration * kMaxDeferredBlocks)) {
|
||||
break;
|
||||
}
|
||||
g_ai.accumulatorSeconds -= blockDuration;
|
||||
|
||||
@@ -25,6 +25,9 @@ void ServiceDeferredCallbacks();
|
||||
// Cheap when the mix already finished (the normal case).
|
||||
void JoinMixWorker();
|
||||
|
||||
// True while a deferred mix is queued or still running.
|
||||
bool MixWorkerBusy();
|
||||
|
||||
// Joins and tears down the mix worker thread. Call before the guest memory map
|
||||
// is destroyed or re-created.
|
||||
void ShutdownMixWorker();
|
||||
|
||||
@@ -112,6 +112,14 @@ public:
|
||||
ConfigureFromTaskLocked(taskPtr);
|
||||
}
|
||||
|
||||
bool MixBusy() {
|
||||
if (!m_mixThread.joinable()) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(m_mixMutex);
|
||||
return m_mixPending || m_mixBusy;
|
||||
}
|
||||
|
||||
void JoinMix() {
|
||||
if (m_mixThread.joinable()) {
|
||||
std::unique_lock<std::mutex> lock(m_mixMutex);
|
||||
@@ -1560,6 +1568,10 @@ void JoinMixWorker() {
|
||||
Instance().JoinMix();
|
||||
}
|
||||
|
||||
bool MixWorkerBusy() {
|
||||
return Instance().MixBusy();
|
||||
}
|
||||
|
||||
void ShutdownMixWorker() {
|
||||
Instance().ShutdownMixWorker();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,951 @@
|
||||
#include "bluetooth/bt_controller.h"
|
||||
|
||||
#include "bluetooth/bt_bytes.h"
|
||||
#include "bluetooth/bt_remote_link.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace Bluetooth {
|
||||
namespace HciDetail {
|
||||
|
||||
|
||||
constexpr uint16_t kOgfLinkControl = 0x01;
|
||||
constexpr uint16_t kOgfLinkPolicy = 0x02;
|
||||
constexpr uint16_t kOgfControllerBaseband = 0x03;
|
||||
constexpr uint16_t kOgfInformational = 0x04;
|
||||
constexpr uint16_t kOgfStatus = 0x05;
|
||||
constexpr uint16_t kOgfVendor = 0x3F;
|
||||
|
||||
constexpr uint8_t kEventInquiryComplete = 0x01;
|
||||
constexpr uint8_t kEventInquiryResult = 0x02;
|
||||
constexpr uint8_t kEventConnectionComplete = 0x03;
|
||||
constexpr uint8_t kEventDisconnectionComplete = 0x05;
|
||||
constexpr uint8_t kEventAuthenticationComplete = 0x06;
|
||||
constexpr uint8_t kEventRemoteNameComplete = 0x07;
|
||||
constexpr uint8_t kEventEncryptionChange = 0x08;
|
||||
constexpr uint8_t kEventRemoteFeatures = 0x0B;
|
||||
constexpr uint8_t kEventRemoteVersion = 0x0C;
|
||||
constexpr uint8_t kEventCommandComplete = 0x0E;
|
||||
constexpr uint8_t kEventCommandStatus = 0x0F;
|
||||
constexpr uint8_t kEventRoleChange = 0x12;
|
||||
constexpr uint8_t kEventNumberOfCompletedPackets = 0x13;
|
||||
constexpr uint8_t kEventModeChange = 0x14;
|
||||
constexpr uint8_t kEventReturnLinkKeys = 0x15;
|
||||
constexpr uint8_t kEventPinCodeRequest = 0x16;
|
||||
constexpr uint8_t kEventLinkKeyRequest = 0x17;
|
||||
constexpr uint8_t kEventLinkKeyNotification = 0x18;
|
||||
constexpr uint8_t kEventClockOffset = 0x1C;
|
||||
constexpr uint8_t kEventPacketTypeChanged = 0x1D;
|
||||
constexpr uint8_t kEventInquiryResultWithRssi = 0x22;
|
||||
constexpr uint8_t kEventExtendedInquiryResult = 0x2F;
|
||||
|
||||
constexpr uint8_t kStatusSuccess = 0x00;
|
||||
constexpr uint8_t kStatusUnknownCommand = 0x01;
|
||||
constexpr uint8_t kStatusUnknownConnection = 0x02;
|
||||
constexpr uint8_t kStatusPageTimeout = 0x04;
|
||||
constexpr uint8_t kStatusAuthenticationFailure = 0x05;
|
||||
constexpr uint8_t kStatusKeyMissing = 0x06;
|
||||
constexpr uint8_t kStatusUnsupportedFeature = 0x11;
|
||||
constexpr uint8_t kReasonLocalHostTerminated = 0x16;
|
||||
|
||||
// What a Wii's BCM2045 reports (the version makes WUD apply its usual patch).
|
||||
constexpr BdAddr kLocalAddress = {0x11, 0x02, 0x19, 0x79, 0x00, 0xFF};
|
||||
constexpr std::array<uint8_t, 8> kLocalFeatures = {0xFF, 0xFF, 0x8D, 0xFE, 0x9B, 0xF9, 0x00, 0x80};
|
||||
// Never advertise more than 10 ACL buffers: the stack keeps the count in a u8.
|
||||
constexpr uint16_t kAclBufferSize = 339;
|
||||
constexpr uint16_t kAclBufferCount = 10;
|
||||
constexpr uint8_t kScoBufferSize = 64;
|
||||
|
||||
// A real RVL-CNT-01.
|
||||
constexpr std::array<uint8_t, 8> kRemoteFeatures = {0xBC, 0x02, 0x04, 0x38, 0x08, 0x00, 0x00, 0x00};
|
||||
constexpr uint8_t kRemoteLmpVersion = 0x02;
|
||||
constexpr uint16_t kRemoteManufacturer = 0x000F;
|
||||
constexpr uint16_t kRemoteLmpSubversion = 0x0229;
|
||||
constexpr uint16_t kRemoteClockOffset = 0x3818;
|
||||
constexpr uint8_t kInquiryRssi = 0xD8; // -40 dBm
|
||||
constexpr uint64_t kInquiryUnitUs = 1'280'000;
|
||||
constexpr uint16_t kMaxStoredKeys = 0x00FF;
|
||||
constexpr uint8_t kKeysPerEvent = 11;
|
||||
|
||||
void PutAddr(std::vector<uint8_t>& out, const BdAddr& addr) {
|
||||
out.insert(out.end(), addr.begin(), addr.end());
|
||||
}
|
||||
|
||||
BdAddr ReadAddr(const uint8_t* p) {
|
||||
BdAddr addr{};
|
||||
std::memcpy(addr.data(), p, addr.size());
|
||||
return addr;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Event(uint8_t code, const std::vector<uint8_t>& params) {
|
||||
std::vector<uint8_t> event;
|
||||
event.reserve(2 + params.size());
|
||||
event.push_back(code);
|
||||
event.push_back(static_cast<uint8_t>(params.size()));
|
||||
event.insert(event.end(), params.begin(), params.end());
|
||||
return event;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 16> SlotLinkKey(size_t slot) {
|
||||
std::array<uint8_t, 16> key{};
|
||||
for (size_t i = 0; i < key.size(); ++i) {
|
||||
key[i] = static_cast<uint8_t>(0xA0 + slot + i * 0x11);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace HciDetail
|
||||
|
||||
using namespace HciDetail;
|
||||
|
||||
Controller::Controller(RemoteHost& host) : m_host(host) {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
m_links[slot] = std::make_unique<RemoteLink>(*this, slot);
|
||||
m_storedKeys.push_back({RemoteAddress(slot), SlotLinkKey(slot)});
|
||||
}
|
||||
ResetState(true);
|
||||
}
|
||||
|
||||
Controller::~Controller() = default;
|
||||
|
||||
BdAddr Controller::LocalAddress() {
|
||||
return kLocalAddress;
|
||||
}
|
||||
|
||||
BdAddr Controller::RemoteAddress(size_t slot) {
|
||||
return {0x11, 0x02, 0x19, 0x79, 0x00, static_cast<uint8_t>(slot)};
|
||||
}
|
||||
|
||||
uint16_t Controller::ConnectionHandle(size_t slot) {
|
||||
return static_cast<uint16_t>(0x0100 + slot);
|
||||
}
|
||||
|
||||
void Controller::Open(uint64_t nowUs) {
|
||||
(void)nowUs;
|
||||
ResetState(true);
|
||||
m_open = true;
|
||||
}
|
||||
|
||||
void Controller::Close() {
|
||||
ResetState(true);
|
||||
m_open = false;
|
||||
}
|
||||
|
||||
void Controller::ResetState(bool keepStoredKeys) {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
RemoteLink& link = *m_links[slot];
|
||||
// A reset drops the radio links: the remotes lose their connection.
|
||||
if (link.State() == LinkState::Ready) {
|
||||
m_host.OnGuestDisconnect(slot);
|
||||
}
|
||||
link.Reset();
|
||||
}
|
||||
if (!keepStoredKeys) {
|
||||
m_storedKeys.clear();
|
||||
}
|
||||
if (m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(false);
|
||||
m_hostInquiryNotified = false;
|
||||
}
|
||||
m_inquiry = {};
|
||||
m_scanEnable = 0;
|
||||
m_pageTimeoutSlots = 0x2000;
|
||||
m_inquiryMode = 0;
|
||||
m_classOfDevice = {};
|
||||
m_localName = {};
|
||||
m_completedPackets = {};
|
||||
for (Reassembly& r : m_reassembly) r = {};
|
||||
m_events.clear();
|
||||
m_acl.clear();
|
||||
m_pending.clear();
|
||||
m_authPending = {};
|
||||
}
|
||||
|
||||
LinkState Controller::SlotLinkState(size_t slot) const {
|
||||
return slot < kRemoteSlots ? m_links[slot]->State() : LinkState::Down;
|
||||
}
|
||||
|
||||
int Controller::SlotForAddress(const BdAddr& addr) const {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (RemoteAddress(slot) == addr) return static_cast<int>(slot);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int Controller::SlotForHandle(uint16_t handle) const {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (ConnectionHandle(slot) == (handle & 0x0FFF) &&
|
||||
m_links[slot]->BasebandState() == RemoteLink::Baseband::Connected) {
|
||||
return static_cast<int>(slot);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const Controller::StoredKey* Controller::FindStoredKey(const BdAddr& addr) const {
|
||||
for (const StoredKey& key : m_storedKeys) {
|
||||
if (key.addr == addr) return &key;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Controller::QueueEvent(std::vector<uint8_t> event) {
|
||||
m_events.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void Controller::QueueAcl(uint16_t handle, const std::vector<uint8_t>& l2capFrame) {
|
||||
std::vector<uint8_t> packet;
|
||||
packet.reserve(4 + l2capFrame.size());
|
||||
// Packet boundary 0x2: first (and only) fragment, automatically flushable.
|
||||
PutLe16(packet, static_cast<uint16_t>((handle & 0x0FFF) | 0x2000));
|
||||
PutLe16(packet, static_cast<uint16_t>(l2capFrame.size()));
|
||||
packet.insert(packet.end(), l2capFrame.begin(), l2capFrame.end());
|
||||
m_acl.push_back(std::move(packet));
|
||||
}
|
||||
|
||||
bool Controller::PopEvent(std::vector<uint8_t>& event) {
|
||||
if (m_events.empty()) return false;
|
||||
event = std::move(m_events.front());
|
||||
m_events.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Controller::PopAcl(std::vector<uint8_t>& packet) {
|
||||
if (!m_events.empty() || m_acl.empty()) return false;
|
||||
packet = std::move(m_acl.front());
|
||||
m_acl.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Controller::CommandComplete(uint16_t opcode, const std::vector<uint8_t>& params) {
|
||||
std::vector<uint8_t> body = {0x01};
|
||||
PutLe16(body, opcode);
|
||||
body.insert(body.end(), params.begin(), params.end());
|
||||
QueueEvent(Event(kEventCommandComplete, body));
|
||||
}
|
||||
|
||||
void Controller::CommandStatus(uint16_t opcode, uint8_t status) {
|
||||
std::vector<uint8_t> body = {status, 0x01};
|
||||
PutLe16(body, opcode);
|
||||
QueueEvent(Event(kEventCommandStatus, body));
|
||||
}
|
||||
|
||||
void Controller::UnknownCommand(uint16_t opcode) {
|
||||
std::cerr << "[bluetooth] unsupported HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
CommandStatus(opcode, kStatusUnknownCommand);
|
||||
}
|
||||
|
||||
void Controller::ExecuteCommand(const uint8_t* packet, size_t size, uint64_t nowUs) {
|
||||
if (size < 3) return;
|
||||
const uint16_t opcode = Le16(packet);
|
||||
const size_t length = packet[2];
|
||||
if (3 + length > size) {
|
||||
std::cerr << "[bluetooth] truncated HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
return;
|
||||
}
|
||||
const uint8_t* p = packet + 3;
|
||||
switch (opcode >> 10) {
|
||||
case kOgfLinkControl: HandleLinkControl(opcode, p, length, nowUs); break;
|
||||
case kOgfLinkPolicy: HandleLinkPolicy(opcode, p, length); break;
|
||||
case kOgfControllerBaseband: HandleControllerBaseband(opcode, p, length, nowUs); break;
|
||||
case kOgfInformational: HandleInformational(opcode, p, length); break;
|
||||
case kOgfStatus: HandleStatusParameters(opcode, p, length); break;
|
||||
case kOgfVendor: HandleVendor(opcode, p, length); break;
|
||||
default: UnknownCommand(opcode); break;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::HandleLinkControl(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs) {
|
||||
const auto handleSlot = [&](size_t offset) { return n >= offset + 2 ? SlotForHandle(Le16(p + offset)) : -1; };
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0001: { // Inquiry
|
||||
if (n < 5) break;
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
m_inquiry = {};
|
||||
m_inquiry.active = true;
|
||||
m_inquiry.endUs = nowUs + std::max<uint8_t>(p[3], 1) * kInquiryUnitUs;
|
||||
m_inquiry.maxResponses = p[4];
|
||||
if (!m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(true);
|
||||
m_hostInquiryNotified = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x0002: // Inquiry Cancel
|
||||
m_inquiry = {};
|
||||
if (m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(false);
|
||||
m_hostInquiryNotified = false;
|
||||
}
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0003: // Periodic Inquiry Mode
|
||||
case 0x0004: // Exit Periodic Inquiry Mode
|
||||
CommandComplete(opcode, {kStatusUnsupportedFeature});
|
||||
return;
|
||||
case 0x0005: { // Create Connection
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_links[slot]->OnPaged(nowUs)) {
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
QueueEvent(Event(kEventConnectionComplete, body));
|
||||
} else {
|
||||
std::vector<uint8_t> body = {kStatusPageTimeout, 0x00, 0x00};
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
m_pending.push_back({nowUs + PageTimeoutUs(), Event(kEventConnectionComplete, body)});
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x0006: { // Disconnect
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0 || n < 3) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.push_back(kReasonLocalHostTerminated);
|
||||
QueueEvent(Event(kEventDisconnectionComplete, body));
|
||||
m_completedPackets[slot] = 0;
|
||||
m_reassembly[slot] = {};
|
||||
m_authPending[slot] = false;
|
||||
m_links[slot]->OnDisconnected(true, nowUs);
|
||||
return;
|
||||
}
|
||||
case 0x0008: { // Create Connection Cancel
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
auto it = std::find_if(m_pending.begin(), m_pending.end(), [&](const PendingEvent& e) {
|
||||
return e.event[0] == kEventConnectionComplete && ReadAddr(&e.event[5]) == addr;
|
||||
});
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(it == m_pending.end() ? kStatusUnknownConnection
|
||||
: kStatusSuccess)};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
if (it != m_pending.end()) {
|
||||
std::vector<uint8_t> complete = it->event;
|
||||
complete[2] = kStatusUnknownConnection;
|
||||
m_pending.erase(it);
|
||||
QueueEvent(std::move(complete));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x0009: { // Accept Connection Request
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const uint8_t role = p[6];
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot < 0 || !m_links[slot]->IsRequesting()) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
if (role == 0x00) {
|
||||
std::vector<uint8_t> change = {kStatusSuccess};
|
||||
PutAddr(change, addr);
|
||||
change.push_back(0x00);
|
||||
QueueEvent(Event(kEventRoleChange, change));
|
||||
}
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
QueueEvent(Event(kEventConnectionComplete, body));
|
||||
m_completedPackets[slot] = 0;
|
||||
m_reassembly[slot] = {};
|
||||
m_links[slot]->OnAccepted(role);
|
||||
return;
|
||||
}
|
||||
case 0x000A: { // Reject Connection Request
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot < 0 || !m_links[slot]->IsRequesting()) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {p[6], 0x00, 0x00};
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
QueueEvent(Event(kEventConnectionComplete, body));
|
||||
m_links[slot]->OnRejected(p[6], nowUs);
|
||||
return;
|
||||
}
|
||||
case 0x000B: { // Link Key Request Reply
|
||||
if (n < 22) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_authPending[slot]) {
|
||||
m_authPending[slot] = false;
|
||||
const bool match = std::equal(p + 6, p + 22, SlotLinkKey(slot).begin());
|
||||
std::vector<uint8_t> auth = {static_cast<uint8_t>(match ? kStatusSuccess : kStatusKeyMissing)};
|
||||
PutLe16(auth, ConnectionHandle(slot));
|
||||
QueueEvent(Event(kEventAuthenticationComplete, auth));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x000C: { // Link Key Request Negative Reply
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_authPending[slot]) {
|
||||
std::vector<uint8_t> request;
|
||||
PutAddr(request, addr);
|
||||
QueueEvent(Event(kEventPinCodeRequest, request));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x000D: { // PIN Code Request Reply
|
||||
if (n < 23) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot < 0 || !m_authPending[slot]) return;
|
||||
m_authPending[slot] = false;
|
||||
// A remote takes the host's address (SYNC button) or its own (1+2) as PIN.
|
||||
const uint8_t pinLength = p[6];
|
||||
const uint8_t* pin = p + 7;
|
||||
const BdAddr local = LocalAddress();
|
||||
const bool valid = pinLength == 6 && (std::equal(pin, pin + 6, local.begin()) ||
|
||||
std::equal(pin, pin + 6, addr.begin()));
|
||||
if (valid) {
|
||||
std::vector<uint8_t> notification;
|
||||
PutAddr(notification, addr);
|
||||
const auto key = SlotLinkKey(slot);
|
||||
notification.insert(notification.end(), key.begin(), key.end());
|
||||
notification.push_back(0x00); // combination key
|
||||
QueueEvent(Event(kEventLinkKeyNotification, notification));
|
||||
}
|
||||
std::vector<uint8_t> auth = {static_cast<uint8_t>(valid ? kStatusSuccess : kStatusAuthenticationFailure)};
|
||||
PutLe16(auth, ConnectionHandle(slot));
|
||||
QueueEvent(Event(kEventAuthenticationComplete, auth));
|
||||
return;
|
||||
}
|
||||
case 0x000E: { // PIN Code Request Negative Reply
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_authPending[slot]) {
|
||||
m_authPending[slot] = false;
|
||||
std::vector<uint8_t> auth = {kStatusKeyMissing};
|
||||
PutLe16(auth, ConnectionHandle(slot));
|
||||
QueueEvent(Event(kEventAuthenticationComplete, auth));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x000F: { // Change Connection Packet Type
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0 || n < 4) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutLe16(body, Le16(p + 2));
|
||||
QueueEvent(Event(kEventPacketTypeChanged, body));
|
||||
return;
|
||||
}
|
||||
case 0x0011: { // Authentication Requested
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
m_authPending[slot] = true;
|
||||
std::vector<uint8_t> request;
|
||||
PutAddr(request, RemoteAddress(slot));
|
||||
QueueEvent(Event(kEventLinkKeyRequest, request));
|
||||
return;
|
||||
}
|
||||
case 0x0013: { // Set Connection Encryption
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0 || n < 3) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.push_back(p[2]);
|
||||
QueueEvent(Event(kEventEncryptionChange, body));
|
||||
return;
|
||||
}
|
||||
case 0x0019: { // Remote Name Request
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
const int slot = SlotForAddress(addr);
|
||||
const bool reachable = slot >= 0 && (m_links[slot]->BasebandState() == RemoteLink::Baseband::Connected ||
|
||||
m_links[slot]->Discoverable());
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(reachable ? kStatusSuccess : kStatusPageTimeout)};
|
||||
PutAddr(body, addr);
|
||||
std::array<char, 248> name{};
|
||||
if (reachable) {
|
||||
std::strncpy(name.data(), m_host.DeviceName(slot), name.size() - 1);
|
||||
}
|
||||
body.insert(body.end(), name.begin(), name.end());
|
||||
if (reachable) {
|
||||
QueueEvent(Event(kEventRemoteNameComplete, body));
|
||||
} else {
|
||||
m_pending.push_back({nowUs + PageTimeoutUs(), Event(kEventRemoteNameComplete, body)});
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x001A: { // Remote Name Request Cancel
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
auto it = std::find_if(m_pending.begin(), m_pending.end(), [&](const PendingEvent& e) {
|
||||
return e.event[0] == kEventRemoteNameComplete && ReadAddr(&e.event[3]) == addr;
|
||||
});
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(it == m_pending.end() ? kStatusUnknownConnection
|
||||
: kStatusSuccess)};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
if (it != m_pending.end()) {
|
||||
std::vector<uint8_t> complete = it->event;
|
||||
complete[2] = kStatusUnknownConnection;
|
||||
m_pending.erase(it);
|
||||
QueueEvent(std::move(complete));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x001B: { // Read Remote Supported Features
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.insert(body.end(), kRemoteFeatures.begin(), kRemoteFeatures.end());
|
||||
QueueEvent(Event(kEventRemoteFeatures, body));
|
||||
return;
|
||||
}
|
||||
case 0x001D: { // Read Remote Version Information
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.push_back(kRemoteLmpVersion);
|
||||
PutLe16(body, kRemoteManufacturer);
|
||||
PutLe16(body, kRemoteLmpSubversion);
|
||||
QueueEvent(Event(kEventRemoteVersion, body));
|
||||
return;
|
||||
}
|
||||
case 0x001F: { // Read Clock Offset
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutLe16(body, kRemoteClockOffset);
|
||||
QueueEvent(Event(kEventClockOffset, body));
|
||||
return;
|
||||
}
|
||||
case 0x0007: // Add SCO Connection
|
||||
case 0x0028: // Setup Synchronous Connection
|
||||
case 0x0029: // Accept Synchronous Connection Request
|
||||
case 0x002A: // Reject Synchronous Connection Request
|
||||
CommandStatus(opcode, kStatusUnsupportedFeature);
|
||||
return;
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
std::cerr << "[bluetooth] short HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
CommandStatus(opcode, kStatusUnknownCommand);
|
||||
}
|
||||
|
||||
void Controller::HandleLinkPolicy(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
|
||||
const auto modeChange = [&](uint8_t mode, uint16_t interval) {
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(static_cast<size_t>(slot)));
|
||||
body.push_back(mode);
|
||||
PutLe16(body, interval);
|
||||
QueueEvent(Event(kEventModeChange, body));
|
||||
};
|
||||
const auto handleReply = [&](std::vector<uint8_t> extra) {
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
|
||||
PutLe16(body, n >= 2 ? Le16(p) : 0);
|
||||
body.insert(body.end(), extra.begin(), extra.end());
|
||||
CommandComplete(opcode, body);
|
||||
};
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0001: modeChange(0x01, n >= 4 ? Le16(p + 2) : 0); return; // Hold Mode
|
||||
case 0x0003: modeChange(0x02, n >= 4 ? Le16(p + 2) : 0); return; // Sniff Mode
|
||||
case 0x0004: modeChange(0x00, 0); return; // Exit Sniff Mode
|
||||
case 0x0005: modeChange(0x03, n >= 4 ? Le16(p + 2) : 0); return; // Park State
|
||||
case 0x0006: modeChange(0x00, 0); return; // Exit Park State
|
||||
case 0x0009: handleReply({0x00}); return; // Role Discovery
|
||||
case 0x000B: { // Switch Role
|
||||
if (n < 7) break;
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
body.insert(body.end(), p, p + 7);
|
||||
QueueEvent(Event(kEventRoleChange, body));
|
||||
return;
|
||||
}
|
||||
case 0x000C: handleReply({0x05, 0x00}); return; // Read Link Policy Settings
|
||||
case 0x000D: handleReply({}); return; // Write Link Policy Settings
|
||||
case 0x000E: CommandComplete(opcode, {kStatusSuccess, 0x05, 0x00}); return; // Read Default Link Policy
|
||||
case 0x000F: CommandComplete(opcode, {kStatusSuccess}); return; // Write Default Link Policy
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusUnknownCommand);
|
||||
}
|
||||
|
||||
void Controller::HandleControllerBaseband(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs) {
|
||||
(void)nowUs;
|
||||
const auto handleReply = [&](std::vector<uint8_t> extra) {
|
||||
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
|
||||
PutLe16(body, n >= 2 ? Le16(p) : 0);
|
||||
body.insert(body.end(), extra.begin(), extra.end());
|
||||
CommandComplete(opcode, body);
|
||||
};
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0003: // Reset
|
||||
ResetState(true);
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x000D: { // Read Stored Link Key
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const bool readAll = p[6] == 0x01;
|
||||
std::vector<StoredKey> keys;
|
||||
for (const StoredKey& key : m_storedKeys) {
|
||||
if (readAll || key.addr == addr) keys.push_back(key);
|
||||
}
|
||||
for (size_t start = 0; start < keys.size(); start += kKeysPerEvent) {
|
||||
const size_t count = std::min<size_t>(kKeysPerEvent, keys.size() - start);
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(count)};
|
||||
for (size_t i = start; i < start + count; ++i) {
|
||||
PutAddr(body, keys[i].addr);
|
||||
body.insert(body.end(), keys[i].key.begin(), keys[i].key.end());
|
||||
}
|
||||
QueueEvent(Event(kEventReturnLinkKeys, body));
|
||||
}
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, kMaxStoredKeys);
|
||||
PutLe16(body, static_cast<uint16_t>(keys.size()));
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0011: { // Write Stored Link Key
|
||||
if (n < 1) break;
|
||||
const uint8_t count = p[0];
|
||||
if (n < 1 + count * size_t{22}) break;
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
const uint8_t* entry = p + 1 + i * 22;
|
||||
StoredKey key{ReadAddr(entry), {}};
|
||||
std::copy(entry + 6, entry + 22, key.key.begin());
|
||||
auto existing = std::find_if(m_storedKeys.begin(), m_storedKeys.end(),
|
||||
[&](const StoredKey& k) { return k.addr == key.addr; });
|
||||
if (existing != m_storedKeys.end()) {
|
||||
*existing = key;
|
||||
} else if (m_storedKeys.size() < kMaxStoredKeys) {
|
||||
m_storedKeys.push_back(key);
|
||||
}
|
||||
}
|
||||
CommandComplete(opcode, {kStatusSuccess, count});
|
||||
return;
|
||||
}
|
||||
case 0x0012: { // Delete Stored Link Key
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const bool deleteAll = p[6] == 0x01;
|
||||
const size_t before = m_storedKeys.size();
|
||||
m_storedKeys.erase(std::remove_if(m_storedKeys.begin(), m_storedKeys.end(),
|
||||
[&](const StoredKey& k) { return deleteAll || k.addr == addr; }),
|
||||
m_storedKeys.end());
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, static_cast<uint16_t>(before - m_storedKeys.size()));
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0013: // Change Local Name
|
||||
if (n < m_localName.size()) break;
|
||||
std::memcpy(m_localName.data(), p, m_localName.size());
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0014: { // Read Local Name
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
body.insert(body.end(), m_localName.begin(), m_localName.end());
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0018: // Write Page Timeout
|
||||
if (n < 2) break;
|
||||
m_pageTimeoutSlots = Le16(p);
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0019: // Read Scan Enable
|
||||
CommandComplete(opcode, {kStatusSuccess, m_scanEnable});
|
||||
return;
|
||||
case 0x001A: // Write Scan Enable
|
||||
if (n < 1) break;
|
||||
m_scanEnable = p[0];
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0023: // Read Class of Device
|
||||
CommandComplete(opcode, {kStatusSuccess, m_classOfDevice[0], m_classOfDevice[1], m_classOfDevice[2]});
|
||||
return;
|
||||
case 0x0024: // Write Class of Device
|
||||
if (n < 3) break;
|
||||
std::copy(p, p + 3, m_classOfDevice.begin());
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0045: // Write Inquiry Mode
|
||||
if (n < 1) break;
|
||||
m_inquiryMode = p[0];
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0028: // Write Automatic Flush Timeout
|
||||
case 0x0037: // Write Link Supervision Timeout
|
||||
handleReply({});
|
||||
return;
|
||||
case 0x0036: // Read Link Supervision Timeout
|
||||
handleReply({0x80, 0x0C});
|
||||
return;
|
||||
case 0x0001: // Set Event Mask
|
||||
case 0x0005: // Set Event Filter
|
||||
case 0x000A: // Write PIN Type
|
||||
case 0x0016: // Write Connection Accept Timeout
|
||||
case 0x001C: // Write Page Scan Activity
|
||||
case 0x001E: // Write Inquiry Scan Activity
|
||||
case 0x0020: // Write Authentication Enable
|
||||
case 0x0022: // Write Encryption Mode
|
||||
case 0x0031: // Set Controller To Host Flow Control
|
||||
case 0x0033: // Host Buffer Size
|
||||
case 0x003A: // Write Current IAC LAP
|
||||
case 0x003F: // Set AFH Host Channel Classification
|
||||
case 0x0043: // Write Inquiry Scan Type
|
||||
case 0x0047: // Write Page Scan Type
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0035: // Host Number Of Completed Packets: no event
|
||||
return;
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
std::cerr << "[bluetooth] short HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
CommandComplete(opcode, {kStatusUnknownCommand});
|
||||
}
|
||||
|
||||
void Controller::HandleInformational(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
(void)p;
|
||||
(void)n;
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0001: { // Read Local Version Information
|
||||
std::vector<uint8_t> body = {kStatusSuccess, 0x03};
|
||||
PutLe16(body, 0x40A7);
|
||||
body.push_back(0x03);
|
||||
PutLe16(body, 0x000F);
|
||||
PutLe16(body, 0x430E);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0003: { // Read Local Supported Features
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
body.insert(body.end(), kLocalFeatures.begin(), kLocalFeatures.end());
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0005: { // Read Buffer Size
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, kAclBufferSize);
|
||||
body.push_back(kScoBufferSize);
|
||||
PutLe16(body, kAclBufferCount);
|
||||
PutLe16(body, 0);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0009: { // Read BD_ADDR
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, LocalAddress());
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::HandleStatusParameters(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
|
||||
PutLe16(body, n >= 2 ? Le16(p) : 0);
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0003: // Get Link Quality
|
||||
body.push_back(0xFF);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
case 0x0005: // Read RSSI: 0 = inside the golden receive power range
|
||||
body.push_back(0x00);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::HandleVendor(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
(void)p;
|
||||
(void)n;
|
||||
// WUD's firmware patch upload (0xFC4C writes, 0xFC4F install) and friends.
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
}
|
||||
|
||||
void Controller::ReceiveAcl(const uint8_t* packet, size_t size, uint64_t nowUs) {
|
||||
(void)nowUs;
|
||||
if (size < 4) return;
|
||||
const uint16_t header = Le16(packet);
|
||||
const uint16_t length = Le16(packet + 2);
|
||||
if (length != size - 4) {
|
||||
std::cerr << "[bluetooth] dropped malformed ACL packet" << std::endl;
|
||||
return;
|
||||
}
|
||||
const int slot = SlotForHandle(header & 0x0FFF);
|
||||
if (slot < 0) return;
|
||||
++m_completedPackets[slot];
|
||||
const uint8_t boundary = static_cast<uint8_t>((header >> 12) & 0x3);
|
||||
Reassembly& r = m_reassembly[slot];
|
||||
const uint8_t* data = packet + 4;
|
||||
if (boundary == 0x1) {
|
||||
if (r.expected == 0) return;
|
||||
r.frame.insert(r.frame.end(), data, data + length);
|
||||
} else {
|
||||
r.frame.assign(data, data + length);
|
||||
r.expected = length >= 2 ? static_cast<size_t>(Le16(data)) + 4 : 0;
|
||||
}
|
||||
if (r.expected != 0 && r.frame.size() >= r.expected) {
|
||||
const std::vector<uint8_t> frame(r.frame.begin(), r.frame.begin() + static_cast<std::ptrdiff_t>(r.expected));
|
||||
r = {};
|
||||
m_links[slot]->ReceiveL2cap(frame.data(), frame.size());
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::Tick(uint64_t nowUs) {
|
||||
if (!m_open) return;
|
||||
for (size_t i = 0; i < m_pending.size();) {
|
||||
if (nowUs >= m_pending[i].dueUs) {
|
||||
QueueEvent(std::move(m_pending[i].event));
|
||||
m_pending.erase(m_pending.begin() + static_cast<std::ptrdiff_t>(i));
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
TickInquiry(nowUs);
|
||||
for (auto& link : m_links) {
|
||||
link->Tick(nowUs);
|
||||
}
|
||||
SendNumberOfCompletedPackets();
|
||||
}
|
||||
|
||||
void Controller::TickInquiry(uint64_t nowUs) {
|
||||
if (!m_inquiry.active) return;
|
||||
if (nowUs >= m_inquiry.endUs ||
|
||||
(m_inquiry.maxResponses != 0 && m_inquiry.reported >= m_inquiry.maxResponses)) {
|
||||
FinishInquiry();
|
||||
return;
|
||||
}
|
||||
// One result per tick, lowest slot first, so remotes keep their order.
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (m_inquiry.slotReported[slot] || !m_links[slot]->Discoverable()) continue;
|
||||
m_inquiry.slotReported[slot] = true;
|
||||
++m_inquiry.reported;
|
||||
const BdAddr addr = RemoteAddress(slot);
|
||||
std::vector<uint8_t> body = {0x01};
|
||||
PutAddr(body, addr);
|
||||
body.push_back(0x01); // page scan repetition mode R1
|
||||
if (m_inquiryMode == 0) {
|
||||
body.insert(body.end(), {0x00, 0x00, 0x04, 0x25, 0x00});
|
||||
PutLe16(body, kRemoteClockOffset);
|
||||
QueueEvent(Event(kEventInquiryResult, body));
|
||||
} else {
|
||||
body.insert(body.end(), {0x00, 0x04, 0x25, 0x00});
|
||||
PutLe16(body, kRemoteClockOffset);
|
||||
body.push_back(kInquiryRssi);
|
||||
if (m_inquiryMode == 2) {
|
||||
std::array<uint8_t, 240> eir{};
|
||||
const char* name = m_host.DeviceName(slot);
|
||||
const size_t nameLength = std::min<size_t>(std::strlen(name), eir.size() - 2);
|
||||
eir[0] = static_cast<uint8_t>(nameLength + 1);
|
||||
eir[1] = 0x09; // complete local name
|
||||
std::memcpy(&eir[2], name, nameLength);
|
||||
body.insert(body.end(), eir.begin(), eir.end());
|
||||
QueueEvent(Event(kEventExtendedInquiryResult, body));
|
||||
} else {
|
||||
QueueEvent(Event(kEventInquiryResultWithRssi, body));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::FinishInquiry() {
|
||||
m_inquiry = {};
|
||||
QueueEvent(Event(kEventInquiryComplete, {kStatusSuccess}));
|
||||
if (m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(false);
|
||||
m_hostInquiryNotified = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::SendNumberOfCompletedPackets() {
|
||||
std::vector<uint8_t> body = {0};
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (m_completedPackets[slot] == 0) continue;
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutLe16(body, m_completedPackets[slot]);
|
||||
m_completedPackets[slot] = 0;
|
||||
++body[0];
|
||||
}
|
||||
if (body[0] != 0) {
|
||||
QueueEvent(Event(kEventNumberOfCompletedPackets, body));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,567 @@
|
||||
#include "bluetooth/bt_remote_link.h"
|
||||
|
||||
#include "bluetooth/bt_bytes.h"
|
||||
#include "bluetooth/bt_sdp.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
namespace Bluetooth {
|
||||
namespace L2capDetail {
|
||||
|
||||
constexpr uint16_t kSignallingCid = 0x0001;
|
||||
constexpr uint16_t kFirstDynamicCid = 0x0040;
|
||||
|
||||
constexpr uint16_t kPsmSdp = 0x0001;
|
||||
constexpr uint16_t kPsmHidControl = 0x0011;
|
||||
constexpr uint16_t kPsmHidInterrupt = 0x0013;
|
||||
|
||||
constexpr uint8_t kCommandReject = 0x01;
|
||||
constexpr uint8_t kConnectionRequest = 0x02;
|
||||
constexpr uint8_t kConnectionResponse = 0x03;
|
||||
constexpr uint8_t kConfigurationRequest = 0x04;
|
||||
constexpr uint8_t kConfigurationResponse = 0x05;
|
||||
constexpr uint8_t kDisconnectionRequest = 0x06;
|
||||
constexpr uint8_t kDisconnectionResponse = 0x07;
|
||||
constexpr uint8_t kEchoRequest = 0x08;
|
||||
constexpr uint8_t kEchoResponse = 0x09;
|
||||
constexpr uint8_t kInformationRequest = 0x0A;
|
||||
constexpr uint8_t kInformationResponse = 0x0B;
|
||||
|
||||
constexpr uint16_t kResultSuccess = 0x0000;
|
||||
constexpr uint16_t kResultPending = 0x0001;
|
||||
constexpr uint16_t kResultPsmNotSupported = 0x0002;
|
||||
constexpr uint16_t kResultNoResources = 0x0004;
|
||||
|
||||
constexpr uint8_t kOptionMtu = 0x01;
|
||||
constexpr uint8_t kOptionFlushTimeout = 0x02;
|
||||
// A real remote asks for this MTU on its own channels.
|
||||
constexpr uint16_t kRemoteMtu = 185;
|
||||
|
||||
constexpr uint8_t kHidHandshake = 0x0;
|
||||
constexpr uint8_t kHidControl = 0x1;
|
||||
constexpr uint8_t kHidSetReport = 0x5;
|
||||
constexpr uint8_t kHidData = 0xA;
|
||||
constexpr uint8_t kHidParamOutput = 0x2;
|
||||
constexpr uint8_t kHidHandshakeSuccess = 0x00;
|
||||
constexpr uint8_t kHidHandshakeUnsupported = 0x03;
|
||||
constexpr uint8_t kHidVirtualCableUnplug = 0x5;
|
||||
constexpr uint8_t kHidDataInput = 0xA1;
|
||||
|
||||
constexpr uint64_t kRequestTimeoutUs = 10'000'000;
|
||||
constexpr uint64_t kRetryDelayUs = 3'000'000;
|
||||
constexpr uint8_t kReasonRemoteUserTerminated = 0x13;
|
||||
// Output reports carry at most 21 payload bytes; anything longer is not a Wii Remote report.
|
||||
constexpr size_t kMaxReportSize = 22;
|
||||
constexpr size_t kInputBacklog = 8;
|
||||
|
||||
} // namespace L2capDetail
|
||||
|
||||
using namespace L2capDetail;
|
||||
|
||||
RemoteLink::RemoteLink(Controller& controller, size_t slot) : m_controller(controller), m_slot(slot) {}
|
||||
|
||||
void RemoteLink::Reset() {
|
||||
ClearConnection();
|
||||
m_baseband = Baseband::Idle;
|
||||
SetState(LinkState::Down);
|
||||
m_retryAtUs = 0;
|
||||
}
|
||||
|
||||
void RemoteLink::ClearConnection() {
|
||||
m_channels.clear();
|
||||
m_guestInitiated = false;
|
||||
m_everReady = false;
|
||||
}
|
||||
|
||||
bool RemoteLink::Discoverable() const {
|
||||
return m_baseband == Baseband::Idle && m_connectWanted && m_controller.Host().IsAttached(m_slot);
|
||||
}
|
||||
|
||||
void RemoteLink::Tick(uint64_t nowUs) {
|
||||
m_nowUs = nowUs;
|
||||
RemoteHost& host = m_controller.Host();
|
||||
if (m_baseband == Baseband::Idle) {
|
||||
if (host.ConsumeWake(m_slot)) {
|
||||
m_connectWanted = true;
|
||||
m_retryAtUs = 0;
|
||||
}
|
||||
if (!host.IsAttached(m_slot)) {
|
||||
m_connectWanted = false;
|
||||
}
|
||||
if (m_connectWanted && m_controller.PageScanEnabled() && nowUs >= m_retryAtUs) {
|
||||
std::vector<uint8_t> event = {0x04, 10};
|
||||
const BdAddr addr = Controller::RemoteAddress(m_slot);
|
||||
event.insert(event.end(), addr.begin(), addr.end());
|
||||
// Class of device 0x002504: peripheral, joystick.
|
||||
event.insert(event.end(), {0x04, 0x25, 0x00, 0x01});
|
||||
m_controller.QueueEvent(std::move(event));
|
||||
m_baseband = Baseband::Requesting;
|
||||
m_stateSinceUs = nowUs;
|
||||
}
|
||||
} else if (m_baseband == Baseband::Requesting) {
|
||||
if (nowUs - m_stateSinceUs > kRequestTimeoutUs) {
|
||||
m_baseband = Baseband::Idle;
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
}
|
||||
} else if (m_baseband == Baseband::Connected) {
|
||||
if (!host.IsAttached(m_slot)) {
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
m_connectWanted = false;
|
||||
return;
|
||||
}
|
||||
if (!m_guestInitiated) {
|
||||
const Channel* control = FindByPsm(kPsmHidControl);
|
||||
if (control == nullptr) {
|
||||
RequestChannel(kPsmHidControl, nowUs);
|
||||
} else if (control->Open() && FindByPsm(kPsmHidInterrupt) == nullptr) {
|
||||
RequestChannel(kPsmHidInterrupt, nowUs);
|
||||
}
|
||||
}
|
||||
for (const Channel& channel : m_channels) {
|
||||
const bool waiting = (channel.weInitiated && !channel.connected) ||
|
||||
(channel.configSent && !channel.localConfigured);
|
||||
if (waiting && nowUs - channel.requestUs > kRequestTimeoutUs) {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": L2CAP channel 0x" << std::hex
|
||||
<< channel.psm << std::dec << " did not open, dropping the connection" << std::endl;
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
ForwardInput();
|
||||
}
|
||||
|
||||
void RemoteLink::ForwardInput() {
|
||||
RemoteHost& host = m_controller.Host();
|
||||
if (m_state != LinkState::Ready) {
|
||||
while (host.PopInputReport(m_slot, m_report)) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const Channel* interrupt = FindByPsm(kPsmHidInterrupt);
|
||||
while (m_controller.AclQueued() < kInputBacklog && host.PopInputReport(m_slot, m_report)) {
|
||||
if (m_report.empty() || m_report.size() > kMaxReportSize) continue;
|
||||
SendInput(*interrupt, m_report);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::SendInput(const Channel& interrupt, const std::vector<uint8_t>& report) {
|
||||
std::vector<uint8_t> payload;
|
||||
payload.reserve(report.size() + 1);
|
||||
payload.push_back(kHidDataInput);
|
||||
payload.insert(payload.end(), report.begin(), report.end());
|
||||
if (payload.size() <= interrupt.remoteMtu) {
|
||||
SendData(interrupt, payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void RemoteLink::OnAccepted(uint8_t role) {
|
||||
(void)role;
|
||||
m_baseband = Baseband::Connected;
|
||||
m_guestInitiated = false;
|
||||
m_everReady = false;
|
||||
m_stateSinceUs = m_nowUs;
|
||||
SetState(LinkState::Connecting);
|
||||
}
|
||||
|
||||
void RemoteLink::OnRejected(uint8_t reason, uint64_t nowUs) {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": connection rejected (0x" << std::hex
|
||||
<< static_cast<int>(reason) << std::dec << ")" << std::endl;
|
||||
m_baseband = Baseband::Idle;
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
}
|
||||
|
||||
bool RemoteLink::OnPaged(uint64_t nowUs) {
|
||||
if (!Discoverable()) {
|
||||
return false;
|
||||
}
|
||||
m_nowUs = nowUs;
|
||||
m_baseband = Baseband::Connected;
|
||||
m_guestInitiated = true;
|
||||
m_everReady = false;
|
||||
m_stateSinceUs = nowUs;
|
||||
SetState(LinkState::Connecting);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RemoteLink::OnDisconnected(bool byGuest, uint64_t nowUs) {
|
||||
const bool wasReady = m_everReady;
|
||||
ClearConnection();
|
||||
m_baseband = Baseband::Idle;
|
||||
SetState(LinkState::Down);
|
||||
if (byGuest && wasReady) {
|
||||
m_connectWanted = false;
|
||||
m_controller.Host().OnGuestDisconnect(m_slot);
|
||||
} else {
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::DropFromRemote(uint8_t reason) {
|
||||
std::vector<uint8_t> event = {0x05, 4, 0x00};
|
||||
PutLe16(event, Controller::ConnectionHandle(m_slot));
|
||||
event.push_back(reason);
|
||||
m_controller.QueueEvent(std::move(event));
|
||||
ClearConnection();
|
||||
m_baseband = Baseband::Idle;
|
||||
SetState(LinkState::Down);
|
||||
}
|
||||
|
||||
void RemoteLink::SetState(LinkState state) {
|
||||
if (m_state == state) return;
|
||||
m_state = state;
|
||||
if (state == LinkState::Ready) {
|
||||
m_everReady = true;
|
||||
m_connectWanted = false;
|
||||
}
|
||||
m_controller.Host().OnLinkStateChanged(m_slot, state);
|
||||
}
|
||||
|
||||
void RemoteLink::UpdateState() {
|
||||
if (m_baseband != Baseband::Connected) {
|
||||
SetState(LinkState::Down);
|
||||
return;
|
||||
}
|
||||
const Channel* control = FindByPsm(kPsmHidControl);
|
||||
const Channel* interrupt = FindByPsm(kPsmHidInterrupt);
|
||||
const bool ready = control != nullptr && interrupt != nullptr && control->Open() && interrupt->Open();
|
||||
SetState(ready ? LinkState::Ready : LinkState::Connecting);
|
||||
}
|
||||
|
||||
RemoteLink::Channel* RemoteLink::FindByLocal(uint16_t cid) {
|
||||
for (Channel& channel : m_channels) {
|
||||
if (channel.localCid == cid) return &channel;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RemoteLink::Channel* RemoteLink::FindByPsm(uint16_t psm) {
|
||||
for (Channel& channel : m_channels) {
|
||||
if (channel.psm == psm) return &channel;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint16_t RemoteLink::AllocateCid() const {
|
||||
uint16_t cid = kFirstDynamicCid;
|
||||
while (std::any_of(m_channels.begin(), m_channels.end(), [&](const Channel& c) { return c.localCid == cid; })) {
|
||||
++cid;
|
||||
}
|
||||
return cid;
|
||||
}
|
||||
|
||||
uint8_t RemoteLink::NextIdent() {
|
||||
m_ident = static_cast<uint8_t>(m_ident + 1);
|
||||
if (m_ident == 0) m_ident = 1;
|
||||
return m_ident;
|
||||
}
|
||||
|
||||
void RemoteLink::SendSignal(uint8_t code, uint8_t ident, const std::vector<uint8_t>& data) {
|
||||
std::vector<uint8_t> frame;
|
||||
frame.reserve(8 + data.size());
|
||||
PutLe16(frame, static_cast<uint16_t>(4 + data.size()));
|
||||
PutLe16(frame, kSignallingCid);
|
||||
frame.push_back(code);
|
||||
frame.push_back(ident);
|
||||
PutLe16(frame, static_cast<uint16_t>(data.size()));
|
||||
frame.insert(frame.end(), data.begin(), data.end());
|
||||
m_controller.QueueAcl(Controller::ConnectionHandle(m_slot), frame);
|
||||
}
|
||||
|
||||
void RemoteLink::SendData(const Channel& channel, const std::vector<uint8_t>& payload) {
|
||||
std::vector<uint8_t> frame;
|
||||
frame.reserve(4 + payload.size());
|
||||
PutLe16(frame, static_cast<uint16_t>(payload.size()));
|
||||
PutLe16(frame, channel.remoteCid);
|
||||
frame.insert(frame.end(), payload.begin(), payload.end());
|
||||
m_controller.QueueAcl(Controller::ConnectionHandle(m_slot), frame);
|
||||
}
|
||||
|
||||
void RemoteLink::RequestChannel(uint16_t psm, uint64_t nowUs) {
|
||||
Channel channel;
|
||||
channel.psm = psm;
|
||||
channel.localCid = AllocateCid();
|
||||
channel.weInitiated = true;
|
||||
channel.connectIdent = NextIdent();
|
||||
channel.requestUs = nowUs;
|
||||
std::vector<uint8_t> data;
|
||||
PutLe16(data, psm);
|
||||
PutLe16(data, channel.localCid);
|
||||
SendSignal(kConnectionRequest, channel.connectIdent, data);
|
||||
m_channels.push_back(channel);
|
||||
}
|
||||
|
||||
void RemoteLink::SendConfigRequest(Channel& channel) {
|
||||
channel.configSent = true;
|
||||
channel.configIdent = NextIdent();
|
||||
channel.requestUs = m_nowUs;
|
||||
std::vector<uint8_t> data;
|
||||
PutLe16(data, channel.remoteCid);
|
||||
PutLe16(data, 0);
|
||||
data.push_back(kOptionMtu);
|
||||
data.push_back(2);
|
||||
PutLe16(data, kRemoteMtu);
|
||||
SendSignal(kConfigurationRequest, channel.configIdent, data);
|
||||
}
|
||||
|
||||
void RemoteLink::ReceiveL2cap(const uint8_t* frame, size_t size) {
|
||||
if (m_baseband != Baseband::Connected || size < 4) return;
|
||||
const uint16_t length = Le16(frame);
|
||||
const uint16_t cid = Le16(frame + 2);
|
||||
if (length != size - 4) {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": dropped malformed L2CAP frame" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (cid == kSignallingCid) {
|
||||
HandleSignalling(frame + 4, length);
|
||||
} else if (Channel* channel = FindByLocal(cid)) {
|
||||
HandleChannelData(*channel, frame + 4, length);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleSignalling(const uint8_t* data, size_t size) {
|
||||
while (size >= 4) {
|
||||
const uint8_t code = data[0];
|
||||
const uint8_t ident = data[1];
|
||||
const uint16_t length = Le16(data + 2);
|
||||
if (static_cast<size_t>(length) + 4 > size) {
|
||||
return;
|
||||
}
|
||||
const uint8_t* p = data + 4;
|
||||
switch (code) {
|
||||
case kConnectionRequest: HandleConnectionRequest(ident, p, length); break;
|
||||
case kConnectionResponse: HandleConnectionResponse(ident, p, length); break;
|
||||
case kConfigurationRequest: HandleConfigurationRequest(ident, p, length); break;
|
||||
case kConfigurationResponse: HandleConfigurationResponse(ident, p, length); break;
|
||||
case kDisconnectionRequest: HandleDisconnectionRequest(ident, p, length); break;
|
||||
case kDisconnectionResponse: HandleDisconnectionResponse(p, length); break;
|
||||
case kEchoRequest: SendSignal(kEchoResponse, ident, std::vector<uint8_t>(p, p + length)); break;
|
||||
case kInformationRequest: {
|
||||
if (length >= 2) {
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, Le16(p));
|
||||
PutLe16(reply, 0x0001); // not supported
|
||||
SendSignal(kInformationResponse, ident, reply);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kCommandReject:
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": the game rejected L2CAP command "
|
||||
<< static_cast<int>(ident) << std::endl;
|
||||
break;
|
||||
case kEchoResponse:
|
||||
case kInformationResponse:
|
||||
break;
|
||||
default: {
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, 0x0000); // command not understood
|
||||
SendSignal(kCommandReject, ident, reply);
|
||||
break;
|
||||
}
|
||||
}
|
||||
data += 4 + length;
|
||||
size -= 4 + length;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConnectionRequest(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t psm = Le16(p);
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
uint16_t result = kResultSuccess;
|
||||
uint16_t dcid = 0;
|
||||
if (psm != kPsmSdp && psm != kPsmHidControl && psm != kPsmHidInterrupt) {
|
||||
result = kResultPsmNotSupported;
|
||||
} else if (FindByPsm(psm) != nullptr) {
|
||||
// A real remote refuses a second channel on the same PSM.
|
||||
result = kResultNoResources;
|
||||
} else {
|
||||
Channel channel;
|
||||
channel.psm = psm;
|
||||
channel.localCid = AllocateCid();
|
||||
channel.remoteCid = scid;
|
||||
channel.connected = true;
|
||||
dcid = channel.localCid;
|
||||
m_channels.push_back(channel);
|
||||
}
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, dcid);
|
||||
PutLe16(reply, scid);
|
||||
PutLe16(reply, result);
|
||||
PutLe16(reply, 0);
|
||||
SendSignal(kConnectionResponse, ident, reply);
|
||||
if (result == kResultSuccess) {
|
||||
SendConfigRequest(*FindByLocal(dcid));
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConnectionResponse(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 8) return;
|
||||
const uint16_t dcid = Le16(p);
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
const uint16_t result = Le16(p + 4);
|
||||
Channel* channel = FindByLocal(scid);
|
||||
if (channel == nullptr || !channel->weInitiated || channel->connected || channel->connectIdent != ident) {
|
||||
return;
|
||||
}
|
||||
if (result == kResultSuccess) {
|
||||
channel->remoteCid = dcid;
|
||||
channel->connected = true;
|
||||
SendConfigRequest(*channel);
|
||||
} else if (result == kResultPending) {
|
||||
channel->requestUs = m_nowUs;
|
||||
} else {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": the game refused L2CAP PSM 0x" << std::hex
|
||||
<< channel->psm << " (result 0x" << result << ")" << std::dec << std::endl;
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
m_retryAtUs = m_nowUs + kRetryDelayUs;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConfigurationRequest(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t dcid = Le16(p);
|
||||
const uint16_t flags = Le16(p + 2);
|
||||
Channel* channel = FindByLocal(dcid);
|
||||
if (channel == nullptr) {
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, 0x0002); // invalid CID
|
||||
PutLe16(reply, dcid);
|
||||
PutLe16(reply, 0);
|
||||
SendSignal(kCommandReject, ident, reply);
|
||||
return;
|
||||
}
|
||||
channel->pendingOptions.insert(channel->pendingOptions.end(), p + 4, p + n);
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, channel->remoteCid);
|
||||
if (flags & 1) {
|
||||
PutLe16(reply, 1);
|
||||
PutLe16(reply, kResultSuccess);
|
||||
SendSignal(kConfigurationResponse, ident, reply);
|
||||
return;
|
||||
}
|
||||
PutLe16(reply, 0);
|
||||
PutLe16(reply, kResultSuccess);
|
||||
const std::vector<uint8_t>& options = channel->pendingOptions;
|
||||
for (size_t i = 0; i + 2 <= options.size();) {
|
||||
const uint8_t type = options[i] & 0x7F;
|
||||
const uint8_t length = options[i + 1];
|
||||
if (i + 2 + length > options.size()) break;
|
||||
if (type == kOptionMtu && length == 2) {
|
||||
channel->remoteMtu = Le16(&options[i + 2]);
|
||||
reply.insert(reply.end(), options.begin() + static_cast<std::ptrdiff_t>(i),
|
||||
options.begin() + static_cast<std::ptrdiff_t>(i + 4));
|
||||
} else if (type == kOptionFlushTimeout && length == 2) {
|
||||
reply.insert(reply.end(), options.begin() + static_cast<std::ptrdiff_t>(i),
|
||||
options.begin() + static_cast<std::ptrdiff_t>(i + 4));
|
||||
}
|
||||
i += 2 + length;
|
||||
}
|
||||
channel->pendingOptions.clear();
|
||||
channel->remoteConfigured = true;
|
||||
SendSignal(kConfigurationResponse, ident, reply);
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConfigurationResponse(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 6) return;
|
||||
const uint16_t scid = Le16(p);
|
||||
const uint16_t flags = Le16(p + 2);
|
||||
const uint16_t result = Le16(p + 4);
|
||||
Channel* channel = FindByLocal(scid);
|
||||
if (channel == nullptr || !channel->configSent || channel->configIdent != ident) return;
|
||||
if (flags & 1) {
|
||||
return;
|
||||
}
|
||||
if (result == kResultSuccess) {
|
||||
channel->localConfigured = true;
|
||||
UpdateState();
|
||||
return;
|
||||
}
|
||||
// Our only option is the MTU; offer the default instead.
|
||||
channel->configIdent = NextIdent();
|
||||
channel->requestUs = m_nowUs;
|
||||
std::vector<uint8_t> data;
|
||||
PutLe16(data, channel->remoteCid);
|
||||
PutLe16(data, 0);
|
||||
SendSignal(kConfigurationRequest, channel->configIdent, data);
|
||||
}
|
||||
|
||||
void RemoteLink::HandleDisconnectionRequest(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t dcid = Le16(p);
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, dcid);
|
||||
PutLe16(reply, scid);
|
||||
SendSignal(kDisconnectionResponse, ident, reply);
|
||||
m_channels.erase(std::remove_if(m_channels.begin(), m_channels.end(),
|
||||
[&](const Channel& c) { return c.localCid == dcid; }),
|
||||
m_channels.end());
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
void RemoteLink::HandleDisconnectionResponse(const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
m_channels.erase(std::remove_if(m_channels.begin(), m_channels.end(),
|
||||
[&](const Channel& c) { return c.localCid == scid; }),
|
||||
m_channels.end());
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
void RemoteLink::HandleChannelData(Channel& channel, const uint8_t* data, size_t size) {
|
||||
if (size == 0) return;
|
||||
if (channel.psm == kPsmSdp) {
|
||||
const std::vector<uint8_t> response = Sdp::HandleRequest(data, size, channel.remoteMtu);
|
||||
SendData(channel, response);
|
||||
return;
|
||||
}
|
||||
if (channel.psm == kPsmHidControl) {
|
||||
HandleHidControl(channel, data, size);
|
||||
return;
|
||||
}
|
||||
const uint8_t type = data[0] >> 4;
|
||||
const uint8_t param = data[0] & 0x0F;
|
||||
if (type == kHidData && param == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
|
||||
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleHidControl(Channel& channel, const uint8_t* data, size_t size) {
|
||||
const uint8_t type = data[0] >> 4;
|
||||
const uint8_t param = data[0] & 0x0F;
|
||||
switch (type) {
|
||||
case kHidHandshake:
|
||||
return;
|
||||
case kHidControl:
|
||||
if (param == kHidVirtualCableUnplug) {
|
||||
const bool wasReady = m_everReady;
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
if (wasReady) {
|
||||
m_connectWanted = false;
|
||||
m_controller.Host().OnGuestDisconnect(m_slot);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case kHidSetReport:
|
||||
SendData(channel, {kHidHandshakeSuccess});
|
||||
if ((param & 0x3) == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
|
||||
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
|
||||
}
|
||||
return;
|
||||
case kHidData:
|
||||
if (param == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
|
||||
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
SendData(channel, {kHidHandshakeUnsupported});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,438 @@
|
||||
#include "bluetooth/bt_sdp.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace Bluetooth::Sdp {
|
||||
namespace {
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
|
||||
constexpr uint8_t kErrorResponse = 0x01;
|
||||
constexpr uint8_t kServiceSearchRequest = 0x02;
|
||||
constexpr uint8_t kServiceSearchResponse = 0x03;
|
||||
constexpr uint8_t kServiceAttributeRequest = 0x04;
|
||||
constexpr uint8_t kServiceAttributeResponse = 0x05;
|
||||
constexpr uint8_t kServiceSearchAttributeRequest = 0x06;
|
||||
constexpr uint8_t kServiceSearchAttributeResponse = 0x07;
|
||||
|
||||
constexpr uint16_t kErrInvalidHandle = 0x0002;
|
||||
constexpr uint16_t kErrInvalidSyntax = 0x0003;
|
||||
constexpr uint16_t kErrInvalidPduSize = 0x0004;
|
||||
constexpr uint16_t kErrInvalidContinuation = 0x0005;
|
||||
|
||||
void PutBe16(Bytes& out, uint16_t v) {
|
||||
out.push_back(static_cast<uint8_t>(v >> 8));
|
||||
out.push_back(static_cast<uint8_t>(v));
|
||||
}
|
||||
|
||||
void PutBe32(Bytes& out, uint32_t v) {
|
||||
PutBe16(out, static_cast<uint16_t>(v >> 16));
|
||||
PutBe16(out, static_cast<uint16_t>(v));
|
||||
}
|
||||
|
||||
uint16_t Be16(const uint8_t* p) {
|
||||
return static_cast<uint16_t>((p[0] << 8) | p[1]);
|
||||
}
|
||||
|
||||
uint32_t Be32(const uint8_t* p) {
|
||||
return (static_cast<uint32_t>(Be16(p)) << 16) | Be16(p + 2);
|
||||
}
|
||||
|
||||
// Data element constructors (Bluetooth Core, Vol 3 Part B, 3.2).
|
||||
Bytes U8(uint8_t v) { return {0x08, v}; }
|
||||
Bytes U16(uint16_t v) { Bytes b{0x09}; PutBe16(b, v); return b; }
|
||||
Bytes U32(uint32_t v) { Bytes b{0x0A}; PutBe32(b, v); return b; }
|
||||
Bytes Uuid16(uint16_t v) { Bytes b{0x19}; PutBe16(b, v); return b; }
|
||||
Bytes Bool(bool v) { return {0x28, static_cast<uint8_t>(v ? 1 : 0)}; }
|
||||
|
||||
Bytes WithLength(uint8_t type, size_t length, bool wide = false) {
|
||||
if (length <= 0xFF && !wide) {
|
||||
return {static_cast<uint8_t>((type << 3) | 5), static_cast<uint8_t>(length)};
|
||||
}
|
||||
Bytes b{static_cast<uint8_t>((type << 3) | 6)};
|
||||
PutBe16(b, static_cast<uint16_t>(length));
|
||||
return b;
|
||||
}
|
||||
|
||||
Bytes Text(const Bytes& raw) {
|
||||
Bytes b = WithLength(4, raw.size());
|
||||
b.insert(b.end(), raw.begin(), raw.end());
|
||||
return b;
|
||||
}
|
||||
|
||||
Bytes Text(std::string_view s) {
|
||||
return Text(Bytes(s.begin(), s.end()));
|
||||
}
|
||||
|
||||
Bytes Seq(std::initializer_list<Bytes> items) {
|
||||
size_t length = 0;
|
||||
for (const Bytes& item : items) length += item.size();
|
||||
Bytes b = WithLength(6, length);
|
||||
for (const Bytes& item : items) b.insert(b.end(), item.begin(), item.end());
|
||||
return b;
|
||||
}
|
||||
|
||||
using Record = std::vector<std::pair<uint16_t, Bytes>>;
|
||||
|
||||
const Bytes kHidDescriptor = {
|
||||
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x10, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95,
|
||||
0x01, 0x06, 0x00, 0xFF, 0x09, 0x01, 0x91, 0x00, 0x85, 0x11, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x12, 0x95, 0x02, 0x09, 0x01, 0x91, 0x00, 0x85, 0x13, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x14, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x15, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x16, 0x95, 0x15, 0x09, 0x01, 0x91, 0x00, 0x85, 0x17, 0x95, 0x06, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x18, 0x95, 0x15, 0x09, 0x01, 0x91, 0x00, 0x85, 0x19, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x1A, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x20, 0x95, 0x06, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x21, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x22, 0x95, 0x04, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x30, 0x95, 0x02, 0x09, 0x01, 0x81, 0x00, 0x85, 0x31, 0x95, 0x05, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x32, 0x95, 0x0A, 0x09, 0x01, 0x81, 0x00, 0x85, 0x33, 0x95, 0x11, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x34, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x35, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x36, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x37, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x3D, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x3E, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x3F, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0xC0,
|
||||
};
|
||||
|
||||
constexpr uint32_t kHidRecordHandle = 0x00010000;
|
||||
constexpr uint32_t kDeviceIdRecordHandle = 0x00010001;
|
||||
|
||||
const Record& HidRecord() {
|
||||
static const Record record = {
|
||||
{0x0000, U32(kHidRecordHandle)},
|
||||
{0x0001, Seq({Uuid16(0x1124)})},
|
||||
{0x0004, Seq({Seq({Uuid16(0x0100), U16(0x0011)}), Seq({Uuid16(0x0011)})})},
|
||||
{0x0005, Seq({Uuid16(0x1002)})},
|
||||
{0x0006, Seq({U16(0x656E), U16(0x006A), U16(0x0100)})},
|
||||
{0x0009, Seq({Seq({Uuid16(0x1124), U16(0x0100)})})},
|
||||
{0x000D, Seq({Seq({Seq({Uuid16(0x0100), U16(0x0013)}), Seq({Uuid16(0x0011)})})})},
|
||||
{0x0100, Text("Nintendo RVL-CNT-01")},
|
||||
{0x0101, Text("Nintendo RVL-CNT-01")},
|
||||
{0x0102, Text("Nintendo")},
|
||||
{0x0200, U16(0x0100)},
|
||||
{0x0201, U16(0x0111)},
|
||||
{0x0202, U8(0x04)},
|
||||
{0x0203, U8(0x33)},
|
||||
{0x0204, Bool(false)},
|
||||
{0x0205, Bool(true)},
|
||||
{0x0206, Seq({Seq({U8(0x22), Text(kHidDescriptor)})})},
|
||||
{0x0207, Seq({Seq({U16(0x0409), U16(0x0100)})})},
|
||||
{0x0208, Bool(false)},
|
||||
{0x0209, Bool(true)},
|
||||
{0x020A, Bool(true)},
|
||||
{0x020B, U16(0x0100)},
|
||||
{0x020C, U16(0x0C80)},
|
||||
{0x020D, Bool(false)},
|
||||
{0x020E, Bool(false)},
|
||||
};
|
||||
return record;
|
||||
}
|
||||
|
||||
const Record& DeviceIdRecord() {
|
||||
static const Record record = {
|
||||
{0x0000, U32(kDeviceIdRecordHandle)},
|
||||
{0x0001, Seq({Uuid16(0x1200)})},
|
||||
{0x0004, Seq({Seq({Uuid16(0x0100), U16(0x0001)}), Seq({Uuid16(0x0001)})})},
|
||||
{0x0005, Seq({Uuid16(0x1002)})},
|
||||
{0x0009, Seq({Seq({Uuid16(0x1200), U16(0x0100)})})},
|
||||
{0x0200, U16(0x0100)},
|
||||
{0x0201, U16(0x057E)},
|
||||
{0x0202, U16(0x0306)},
|
||||
{0x0203, U16(0x0600)},
|
||||
{0x0204, Bool(true)},
|
||||
{0x0205, U16(0x0002)},
|
||||
};
|
||||
return record;
|
||||
}
|
||||
|
||||
struct NamedRecord {
|
||||
uint32_t handle;
|
||||
const Record& (*get)();
|
||||
};
|
||||
constexpr std::array<NamedRecord, 2> kRecords = {{
|
||||
{kHidRecordHandle, HidRecord},
|
||||
{kDeviceIdRecordHandle, DeviceIdRecord},
|
||||
}};
|
||||
|
||||
using Uuid = std::array<uint8_t, 16>;
|
||||
|
||||
Uuid FromShort(uint32_t value) {
|
||||
// Bluetooth base UUID 00000000-0000-1000-8000-00805F9B34FB.
|
||||
Uuid u = {0, 0, 0, 0, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB};
|
||||
u[0] = static_cast<uint8_t>(value >> 24);
|
||||
u[1] = static_cast<uint8_t>(value >> 16);
|
||||
u[2] = static_cast<uint8_t>(value >> 8);
|
||||
u[3] = static_cast<uint8_t>(value);
|
||||
return u;
|
||||
}
|
||||
|
||||
// One parsed data element header: type, header size and payload size.
|
||||
struct ElementHeader {
|
||||
uint8_t type = 0;
|
||||
size_t headerSize = 0;
|
||||
size_t dataSize = 0;
|
||||
};
|
||||
|
||||
std::optional<ElementHeader> ParseHeader(const uint8_t* p, size_t n) {
|
||||
if (n == 0) return std::nullopt;
|
||||
ElementHeader h;
|
||||
h.type = static_cast<uint8_t>(p[0] >> 3);
|
||||
const uint8_t sizeIndex = p[0] & 7;
|
||||
if (h.type == 0) {
|
||||
h.headerSize = 1;
|
||||
h.dataSize = 0;
|
||||
} else if (sizeIndex <= 4) {
|
||||
h.headerSize = 1;
|
||||
h.dataSize = size_t{1} << sizeIndex;
|
||||
} else if (sizeIndex == 5) {
|
||||
if (n < 2) return std::nullopt;
|
||||
h.headerSize = 2;
|
||||
h.dataSize = p[1];
|
||||
} else if (sizeIndex == 6) {
|
||||
if (n < 3) return std::nullopt;
|
||||
h.headerSize = 3;
|
||||
h.dataSize = Be16(p + 1);
|
||||
} else {
|
||||
if (n < 5) return std::nullopt;
|
||||
h.headerSize = 5;
|
||||
h.dataSize = Be32(p + 1);
|
||||
}
|
||||
if (h.headerSize + h.dataSize > n) return std::nullopt;
|
||||
return h;
|
||||
}
|
||||
|
||||
std::optional<Uuid> ReadUuid(const uint8_t* p, const ElementHeader& h) {
|
||||
if (h.type != 3) return std::nullopt;
|
||||
const uint8_t* d = p + h.headerSize;
|
||||
if (h.dataSize == 2) return FromShort(Be16(d));
|
||||
if (h.dataSize == 4) return FromShort(Be32(d));
|
||||
if (h.dataSize == 16) {
|
||||
Uuid u{};
|
||||
std::copy(d, d + 16, u.begin());
|
||||
return u;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void CollectUuids(const uint8_t* p, size_t n, std::vector<Uuid>& out) {
|
||||
while (n > 0) {
|
||||
const auto h = ParseHeader(p, n);
|
||||
if (!h) return;
|
||||
if (h->type == 6 || h->type == 7) {
|
||||
CollectUuids(p + h->headerSize, h->dataSize, out);
|
||||
} else if (const auto uuid = ReadUuid(p, *h)) {
|
||||
out.push_back(*uuid);
|
||||
}
|
||||
const size_t total = h->headerSize + h->dataSize;
|
||||
p += total;
|
||||
n -= total;
|
||||
}
|
||||
}
|
||||
|
||||
bool RecordMatches(const Record& record, const std::vector<Uuid>& pattern) {
|
||||
std::vector<Uuid> present;
|
||||
for (const auto& [id, value] : record) CollectUuids(value.data(), value.size(), present);
|
||||
return std::all_of(pattern.begin(), pattern.end(), [&](const Uuid& wanted) {
|
||||
return std::find(present.begin(), present.end(), wanted) != present.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Parses a data element sequence at `p`; advances `p`/`n` past it.
|
||||
std::optional<std::pair<const uint8_t*, size_t>> TakeSequence(const uint8_t*& p, size_t& n) {
|
||||
const auto h = ParseHeader(p, n);
|
||||
if (!h || (h->type != 6)) return std::nullopt;
|
||||
const std::pair<const uint8_t*, size_t> body{p + h->headerSize, h->dataSize};
|
||||
p += h->headerSize + h->dataSize;
|
||||
n -= h->headerSize + h->dataSize;
|
||||
return body;
|
||||
}
|
||||
|
||||
std::optional<std::vector<Uuid>> ParsePattern(const uint8_t*& p, size_t& n) {
|
||||
const auto body = TakeSequence(p, n);
|
||||
if (!body) return std::nullopt;
|
||||
std::vector<Uuid> pattern;
|
||||
const uint8_t* q = body->first;
|
||||
size_t m = body->second;
|
||||
while (m > 0) {
|
||||
const auto h = ParseHeader(q, m);
|
||||
if (!h) return std::nullopt;
|
||||
const auto uuid = ReadUuid(q, *h);
|
||||
if (!uuid) return std::nullopt;
|
||||
pattern.push_back(*uuid);
|
||||
q += h->headerSize + h->dataSize;
|
||||
m -= h->headerSize + h->dataSize;
|
||||
}
|
||||
if (pattern.empty() || pattern.size() > 12) return std::nullopt;
|
||||
return pattern;
|
||||
}
|
||||
|
||||
using IdRange = std::pair<uint16_t, uint16_t>;
|
||||
|
||||
std::optional<std::vector<IdRange>> ParseAttributeIds(const uint8_t*& p, size_t& n) {
|
||||
const auto body = TakeSequence(p, n);
|
||||
if (!body) return std::nullopt;
|
||||
std::vector<IdRange> ranges;
|
||||
const uint8_t* q = body->first;
|
||||
size_t m = body->second;
|
||||
while (m > 0) {
|
||||
const auto h = ParseHeader(q, m);
|
||||
if (!h || h->type != 1) return std::nullopt;
|
||||
const uint8_t* d = q + h->headerSize;
|
||||
if (h->dataSize == 2) {
|
||||
ranges.emplace_back(Be16(d), Be16(d));
|
||||
} else if (h->dataSize == 4) {
|
||||
ranges.emplace_back(Be16(d), Be16(d + 2));
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
q += h->headerSize + h->dataSize;
|
||||
m -= h->headerSize + h->dataSize;
|
||||
}
|
||||
if (ranges.empty()) return std::nullopt;
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// Continuation state is the byte offset into the complete response (2 bytes BE).
|
||||
std::optional<size_t> ParseContinuation(const uint8_t* p, size_t n) {
|
||||
if (n < 1 || p[0] + size_t{1} != n) return std::nullopt;
|
||||
if (p[0] == 0) return 0;
|
||||
if (p[0] != 2) return std::nullopt;
|
||||
return Be16(p + 1);
|
||||
}
|
||||
|
||||
Bytes AttributeList(const Record& record, const std::vector<IdRange>& ranges) {
|
||||
Bytes inner;
|
||||
for (const auto& [id, value] : record) {
|
||||
const bool wanted = std::any_of(ranges.begin(), ranges.end(),
|
||||
[id = id](const IdRange& r) { return id >= r.first && id <= r.second; });
|
||||
if (!wanted) continue;
|
||||
const Bytes idElement = U16(id);
|
||||
inner.insert(inner.end(), idElement.begin(), idElement.end());
|
||||
inner.insert(inner.end(), value.begin(), value.end());
|
||||
}
|
||||
// A real remote always sends attribute lists with a 16-bit length.
|
||||
Bytes list = WithLength(6, inner.size(), true);
|
||||
list.insert(list.end(), inner.begin(), inner.end());
|
||||
return list;
|
||||
}
|
||||
|
||||
Bytes Pdu(uint8_t id, uint16_t tid, const Bytes& params) {
|
||||
Bytes out{id};
|
||||
PutBe16(out, tid);
|
||||
PutBe16(out, static_cast<uint16_t>(params.size()));
|
||||
out.insert(out.end(), params.begin(), params.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
Bytes Error(uint16_t tid, uint16_t code) {
|
||||
Bytes params;
|
||||
PutBe16(params, code);
|
||||
return Pdu(kErrorResponse, tid, params);
|
||||
}
|
||||
|
||||
// Emits the slice of `full` starting at `offset` that fits, plus continuation.
|
||||
Bytes Chunked(uint8_t responseId, uint16_t tid, const Bytes& full, size_t offset, uint16_t maxBytes,
|
||||
size_t maxResponse) {
|
||||
// PDU header 5, byte count 2, continuation up to 3.
|
||||
const size_t room = maxResponse > 10 ? maxResponse - 10 : 1;
|
||||
const size_t limit = std::max<size_t>(1, std::min<size_t>(maxBytes, room));
|
||||
const size_t count = std::min(limit, full.size() - offset);
|
||||
Bytes params;
|
||||
PutBe16(params, static_cast<uint16_t>(count));
|
||||
params.insert(params.end(), full.begin() + static_cast<std::ptrdiff_t>(offset),
|
||||
full.begin() + static_cast<std::ptrdiff_t>(offset + count));
|
||||
const size_t next = offset + count;
|
||||
if (next < full.size()) {
|
||||
params.push_back(2);
|
||||
PutBe16(params, static_cast<uint16_t>(next));
|
||||
} else {
|
||||
params.push_back(0);
|
||||
}
|
||||
return Pdu(responseId, tid, params);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<uint8_t>& WiiRemoteHidDescriptor() {
|
||||
return kHidDescriptor;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> HandleRequest(const uint8_t* pdu, size_t size, size_t maxResponse) {
|
||||
if (size < 5) {
|
||||
return Error(0, kErrInvalidPduSize);
|
||||
}
|
||||
const uint8_t id = pdu[0];
|
||||
const uint16_t tid = Be16(pdu + 1);
|
||||
const uint16_t paramLength = Be16(pdu + 3);
|
||||
if (paramLength != size - 5) {
|
||||
return Error(tid, kErrInvalidPduSize);
|
||||
}
|
||||
const uint8_t* p = pdu + 5;
|
||||
size_t n = paramLength;
|
||||
|
||||
switch (id) {
|
||||
case kServiceSearchRequest: {
|
||||
const auto pattern = ParsePattern(p, n);
|
||||
if (!pattern || n < 2) return Error(tid, kErrInvalidSyntax);
|
||||
const uint16_t maxCount = Be16(p);
|
||||
if (!ParseContinuation(p + 2, n - 2)) return Error(tid, kErrInvalidContinuation);
|
||||
Bytes handles;
|
||||
uint16_t found = 0;
|
||||
for (const NamedRecord& record : kRecords) {
|
||||
if (found < maxCount && RecordMatches(record.get(), *pattern)) {
|
||||
PutBe32(handles, record.handle);
|
||||
++found;
|
||||
}
|
||||
}
|
||||
Bytes params;
|
||||
PutBe16(params, found);
|
||||
PutBe16(params, found);
|
||||
params.insert(params.end(), handles.begin(), handles.end());
|
||||
params.push_back(0);
|
||||
return Pdu(kServiceSearchResponse, tid, params);
|
||||
}
|
||||
case kServiceAttributeRequest: {
|
||||
if (n < 6) return Error(tid, kErrInvalidSyntax);
|
||||
const uint32_t handle = Be32(p);
|
||||
const uint16_t maxBytes = Be16(p + 4);
|
||||
p += 6;
|
||||
n -= 6;
|
||||
const auto ranges = ParseAttributeIds(p, n);
|
||||
if (!ranges || maxBytes < 7) return Error(tid, kErrInvalidSyntax);
|
||||
const auto record = std::find_if(kRecords.begin(), kRecords.end(),
|
||||
[&](const NamedRecord& r) { return r.handle == handle; });
|
||||
if (record == kRecords.end()) return Error(tid, kErrInvalidHandle);
|
||||
const Bytes full = AttributeList(record->get(), *ranges);
|
||||
const auto offset = ParseContinuation(p, n);
|
||||
if (!offset || *offset > full.size()) return Error(tid, kErrInvalidContinuation);
|
||||
return Chunked(kServiceAttributeResponse, tid, full, *offset, maxBytes, maxResponse);
|
||||
}
|
||||
case kServiceSearchAttributeRequest: {
|
||||
const auto pattern = ParsePattern(p, n);
|
||||
if (!pattern || n < 2) return Error(tid, kErrInvalidSyntax);
|
||||
const uint16_t maxBytes = Be16(p);
|
||||
p += 2;
|
||||
n -= 2;
|
||||
const auto ranges = ParseAttributeIds(p, n);
|
||||
if (!ranges || maxBytes < 7) return Error(tid, kErrInvalidSyntax);
|
||||
Bytes lists;
|
||||
for (const NamedRecord& record : kRecords) {
|
||||
if (RecordMatches(record.get(), *pattern)) {
|
||||
const Bytes list = AttributeList(record.get(), *ranges);
|
||||
lists.insert(lists.end(), list.begin(), list.end());
|
||||
}
|
||||
}
|
||||
Bytes full = WithLength(6, lists.size());
|
||||
full.insert(full.end(), lists.begin(), lists.end());
|
||||
const auto offset = ParseContinuation(p, n);
|
||||
if (!offset || *offset > full.size()) return Error(tid, kErrInvalidContinuation);
|
||||
return Chunked(kServiceSearchAttributeResponse, tid, full, *offset, maxBytes, maxResponse);
|
||||
}
|
||||
default:
|
||||
return Error(tid, kErrInvalidSyntax);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bluetooth::Sdp
|
||||
@@ -0,0 +1,126 @@
|
||||
// SYSCONF items behind the Wii Remote stack. The runtime never parses a
|
||||
// SYSCONF (SCCheckStatus is HLE'd), so these accessors serve the Bluetooth items
|
||||
// directly: BT.DINF registers the four virtual remotes of the emulated
|
||||
// controller, and the console settings come from Config.toml.
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
namespace BluetoothSysconf {
|
||||
|
||||
constexpr uint32_t kDeviceInfoSize = 0x461;
|
||||
constexpr uint32_t kCmpDeviceInfoSize = 0x205;
|
||||
constexpr uint32_t kDeviceEntrySize = 0x46;
|
||||
// [0..9] registered remotes, [10..13] the remote last connected on each channel.
|
||||
constexpr uint32_t kActiveEntriesStart = 10;
|
||||
constexpr const char kRemoteName[] = "Nintendo RVL-CNT-01";
|
||||
|
||||
void Fill(uint32_t buffer, uint32_t size, uint8_t value) {
|
||||
std::memset(Memory::GetPointer(buffer, size), value, size);
|
||||
}
|
||||
|
||||
void WriteEntry(uint32_t buffer, uint32_t index, size_t slot) {
|
||||
const uint32_t entry = buffer + 1 + index * kDeviceEntrySize;
|
||||
// SYSCONF keeps addresses most significant byte first.
|
||||
const Bluetooth::BdAddr addr = Bluetooth::Controller::RemoteAddress(slot);
|
||||
for (size_t i = 0; i < addr.size(); ++i) {
|
||||
Memory::Write8(entry + static_cast<uint32_t>(i), addr[addr.size() - 1 - i]);
|
||||
}
|
||||
std::memcpy(Memory::GetPointer(entry + 6, sizeof(kRemoteName)), kRemoteName, sizeof(kRemoteName));
|
||||
}
|
||||
|
||||
void SetReturn(CpuContext* ctx, uint32_t value) {
|
||||
ctx->gpr[3] = value;
|
||||
}
|
||||
|
||||
} // namespace BluetoothSysconf
|
||||
|
||||
extern "C" void SCGetBtDeviceInfoArray_HLE(CpuContext* ctx) {
|
||||
const uint32_t buffer = ctx->gpr[3];
|
||||
try {
|
||||
BluetoothSysconf::Fill(buffer, BluetoothSysconf::kDeviceInfoSize, 0);
|
||||
Memory::Write8(buffer, static_cast<uint8_t>(Bluetooth::kRemoteSlots));
|
||||
for (size_t slot = 0; slot < Bluetooth::kRemoteSlots; ++slot) {
|
||||
BluetoothSysconf::WriteEntry(buffer, static_cast<uint32_t>(slot), slot);
|
||||
BluetoothSysconf::WriteEntry(buffer, BluetoothSysconf::kActiveEntriesStart + static_cast<uint32_t>(slot), slot);
|
||||
}
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "SCGetBtDeviceInfoArray", e);
|
||||
BluetoothSysconf::SetReturn(ctx, 0);
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1ED0, SCGetBtDeviceInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// WUD writes the list back after reconciling it with the controller's link
|
||||
// keys, and WPAD whenever a channel's remote changes. The virtual remotes are
|
||||
// fixed, so the list is rebuilt on every boot.
|
||||
extern "C" void SCSetBtDeviceInfoArray_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1EDC, SCSetBtDeviceInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// Remotes paired with 1+2 on the game's own sync screens. The virtual remotes
|
||||
// are all registered, so the list is always empty.
|
||||
extern "C" void SCGetBtCmpDevInfoArray_HLE(CpuContext* ctx) {
|
||||
try {
|
||||
BluetoothSysconf::Fill(ctx->gpr[3], BluetoothSysconf::kCmpDeviceInfoSize, 0);
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "SCGetBtCmpDevInfoArray", e);
|
||||
BluetoothSysconf::SetReturn(ctx, 0);
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1EE8, SCGetBtCmpDevInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCSetBtCmpDevInfoArray_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1EF4, SCSetBtCmpDevInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetBtDpdSensibility_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, static_cast<uint32_t>(std::clamp(RuntimeConfigFile::WiiIrSensitivity(), 1, 5)));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1F00, SCGetBtDpdSensibility_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetWpadMotorMode_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, RuntimeConfigFile::WiiRumbleEnabled() ? 1u : 0u);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1F68, SCGetWpadMotorMode_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// The Home Menu's vibration switch, saved by WPADSaveConfig.
|
||||
extern "C" void SCSetWpadMotorMode_HLE(CpuContext* ctx) {
|
||||
const bool enabled = (ctx->gpr[3] & 0xFF) == 1;
|
||||
if (enabled != RuntimeConfigFile::WiiRumbleEnabled()) {
|
||||
RuntimeConfigFile::SetWiiRumbleEnabled(enabled);
|
||||
}
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1FBC, SCSetWpadMotorMode_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetWpadSensorBarPosition_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, RuntimeConfigFile::WiiSensorBarAbove() ? 1u : 0u);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1FC4, SCGetWpadSensorBarPosition_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetWpadSpeakerVolume_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, static_cast<uint32_t>(std::clamp(RuntimeConfigFile::WiiSpeakerVolume(), 0, 0x7F)));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B2018, SCGetWpadSpeakerVolume_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// The Home Menu's speaker volume, saved by WPADSaveConfig.
|
||||
extern "C" void SCSetWpadSpeakerVolume_HLE(CpuContext* ctx) {
|
||||
const int32_t volume = std::min<int32_t>(static_cast<int32_t>(ctx->gpr[3] & 0xFF), 0x7F);
|
||||
if (volume != RuntimeConfigFile::WiiSpeakerVolume()) {
|
||||
RuntimeConfigFile::SetWiiSpeakerVolume(volume);
|
||||
}
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B206C, SCSetWpadSpeakerVolume_HLE, (CpuContext* ctx), (ctx));
|
||||
@@ -0,0 +1,270 @@
|
||||
#include "hle/bluetooth_hle.h"
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
#include "hle/runtime_parse_helpers.h"
|
||||
#include "memory.h"
|
||||
#include "runtime_log.h"
|
||||
#include "wiimote_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
|
||||
|
||||
namespace BluetoothUsb {
|
||||
|
||||
constexpr const char* kDevicePath = "/dev/usb/oh1/57e/305";
|
||||
constexpr int32_t kDeviceFd = 5;
|
||||
constexpr int32_t kIpcSuccess = 0;
|
||||
constexpr int32_t kIpcEinval = -4;
|
||||
|
||||
constexpr uint32_t kCtrlMsg = 0;
|
||||
constexpr uint32_t kBulkMsg = 1;
|
||||
constexpr uint32_t kIntrMsg = 2;
|
||||
|
||||
constexpr uint8_t kEndpointEvents = 0x81;
|
||||
constexpr uint8_t kEndpointAclIn = 0x82;
|
||||
constexpr uint8_t kEndpointAclOut = 0x02;
|
||||
|
||||
// The guest copies each delivered packet into a GKI buffer that its 2 ms
|
||||
// Bluetooth tick frees; a pool it cannot refill drops the packet, so ACL data is
|
||||
// paced. Plenty for four remotes reporting at 200 Hz.
|
||||
constexpr double kAclPacketsPerMs = 3.0;
|
||||
constexpr double kAclBurst = 6.0;
|
||||
|
||||
struct HeldRead {
|
||||
bool active = false;
|
||||
uint32_t buffer = 0;
|
||||
uint32_t length = 0;
|
||||
uint32_t callback = 0;
|
||||
uint32_t callbackArg = 0;
|
||||
};
|
||||
|
||||
struct Device {
|
||||
bool open = false;
|
||||
std::unique_ptr<Bluetooth::Controller> controller;
|
||||
HeldRead eventRead;
|
||||
HeldRead aclRead;
|
||||
double aclTokens = kAclBurst;
|
||||
uint64_t lastRefillUs = 0;
|
||||
};
|
||||
|
||||
Device& Dev() {
|
||||
static Device device;
|
||||
return device;
|
||||
}
|
||||
|
||||
uint64_t NowUs() {
|
||||
using namespace std::chrono;
|
||||
return static_cast<uint64_t>(duration_cast<microseconds>(steady_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
|
||||
Bluetooth::Controller& ControllerFor(Device& device) {
|
||||
if (!device.controller) {
|
||||
device.controller = std::make_unique<Bluetooth::Controller>(Wiimote::Host());
|
||||
}
|
||||
return *device.controller;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ReadGuest(uint32_t address, uint32_t length) {
|
||||
std::vector<uint8_t> data(length);
|
||||
if (length != 0) {
|
||||
std::memcpy(data.data(), Memory::GetPointer(address, length), length);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void Complete(HeldRead& read, const std::vector<uint8_t>& data) {
|
||||
const uint32_t size = static_cast<uint32_t>(std::min<size_t>(data.size(), read.length));
|
||||
if (size < data.size()) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: packet of " << data.size() << " bytes truncated to " << size << std::endl;
|
||||
}
|
||||
if (size != 0) {
|
||||
std::memcpy(Memory::GetPointer(read.buffer, size), data.data(), size);
|
||||
}
|
||||
read.active = false;
|
||||
NandQueueIosCallback(read.callback, static_cast<int32_t>(size), read.callbackArg);
|
||||
}
|
||||
|
||||
// Hands queued events and ACL data to whichever reads the guest has armed.
|
||||
bool Deliver(Device& device, uint64_t nowUs) {
|
||||
Bluetooth::Controller& controller = ControllerFor(device);
|
||||
bool delivered = false;
|
||||
std::vector<uint8_t> packet;
|
||||
if (device.eventRead.active && controller.PopEvent(packet)) {
|
||||
Complete(device.eventRead, packet);
|
||||
delivered = true;
|
||||
}
|
||||
if (nowUs > device.lastRefillUs) {
|
||||
device.aclTokens = std::min(kAclBurst, device.aclTokens + (nowUs - device.lastRefillUs) / 1000.0 * kAclPacketsPerMs);
|
||||
device.lastRefillUs = nowUs;
|
||||
}
|
||||
if (device.aclRead.active && device.aclTokens >= 1.0 && controller.PopAcl(packet)) {
|
||||
device.aclTokens -= 1.0;
|
||||
Complete(device.aclRead, packet);
|
||||
delivered = true;
|
||||
}
|
||||
return delivered;
|
||||
}
|
||||
|
||||
bool Hold(HeldRead& read, uint32_t buffer, uint32_t length, uint32_t callback, uint32_t callbackArg) {
|
||||
if (length != 0) {
|
||||
Memory::GetPointer(buffer, length);
|
||||
}
|
||||
if (read.active) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: a second read on the same endpoint replaced the first" << std::endl;
|
||||
NandQueueIosCallback(read.callback, kIpcEinval, read.callbackArg);
|
||||
}
|
||||
read = {true, buffer, length, callback, callbackArg};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Executes a transfer. Returns true with `result` when it completed now.
|
||||
bool Transfer(uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr, uint32_t callback,
|
||||
uint32_t callbackArg, bool async, int32_t& result) {
|
||||
Device& device = Dev();
|
||||
Bluetooth::Controller& controller = ControllerFor(device);
|
||||
const uint64_t nowUs = NowUs();
|
||||
try {
|
||||
if (cmd == kCtrlMsg && numIn == 6 && numOut == 1) {
|
||||
const auto requestType = RuntimeHle::ReadIoVector(vectorPtr, 0);
|
||||
const auto lengthVector = RuntimeHle::ReadIoVector(vectorPtr, 4);
|
||||
const auto data = RuntimeHle::ReadIoVector(vectorPtr, 6);
|
||||
const uint8_t bmRequestType = Memory::Read8(requestType.address);
|
||||
// wLength is little-endian in the request block.
|
||||
const uint16_t wLength = static_cast<uint16_t>(Memory::Read8(lengthVector.address) |
|
||||
(Memory::Read8(lengthVector.address + 1) << 8));
|
||||
if (bmRequestType != 0x20 || wLength > data.size) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: unexpected control transfer type 0x" << std::hex
|
||||
<< static_cast<int>(bmRequestType) << std::dec << std::endl;
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
const std::vector<uint8_t> command = ReadGuest(data.address, wLength);
|
||||
controller.ExecuteCommand(command.data(), command.size(), nowUs);
|
||||
result = wLength;
|
||||
return true;
|
||||
}
|
||||
if ((cmd == kBulkMsg || cmd == kIntrMsg) && numIn == 2 && numOut == 1) {
|
||||
const auto endpointVector = RuntimeHle::ReadIoVector(vectorPtr, 0);
|
||||
const auto lengthVector = RuntimeHle::ReadIoVector(vectorPtr, 1);
|
||||
const auto data = RuntimeHle::ReadIoVector(vectorPtr, 2);
|
||||
const uint8_t endpoint = Memory::Read8(endpointVector.address);
|
||||
// Bulk / interrupt lengths are big-endian.
|
||||
const uint16_t length = Memory::Read16(lengthVector.address);
|
||||
if (length > data.size) {
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
if (cmd == kBulkMsg && endpoint == kEndpointAclOut) {
|
||||
const std::vector<uint8_t> packet = ReadGuest(data.address, length);
|
||||
controller.ReceiveAcl(packet.data(), packet.size(), nowUs);
|
||||
result = length;
|
||||
return true;
|
||||
}
|
||||
if (async && cmd == kBulkMsg && endpoint == kEndpointAclIn) {
|
||||
Hold(device.aclRead, data.address, length, callback, callbackArg);
|
||||
Deliver(device, nowUs);
|
||||
return false;
|
||||
}
|
||||
if (async && cmd == kIntrMsg && endpoint == kEndpointEvents) {
|
||||
Hold(device.eventRead, data.address, length, callback, callbackArg);
|
||||
Deliver(device, nowUs);
|
||||
return false;
|
||||
}
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: unsupported transfer cmd " << cmd << " endpoint 0x" << std::hex
|
||||
<< static_cast<int>(endpoint) << std::dec << (async ? "" : " (sync)") << std::endl;
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "Bluetooth transfer", e);
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: unsupported ioctlv " << cmd << " (" << numIn << " in, " << numOut << " out)"
|
||||
<< std::endl;
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace BluetoothUsb
|
||||
|
||||
int32_t Bluetooth_HLE_OpenDevice(const char* path) {
|
||||
if (path == nullptr || std::strcmp(path, BluetoothUsb::kDevicePath) != 0) {
|
||||
return 0;
|
||||
}
|
||||
BluetoothUsb::Device& device = BluetoothUsb::Dev();
|
||||
if (device.open) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: device opened twice" << std::endl;
|
||||
}
|
||||
device.open = true;
|
||||
device.eventRead = {};
|
||||
device.aclRead = {};
|
||||
device.aclTokens = BluetoothUsb::kAclBurst;
|
||||
device.lastRefillUs = BluetoothUsb::NowUs();
|
||||
BluetoothUsb::ControllerFor(device).Open(device.lastRefillUs);
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: " << BluetoothUsb::kDevicePath << " opened" << std::endl;
|
||||
return BluetoothUsb::kDeviceFd;
|
||||
}
|
||||
|
||||
bool Bluetooth_HLE_IsFd(uint32_t fd) {
|
||||
return static_cast<int32_t>(fd) == BluetoothUsb::kDeviceFd;
|
||||
}
|
||||
|
||||
int32_t Bluetooth_HLE_Close(uint32_t fd) {
|
||||
if (!Bluetooth_HLE_IsFd(fd)) {
|
||||
return BluetoothUsb::kIpcEinval;
|
||||
}
|
||||
BluetoothUsb::Device& device = BluetoothUsb::Dev();
|
||||
// IOS cancels the transfers still queued on a device it closes.
|
||||
for (BluetoothUsb::HeldRead* read : {&device.eventRead, &device.aclRead}) {
|
||||
if (read->active) {
|
||||
read->active = false;
|
||||
NandQueueIosCallback(read->callback, BluetoothUsb::kIpcEinval, read->callbackArg);
|
||||
}
|
||||
}
|
||||
BluetoothUsb::ControllerFor(device).Close();
|
||||
device.open = false;
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: " << BluetoothUsb::kDevicePath << " closed" << std::endl;
|
||||
return BluetoothUsb::kIpcSuccess;
|
||||
}
|
||||
|
||||
bool Bluetooth_HLE_IoctlvAsync(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr,
|
||||
uint32_t callback, uint32_t callbackArg, int32_t& result) {
|
||||
(void)fd;
|
||||
if (!BluetoothUsb::Dev().open) {
|
||||
result = BluetoothUsb::kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
return BluetoothUsb::Transfer(cmd, numIn, numOut, vectorPtr, callback, callbackArg, true, result);
|
||||
}
|
||||
|
||||
int32_t Bluetooth_HLE_Ioctlv(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr) {
|
||||
(void)fd;
|
||||
if (!BluetoothUsb::Dev().open) {
|
||||
return BluetoothUsb::kIpcEinval;
|
||||
}
|
||||
int32_t result = BluetoothUsb::kIpcEinval;
|
||||
BluetoothUsb::Transfer(cmd, numIn, numOut, vectorPtr, 0, 0, false, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Bluetooth_HLE_ProcessCompletions(CpuContext* cpu) {
|
||||
(void)cpu;
|
||||
BluetoothUsb::Device& device = BluetoothUsb::Dev();
|
||||
if (!device.open) {
|
||||
return false;
|
||||
}
|
||||
const uint64_t nowUs = BluetoothUsb::NowUs();
|
||||
try {
|
||||
BluetoothUsb::ControllerFor(device).Tick(nowUs);
|
||||
return BluetoothUsb::Deliver(device, nowUs);
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "Bluetooth delivery", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -71,11 +71,14 @@ void InvalidateEfbCopyDestinationsForRange(uint32_t addr, uint32_t size) {
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetDispCopySrc_8016f438(uint32_t l, uint32_t t, uint32_t w, uint32_t h) {
|
||||
GX_DEFER_CALL(GX__SetDispCopySrc_8016f438, l, t, w, h);
|
||||
GXSetDispCopySrc((u16)l, (u16)t, (u16)w, (u16)h);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f438, GX__SetDispCopySrc_8016f438, (uint32_t l, uint32_t t, uint32_t w, uint32_t h), (l, t, w, h));
|
||||
|
||||
extern "C" void GX__SetDispCopyDst_8016f4b8(uint32_t w, uint32_t h) { GXSetDispCopyDst((u16)w, (u16)h); }
|
||||
extern "C" void GX__SetDispCopyDst_8016f4b8(uint32_t w, uint32_t h) {
|
||||
GxWorker::Run([w, h] { GXSetDispCopyDst((u16)w, (u16)h); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f4b8, GX__SetDispCopyDst_8016f4b8, (uint32_t w, uint32_t h), (w, h));
|
||||
|
||||
// ============================================================================
|
||||
@@ -83,14 +86,14 @@ PPC_NATIVE_OVERRIDE_VOID(8016f4b8, GX__SetDispCopyDst_8016f4b8, (uint32_t w, uin
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetTexCopySrc_8016f478(uint32_t l, uint32_t t, uint32_t w, uint32_t h) {
|
||||
GXSetTexCopySrc((u16)l, (u16)t, (u16)w, (u16)h);
|
||||
GxWorker::Run([l, t, w, h] { GXSetTexCopySrc((u16)l, (u16)t, (u16)w, (u16)h); });
|
||||
g_texCopyState.srcLeft=(u16)l; g_texCopyState.srcTop=(u16)t;
|
||||
g_texCopyState.srcWidth=(u16)w; g_texCopyState.srcHeight=(u16)h;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f478, GX__SetTexCopySrc_8016f478, (uint32_t l, uint32_t t, uint32_t w, uint32_t h), (l, t, w, h));
|
||||
|
||||
extern "C" void GX__SetTexCopyDst_8016f4dc(uint32_t w, uint32_t h, uint32_t f, uint32_t m) {
|
||||
GXSetTexCopyDst((u16)w, (u16)h, (GXTexFmt)f, (GXBool)m);
|
||||
GxWorker::Run([w, h, f, m] { GXSetTexCopyDst((u16)w, (u16)h, (GXTexFmt)f, (GXBool)m); });
|
||||
g_texCopyState.dstWidth=(u16)w; g_texCopyState.dstHeight=(u16)h;
|
||||
g_texCopyState.dstFormat=f; g_texCopyState.dstMipmap=m;
|
||||
}
|
||||
@@ -100,11 +103,19 @@ extern "C" void GX__SetCopyFilter_8016fa40(uint32_t aa, uint32_t spa, uint32_t v
|
||||
uint8_t sp[12][2]={}, vfb[7]={};
|
||||
if(spa) std::memcpy(sp, GuestToHostPtr(spa, 24), 24);
|
||||
if(vfa) std::memcpy(vfb, GuestToHostPtr(vfa, 7), 7);
|
||||
GXSetCopyFilter((GXBool)aa, sp, (GXBool)vf, vfb);
|
||||
std::array<uint8_t, 24> spCopy{};
|
||||
std::array<uint8_t, 7> vfCopy{};
|
||||
std::memcpy(spCopy.data(), sp, 24);
|
||||
std::memcpy(vfCopy.data(), vfb, 7);
|
||||
GxWorker::Run([aa, vf, spCopy, vfCopy]() mutable {
|
||||
GXSetCopyFilter((GXBool)aa, reinterpret_cast<u8(*)[2]>(spCopy.data()), (GXBool)vf, vfCopy.data());
|
||||
});
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016fa40, GX__SetCopyFilter_8016fa40, (uint32_t aa, uint32_t spa, uint32_t vf, uint32_t vfa), (aa, spa, vf, vfa));
|
||||
|
||||
extern "C" void GX__SetDispCopyGamma_8016fc24(uint32_t g) { GXSetDispCopyGamma((GXGamma)g); }
|
||||
extern "C" void GX__SetDispCopyGamma_8016fc24(uint32_t g) {
|
||||
GxWorker::Run([g] { GXSetDispCopyGamma((GXGamma)g); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016fc24, GX__SetDispCopyGamma_8016fc24, (uint32_t g), (g));
|
||||
|
||||
// ============================================================================
|
||||
@@ -113,18 +124,24 @@ PPC_NATIVE_OVERRIDE_VOID(8016fc24, GX__SetDispCopyGamma_8016fc24, (uint32_t g),
|
||||
|
||||
extern "C" void GX__CopyDisp_8016fc38(uint32_t da, uint32_t c) {
|
||||
EnsureAuroraFrameActive();
|
||||
// GX copies are FIFO-ordered on hardware. Drain submitted draws before
|
||||
// resolving the EFB so high-level copies see the same contents.
|
||||
GXDrawDone();
|
||||
GXCopyDisp(GuestToHostPtr(da), (GXBool)c);
|
||||
// No second GXDrawDone here: the frame-worker wait below is for the DONE
|
||||
// phase, which strictly subsumes the drain this call would perform.
|
||||
if (GxWorker::ShouldDefer()) {
|
||||
// The worker drains like the inline path; EndAuroraFrame joins it before the next frame.
|
||||
GxWorker::Submit([da, c] {
|
||||
GXDrawDone();
|
||||
GXCopyDisp(GuestToHostPtr(da), (GXBool)c);
|
||||
});
|
||||
} else {
|
||||
// GX copies are FIFO-ordered on hardware. Drain submitted draws before
|
||||
// resolving the EFB so high-level copies see the same contents.
|
||||
GXDrawDone();
|
||||
GXCopyDisp(GuestToHostPtr(da), (GXBool)c);
|
||||
// Present immediately so post-copy draws don't leak into this frame. Join at the DONE phase
|
||||
// (not the cheaper SEALED phase GXDrawDone waits for) because ImGui's draw lists, owned by
|
||||
// Aurora's render worker, replay during encode; aurora_end_frame would join here anyway.
|
||||
aurora_wait_for_frame_worker();
|
||||
}
|
||||
++g_gxFrameCount;
|
||||
VI_HLE_SetXfbReady(da);
|
||||
// Present immediately so post-copy draws don't leak into this frame. Join at the DONE phase
|
||||
// (not the cheaper SEALED phase GXDrawDone waits for) because ImGui's draw lists, owned by
|
||||
// Aurora's render worker, replay during encode; aurora_end_frame would join here anyway.
|
||||
aurora_wait_for_frame_worker();
|
||||
settings_overlay::Draw();
|
||||
// Seal, pace to the VI retrace boundary (Aurora renders the sealed frame
|
||||
// during the wait), and pre-warm the next frame.
|
||||
@@ -135,7 +152,9 @@ PPC_NATIVE_OVERRIDE_VOID(8016fc38, GX__CopyDisp_8016fc38, (uint32_t da, uint32_t
|
||||
|
||||
|
||||
extern "C" void GX__CopyTex_8016fd74(uint32_t da, uint32_t c) {
|
||||
// Begin the frame before going inline so a deferred begin reaches the worker first.
|
||||
EnsureAuroraFrameActive();
|
||||
GX_SYNC_INLINE();
|
||||
// Match GX FIFO ordering: texture copies observe all prior draws.
|
||||
GXDrawDone();
|
||||
const uint16_t rawSrcLeft = g_texCopyState.srcLeft;
|
||||
|
||||
@@ -89,39 +89,42 @@ static inline uint32_t ReadModulatedLytColor(uint32_t colorAddr, int index, uint
|
||||
static_cast<uint32_t>(a);
|
||||
}
|
||||
|
||||
static inline void AppendLytTexCoords(uint8_t* packet, uint32_t& pos, uint32_t texCoordAddr,
|
||||
int texCoordCount, uint32_t cornerOffset) {
|
||||
if (texCoordCount <= 0) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < texCoordCount; ++i) {
|
||||
const uint32_t coordAddr = texCoordAddr + static_cast<uint32_t>(i * 32) + cornerOffset;
|
||||
BigEndian::AppendFloat32(packet, pos, Memory::ReadFloat32(coordAddr));
|
||||
BigEndian::AppendFloat32(packet, pos, Memory::ReadFloat32(coordAddr + 4));
|
||||
// An nw4r::lyt quad as read on the game thread: texcoords are 8 sets of 4 corners x (s, t).
|
||||
struct LytQuad {
|
||||
float x0 = 0, y0 = 0, x1 = 0, y1 = 0;
|
||||
int texCoordCount = 0;
|
||||
std::array<float, 8 * 4 * 2> texCoords{};
|
||||
bool hasColors = false;
|
||||
std::array<uint32_t, 4> colors{};
|
||||
};
|
||||
|
||||
static inline void AppendLytTexCoords(uint8_t* packet, uint32_t& pos, const LytQuad& quad,
|
||||
uint32_t cornerOffset) {
|
||||
for (int i = 0; i < quad.texCoordCount; ++i) {
|
||||
const size_t at = static_cast<size_t>(i) * 8u + cornerOffset / 4u;
|
||||
BigEndian::AppendFloat32(packet, pos, quad.texCoords[at]);
|
||||
BigEndian::AppendFloat32(packet, pos, quad.texCoords[at + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void AppendLytQuadVertex(uint8_t* packet, uint32_t& pos, float x, float y,
|
||||
uint32_t texCoordAddr, int texCoordCount,
|
||||
uint32_t cornerOffset, const uint32_t* colors, int colorIndex) {
|
||||
static inline void AppendLytQuadVertex(uint8_t* packet, uint32_t& pos, float x, float y, const LytQuad& quad,
|
||||
uint32_t cornerOffset, int colorIndex) {
|
||||
BigEndian::AppendFloat32(packet, pos, x);
|
||||
BigEndian::AppendFloat32(packet, pos, y);
|
||||
if (colors) {
|
||||
BigEndian::Append32(packet, pos, colors[colorIndex]);
|
||||
if (quad.hasColors) {
|
||||
BigEndian::Append32(packet, pos, quad.colors[colorIndex]);
|
||||
}
|
||||
AppendLytTexCoords(packet, pos, texCoordAddr, texCoordCount, cornerOffset);
|
||||
AppendLytTexCoords(packet, pos, quad, cornerOffset);
|
||||
}
|
||||
|
||||
// The four corners of an nw4r::lyt quad, in the order GX_QUADS expects, with
|
||||
// each corner's texcoord byte offset and colour index. The winding order is
|
||||
// load-bearing and both emit paths below share this one copy of it.
|
||||
static inline void AppendLytQuadVertices(uint8_t* packet, uint32_t& pos, float x0, float y0,
|
||||
float x1, float y1, uint32_t texCoordAddr,
|
||||
int texCoordCount, const uint32_t* colors) {
|
||||
AppendLytQuadVertex(packet, pos, x0, y0, texCoordAddr, texCoordCount, 0, colors, 0);
|
||||
AppendLytQuadVertex(packet, pos, x1, y0, texCoordAddr, texCoordCount, 8, colors, 1);
|
||||
AppendLytQuadVertex(packet, pos, x1, y1, texCoordAddr, texCoordCount, 24, colors, 3);
|
||||
AppendLytQuadVertex(packet, pos, x0, y1, texCoordAddr, texCoordCount, 16, colors, 2);
|
||||
static inline void AppendLytQuadVertices(uint8_t* packet, uint32_t& pos, const LytQuad& quad) {
|
||||
AppendLytQuadVertex(packet, pos, quad.x0, quad.y0, quad, 0, 0);
|
||||
AppendLytQuadVertex(packet, pos, quad.x1, quad.y0, quad, 8, 1);
|
||||
AppendLytQuadVertex(packet, pos, quad.x1, quad.y1, quad, 24, 3);
|
||||
AppendLytQuadVertex(packet, pos, quad.x0, quad.y1, quad, 16, 2);
|
||||
}
|
||||
|
||||
static bool CanSubmitLytDrawDirect(int texCoordCount, const uint32_t* colors) {
|
||||
@@ -175,9 +178,8 @@ static bool CanSubmitLytDrawDirect(int texCoordCount, const uint32_t* colors) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SubmitLytDrawDirect(float x0, float y0, float x1, float y1, int texCoordCount,
|
||||
uint32_t texCoordAddr, const uint32_t* colors) {
|
||||
if (!CanSubmitLytDrawDirect(texCoordCount, colors)) {
|
||||
static bool SubmitLytDrawDirect(const LytQuad& quad) {
|
||||
if (!CanSubmitLytDrawDirect(quad.texCoordCount, quad.hasColors ? quad.colors.data() : nullptr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -193,7 +195,7 @@ static bool SubmitLytDrawDirect(float x0, float y0, float x1, float y1, int texC
|
||||
|
||||
std::array<uint8_t, 4u * (8u + 4u + 8u * 8u)> vertices{};
|
||||
uint32_t pos = 0;
|
||||
AppendLytQuadVertices(vertices.data(), pos, x0, y0, x1, y1, texCoordAddr, texCoordCount, colors);
|
||||
AppendLytQuadVertices(vertices.data(), pos, quad);
|
||||
|
||||
if (!aurora::gx::fifo::submit_raw_draw(GX_QUADS, GX_VTXFMT0, vertices.data(), 4, pos)) {
|
||||
return false;
|
||||
@@ -204,23 +206,8 @@ static bool SubmitLytDrawDirect(float x0, float y0, float x1, float y1, int texC
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void EmitLytDrawQuad(uint32_t posAddr, uint32_t sizeAddr, int texCoordCount,
|
||||
uint32_t texCoordAddr, const uint32_t* colors) {
|
||||
const float x0 = Memory::ReadFloat32(posAddr);
|
||||
const float y0 = Memory::ReadFloat32(posAddr + 4);
|
||||
// The casts pin the arithmetic to PPC single precision regardless of the
|
||||
// host's FLT_EVAL_METHOD.
|
||||
const float x1 = static_cast<float>(x0 + Memory::ReadFloat32(sizeAddr));
|
||||
const float y1 = static_cast<float>(y0 - Memory::ReadFloat32(sizeAddr + 4));
|
||||
|
||||
if (SubmitLytDrawDirect(x0, y0, x1, y1, texCoordCount, texCoordAddr, colors)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// GX has exactly 8 texture coordinates, so nw4r::lyt cannot ask for more.
|
||||
// The fixed packet buffer below is sized for that maximum; bail rather than
|
||||
// overrun it if the guest ever hands us something else.
|
||||
if (texCoordCount > 8) {
|
||||
static void EmitLytQuad(const LytQuad& quad) {
|
||||
if (SubmitLytDrawDirect(quad)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,10 +215,39 @@ static inline void EmitLytDrawQuad(uint32_t posAddr, uint32_t sizeAddr, int texC
|
||||
uint32_t pos = 0;
|
||||
packet[pos++] = GX_DRAW_QUADS_CMD | GX_VTXFMT0;
|
||||
BigEndian::Append16(packet.data(), pos, 4);
|
||||
AppendLytQuadVertices(packet.data(), pos, x0, y0, x1, y1, texCoordAddr, texCoordCount, colors);
|
||||
AppendLytQuadVertices(packet.data(), pos, quad);
|
||||
SubmitLytDrawPacket(packet.data(), pos);
|
||||
}
|
||||
|
||||
static inline void EmitLytDrawQuad(uint32_t posAddr, uint32_t sizeAddr, int texCoordCount,
|
||||
uint32_t texCoordAddr, const uint32_t* colors) {
|
||||
// GX has exactly 8 texture coordinates, so nw4r::lyt cannot ask for more; the packet buffer
|
||||
// is sized for that maximum.
|
||||
if (texCoordCount > 8) {
|
||||
return;
|
||||
}
|
||||
LytQuad quad;
|
||||
quad.x0 = Memory::ReadFloat32(posAddr);
|
||||
quad.y0 = Memory::ReadFloat32(posAddr + 4);
|
||||
// The casts pin the arithmetic to PPC single precision regardless of the
|
||||
// host's FLT_EVAL_METHOD.
|
||||
quad.x1 = static_cast<float>(quad.x0 + Memory::ReadFloat32(sizeAddr));
|
||||
quad.y1 = static_cast<float>(quad.y0 - Memory::ReadFloat32(sizeAddr + 4));
|
||||
quad.texCoordCount = std::max(texCoordCount, 0);
|
||||
for (int i = 0; i < quad.texCoordCount; ++i) {
|
||||
for (uint32_t f = 0; f < 8; ++f) {
|
||||
quad.texCoords[static_cast<size_t>(i) * 8u + f] =
|
||||
Memory::ReadFloat32(texCoordAddr + static_cast<uint32_t>(i * 32) + f * 4u);
|
||||
}
|
||||
}
|
||||
if (colors != nullptr) {
|
||||
quad.hasColors = true;
|
||||
std::copy(colors, colors + 4, quad.colors.begin());
|
||||
}
|
||||
EnsureAuroraFrameActive();
|
||||
GxWorker::Run([quad] { EmitLytQuad(quad); });
|
||||
}
|
||||
|
||||
static uint32_t SubmitDLVertex(const uint8_t* ptr, GXVtxFmt vtxfmt, const GXAttrType* sourceVtxDesc) {
|
||||
uint32_t offset = 0;
|
||||
for (int attr = 0; attr < 26; ++attr) {
|
||||
@@ -1284,6 +1300,12 @@ extern "C" void GxNotifyDisplayListMemoryWrite(uint32_t addr, uint32_t size) {
|
||||
|
||||
extern "C" void GX__CallDisplayList_80172f64(uint32_t listAddr, uint32_t nbytes) {
|
||||
if (nbytes == 0 || listAddr == 0) return;
|
||||
// List bytes and the arrays it indexes are GPU-side reads, so they are read when the worker runs.
|
||||
if (GxWorker::ShouldDefer()) {
|
||||
EnsureAuroraFrameActive();
|
||||
GxWorker::Submit([=] { GX__CallDisplayList_80172f64(listAddr, nbytes); });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const uint8_t* list = static_cast<const uint8_t*>(GuestToHostPtr(listAddr, nbytes));
|
||||
if (!list) return;
|
||||
|
||||
@@ -81,8 +81,10 @@ extern "C" void EGG__LightTexture__SetupTevFinish_HLE_8022e2bc(CpuContext* ctx)
|
||||
if (remainder > 0) {
|
||||
const GXColor black{0, 0, 0, 255};
|
||||
while (remainder < stageCount) {
|
||||
GXSetTevColor(static_cast<GXTevRegID>(remainder + 1), black);
|
||||
GXSetTevKColor(static_cast<GXTevKColorID>(remainder), black);
|
||||
GxWorker::Run([remainder, black] {
|
||||
GXSetTevColor(static_cast<GXTevRegID>(remainder + 1), black);
|
||||
GXSetTevKColor(static_cast<GXTevKColorID>(remainder), black);
|
||||
});
|
||||
++remainder;
|
||||
}
|
||||
|
||||
|
||||
@@ -775,11 +775,25 @@ static bool WriteDisplayListBurst(const uint8_t* data, uint32_t sizeBytes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void FifoWriteBurstImpl(const uint8_t* data, uint32_t sizeBytes);
|
||||
|
||||
extern "C" void GX_HLE_FIFO_WriteBurst(const uint8_t* data, uint32_t sizeBytes) {
|
||||
if (data == nullptr || sizeBytes == 0) {
|
||||
return;
|
||||
}
|
||||
if (GxWorker::ShouldDefer()) {
|
||||
EnsureAuroraFrameForFifo();
|
||||
GxWorker::FifoBurst(data, sizeBytes);
|
||||
return;
|
||||
}
|
||||
FifoWriteBurstImpl(data, sizeBytes);
|
||||
}
|
||||
|
||||
void GxWorkerReplayFifoWrite(uint32_t value, uint32_t sizeBytes) { HleFifoWrite(value, sizeBytes); }
|
||||
|
||||
void GxWorkerReplayFifoBurst(const uint8_t* data, uint32_t sizeBytes) { FifoWriteBurstImpl(data, sizeBytes); }
|
||||
|
||||
static void FifoWriteBurstImpl(const uint8_t* data, uint32_t sizeBytes) {
|
||||
if (IsDisplayListActive() && WriteDisplayListBurst(data, sizeBytes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,8 +87,51 @@ void BeginNextAuroraFrameWithRetry(std::chrono::milliseconds timeout) {
|
||||
++s_beginExpiredLogCount;
|
||||
}
|
||||
|
||||
void EnsureAuroraFrameForFifo() {
|
||||
// The game thread can't see where draws start in recorded FIFO bytes, so any write may begin the
|
||||
// frame. A failed begin is only retried once the next frame is presented.
|
||||
static int s_attemptedFrame = -1;
|
||||
if (g_auroraFrameActive.load(std::memory_order_acquire) || s_attemptedFrame == g_gxFrameCount) {
|
||||
return;
|
||||
}
|
||||
s_attemptedFrame = g_gxFrameCount;
|
||||
UpdateAuroraAndProcessEvents();
|
||||
if (BeginAuroraFrame()) {
|
||||
g_auroraFrameActive.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
void EnsureAuroraFrameActive() {
|
||||
// The game thread begins frames before it records work for the GX worker.
|
||||
if (GxWorker::OnWorker()) {
|
||||
return;
|
||||
}
|
||||
if (!g_auroraFrameActive.load(std::memory_order_acquire)) {
|
||||
BeginNextAuroraFrameWithRetry();
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// GX worker only.
|
||||
bool g_workerFrameBegun = false;
|
||||
}
|
||||
|
||||
void GxWorkerBeginFrame() {
|
||||
// gfx::begin_frame can briefly fail to map a staging buffer; retry like the inline path does.
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
|
||||
while (!(g_workerFrameBegun = aurora_begin_frame_gx()) && std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
if (!g_workerFrameBegun) {
|
||||
RT_LOGF(RT_TAG_GX, "GX worker could not begin an aurora frame\n");
|
||||
}
|
||||
}
|
||||
|
||||
void GxWorkerEndFrame(AuroraImguiFrame* imgui) {
|
||||
if (g_workerFrameBegun) {
|
||||
aurora_end_frame_gx(imgui);
|
||||
} else {
|
||||
aurora_release_imgui_frame(imgui);
|
||||
}
|
||||
g_workerFrameBegun = false;
|
||||
}
|
||||
|
||||
@@ -5,22 +5,25 @@
|
||||
// Indirect Texture Stages
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetNumIndStages_80171b38(uint32_t n) { GXSetNumIndStages((u8)n); }
|
||||
extern "C" void GX__SetNumIndStages_80171b38(uint32_t n) {
|
||||
GX_DEFER_CALL(GX__SetNumIndStages_80171b38, n); GXSetNumIndStages((u8)n); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171b38, GX__SetNumIndStages_80171b38, (uint32_t n), (n));
|
||||
|
||||
extern "C" void GX__SetIndTexOrder_80171a6c(uint32_t s, uint32_t c, uint32_t m) {
|
||||
GX_DEFER_CALL(GX__SetIndTexOrder_80171a6c, s, c, m);
|
||||
GXSetIndTexOrder((GXIndTexStageID)s, (GXTexCoordID)(c==0xFFu?0:c), (GXTexMapID)(m==0xFFu?0:m));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171a6c, GX__SetIndTexOrder_80171a6c, (uint32_t s, uint32_t c, uint32_t m), (s, c, m));
|
||||
|
||||
extern "C" void GX__SetIndTexCoordScale_80171968(uint32_t s, uint32_t ss, uint32_t ts) {
|
||||
GX_DEFER_CALL(GX__SetIndTexCoordScale_80171968, s, ss, ts);
|
||||
GXSetIndTexCoordScale((GXIndTexStageID)s, (GXIndTexScale)ss, (GXIndTexScale)ts);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171968, GX__SetIndTexCoordScale_80171968, (uint32_t s, uint32_t ss, uint32_t ts), (s, ss, ts));
|
||||
|
||||
extern "C" void GX__SetIndTexMtx_80171814(uint32_t id, uint32_t ma, uint32_t se) {
|
||||
float m[6]; for(int i=0; i<6; ++i) m[i]=Memory::ReadFloat32(ma+i*4);
|
||||
GXSetIndTexMtx((GXIndTexMtxID)id, m, (s8)se);
|
||||
GxWorker::Run([id, se, mtx = std::to_array(m)]() mutable { GXSetIndTexMtx((GXIndTexMtxID)id, mtx.data(), (s8)se); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171814, GX__SetIndTexMtx_80171814, (uint32_t id, uint32_t ma, uint32_t se), (id, ma, se));
|
||||
|
||||
@@ -28,16 +31,19 @@ PPC_NATIVE_OVERRIDE_VOID(80171814, GX__SetIndTexMtx_80171814, (uint32_t id, uint
|
||||
// TEV Indirect Texture Control
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetTevDirect_80171b58(uint32_t s) { GXSetTevDirect((GXTevStageID)s); }
|
||||
extern "C" void GX__SetTevDirect_80171b58(uint32_t s) {
|
||||
GX_DEFER_CALL(GX__SetTevDirect_80171b58, s); GXSetTevDirect((GXTevStageID)s); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171b58, GX__SetTevDirect_80171b58, (uint32_t s), (s));
|
||||
|
||||
extern "C" void GX__SetTevIndWarp_80171ba0(uint32_t ts, uint32_t is, uint32_t so, uint32_t rm, uint32_t ms) {
|
||||
GX_DEFER_CALL(GX__SetTevIndWarp_80171ba0, ts, is, so, rm, ms);
|
||||
GXSetTevIndWarp((GXTevStageID)std::min(ts, 15u), (GXIndTexStageID)std::min(is, 3u),
|
||||
(GXBool)so, (GXBool)rm, (GXIndTexMtxID)ms);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171ba0, GX__SetTevIndWarp_80171ba0, (uint32_t ts, uint32_t is, uint32_t so, uint32_t rm, uint32_t ms), (ts, is, so, rm, ms));
|
||||
|
||||
extern "C" void GX__SetTevIndirect_801717ac(uint32_t ts, uint32_t is, uint32_t f, uint32_t bs, uint32_t ms, uint32_t ws, uint32_t wt, uint32_t ap, uint32_t il, uint32_t as) {
|
||||
GX_DEFER_CALL(GX__SetTevIndirect_801717ac, ts, is, f, bs, ms, ws, wt, ap, il, as);
|
||||
GXSetTevIndirect((GXTevStageID)std::min(ts,15u), (GXIndTexStageID)std::min(is,3u), (GXIndTexFormat)f,
|
||||
(GXIndTexBiasSel)bs, (GXIndTexMtxID)ms, (GXIndTexWrap)ws, (GXIndTexWrap)wt,
|
||||
(GXBool)ap, (GXBool)il, (GXIndTexAlphaSel)as);
|
||||
|
||||
@@ -24,6 +24,7 @@ extern "C" GXFifoObj* GXInit(void* base, u32 size);
|
||||
*/
|
||||
extern "C" uint32_t GX__Init_8016b850(uint32_t fifoBase, uint32_t fifoSize)
|
||||
{
|
||||
GX_SYNC_INLINE();
|
||||
constexpr uint32_t kFifoObjAddr = 0x80343740u;
|
||||
constexpr uint32_t kGXDataAddr = 0x803437C0u;
|
||||
constexpr uint32_t kGXDataSize = 0x600u;
|
||||
@@ -91,16 +92,16 @@ PPC_NATIVE_OVERRIDE(8016b850, GX__Init_8016b850, uint32_t, (uint32_t fifoBase, u
|
||||
|
||||
extern "C" void GX__InitFifoBase_8016c7c8(uint32_t fa, uint32_t ba, uint32_t s) { GXInitFifoBase((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj)), GuestToHostPtr(ba, s), s); }
|
||||
|
||||
extern "C" void GX__SetCPUFifo_8016c94c(uint32_t fa) { GXSetCPUFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); }
|
||||
extern "C" void GX__SetCPUFifo_8016c94c(uint32_t fa) { GX_SYNC_INLINE(); GXSetCPUFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016c94c, GX__SetCPUFifo_8016c94c, (uint32_t fa), (fa));
|
||||
|
||||
extern "C" void GX__SetGPFifo_8016cb2c(uint32_t fa) { GXSetGPFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); }
|
||||
extern "C" void GX__SetGPFifo_8016cb2c(uint32_t fa) { GX_SYNC_INLINE(); GXSetGPFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016cb2c, GX__SetGPFifo_8016cb2c, (uint32_t fa), (fa));
|
||||
|
||||
extern "C" void __GX__SaveFifo_8016cdbc(uint32_t fa) { GXSaveCPUFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); }
|
||||
extern "C" void __GX__SaveFifo_8016cdbc(uint32_t fa) { GX_SYNC_INLINE(); GXSaveCPUFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016cdbc, __GX__SaveFifo_8016cdbc, (uint32_t fa), (fa));
|
||||
|
||||
extern "C" void GX__GetCPUFifo_8016cf10(uint32_t fa) { auto* d=(GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj)); auto* s=GXGetCPUFifo(); if(d&&s) std::memcpy(d,s,sizeof(GXFifoObj)); }
|
||||
extern "C" void GX__GetCPUFifo_8016cf10(uint32_t fa) { GX_SYNC_INLINE(); auto* d=(GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj)); auto* s=GXGetCPUFifo(); if(d&&s) std::memcpy(d,s,sizeof(GXFifoObj)); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016cf10, GX__GetCPUFifo_8016cf10, (uint32_t fa), (fa));
|
||||
|
||||
extern "C" void __GX__FifoInit_8016d180()
|
||||
@@ -137,6 +138,7 @@ extern "C" void __GX__FifoInit_8016d180()
|
||||
|
||||
extern "C" void __GX__PEInit_8016ee14()
|
||||
{
|
||||
GX_SYNC_INLINE();
|
||||
constexpr uint32_t kPeTokenInterruptId = 0x12u;
|
||||
constexpr uint32_t kPeFinishInterruptId = 0x13u;
|
||||
constexpr uint32_t kPeTokenInterruptMask = 0x1000u;
|
||||
@@ -165,7 +167,16 @@ PPC_NATIVE_OVERRIDE_VOID(8016ee14, __GX__PEInit_8016ee14, (), ());
|
||||
// Display List Recording
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
// Display-list recording runs inline: the guest list buffer and its size are needed synchronously.
|
||||
bool g_dlInline = false;
|
||||
}
|
||||
|
||||
extern "C" void GX__BeginDisplayList_80172e00(uint32_t la, uint32_t s) {
|
||||
if (!g_dlInline) {
|
||||
GxWorker::BeginInline();
|
||||
g_dlInline = true;
|
||||
}
|
||||
try {
|
||||
uint32_t gd = Memory::Read32(kGXDataPtrAddr);
|
||||
if (!gd) return;
|
||||
@@ -188,6 +199,13 @@ extern "C" void GX__BeginDisplayList_80172e00(uint32_t la, uint32_t s) {
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80172e00, GX__BeginDisplayList_80172e00, (uint32_t la, uint32_t s), (la, s));
|
||||
|
||||
static void EndDisplayListInline() {
|
||||
if (g_dlInline) {
|
||||
g_dlInline = false;
|
||||
GxWorker::EndInline();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" uint32_t GX__EndDisplayList_80172eb4() {
|
||||
try {
|
||||
GXFlush();
|
||||
@@ -214,12 +232,14 @@ extern "C" uint32_t GX__EndDisplayList_80172eb4() {
|
||||
// "inactive" whenever the GXData pointer was null, so recording must
|
||||
// stop here even on the gd == 0 path.
|
||||
EndDisplayListRecording();
|
||||
EndDisplayListInline();
|
||||
|
||||
return wrapped == 0 ? Memory::Read32(kDlCountAddr) : 0;
|
||||
} catch (...) {
|
||||
// Leaving the shadow active would silently swallow every subsequent
|
||||
// FIFO write into a dead list.
|
||||
EndDisplayListRecording();
|
||||
EndDisplayListInline();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -229,5 +249,8 @@ PPC_NATIVE_OVERRIDE(80172EB4, GX__EndDisplayList_80172eb4, uint32_t, (), ());
|
||||
// Flush
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__Flush_8016e654() { GXFlush(); }
|
||||
extern "C" void GX__Flush_8016e654() {
|
||||
GX_DEFER_CALL0(GX__Flush_8016e654);
|
||||
GXFlush();
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016e654, GX__Flush_8016e654, (), ());
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <vector>
|
||||
#include <type_traits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
@@ -300,6 +301,9 @@ void EndDisplayListRecording();
|
||||
void WriteDisplayListData(uint32_t val, uint32_t sizeBytes);
|
||||
void BeginNextAuroraFrameWithRetry(std::chrono::milliseconds timeout = std::chrono::milliseconds(100));
|
||||
void EnsureAuroraFrameActive();
|
||||
void EnsureAuroraFrameForFifo();
|
||||
// Array base/stride as last set through GXSetArray on the game thread.
|
||||
bool ApiVtxArray(uint32_t attr, uint32_t& base, uint32_t& stride);
|
||||
|
||||
GXColor DecodeGxColor(uint32_t colorWord);
|
||||
void WriteGuestFloat(uint32_t addr, float value, const char* label = nullptr);
|
||||
@@ -342,6 +346,17 @@ bool ValidateTlutData(uint32_t objAddr, const TlutObjMeta& meta);
|
||||
TexObjMeta& GetTexObjMeta(uint32_t addr);
|
||||
TexObjMeta ExtractTexObjMetaFromGuest(uint32_t addr);
|
||||
bool TryGetOrExtractTexObjMeta(uint32_t addr, TexObjMeta& outMeta);
|
||||
|
||||
// Guest GXTexObj bytes captured on the game thread; while a scope is active on a thread, texobj
|
||||
// decoding for that address reads the captured bytes instead of live guest memory.
|
||||
using TexObjBytes = std::array<uint8_t, 32>;
|
||||
TexObjBytes CaptureTexObjBytes(uint32_t addr);
|
||||
struct TexObjSnapshotScope {
|
||||
TexObjSnapshotScope(uint32_t addr, const TexObjBytes& bytes) noexcept;
|
||||
~TexObjSnapshotScope();
|
||||
TexObjSnapshotScope(const TexObjSnapshotScope&) = delete;
|
||||
TexObjSnapshotScope& operator=(const TexObjSnapshotScope&) = delete;
|
||||
};
|
||||
TlutObjMeta& GetTlutObjMeta(uint32_t addr);
|
||||
GXTexObj* GetHostTexObj(uint32_t addr);
|
||||
GXTexObj* TryGetHostTexObj(uint32_t addr);
|
||||
|
||||
@@ -66,7 +66,7 @@ extern "C" void GX__LoadLightObjImm_80170320(uint32_t la, uint32_t lid) {
|
||||
if (!ValidateGuestLightObj(la, "GXLoadLightObjImm")) return;
|
||||
GXLightObj host{};
|
||||
InitializeHostLightFromGuest(host, la);
|
||||
GXLoadLightObjImm(&host, static_cast<GXLightID>(lid));
|
||||
GxWorker::Run([host, lid]() mutable { GXLoadLightObjImm(&host, static_cast<GXLightID>(lid)); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170320, GX__LoadLightObjImm_80170320, (uint32_t la, uint32_t lid), (la, lid));
|
||||
|
||||
@@ -77,21 +77,23 @@ PPC_NATIVE_OVERRIDE_VOID(80170320, GX__LoadLightObjImm_80170320, (uint32_t la, u
|
||||
extern "C" void GX__SetChanAmbColor_8017039c(uint32_t c, uint32_t cp) {
|
||||
EnsureAuroraFrameActive();
|
||||
GXColor color = DecodeGxColor(Memory::Read32(cp));
|
||||
GXSetChanAmbColor((GXChannelID)c, color);
|
||||
GxWorker::Run([c, color] { GXSetChanAmbColor((GXChannelID)c, color); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8017039c, GX__SetChanAmbColor_8017039c, (uint32_t c, uint32_t cp), (c, cp));
|
||||
|
||||
extern "C" void GX__SetChanMatColor_80170474(uint32_t c, uint32_t cp) {
|
||||
EnsureAuroraFrameActive();
|
||||
GXColor color = DecodeGxColor(Memory::Read32(cp));
|
||||
GXSetChanMatColor((GXChannelID)c, color);
|
||||
GxWorker::Run([c, color] { GXSetChanMatColor((GXChannelID)c, color); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170474, GX__SetChanMatColor_80170474, (uint32_t c, uint32_t cp), (c, cp));
|
||||
|
||||
extern "C" void GX__SetNumChans_8017054c(uint32_t n) { GXSetNumChans((u8)n); }
|
||||
extern "C" void GX__SetNumChans_8017054c(uint32_t n) {
|
||||
GX_DEFER_CALL(GX__SetNumChans_8017054c, n); GXSetNumChans((u8)n); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(8017054c, GX__SetNumChans_8017054c, (uint32_t n), (n));
|
||||
|
||||
extern "C" void GX__SetChanCtrl_80170570(uint32_t ch, uint32_t en, uint32_t as, uint32_t ms, uint32_t lm, uint32_t df, uint32_t af) {
|
||||
GX_DEFER_CALL(GX__SetChanCtrl_80170570, ch, en, as, ms, lm, df, af);
|
||||
GXSetChanCtrl((GXChannelID)ch, en!=0, (GXColorSrc)as, (GXColorSrc)ms, lm, (GXDiffuseFn)df, (GXAttnFn)af);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170570, GX__SetChanCtrl_80170570, (uint32_t ch, uint32_t en, uint32_t as, uint32_t ms, uint32_t lm, uint32_t df, uint32_t af), (ch, en, as, ms, lm, df, af));
|
||||
|
||||
@@ -110,17 +110,65 @@ bool g_tlutObjBoundsDirty = true;
|
||||
// word3 data address (<<5), word4 user data, word5 texFmt, word6 tlut, byte 0x1F flags. Exact
|
||||
// bit positions are decoded inline below.
|
||||
|
||||
namespace {
|
||||
thread_local uint32_t t_texObjSnapshotAddr = 0;
|
||||
thread_local const uint8_t* t_texObjSnapshotBytes = nullptr;
|
||||
|
||||
const uint8_t* TexObjSnapshotFor(uint32_t addr) noexcept {
|
||||
return (t_texObjSnapshotBytes != nullptr && addr == t_texObjSnapshotAddr) ? t_texObjSnapshotBytes : nullptr;
|
||||
}
|
||||
|
||||
uint32_t ReadTexObj32(uint32_t addr, uint32_t offset) {
|
||||
if (const uint8_t* bytes = TexObjSnapshotFor(addr)) return BigEndian::Read32(bytes + offset);
|
||||
return Memory::Read32(addr + offset);
|
||||
}
|
||||
|
||||
uint8_t ReadTexObj8(uint32_t addr, uint32_t offset) {
|
||||
if (const uint8_t* bytes = TexObjSnapshotFor(addr)) return bytes[offset];
|
||||
return Memory::Read8(addr + offset);
|
||||
}
|
||||
|
||||
uint64_t ReadTexObj64(uint32_t addr, uint32_t offset) {
|
||||
if (const uint8_t* bytes = TexObjSnapshotFor(addr)) {
|
||||
return (static_cast<uint64_t>(BigEndian::Read32(bytes + offset)) << 32) | BigEndian::Read32(bytes + offset + 4);
|
||||
}
|
||||
return Memory::Read64(addr + offset);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TexObjBytes CaptureTexObjBytes(uint32_t addr) {
|
||||
TexObjBytes bytes{};
|
||||
for (uint32_t offset = 0; offset < bytes.size(); offset += 4) {
|
||||
const uint32_t word = Memory::Read32(addr + offset);
|
||||
bytes[offset + 0] = static_cast<uint8_t>(word >> 24);
|
||||
bytes[offset + 1] = static_cast<uint8_t>(word >> 16);
|
||||
bytes[offset + 2] = static_cast<uint8_t>(word >> 8);
|
||||
bytes[offset + 3] = static_cast<uint8_t>(word);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
TexObjSnapshotScope::TexObjSnapshotScope(uint32_t addr, const TexObjBytes& bytes) noexcept {
|
||||
t_texObjSnapshotAddr = addr;
|
||||
t_texObjSnapshotBytes = bytes.data();
|
||||
}
|
||||
|
||||
TexObjSnapshotScope::~TexObjSnapshotScope() {
|
||||
t_texObjSnapshotAddr = 0;
|
||||
t_texObjSnapshotBytes = nullptr;
|
||||
}
|
||||
|
||||
TexObjMeta ExtractTexObjMetaFromGuest(uint32_t addr) {
|
||||
TexObjMeta meta{};
|
||||
try {
|
||||
uint32_t word0 = Memory::Read32(addr + 0x00);
|
||||
uint32_t word1 = Memory::Read32(addr + 0x04);
|
||||
uint32_t word2 = Memory::Read32(addr + 0x08);
|
||||
uint32_t word3 = Memory::Read32(addr + 0x0C);
|
||||
uint32_t word4 = Memory::Read32(addr + 0x10);
|
||||
uint32_t word5 = Memory::Read32(addr + 0x14);
|
||||
uint32_t word6 = Memory::Read32(addr + 0x18);
|
||||
uint8_t flags = Memory::Read8(addr + 0x1F);
|
||||
uint32_t word0 = ReadTexObj32(addr, 0x00);
|
||||
uint32_t word1 = ReadTexObj32(addr, 0x04);
|
||||
uint32_t word2 = ReadTexObj32(addr, 0x08);
|
||||
uint32_t word3 = ReadTexObj32(addr, 0x0C);
|
||||
uint32_t word4 = ReadTexObj32(addr, 0x10);
|
||||
uint32_t word5 = ReadTexObj32(addr, 0x14);
|
||||
uint32_t word6 = ReadTexObj32(addr, 0x18);
|
||||
uint8_t flags = ReadTexObj8(addr, 0x1F);
|
||||
|
||||
meta.wrapS = word0 & 0x3;
|
||||
meta.wrapT = (word0 >> 2) & 0x3;
|
||||
@@ -449,10 +497,10 @@ TexObjMeta MergeGuestTexObjMeta(const TexObjMeta& cached, const TexObjMeta& gues
|
||||
// caller must treat the shadow as absent) when the address is unreadable.
|
||||
bool ReadTexObjShadow(uint32_t addr, uint64_t (&out)[4]) noexcept {
|
||||
try {
|
||||
out[0] = Memory::Read64(addr + 0x00);
|
||||
out[1] = Memory::Read64(addr + 0x08);
|
||||
out[2] = Memory::Read64(addr + 0x10);
|
||||
out[3] = Memory::Read64(addr + 0x18);
|
||||
out[0] = ReadTexObj64(addr, 0x00);
|
||||
out[1] = ReadTexObj64(addr, 0x08);
|
||||
out[2] = ReadTexObj64(addr, 0x10);
|
||||
out[3] = ReadTexObj64(addr, 0x18);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
@@ -642,8 +690,11 @@ extern "C" void GxNotifyGuestRamDmaWrite(uint32_t addr, uint32_t size) {
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
MarkTexObjsDirtyForRange(addr, size);
|
||||
MarkTlutObjsDirtyForRange(addr, size);
|
||||
// Ordered with recorded GX work: retiring EFB copy textures emits aurora commands.
|
||||
GxWorker::Run([addr, size] {
|
||||
MarkTexObjsDirtyForRange(addr, size);
|
||||
MarkTlutObjsDirtyForRange(addr, size);
|
||||
});
|
||||
GxNotifyDisplayListMemoryWrite(addr, size);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,20 @@
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetBlendMode_8017277c(uint32_t t, uint32_t s, uint32_t d, uint32_t op) {
|
||||
GX_DEFER_CALL(GX__SetBlendMode_8017277c, t, s, d, op);
|
||||
GXSetBlendMode(static_cast<GXBlendMode>(t), static_cast<GXBlendFactor>(s),
|
||||
static_cast<GXBlendFactor>(d), static_cast<GXLogicOp>(op));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8017277c, GX__SetBlendMode_8017277c, (uint32_t t, uint32_t s, uint32_t d, uint32_t op), (t, s, d, op));
|
||||
|
||||
extern "C" void GX__SetColorUpdate_801727cc(uint32_t en) {
|
||||
GX_DEFER_CALL(GX__SetColorUpdate_801727cc, en);
|
||||
GXSetColorUpdate(static_cast<GXBool>(en));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801727cc, GX__SetColorUpdate_801727cc, (uint32_t en), (en));
|
||||
|
||||
extern "C" void GX__SetAlphaUpdate_801727f8(uint32_t en) {
|
||||
GX_DEFER_CALL(GX__SetAlphaUpdate_801727f8, en);
|
||||
GXSetAlphaUpdate(static_cast<GXBool>(en));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801727f8, GX__SetAlphaUpdate_801727f8, (uint32_t en), (en));
|
||||
@@ -26,11 +29,13 @@ PPC_NATIVE_OVERRIDE_VOID(801727f8, GX__SetAlphaUpdate_801727f8, (uint32_t en), (
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetZMode_80172824(uint32_t ce, uint32_t f, uint32_t ue) {
|
||||
GX_DEFER_CALL(GX__SetZMode_80172824, ce, f, ue);
|
||||
GXSetZMode(static_cast<GXBool>(ce), static_cast<GXCompare>(f), static_cast<GXBool>(ue));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80172824, GX__SetZMode_80172824, (uint32_t ce, uint32_t f, uint32_t ue), (ce, f, ue));
|
||||
|
||||
extern "C" void GX__SetZCompLoc_80172858(uint32_t bt) {
|
||||
GX_DEFER_CALL(GX__SetZCompLoc_80172858, bt);
|
||||
GXSetZCompLoc(static_cast<GXBool>(bt));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80172858, GX__SetZCompLoc_80172858, (uint32_t bt), (bt));
|
||||
@@ -40,16 +45,19 @@ PPC_NATIVE_OVERRIDE_VOID(80172858, GX__SetZCompLoc_80172858, (uint32_t bt), (bt)
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetPixelFmt_80172888(uint32_t pf, uint32_t zf) {
|
||||
GX_DEFER_CALL(GX__SetPixelFmt_80172888, pf, zf);
|
||||
GXSetPixelFmt(static_cast<GXPixelFmt>(pf), static_cast<GXZFmt16>(zf));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80172888, GX__SetPixelFmt_80172888, (uint32_t pf, uint32_t zf), (pf, zf));
|
||||
|
||||
extern "C" void GX__SetDither_80172930(uint32_t d) {
|
||||
GX_DEFER_CALL(GX__SetDither_80172930, d);
|
||||
GXSetDither(static_cast<GXBool>(d));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80172930, GX__SetDither_80172930, (uint32_t d), (d));
|
||||
|
||||
extern "C" void GX__SetDstAlpha_8017295c(uint32_t en, uint32_t a) {
|
||||
GX_DEFER_CALL(GX__SetDstAlpha_8017295c, en, a);
|
||||
GXSetDstAlpha(static_cast<GXBool>(en), static_cast<u8>(a));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8017295c, GX__SetDstAlpha_8017295c, (uint32_t en, uint32_t a), (en, a));
|
||||
@@ -59,17 +67,20 @@ PPC_NATIVE_OVERRIDE_VOID(8017295c, GX__SetDstAlpha_8017295c, (uint32_t en, uint3
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetFog_801722cc(uint32_t t, float sz, float ez, float nz, float fz, uint32_t cp) {
|
||||
GXSetFog((GXFogType)t, sz, ez, nz, fz, DecodeGxColor(Memory::Read32(cp)));
|
||||
const GXColor color = DecodeGxColor(Memory::Read32(cp));
|
||||
GxWorker::Run([=] { GXSetFog((GXFogType)t, sz, ez, nz, fz, color); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801722cc, GX__SetFog_801722cc, (uint32_t t, float sz, float ez, float nz, float fz, uint32_t cp), (t, sz, ez, nz, fz, cp));
|
||||
|
||||
extern "C" void GX__SetAlphaCompare_80172088(uint32_t c0, uint32_t r0, uint32_t op, uint32_t c1, uint32_t r1) {
|
||||
GX_DEFER_CALL(GX__SetAlphaCompare_80172088, c0, r0, op, c1, r1);
|
||||
g_alphaCompareValid = true;
|
||||
GXSetAlphaCompare((GXCompare)c0, (u8)r0, (GXAlphaOp)op, (GXCompare)c1, (u8)r1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80172088, GX__SetAlphaCompare_80172088, (uint32_t c0, uint32_t r0, uint32_t op, uint32_t c1, uint32_t r1), (c0, r0, op, c1, r1));
|
||||
|
||||
extern "C" void GX__SetZTexture_801720c0(uint32_t op, uint32_t f, uint32_t b) { GXSetZTexture((GXZTexOp)op, (GXTexFmt)f, b); }
|
||||
extern "C" void GX__SetZTexture_801720c0(uint32_t op, uint32_t f, uint32_t b) {
|
||||
GX_DEFER_CALL(GX__SetZTexture_801720c0, op, f, b); GXSetZTexture((GXZTexOp)op, (GXTexFmt)f, b); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(801720c0, GX__SetZTexture_801720c0, (uint32_t op, uint32_t f, uint32_t b), (op, f, b));
|
||||
|
||||
// ============================================================================
|
||||
@@ -77,12 +88,15 @@ PPC_NATIVE_OVERRIDE_VOID(801720c0, GX__SetZTexture_801720c0, (uint32_t op, uint3
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetCullMode_8016f3b8(uint32_t m) {
|
||||
GX_DEFER_CALL(GX__SetCullMode_8016f3b8, m);
|
||||
GXSetCullMode(static_cast<GXCullMode>(m));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f3b8, GX__SetCullMode_8016f3b8, (uint32_t m), (m));
|
||||
|
||||
extern "C" void GX__SetCoPlanar_8016f3e0(uint32_t en) { GXSetCoPlanar((GXBool)en); }
|
||||
extern "C" void GX__SetCoPlanar_8016f3e0(uint32_t en) {
|
||||
GX_DEFER_CALL(GX__SetCoPlanar_8016f3e0, en); GXSetCoPlanar((GXBool)en); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f3e0, GX__SetCoPlanar_8016f3e0, (uint32_t en), (en));
|
||||
|
||||
extern "C" void GX__SetClipMode_8017351c(uint32_t m) { GXSetClipMode((GXClipMode)m); }
|
||||
extern "C" void GX__SetClipMode_8017351c(uint32_t m) {
|
||||
GX_DEFER_CALL(GX__SetClipMode_8017351c, m); GXSetClipMode((GXClipMode)m); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(8017351c, GX__SetClipMode_8017351c, (uint32_t m), (m));
|
||||
|
||||
@@ -7,14 +7,23 @@ extern "C" void __GXSetSUTexRegs();
|
||||
// FIFO Write Helpers
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX_HLE_FIFO_WriteFloat(float val) {
|
||||
u32 raw; std::memcpy(&raw, &val, 4);
|
||||
try { HleFifoWrite(raw, 4); } catch (...) { RT_LOGF(RT_TAG_GX, "FIFO write float failed\n"); }
|
||||
static void FifoWrite(u32 val, uint32_t sizeBytes) {
|
||||
if (GxWorker::ShouldDefer()) {
|
||||
EnsureAuroraFrameForFifo();
|
||||
GxWorker::FifoWrite(val, sizeBytes);
|
||||
return;
|
||||
}
|
||||
HleFifoWrite(val, sizeBytes);
|
||||
}
|
||||
|
||||
extern "C" void GX_HLE_FIFO_Write32(uint32_t val) { HleFifoWrite(val, 4); }
|
||||
extern "C" void GX_HLE_FIFO_Write16(uint16_t val) { HleFifoWrite(static_cast<u32>(val), 2); }
|
||||
extern "C" void GX_HLE_FIFO_Write8(uint8_t val) { HleFifoWrite(static_cast<u32>(val), 1); }
|
||||
extern "C" void GX_HLE_FIFO_WriteFloat(float val) {
|
||||
u32 raw; std::memcpy(&raw, &val, 4);
|
||||
try { FifoWrite(raw, 4); } catch (...) { RT_LOGF(RT_TAG_GX, "FIFO write float failed\n"); }
|
||||
}
|
||||
|
||||
extern "C" void GX_HLE_FIFO_Write32(uint32_t val) { FifoWrite(val, 4); }
|
||||
extern "C" void GX_HLE_FIFO_Write16(uint16_t val) { FifoWrite(static_cast<u32>(val), 2); }
|
||||
extern "C" void GX_HLE_FIFO_Write8(uint8_t val) { FifoWrite(static_cast<u32>(val), 1); }
|
||||
|
||||
extern "C" void GX__SetDrawSync_8016ed08(uint32_t token) {
|
||||
(void)token;
|
||||
@@ -37,6 +46,7 @@ extern "C" void GX__FinishInterruptHandler_8016ed94() {
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016ed94, GX__FinishInterruptHandler_8016ed94, (), ());
|
||||
|
||||
extern "C" void GX__DrawDone_8016eab0() {
|
||||
GX_SYNC_INLINE();
|
||||
try { Memory::Write8(kGxDrawDoneFlagAddr, 0); } catch (...) {}
|
||||
GXDrawDone(); GX__FinishInterruptHandler_8016ed94();
|
||||
}
|
||||
@@ -44,7 +54,7 @@ PPC_NATIVE_OVERRIDE_VOID(8016eab0, GX__DrawDone_8016eab0, (), ());
|
||||
|
||||
extern "C" void GX__PixModeSync_8016eb70() {
|
||||
try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) Memory::Write16(gd + 2, 0); } catch (...) {}
|
||||
GXPixModeSync();
|
||||
GxWorker::Run([] { GXPixModeSync(); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016eb70, GX__PixModeSync_8016eb70, (), ());
|
||||
|
||||
@@ -60,7 +70,7 @@ PPC_NATIVE_OVERRIDE_VOID(8016b720, __GX__InitRevisionBits_8016b720, (), ());
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void __GX__SetSUTexRegs_801712f0() {
|
||||
__GXSetSUTexRegs();
|
||||
GxWorker::Run([] { __GXSetSUTexRegs(); });
|
||||
try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) Memory::Write16(gd + 2, 0); } catch (...) {}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801712f0, __GX__SetSUTexRegs_801712f0, (), ());
|
||||
@@ -82,7 +92,7 @@ PPC_NATIVE_OVERRIDE_VOID(80171c28, __GX__FlushTextureState_80171c28, (), ());
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetDispCopyFrame2Field_8016f5f8(uint32_t f) {
|
||||
GXSetDispCopyFrame2Field(f);
|
||||
GxWorker::Run([f] { GXSetDispCopyFrame2Field(f); });
|
||||
try {
|
||||
const uint32_t gd = Memory::Read32(kGXDataPtrAddr);
|
||||
if (gd) {
|
||||
@@ -94,7 +104,7 @@ extern "C" void GX__SetDispCopyFrame2Field_8016f5f8(uint32_t f) {
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f5f8, GX__SetDispCopyFrame2Field_8016f5f8, (uint32_t f), (f));
|
||||
|
||||
extern "C" void GX__SetCopyClamp_8016f618(uint32_t c) {
|
||||
GXSetCopyClamp(static_cast<GXFBClamp>(c));
|
||||
GxWorker::Run([c] { GXSetCopyClamp(static_cast<GXFBClamp>(c)); });
|
||||
try {
|
||||
const uint32_t gd = Memory::Read32(kGXDataPtrAddr);
|
||||
if (gd) {
|
||||
@@ -107,7 +117,7 @@ extern "C" void GX__SetCopyClamp_8016f618(uint32_t c) {
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f618, GX__SetCopyClamp_8016f618, (uint32_t c), (c));
|
||||
|
||||
extern "C" void GX__ClearBoundingBox_8016fecc() {
|
||||
GXClearBoundingBox();
|
||||
GxWorker::Run([] { GXClearBoundingBox(); });
|
||||
try {
|
||||
const uint32_t gd = Memory::Read32(kGXDataPtrAddr);
|
||||
if (gd) Memory::Write16(gd + 2, 0);
|
||||
|
||||
@@ -25,6 +25,7 @@ inline bool TevSwapOk(uint32_t id) { return GxTevIdOk(id, GX_MAX_TEVSWAP, "TEV s
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetNumTevStages_801722a8(uint32_t n) {
|
||||
GX_DEFER_CALL(GX__SetNumTevStages_801722a8, n);
|
||||
// GXSetNumTevStages takes a count, not an index, so the inclusive bound is
|
||||
// GX_MAX_TEVSTAGE itself.
|
||||
if (n > GX_MAX_TEVSTAGE) {
|
||||
@@ -36,12 +37,14 @@ extern "C" void GX__SetNumTevStages_801722a8(uint32_t n) {
|
||||
PPC_NATIVE_OVERRIDE_VOID(801722a8, GX__SetNumTevStages_801722a8, (uint32_t n), (n));
|
||||
|
||||
extern "C" void GX__SetTevOp_80171c4c(uint32_t s, uint32_t m) {
|
||||
GX_DEFER_CALL(GX__SetTevOp_80171c4c, s, m);
|
||||
if (!TevStageOk(s)) return;
|
||||
GXSetTevOp((GXTevStageID)s, (GXTevMode)m);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171c4c, GX__SetTevOp_80171c4c, (uint32_t s, uint32_t m), (s, m));
|
||||
|
||||
extern "C" void GX__SetTevOrder_8017214c(uint32_t s, uint32_t c, uint32_t m, uint32_t col) {
|
||||
GX_DEFER_CALL(GX__SetTevOrder_8017214c, s, c, m, col);
|
||||
if (!TevStageOk(s)) return;
|
||||
GXSetTevOrder((GXTevStageID)s, (GXTexCoordID)c, (GXTexMapID)m, (GXChannelID)col);
|
||||
}
|
||||
@@ -52,12 +55,14 @@ PPC_NATIVE_OVERRIDE_VOID(8017214c, GX__SetTevOrder_8017214c, (uint32_t s, uint32
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetTevColorIn_80171ce0(uint32_t s, uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
|
||||
GX_DEFER_CALL(GX__SetTevColorIn_80171ce0, s, a, b, c, d);
|
||||
if (!TevStageOk(s)) return;
|
||||
GXSetTevColorIn((GXTevStageID)s, (GXTevColorArg)a, (GXTevColorArg)b, (GXTevColorArg)c, (GXTevColorArg)d);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171ce0, GX__SetTevColorIn_80171ce0, (uint32_t s, uint32_t a, uint32_t b, uint32_t c, uint32_t d), (s, a, b, c, d));
|
||||
|
||||
extern "C" void GX__SetTevAlphaIn_80171d20(uint32_t s, uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
|
||||
GX_DEFER_CALL(GX__SetTevAlphaIn_80171d20, s, a, b, c, d);
|
||||
if (!TevStageOk(s)) return;
|
||||
GXSetTevAlphaIn((GXTevStageID)s, (GXTevAlphaArg)a, (GXTevAlphaArg)b, (GXTevAlphaArg)c, (GXTevAlphaArg)d);
|
||||
}
|
||||
@@ -68,12 +73,14 @@ PPC_NATIVE_OVERRIDE_VOID(80171d20, GX__SetTevAlphaIn_80171d20, (uint32_t s, uint
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetTevColorOp_80171d60(uint32_t s, uint32_t op, uint32_t b, uint32_t sc, uint32_t cl, uint32_t or_) {
|
||||
GX_DEFER_CALL(GX__SetTevColorOp_80171d60, s, op, b, sc, cl, or_);
|
||||
if (!TevStageOk(s) || !TevRegOk(or_)) return;
|
||||
GXSetTevColorOp((GXTevStageID)s, (GXTevOp)op, (GXTevBias)b, (GXTevScale)sc, (GXBool)cl, (GXTevRegID)or_);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171d60, GX__SetTevColorOp_80171d60, (uint32_t s, uint32_t op, uint32_t b, uint32_t sc, uint32_t cl, uint32_t or_), (s, op, b, sc, cl, or_));
|
||||
|
||||
extern "C" void GX__SetTevAlphaOp_80171db8(uint32_t s, uint32_t op, uint32_t b, uint32_t sc, uint32_t cl, uint32_t or_) {
|
||||
GX_DEFER_CALL(GX__SetTevAlphaOp_80171db8, s, op, b, sc, cl, or_);
|
||||
if (!TevStageOk(s) || !TevRegOk(or_)) return;
|
||||
GXSetTevAlphaOp((GXTevStageID)s, (GXTevOp)op, (GXTevBias)b, (GXTevScale)sc, (GXBool)cl, (GXTevRegID)or_);
|
||||
}
|
||||
@@ -86,7 +93,7 @@ PPC_NATIVE_OVERRIDE_VOID(80171db8, GX__SetTevAlphaOp_80171db8, (uint32_t s, uint
|
||||
extern "C" void GX__SetTevColor_80171e10(uint32_t id, uint32_t cp) {
|
||||
if (!TevRegOk(id)) return;
|
||||
const uint8_t* p=Memory::GetPointer(cp, 4); GXColor c; c.r=p[0]; c.g=p[1]; c.b=p[2]; c.a=p[3];
|
||||
GXSetTevColor((GXTevRegID)id, c);
|
||||
GxWorker::Run([id, c] { GXSetTevColor((GXTevRegID)id, c); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171e10, GX__SetTevColor_80171e10, (uint32_t id, uint32_t cp), (id, cp));
|
||||
|
||||
@@ -94,21 +101,23 @@ extern "C" void GX__SetTevColorS10_80171e70(uint32_t id, uint32_t cp) {
|
||||
if (!TevRegOk(id)) return;
|
||||
const uint8_t* p=Memory::GetPointer(cp, 8); GXColorS10 c;
|
||||
c.r=(p[0]<<8)|p[1]; c.g=(p[2]<<8)|p[3]; c.b=(p[4]<<8)|p[5]; c.a=(p[6]<<8)|p[7];
|
||||
GXSetTevColorS10((GXTevRegID)id, c);
|
||||
GxWorker::Run([id, c] { GXSetTevColorS10((GXTevRegID)id, c); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171e70, GX__SetTevColorS10_80171e70, (uint32_t id, uint32_t cp), (id, cp));
|
||||
|
||||
extern "C" void GX__SetTevKColor_80171ed4(uint32_t id, uint32_t cp) {
|
||||
if (!TevKColorOk(id)) return;
|
||||
const uint8_t* p=Memory::GetPointer(cp, 4); GXColor c; c.r=p[0]; c.g=p[1]; c.b=p[2]; c.a=p[3];
|
||||
GXSetTevKColor((GXTevKColorID)id, c);
|
||||
GxWorker::Run([id, c] { GXSetTevKColor((GXTevKColorID)id, c); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171ed4, GX__SetTevKColor_80171ed4, (uint32_t id, uint32_t cp), (id, cp));
|
||||
|
||||
extern "C" void GX__SetTevKColorSel_80171f30(uint32_t s, uint32_t sel) { if (!TevStageOk(s)) return; GXSetTevKColorSel((GXTevStageID)s, (GXTevKColorSel)sel); }
|
||||
extern "C" void GX__SetTevKColorSel_80171f30(uint32_t s, uint32_t sel) {
|
||||
GX_DEFER_CALL(GX__SetTevKColorSel_80171f30, s, sel); if (!TevStageOk(s)) return; GXSetTevKColorSel((GXTevStageID)s, (GXTevKColorSel)sel); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171f30, GX__SetTevKColorSel_80171f30, (uint32_t s, uint32_t sel), (s, sel));
|
||||
|
||||
extern "C" void GX__SetTevKAlphaSel_80171f80(uint32_t s, uint32_t sel) { if (!TevStageOk(s)) return; GXSetTevKAlphaSel((GXTevStageID)s, (GXTevKAlphaSel)sel); }
|
||||
extern "C" void GX__SetTevKAlphaSel_80171f80(uint32_t s, uint32_t sel) {
|
||||
GX_DEFER_CALL(GX__SetTevKAlphaSel_80171f80, s, sel); if (!TevStageOk(s)) return; GXSetTevKAlphaSel((GXTevStageID)s, (GXTevKAlphaSel)sel); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171f80, GX__SetTevKAlphaSel_80171f80, (uint32_t s, uint32_t sel), (s, sel));
|
||||
|
||||
// ============================================================================
|
||||
@@ -116,12 +125,14 @@ PPC_NATIVE_OVERRIDE_VOID(80171f80, GX__SetTevKAlphaSel_80171f80, (uint32_t s, ui
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetTevSwapModeTable_8017200c(uint32_t id, uint32_t r, uint32_t g, uint32_t b, uint32_t a) {
|
||||
GX_DEFER_CALL(GX__SetTevSwapModeTable_8017200c, id, r, g, b, a);
|
||||
if (!TevSwapOk(id)) return;
|
||||
GXSetTevSwapModeTable((GXTevSwapSel)id, (GXTevColorChan)r, (GXTevColorChan)g, (GXTevColorChan)b, (GXTevColorChan)a);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8017200c, GX__SetTevSwapModeTable_8017200c, (uint32_t id, uint32_t r, uint32_t g, uint32_t b, uint32_t a), (id, r, g, b, a));
|
||||
|
||||
extern "C" void GX__SetTevSwapMode_80171fd0(uint32_t s, uint32_t rs, uint32_t ts) {
|
||||
GX_DEFER_CALL(GX__SetTevSwapMode_80171fd0, s, rs, ts);
|
||||
if (!TevStageOk(s) || !TevSwapOk(rs) || !TevSwapOk(ts)) return;
|
||||
GXSetTevSwapMode((GXTevStageID)s, (GXTevSwapSel)rs, (GXTevSwapSel)ts);
|
||||
}
|
||||
|
||||
+179
-102
@@ -242,6 +242,31 @@ static uint32_t ComputeMaxMipLevel(uint16_t width, uint16_t height) noexcept {
|
||||
return levels;
|
||||
}
|
||||
|
||||
// GXInitTexObj* writes the guest struct synchronously; the host half runs on the GX worker with the
|
||||
// struct bytes from before that write, which is what an inline call's shadow capture sees.
|
||||
template <typename Hle, typename Guest>
|
||||
static void InitTexObjSplit(uint32_t oa, Hle&& hle, Guest&& guest) {
|
||||
if (!GxWorker::ShouldDefer()) {
|
||||
hle();
|
||||
guest();
|
||||
return;
|
||||
}
|
||||
std::optional<TexObjBytes> before;
|
||||
try {
|
||||
before = CaptureTexObjBytes(oa);
|
||||
} catch (...) {
|
||||
}
|
||||
guest();
|
||||
GxWorker::Submit([oa, before, hle = std::forward<Hle>(hle)] {
|
||||
if (before) {
|
||||
TexObjSnapshotScope snapshot(oa, *before);
|
||||
hle();
|
||||
} else {
|
||||
hle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
extern "C" void GX__InitTexObj_801707f8(uint32_t oa, uint32_t da, uint32_t w, uint32_t h, uint32_t f, uint32_t ws, uint32_t wt, uint32_t m) {
|
||||
if (w == 0 || h == 0) {
|
||||
auto* cpu = TryGetCpuContext();
|
||||
@@ -250,12 +275,15 @@ extern "C" void GX__InitTexObj_801707f8(uint32_t oa, uint32_t da, uint32_t w, ui
|
||||
oa, da, w, h, f, ws, wt, m, cpu ? cpu->pc : 0u, cpu ? cpu->lr : 0u);
|
||||
}
|
||||
const uint32_t canonicalDataAddr = CanonicalizeGxMainRamAddress(da);
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex); GXTexObj* obj = CreateHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.dataAddr=canonicalDataAddr; meta.width=(u16)w; meta.height=(u16)h; meta.format=f; meta.wrapS=ws; meta.wrapT=wt; meta.mipmap=(m!=0); meta.userData=0; meta.needsUpload=true;
|
||||
GXInitTexObj(obj, GuestToHostPtr(da), (u16)w, (u16)h, (GXTexFmt)f, (GXTexWrapMode)ws, (GXTexWrapMode)wt, (GXBool)m); MarkHostTexObjConstructed(oa);
|
||||
// Also write to guest memory so reads work
|
||||
WriteGuestTexObj(oa, canonicalDataAddr, (u16)w, (u16)h, f, ws, wt, m != 0, false, 0);
|
||||
InitTexObjSplit(oa, [=] {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex); GXTexObj* obj = CreateHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.dataAddr=canonicalDataAddr; meta.width=(u16)w; meta.height=(u16)h; meta.format=f; meta.wrapS=ws; meta.wrapT=wt; meta.mipmap=(m!=0); meta.userData=0; meta.needsUpload=true;
|
||||
GXInitTexObj(obj, GuestToHostPtr(da), (u16)w, (u16)h, (GXTexFmt)f, (GXTexWrapMode)ws, (GXTexWrapMode)wt, (GXBool)m); MarkHostTexObjConstructed(oa);
|
||||
}, [=] {
|
||||
WriteGuestTexObj(oa, canonicalDataAddr, (u16)w, (u16)h, f, ws, wt, m != 0, false, 0);
|
||||
});
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE_VOID(801707f8, GX__InitTexObj_801707f8, (uint32_t oa, uint32_t da, uint32_t w, uint32_t h, uint32_t f, uint32_t ws, uint32_t wt, uint32_t m), (oa, da, w, h, f, ws, wt, m));
|
||||
|
||||
extern "C" void GX__InitTexObj_switch_80170938(uint32_t oa, uint32_t da, uint32_t w, uint32_t h, uint32_t f, uint32_t ws, uint32_t wt, uint32_t m) { GX__InitTexObj_801707f8(oa, da, w, h, f, ws, wt, m); }
|
||||
@@ -291,132 +319,151 @@ extern "C" void GX__InitTexObjCI_80170a04(uint32_t oa, uint32_t da, uint32_t w,
|
||||
oa, da, w, h, f, ws, wt, m, tl, cpu ? cpu->pc : 0u, cpu ? cpu->lr : 0u);
|
||||
}
|
||||
const uint32_t canonicalDataAddr = CanonicalizeGxMainRamAddress(da);
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex); GXTexObj* obj = CreateHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.dataAddr=canonicalDataAddr; meta.width=(u16)w; meta.height=(u16)h; meta.format=f; meta.wrapS=ws; meta.wrapT=wt; meta.mipmap=(m!=0); meta.tlut=tl; meta.userData=0; meta.needsUpload=true;
|
||||
GXInitTexObjCI(obj, GuestToHostPtr(da), (u16)w, (u16)h, (GXCITexFmt)f, (GXTexWrapMode)ws, (GXTexWrapMode)wt, (GXBool)m, tl); MarkHostTexObjConstructed(oa);
|
||||
// Also write to guest memory so reads work
|
||||
WriteGuestTexObj(oa, canonicalDataAddr, (u16)w, (u16)h, f, ws, wt, m != 0, true, tl);
|
||||
// GXInitTexObjCI clears bit1 in the flags byte; keep guest memory consistent.
|
||||
try {
|
||||
uint8_t flags = Memory::Read8(oa + 0x1F);
|
||||
Memory::Write8(oa + 0x1F, static_cast<uint8_t>(flags & 0xFD));
|
||||
} catch (...) {
|
||||
}
|
||||
InitTexObjSplit(oa, [=] {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex); GXTexObj* obj = CreateHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.dataAddr=canonicalDataAddr; meta.width=(u16)w; meta.height=(u16)h; meta.format=f; meta.wrapS=ws; meta.wrapT=wt; meta.mipmap=(m!=0); meta.tlut=tl; meta.userData=0; meta.needsUpload=true;
|
||||
GXInitTexObjCI(obj, GuestToHostPtr(da), (u16)w, (u16)h, (GXCITexFmt)f, (GXTexWrapMode)ws, (GXTexWrapMode)wt, (GXBool)m, tl); MarkHostTexObjConstructed(oa);
|
||||
}, [=] {
|
||||
WriteGuestTexObj(oa, canonicalDataAddr, (u16)w, (u16)h, f, ws, wt, m != 0, true, tl);
|
||||
// GXInitTexObjCI clears bit1 in the flags byte; keep guest memory consistent.
|
||||
try {
|
||||
uint8_t flags = Memory::Read8(oa + 0x1F);
|
||||
Memory::Write8(oa + 0x1F, static_cast<uint8_t>(flags & 0xFD));
|
||||
} catch (...) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170a04, GX__InitTexObjCI_80170a04, (uint32_t oa, uint32_t da, uint32_t w, uint32_t h, uint32_t f, uint32_t ws, uint32_t wt, uint32_t m, uint32_t tl), (oa, da, w, h, f, ws, wt, m, tl));
|
||||
|
||||
extern "C" void GX__InitTexObjLOD_80170a4c(uint32_t oa, uint32_t mif, uint32_t maf, float mil, float mal, float lb, uint32_t bc, uint32_t el, uint32_t ma) {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex); GXTexObj* obj = GetHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
float fmal = std::isfinite(mal) && mal >= 0.f ? mal : 0.f, fmil = std::isfinite(mil) && mil >= 0.f ? std::min(mil, fmal) : 0.f;
|
||||
meta.minFilter=mif; meta.magFilter=maf; meta.minLod=fmil; meta.maxLod=fmal; meta.lodBias=lb; meta.biasClamp=(bc!=0); meta.edgeLod=(el!=0); meta.maxAniso=ma;
|
||||
GXInitTexObjLOD(obj, (GXTexFilter)mif, (GXTexFilter)maf, fmil, fmal, lb, (GXBool)bc, (GXBool)el, (GXAnisotropy)ma);
|
||||
// Also write LOD info to guest memory
|
||||
WriteGuestTexObjLOD(oa, mif, maf, fmil, fmal, lb, bc != 0, el != 0, ma);
|
||||
const float fmal = std::isfinite(mal) && mal >= 0.f ? mal : 0.f, fmil = std::isfinite(mil) && mil >= 0.f ? std::min(mil, fmal) : 0.f;
|
||||
InitTexObjSplit(oa, [=] {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex); GXTexObj* obj = GetHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.minFilter=mif; meta.magFilter=maf; meta.minLod=fmil; meta.maxLod=fmal; meta.lodBias=lb; meta.biasClamp=(bc!=0); meta.edgeLod=(el!=0); meta.maxAniso=ma;
|
||||
GXInitTexObjLOD(obj, (GXTexFilter)mif, (GXTexFilter)maf, fmil, fmal, lb, (GXBool)bc, (GXBool)el, (GXAnisotropy)ma);
|
||||
}, [=] {
|
||||
WriteGuestTexObjLOD(oa, mif, maf, fmil, fmal, lb, bc != 0, el != 0, ma);
|
||||
});
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170a4c, GX__InitTexObjLOD_80170a4c, (uint32_t oa, uint32_t mif, uint32_t maf, float mil, float mal, float lb, uint32_t bc, uint32_t el, uint32_t ma), (oa, mif, maf, mil, mal, lb, bc, el, ma));
|
||||
|
||||
extern "C" void GX__InitTexObjWrapMode_80170b50(uint32_t oa, uint32_t ws, uint32_t wt) {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
GXTexObj* obj = GetHostTexObj(oa);
|
||||
TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.wrapS = ws;
|
||||
meta.wrapT = wt;
|
||||
try {
|
||||
uint32_t word0 = Memory::Read32(oa + 0x00);
|
||||
word0 = (word0 & ~0xFu) | (ws & 0x3u) | ((wt & 0x3u) << 2);
|
||||
Memory::Write32(oa + 0x00, word0);
|
||||
} catch (...) {
|
||||
}
|
||||
GXInitTexObjWrapMode(obj, (GXTexWrapMode)ws, (GXTexWrapMode)wt);
|
||||
InitTexObjSplit(oa, [=] {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
GXTexObj* obj = GetHostTexObj(oa);
|
||||
TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.wrapS = ws;
|
||||
meta.wrapT = wt;
|
||||
GXInitTexObjWrapMode(obj, (GXTexWrapMode)ws, (GXTexWrapMode)wt);
|
||||
}, [=] {
|
||||
try {
|
||||
uint32_t word0 = Memory::Read32(oa + 0x00);
|
||||
word0 = (word0 & ~0xFu) | (ws & 0x3u) | ((wt & 0x3u) << 2);
|
||||
Memory::Write32(oa + 0x00, word0);
|
||||
} catch (...) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170b50, GX__InitTexObjWrapMode_80170b50, (uint32_t oa, uint32_t ws, uint32_t wt), (oa, ws, wt));
|
||||
|
||||
extern "C" void GX__InitTexObjTlut_80170b64(uint32_t oa, uint32_t tl) {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
GXTexObj* obj = GetHostTexObj(oa);
|
||||
TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.tlut = tl;
|
||||
GXInitTexObjTlut(obj, tl);
|
||||
// Keep guest GXTexObj coherent for later GXLoadTexObj from memory.
|
||||
try {
|
||||
Memory::Write32(oa + 0x18, tl);
|
||||
} catch (...) {
|
||||
}
|
||||
InitTexObjSplit(oa, [=] {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
GXTexObj* obj = GetHostTexObj(oa);
|
||||
TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.tlut = tl;
|
||||
GXInitTexObjTlut(obj, tl);
|
||||
}, [=] {
|
||||
// Keep guest GXTexObj coherent for later GXLoadTexObj from memory.
|
||||
try {
|
||||
Memory::Write32(oa + 0x18, tl);
|
||||
} catch (...) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170b64, GX__InitTexObjTlut_80170b64, (uint32_t oa, uint32_t tl), (oa, tl));
|
||||
|
||||
// GXInitTexObjFilter - sets mag/min filter modes
|
||||
extern "C" void GX__InitTexObjFilter_80170b6c(uint32_t oa, uint32_t minFilter, uint32_t magFilter) {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.minFilter = minFilter;
|
||||
meta.magFilter = magFilter;
|
||||
|
||||
try {
|
||||
uint32_t word0 = Memory::Read32(oa + 0x00);
|
||||
// bit 4: magFilter - set if magFilter == GX_LINEAR (1), clear otherwise
|
||||
// Original code used cntlzw to check: cntlzw(magFilter-1) >> 1 & 0x10
|
||||
// This sets bit 4 when magFilter-1 is 0 (i.e., magFilter == 1)
|
||||
uint32_t magBit = (magFilter == 1) ? 0x10 : 0;
|
||||
word0 = (word0 & 0xFFFFFFEF) | magBit;
|
||||
// bits [7:5]: minFilter from LUT
|
||||
uint32_t minBits = (kGxToHwMinFilter[minFilter & 7] & 7) << 5;
|
||||
word0 = (word0 & 0xFFFFFF0F) | magBit | minBits;
|
||||
Memory::Write32(oa + 0x00, word0);
|
||||
} catch (...) {}
|
||||
|
||||
// Update host texture object if it exists
|
||||
if (GXTexObj* obj = TryGetHostTexObj(oa)) {
|
||||
ApplyHostTexObjLod(obj, meta, (GXTexFilter)minFilter, (GXTexFilter)magFilter, meta.lodBias,
|
||||
(GXAnisotropy)meta.maxAniso);
|
||||
}
|
||||
InitTexObjSplit(oa, [=] {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.minFilter = minFilter;
|
||||
meta.magFilter = magFilter;
|
||||
// Update host texture object if it exists
|
||||
if (GXTexObj* obj = TryGetHostTexObj(oa)) {
|
||||
ApplyHostTexObjLod(obj, meta, (GXTexFilter)minFilter, (GXTexFilter)magFilter, meta.lodBias,
|
||||
(GXAnisotropy)meta.maxAniso);
|
||||
}
|
||||
}, [=] {
|
||||
try {
|
||||
uint32_t word0 = Memory::Read32(oa + 0x00);
|
||||
// bit 4: magFilter - set if magFilter == GX_LINEAR (1), clear otherwise
|
||||
// Original code used cntlzw to check: cntlzw(magFilter-1) >> 1 & 0x10
|
||||
// This sets bit 4 when magFilter-1 is 0 (i.e., magFilter == 1)
|
||||
uint32_t magBit = (magFilter == 1) ? 0x10 : 0;
|
||||
word0 = (word0 & 0xFFFFFFEF) | magBit;
|
||||
// bits [7:5]: minFilter from LUT
|
||||
uint32_t minBits = (kGxToHwMinFilter[minFilter & 7] & 7) << 5;
|
||||
word0 = (word0 & 0xFFFFFF0F) | magBit | minBits;
|
||||
Memory::Write32(oa + 0x00, word0);
|
||||
} catch (...) {}
|
||||
});
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170b6c, GX__InitTexObjFilter_80170b6c, (uint32_t oa, uint32_t minFilter, uint32_t magFilter), (oa, minFilter, magFilter));
|
||||
|
||||
// GXInitTexObjLODBias - sets LOD bias value
|
||||
extern "C" void GX__InitTexObjLODBias_80170b94(uint32_t oa, float bias) {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
|
||||
// Clamp bias to [-4.0, 3.99...] range as per SDK
|
||||
constexpr float kMinBias = -4.0f;
|
||||
constexpr float kMaxBias = 3.99f; // Actually ~3.990000009536743
|
||||
float clampedBias = bias;
|
||||
if (clampedBias < kMinBias) clampedBias = kMinBias;
|
||||
if (clampedBias >= 4.0f) clampedBias = kMaxBias;
|
||||
|
||||
meta.lodBias = clampedBias;
|
||||
|
||||
// Update guest GXTexObj word0 with bias (scale by 32, store in bits [16:9])
|
||||
try {
|
||||
uint32_t word0 = Memory::Read32(oa + 0x00);
|
||||
int8_t biasScaled = static_cast<int8_t>(clampedBias * 32.0f);
|
||||
word0 = (word0 & 0xFFFE01FF) | ((static_cast<uint32_t>(static_cast<uint8_t>(biasScaled)) & 0xFF) << 9);
|
||||
Memory::Write32(oa + 0x00, word0);
|
||||
} catch (...) {}
|
||||
|
||||
// Update host texture object if it exists
|
||||
if (GXTexObj* obj = TryGetHostTexObj(oa)) {
|
||||
ApplyHostTexObjLod(obj, meta, (GXTexFilter)meta.minFilter, (GXTexFilter)meta.magFilter,
|
||||
clampedBias, (GXAnisotropy)meta.maxAniso);
|
||||
}
|
||||
|
||||
InitTexObjSplit(oa, [=] {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.lodBias = clampedBias;
|
||||
// Update host texture object if it exists
|
||||
if (GXTexObj* obj = TryGetHostTexObj(oa)) {
|
||||
ApplyHostTexObjLod(obj, meta, (GXTexFilter)meta.minFilter, (GXTexFilter)meta.magFilter,
|
||||
clampedBias, (GXAnisotropy)meta.maxAniso);
|
||||
}
|
||||
}, [=] {
|
||||
// Update guest GXTexObj word0 with bias (scale by 32, store in bits [16:9])
|
||||
try {
|
||||
uint32_t word0 = Memory::Read32(oa + 0x00);
|
||||
int8_t biasScaled = static_cast<int8_t>(clampedBias * 32.0f);
|
||||
word0 = (word0 & 0xFFFE01FF) | ((static_cast<uint32_t>(static_cast<uint8_t>(biasScaled)) & 0xFF) << 9);
|
||||
Memory::Write32(oa + 0x00, word0);
|
||||
} catch (...) {}
|
||||
});
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170b94, GX__InitTexObjLODBias_80170b94, (uint32_t oa, float bias), (oa, bias));
|
||||
|
||||
extern "C" void GX__InitTexObjUserData_80170be8(uint32_t oa, uint32_t userData) {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.userData = userData;
|
||||
|
||||
// The RVL SDK stores this opaque guest pointer verbatim at GXTexObj + 0x10.
|
||||
WriteGuest32(oa + 0x10, userData, "GXInitTexObjUserData");
|
||||
|
||||
// Aurora keeps an expanded host-side object, so mirror the pointer when
|
||||
// that representation has already been constructed.
|
||||
if (GXTexObj* obj = TryGetHostTexObj(oa)) {
|
||||
GXInitTexObjUserData(obj, GuestToHostPtr(userData));
|
||||
}
|
||||
InitTexObjSplit(oa, [=] {
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
TexObjMeta& meta = GetTexObjMeta(oa);
|
||||
meta.userData = userData;
|
||||
// Aurora keeps an expanded host-side object, so mirror the pointer when
|
||||
// that representation has already been constructed.
|
||||
if (GXTexObj* obj = TryGetHostTexObj(oa)) {
|
||||
GXInitTexObjUserData(obj, GuestToHostPtr(userData));
|
||||
}
|
||||
}, [=] {
|
||||
// The RVL SDK stores this opaque guest pointer verbatim at GXTexObj + 0x10.
|
||||
WriteGuest32(oa + 0x10, userData, "GXInitTexObjUserData");
|
||||
});
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170be8, GX__InitTexObjUserData_80170be8,
|
||||
(uint32_t oa, uint32_t userData), (oa, userData));
|
||||
|
||||
@@ -446,7 +493,7 @@ static void BindUnloadableTexturePlaceholder(uint32_t tid) {
|
||||
GXLoadTexObj(&s_placeholder, (GXTexMapID)tid);
|
||||
}
|
||||
|
||||
extern "C" void GX__LoadTexObj_80170f2c(uint32_t oa, uint32_t tid) {
|
||||
static void LoadTexObjImpl(uint32_t oa, uint32_t tid) {
|
||||
static uint32_t s_invalidMetaLogCount = 0;
|
||||
static uint32_t s_invalidTidLogCount = 0;
|
||||
static uint32_t s_invalidDimLogCount = 0;
|
||||
@@ -631,6 +678,27 @@ extern "C" void GX__LoadTexObj_80170f2c(uint32_t oa, uint32_t tid) {
|
||||
BindUnloadableTexturePlaceholder(tid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void GX__LoadTexObj_80170f2c(uint32_t oa, uint32_t tid) {
|
||||
if (GxWorker::ShouldDefer()) {
|
||||
// The CPU-side SDK reads the GXTexObj at call time; the texture data itself is read later.
|
||||
std::optional<TexObjBytes> bytes;
|
||||
try {
|
||||
bytes = CaptureTexObjBytes(oa);
|
||||
} catch (...) {
|
||||
}
|
||||
GxWorker::Submit([oa, tid, bytes] {
|
||||
if (bytes) {
|
||||
TexObjSnapshotScope snapshot(oa, *bytes);
|
||||
LoadTexObjImpl(oa, tid);
|
||||
} else {
|
||||
LoadTexObjImpl(oa, tid);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LoadTexObjImpl(oa, tid);
|
||||
}
|
||||
try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) { Memory::Write32(gd + 0x5FCu, Memory::Read32(gd + 0x5FCu) | 1u); Memory::Write16(gd + 2, 0); } } catch (...) {}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170f2c, GX__LoadTexObj_80170f2c, (uint32_t oa, uint32_t tid), (oa, tid));
|
||||
@@ -643,6 +711,7 @@ PPC_NATIVE_OVERRIDE_VOID(80170dc8, GX__LoadTexObjPreLoaded_80170dc8, (uint32_t o
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__GetTexObjAll_80170bf8(uint32_t oa, uint32_t dp, uint32_t wp, uint32_t hp, uint32_t fp, uint32_t wsp, uint32_t wtp, uint32_t mp) {
|
||||
GX_SYNC_INLINE();
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
TexObjMeta meta;
|
||||
if (!TryGetOrExtractTexObjMeta(oa, meta)) {
|
||||
@@ -657,6 +726,7 @@ extern "C" void GX__GetTexObjAll_80170bf8(uint32_t oa, uint32_t dp, uint32_t wp,
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170bf8, GX__GetTexObjAll_80170bf8, (uint32_t oa, uint32_t dp, uint32_t wp, uint32_t hp, uint32_t fp, uint32_t wsp, uint32_t wtp, uint32_t mp), (oa, dp, wp, hp, fp, wsp, wtp, mp));
|
||||
|
||||
extern "C" void GX__GetTexObjLODAll_80170cbc(uint32_t oa, uint32_t mifp, uint32_t mafp, uint32_t milp, uint32_t malp, uint32_t lbp, uint32_t bcp, uint32_t elp, uint32_t map) {
|
||||
GX_SYNC_INLINE();
|
||||
std::lock_guard<std::mutex> guard(g_texObjMutex);
|
||||
TexObjMeta meta;
|
||||
if (!TryGetOrExtractTexObjMeta(oa, meta)) {
|
||||
@@ -676,6 +746,7 @@ PPC_NATIVE_OVERRIDE_VOID(80170cbc, GX__GetTexObjLODAll_80170cbc, (uint32_t oa, u
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__InitTlutObj_80170f80(uint32_t oa, uint32_t da, uint32_t f, uint32_t e) {
|
||||
GX_DEFER_CALL(GX__InitTlutObj_80170f80, oa, da, f, e);
|
||||
std::lock_guard<std::mutex> guard(g_tlutObjMutex); GXTlutObj* obj = CreateHostTlutObj(oa); TlutObjMeta& meta = GetTlutObjMeta(oa);
|
||||
meta.dataAddr=CanonicalizeGxMainRamAddress(da); meta.format=f; meta.entries=(u16)e; meta.dirty=false;
|
||||
// Leave the object unconstructed on a bad descriptor. A later GXLoadTlut
|
||||
@@ -686,7 +757,12 @@ extern "C" void GX__InitTlutObj_80170f80(uint32_t oa, uint32_t da, uint32_t f, u
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170f80, GX__InitTlutObj_80170f80, (uint32_t oa, uint32_t da, uint32_t f, uint32_t e), (oa, da, f, e));
|
||||
|
||||
extern "C" void GX__LoadTlut_80170fa8(uint32_t oa, uint32_t tl) { std::lock_guard<std::mutex> guard(g_tlutObjMutex); if (tl >= kMaxTluts) { RT_LOGF(RT_TAG_GX, "GXLoadTlut: invalid TLUT index %u (oa=0x%08X)\n", tl, oa); return; } auto metaIt = g_TlutObjMeta.find(oa); if (metaIt != g_TlutObjMeta.end() && metaIt->second.dirty) { if (ValidateTlutData(oa, metaIt->second)) { GXTlutObj* rebuild = CreateHostTlutObj(oa); GXInitTlutObj(rebuild, GuestToHostPtr(metaIt->second.dataAddr), (GXTlutFmt)metaIt->second.format, metaIt->second.entries); MarkHostTlutObjConstructed(oa); } metaIt->second.dirty = false; } GXTlutObj* obj = GetHostTlutObj(oa); GXLoadTlut(obj, (GXTlut)tl); try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) Memory::Write16(gd + 2, 0); } catch (...) {} }
|
||||
static void LoadTlutImpl(uint32_t oa, uint32_t tl) { std::lock_guard<std::mutex> guard(g_tlutObjMutex); if (tl >= kMaxTluts) { RT_LOGF(RT_TAG_GX, "GXLoadTlut: invalid TLUT index %u (oa=0x%08X)\n", tl, oa); return; } auto metaIt = g_TlutObjMeta.find(oa); if (metaIt != g_TlutObjMeta.end() && metaIt->second.dirty) { if (ValidateTlutData(oa, metaIt->second)) { GXTlutObj* rebuild = CreateHostTlutObj(oa); GXInitTlutObj(rebuild, GuestToHostPtr(metaIt->second.dataAddr), (GXTlutFmt)metaIt->second.format, metaIt->second.entries); MarkHostTlutObjConstructed(oa); } metaIt->second.dirty = false; } GXTlutObj* obj = GetHostTlutObj(oa); GXLoadTlut(obj, (GXTlut)tl); }
|
||||
|
||||
extern "C" void GX__LoadTlut_80170fa8(uint32_t oa, uint32_t tl) {
|
||||
GxWorker::Run([oa, tl] { LoadTlutImpl(oa, tl); });
|
||||
try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) Memory::Write16(gd + 2, 0); } catch (...) {}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80170fa8, GX__LoadTlut_80170fa8, (uint32_t oa, uint32_t tl), (oa, tl));
|
||||
|
||||
// ============================================================================
|
||||
@@ -694,6 +770,7 @@ PPC_NATIVE_OVERRIDE_VOID(80170fa8, GX__LoadTlut_80170fa8, (uint32_t oa, uint32_t
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__InvalidateTexAll_80171110() {
|
||||
GX_DEFER_CALL0(GX__InvalidateTexAll_80171110);
|
||||
// Real GX invalidates its internal texture cache here. Aurora forwards the
|
||||
// invalidate to the renderer, which keeps unchanged source uploads hot but
|
||||
// revalidates reused guest buffers before serving cached texture handles.
|
||||
@@ -701,8 +778,8 @@ extern "C" void GX__InvalidateTexAll_80171110() {
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171110, GX__InvalidateTexAll_80171110, (), ());
|
||||
|
||||
extern "C" void GX__SetTexCoordScaleManually_80171180(uint32_t c, uint32_t en, uint32_t ss, uint32_t ts) { GXSetTexCoordScaleManually((GXTexCoordID)c, (GXBool)en, (u16)ss, (u16)ts); try{ uint32_t gd=Memory::Read32(kGXDataPtrAddr); if(gd){ Memory::Write32(gd+0x5E4u, (Memory::Read32(gd+0x5E4u)&~(1u<<c))|((en&1u)<<c)); if(en){ uint32_t sa=gd+0x108u+c*4u, ta=gd+0x128u+c*4u; Memory::Write32(sa, (Memory::Read32(sa)&0xFFFF0000u)|((ss-1)&0xFFFFu)); Memory::Write32(ta, (Memory::Read32(ta)&0xFFFF0000u)|((ts-1)&0xFFFFu)); Memory::Write16(gd+2, 0); } } }catch(...){} }
|
||||
extern "C" void GX__SetTexCoordScaleManually_80171180(uint32_t c, uint32_t en, uint32_t ss, uint32_t ts) { GxWorker::Run([c, en, ss, ts] { GXSetTexCoordScaleManually((GXTexCoordID)c, (GXBool)en, (u16)ss, (u16)ts); }); try{ uint32_t gd=Memory::Read32(kGXDataPtrAddr); if(gd){ Memory::Write32(gd+0x5E4u, (Memory::Read32(gd+0x5E4u)&~(1u<<c))|((en&1u)<<c)); if(en){ uint32_t sa=gd+0x108u+c*4u, ta=gd+0x128u+c*4u; Memory::Write32(sa, (Memory::Read32(sa)&0xFFFF0000u)|((ss-1)&0xFFFFu)); Memory::Write32(ta, (Memory::Read32(ta)&0xFFFF0000u)|((ts-1)&0xFFFFu)); Memory::Write16(gd+2, 0); } } }catch(...){} }
|
||||
PPC_NATIVE_OVERRIDE_VOID(80171180, GX__SetTexCoordScaleManually_80171180, (uint32_t c, uint32_t en, uint32_t ss, uint32_t ts), (c, en, ss, ts));
|
||||
|
||||
extern "C" void GX__SetTexCoordBias_801711fc(uint32_t c, uint32_t se, uint32_t te) { GXSetTexCoordBias((GXTexCoordID)c, (GXBool)se, (GXBool)te); try{ uint32_t gd=Memory::Read32(kGXDataPtrAddr); if(gd){ uint32_t sa=gd+0x108u+c*4u, ta=gd+0x128u+c*4u; Memory::Write32(sa, (Memory::Read32(sa)&0xFFFEFFFFu)|((se&1u)<<16)); Memory::Write32(ta, (Memory::Read32(ta)&0xFFFEFFFFu)|((te&1u)<<16)); if(Memory::Read32(gd+0x5E4u)&(1u<<c)) Memory::Write16(gd+2, 0); } }catch(...){} }
|
||||
extern "C" void GX__SetTexCoordBias_801711fc(uint32_t c, uint32_t se, uint32_t te) { GxWorker::Run([c, se, te] { GXSetTexCoordBias((GXTexCoordID)c, (GXBool)se, (GXBool)te); }); try{ uint32_t gd=Memory::Read32(kGXDataPtrAddr); if(gd){ uint32_t sa=gd+0x108u+c*4u, ta=gd+0x128u+c*4u; Memory::Write32(sa, (Memory::Read32(sa)&0xFFFEFFFFu)|((se&1u)<<16)); Memory::Write32(ta, (Memory::Read32(ta)&0xFFFEFFFFu)|((te&1u)<<16)); if(Memory::Read32(gd+0x5E4u)&(1u<<c)) Memory::Write16(gd+2, 0); } }catch(...){} }
|
||||
PPC_NATIVE_OVERRIDE_VOID(801711fc, GX__SetTexCoordBias_801711fc, (uint32_t c, uint32_t se, uint32_t te), (c, se, te));
|
||||
|
||||
@@ -40,13 +40,13 @@ namespace {
|
||||
// transform double-transformed menu viewports and broke 640x480 overscan, so it was removed.
|
||||
extern "C" void GX__SetViewportJitter_80173378(float l, float t, float w, float h, float nz, float fz, uint32_t f) {
|
||||
g_viewportState[0]=l; g_viewportState[1]=t; g_viewportState[2]=w; g_viewportState[3]=h; g_viewportState[4]=nz; g_viewportState[5]=fz;
|
||||
GXSetViewportJitter(l, t, w, h, nz, fz, f);
|
||||
GxWorker::Run([=] { GXSetViewportJitter(l, t, w, h, nz, fz, f); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80173378, GX__SetViewportJitter_80173378, (float l, float t, float w, float h, float nz, float fz, uint32_t f), (l, t, w, h, nz, fz, f));
|
||||
|
||||
extern "C" void GX__SetViewport_801733b4(float l, float t, float w, float h, float nz, float fz) {
|
||||
g_viewportState[0]=l; g_viewportState[1]=t; g_viewportState[2]=w; g_viewportState[3]=h; g_viewportState[4]=nz; g_viewportState[5]=fz;
|
||||
GXSetViewport(l, t, w, h, nz, fz);
|
||||
GxWorker::Run([=] { GXSetViewport(l, t, w, h, nz, fz); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801733b4, GX__SetViewport_801733b4, (float l, float t, float w, float h, float nz, float fz), (l, t, w, h, nz, fz));
|
||||
|
||||
@@ -66,7 +66,7 @@ extern "C" void GX__GetViewportv_801733e0(uint32_t oa) {
|
||||
PPC_NATIVE_OVERRIDE_VOID(801733e0, GX__GetViewportv_801733e0, (uint32_t oa), (oa));
|
||||
|
||||
extern "C" void GX__SetZScaleOffset_80173400(float s, float o) {
|
||||
GXSetZScaleOffset(s, o);
|
||||
GxWorker::Run([s, o] { GXSetZScaleOffset(s, o); });
|
||||
try {
|
||||
const uint32_t gd = Memory::Read32(kGXDataPtrAddr);
|
||||
if (gd) {
|
||||
@@ -80,7 +80,7 @@ extern "C" void GX__SetZScaleOffset_80173400(float s, float o) {
|
||||
PPC_NATIVE_OVERRIDE_VOID(80173400, GX__SetZScaleOffset_80173400, (float s, float o), (s, o));
|
||||
|
||||
extern "C" void GX__SetScissorBoxOffset_801734e0(int32_t xo, int32_t yo) {
|
||||
GXSetScissorBoxOffset(xo, yo);
|
||||
GxWorker::Run([xo, yo] { GXSetScissorBoxOffset(xo, yo); });
|
||||
try {
|
||||
const uint32_t gd = Memory::Read32(kGXDataPtrAddr);
|
||||
if (gd) Memory::Write16(gd + 2, 0);
|
||||
@@ -104,7 +104,7 @@ extern "C" void GX__SetScissor_80173430(uint32_t l, uint32_t t, uint32_t w, uint
|
||||
// No viewport replay here: aurora recomputes viewport and scissor together
|
||||
// on every scissor change (set_logical_scissor -> apply_logical_render_state),
|
||||
// so re-issuing the current viewport would be duplicate work.
|
||||
GXSetScissor(l, t, w, h);
|
||||
GxWorker::Run([l, t, w, h] { GXSetScissor(l, t, w, h); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80173430, GX__SetScissor_80173430, (uint32_t l, uint32_t t, uint32_t w, uint32_t h), (l, t, w, h));
|
||||
|
||||
@@ -115,7 +115,7 @@ PPC_NATIVE_OVERRIDE_VOID(80173430, GX__SetScissor_80173430, (uint32_t l, uint32_
|
||||
extern "C" void GX__SetProjection_8017301c(uint32_t ma, uint32_t pt) {
|
||||
const uint32_t* raw=(const uint32_t*)GuestToHostPtr(ma, 64); float m[16];
|
||||
SwapBeF32ArrayToHost(raw, m, 16);
|
||||
GXSetProjection(m, (GXProjectionType)pt);
|
||||
GxWorker::Run([mtx = std::to_array(m), pt]() mutable { GXSetProjection(mtx.data(), (GXProjectionType)pt); });
|
||||
UpdateProjectionVectorFromMatrix(m, (GXProjectionType)pt);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8017301c, GX__SetProjection_8017301c, (uint32_t ma, uint32_t pt), (ma, pt));
|
||||
@@ -126,7 +126,7 @@ extern "C" void GX__SetProjectionv_80173080(uint32_t pa) {
|
||||
GXProjectionType pt=(v[0]!=0.f)?GX_ORTHOGRAPHIC:GX_PERSPECTIVE;
|
||||
float m[16]={0.f}; m[0]=v[1]; m[5]=v[3]; m[10]=v[5]; m[11]=v[6];
|
||||
if(pt==GX_PERSPECTIVE){ m[2]=v[2]; m[6]=v[4]; m[14]=-1.f; } else { m[3]=v[2]; m[7]=v[4]; m[15]=1.f; }
|
||||
GXSetProjection(m, pt);
|
||||
GxWorker::Run([mtx = std::to_array(m), pt]() mutable { GXSetProjection(mtx.data(), pt); });
|
||||
UpdateProjectionVectorFromProjV(v);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80173080, GX__SetProjectionv_80173080, (uint32_t pa), (pa));
|
||||
@@ -146,28 +146,51 @@ PPC_NATIVE_OVERRIDE_VOID(801730cc, GX__GetProjectionv_801730cc, (uint32_t pa), (
|
||||
extern "C" void GX__LoadPosMtxImm_8017310c(uint32_t ma, uint32_t id) {
|
||||
const uint32_t* raw=(const uint32_t*)GuestToHostPtr(ma); float m[12];
|
||||
SwapBeF32ArrayToHost(raw, m, 12);
|
||||
GXLoadPosMtxImm((float(*)[4])m, id);
|
||||
GxWorker::Run([mtx = std::to_array(m), id]() mutable { GXLoadPosMtxImm((float(*)[4])mtx.data(), id); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8017310c, GX__LoadPosMtxImm_8017310c, (uint32_t ma, uint32_t id), (ma, id));
|
||||
|
||||
// The matrix is read at load time, as Dolphin does, since the worker can run well behind the game.
|
||||
// Returns false only when the game thread doesn't know the array and the worker must resolve it.
|
||||
static bool IndexedMtxArray(uint32_t attr, uint32_t& base, uint32_t& stride) {
|
||||
if (GxWorker::ShouldDefer()) {
|
||||
return ApiVtxArray(attr, base, stride);
|
||||
}
|
||||
base = g_hleGxState.vtxArray[attr].base;
|
||||
stride = g_hleGxState.vtxArray[attr].stride;
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" void GX__LoadPosMtxIndx_8017315c(uint32_t mi, uint32_t id) {
|
||||
const auto& arr=g_hleGxState.vtxArray[GX_POS_MTX_ARRAY];
|
||||
if(arr.base==0||arr.stride==0) return;
|
||||
const uint32_t* raw=(const uint32_t*)GuestToHostPtr(arr.base+mi*arr.stride, 48);
|
||||
if(raw){ float m[12]; SwapBeF32ArrayToHost(raw,m,12); GXLoadPosMtxImm((float(*)[4])m,id); }
|
||||
uint32_t base = 0, stride = 0;
|
||||
if (!IndexedMtxArray(GX_POS_MTX_ARRAY, base, stride)) {
|
||||
GX_DEFER_CALL(GX__LoadPosMtxIndx_8017315c, mi, id);
|
||||
return;
|
||||
}
|
||||
if (base == 0 || stride == 0) return;
|
||||
const uint32_t* raw=(const uint32_t*)GuestToHostPtr(base+mi*stride, 48);
|
||||
if(raw){
|
||||
float m[12]; SwapBeF32ArrayToHost(raw,m,12);
|
||||
GxWorker::Run([mtx = std::to_array(m), id]() mutable { GXLoadPosMtxImm((float(*)[4])mtx.data(), id); });
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8017315c, GX__LoadPosMtxIndx_8017315c, (uint32_t mi, uint32_t id), (mi, id));
|
||||
|
||||
extern "C" void GX__LoadNrmMtxImm_80173188(uint32_t ma, uint32_t id) {
|
||||
const uint32_t* raw=(const uint32_t*)GuestToHostPtr(ma, 48); float m[12];
|
||||
SwapBeF32ArrayToHost(raw, m, 12); GXLoadNrmMtxImm((float(*)[4])m, id);
|
||||
SwapBeF32ArrayToHost(raw, m, 12);
|
||||
GxWorker::Run([mtx = std::to_array(m), id]() mutable { GXLoadNrmMtxImm((float(*)[4])mtx.data(), id); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80173188, GX__LoadNrmMtxImm_80173188, (uint32_t ma, uint32_t id), (ma, id));
|
||||
|
||||
extern "C" void GX__LoadNrmMtxIndx3x3_801731e0(uint32_t mi, uint32_t id) {
|
||||
const auto& arr=g_hleGxState.vtxArray[GX_NRM_MTX_ARRAY];
|
||||
if(arr.base==0||arr.stride==0) return;
|
||||
const uint32_t* raw=(const uint32_t*)GuestToHostPtr(arr.base+mi*arr.stride, 36);
|
||||
uint32_t base = 0, stride = 0;
|
||||
if (!IndexedMtxArray(GX_NRM_MTX_ARRAY, base, stride)) {
|
||||
GX_DEFER_CALL(GX__LoadNrmMtxIndx3x3_801731e0, mi, id);
|
||||
return;
|
||||
}
|
||||
if (base == 0 || stride == 0) return;
|
||||
const uint32_t* raw=(const uint32_t*)GuestToHostPtr(base+mi*stride, 36);
|
||||
if(raw){
|
||||
float src[9]{};
|
||||
float m[12]{};
|
||||
@@ -177,7 +200,7 @@ extern "C" void GX__LoadNrmMtxIndx3x3_801731e0(uint32_t mi, uint32_t id) {
|
||||
m[row * 4 + col] = src[row * 3 + col];
|
||||
}
|
||||
}
|
||||
GXLoadNrmMtxImm((float(*)[4])m, id);
|
||||
GxWorker::Run([mtx = std::to_array(m), id]() mutable { GXLoadNrmMtxImm((float(*)[4])mtx.data(), id); });
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801731e0, GX__LoadNrmMtxIndx3x3_801731e0, (uint32_t mi, uint32_t id), (mi, id));
|
||||
@@ -185,9 +208,13 @@ PPC_NATIVE_OVERRIDE_VOID(801731e0, GX__LoadNrmMtxIndx3x3_801731e0, (uint32_t mi,
|
||||
extern "C" void GX__LoadTexMtxImm_80173234(uint32_t ma, uint32_t id, uint32_t t) {
|
||||
size_t c=(t==(uint32_t)GX_MTX3x4)?12:8;
|
||||
const uint32_t* raw=(const uint32_t*)GuestToHostPtr(ma,c*4); float l[12]={};
|
||||
SwapBeF32ArrayToHost(raw,l,c); GXLoadTexMtxImm(l, id, (GXTexMtxType)t);
|
||||
SwapBeF32ArrayToHost(raw,l,c);
|
||||
GxWorker::Run([mtx = std::to_array(l), id, t]() mutable { GXLoadTexMtxImm(mtx.data(), id, (GXTexMtxType)t); });
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80173234, GX__LoadTexMtxImm_80173234, (uint32_t ma, uint32_t id, uint32_t t), (ma, id, t));
|
||||
|
||||
extern "C" void GX__SetCurrentMtx_80173214(uint32_t id) { GXSetCurrentMtx(id); }
|
||||
extern "C" void GX__SetCurrentMtx_80173214(uint32_t id) {
|
||||
GX_DEFER_CALL(GX__SetCurrentMtx_80173214, id);
|
||||
GXSetCurrentMtx(id);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(80173214, GX__SetCurrentMtx_80173214, (uint32_t id), (id));
|
||||
|
||||
@@ -13,6 +13,9 @@ uint32_t CanonicalVtxAttr(uint32_t attr) {
|
||||
return attr == GX_VA_NBT ? GX_VA_NRM : attr;
|
||||
}
|
||||
|
||||
// Array bases as last set through GXSetArray on the game thread.
|
||||
std::array<std::array<uint32_t, 2>, 26> g_apiVtxArrays{};
|
||||
|
||||
// There is deliberately no indexed-aurora path here. The immediate begin path
|
||||
// streams vertex data incrementally, and aurora's indexed-array upload must
|
||||
// precede the draw carrying the max-index bounds, which cannot be known without
|
||||
@@ -27,11 +30,19 @@ void ApplyAuroraVtxStateForBegin(GXVtxFmt fmt) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ApiVtxArray(uint32_t attr, uint32_t& base, uint32_t& stride) {
|
||||
if (attr >= 26) return false;
|
||||
base = g_apiVtxArrays[attr][0];
|
||||
stride = g_apiVtxArrays[attr][1];
|
||||
return base != 0 && stride != 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Vertex Descriptor
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__ClearVtxDesc_8016dc34() {
|
||||
GX_DEFER_CALL0(GX__ClearVtxDesc_8016dc34);
|
||||
bool changed = false;
|
||||
for(int i=0; i<26; ++i){
|
||||
changed |= g_hleGxState.vtxDesc[i] != GX_NONE;
|
||||
@@ -44,6 +55,7 @@ extern "C" void GX__ClearVtxDesc_8016dc34() {
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016dc34, GX__ClearVtxDesc_8016dc34, (), ());
|
||||
|
||||
extern "C" void GX__SetVtxDesc_8016d3a4(uint32_t a, uint32_t t) {
|
||||
GX_DEFER_CALL(GX__SetVtxDesc_8016d3a4, a, t);
|
||||
const uint32_t attr = CanonicalVtxAttr(a);
|
||||
if(attr>=26||attr==GX_VA_NULL) return;
|
||||
const GXAttrType oldType = g_hleGxState.vtxDesc[attr];
|
||||
@@ -59,16 +71,22 @@ PPC_NATIVE_OVERRIDE_VOID(8016d3a4, GX__SetVtxDesc_8016d3a4, (uint32_t a, uint32_
|
||||
|
||||
extern "C" void GX__SetVtxDescv_8016d608(uint32_t la) {
|
||||
if(!la) return; uint32_t p=la;
|
||||
std::vector<std::array<uint32_t, 2>> list;
|
||||
while(true){
|
||||
uint32_t a=Memory::Read32(p);
|
||||
if(a==0xFFu) break;
|
||||
GX__SetVtxDesc_8016d3a4(a, Memory::Read32(p+4));
|
||||
list.push_back({a, Memory::Read32(p+4)});
|
||||
p+=8;
|
||||
}
|
||||
GxWorker::Run([list = std::move(list)] {
|
||||
for (const auto& [a, t] : list) GX__SetVtxDesc_8016d3a4(a, t);
|
||||
});
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016d608, GX__SetVtxDescv_8016d608, (uint32_t la), (la));
|
||||
|
||||
// Display lists can change the vertex layout, so answer from the state the worker has applied.
|
||||
extern "C" void GX__GetVtxDesc_8016d9f0(uint32_t a, uint32_t tp) {
|
||||
GX_SYNC_INLINE();
|
||||
GXAttrType type = GX_NONE;
|
||||
const uint32_t attr = CanonicalVtxAttr(a);
|
||||
if (attr < GX_VA_MAX_ATTR && attr != GX_VA_NULL) {
|
||||
@@ -84,6 +102,7 @@ extern "C" void GX__GetVtxDescv_8016dba4(uint32_t la) {
|
||||
if (!la) {
|
||||
return;
|
||||
}
|
||||
GX_SYNC_INLINE();
|
||||
|
||||
uint32_t p = la;
|
||||
for (uint32_t a = GX_VA_PNMTXIDX; a <= GX_VA_TEX7; ++a) {
|
||||
@@ -103,6 +122,7 @@ PPC_NATIVE_OVERRIDE_VOID(8016dba4, GX__GetVtxDescv_8016dba4, (uint32_t la), (la)
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetVtxAttrFmt_8016dc68(uint32_t vf, uint32_t a, uint32_t c, uint32_t t, uint32_t fr) {
|
||||
GX_DEFER_CALL(GX__SetVtxAttrFmt_8016dc68, vf, a, c, t, fr);
|
||||
const uint32_t attr = CanonicalVtxAttr(a);
|
||||
if(vf<8&&attr<26){
|
||||
const VtxAttrFmt oldFmt = g_hleGxState.vtxAttrFmt[vf][attr];
|
||||
@@ -126,16 +146,21 @@ PPC_NATIVE_OVERRIDE_VOID(8016dc68, GX__SetVtxAttrFmt_8016dc68, (uint32_t vf, uin
|
||||
|
||||
extern "C" void GX__SetVtxAttrFmtv_8016de08(uint32_t vf, uint32_t la) {
|
||||
if(!la) return; uint32_t p=la;
|
||||
std::vector<std::array<uint32_t, 4>> list;
|
||||
while(true){
|
||||
uint32_t a=Memory::Read32(p);
|
||||
if(a==0xFFu) break;
|
||||
GX__SetVtxAttrFmt_8016dc68(vf, a, Memory::Read32(p+4), Memory::Read32(p+8), Memory::Read32(p+12)&0xFFu);
|
||||
list.push_back({a, Memory::Read32(p+4), Memory::Read32(p+8), Memory::Read32(p+12)&0xFFu});
|
||||
p+=16;
|
||||
}
|
||||
GxWorker::Run([vf, list = std::move(list)] {
|
||||
for (const auto& [a, c, t, fr] : list) GX__SetVtxAttrFmt_8016dc68(vf, a, c, t, fr);
|
||||
});
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016de08, GX__SetVtxAttrFmtv_8016de08, (uint32_t vf, uint32_t la), (vf, la));
|
||||
|
||||
extern "C" void GX__GetVtxAttrFmt_8016e04c(uint32_t vf, uint32_t a, uint32_t cp, uint32_t tp, uint32_t fp) {
|
||||
GX_SYNC_INLINE();
|
||||
VtxAttrFmt fmt{};
|
||||
const uint32_t attr = CanonicalVtxAttr(a);
|
||||
if (vf < 8 && attr < GX_VA_MAX_ATTR && attr != GX_VA_NULL) {
|
||||
@@ -157,6 +182,7 @@ extern "C" void GX__GetVtxAttrFmtv_8016e2b8(uint32_t vf, uint32_t la) {
|
||||
if (!la) {
|
||||
return;
|
||||
}
|
||||
GX_SYNC_INLINE();
|
||||
|
||||
uint32_t p = la;
|
||||
for (uint32_t a = GX_VA_POS; a <= GX_VA_TEX7; ++a) {
|
||||
@@ -179,6 +205,11 @@ PPC_NATIVE_OVERRIDE_VOID(8016e2b8, GX__GetVtxAttrFmtv_8016e2b8, (uint32_t vf, ui
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetArray_8016e32c(uint32_t a, uint32_t ba, uint32_t str) {
|
||||
if (!GxWorker::OnWorker()) {
|
||||
const uint32_t attr = CanonicalVtxAttr(a);
|
||||
if (attr < 26) g_apiVtxArrays[attr] = {ba, str};
|
||||
}
|
||||
GX_DEFER_CALL(GX__SetArray_8016e32c, a, ba, str);
|
||||
const uint32_t attr = CanonicalVtxAttr(a);
|
||||
if(attr<26){ g_hleGxState.vtxArray[attr].base=ba; g_hleGxState.vtxArray[attr].stride=str; }
|
||||
}
|
||||
@@ -194,11 +225,13 @@ PPC_NATIVE_OVERRIDE_VOID(8016e1c4, GX__SetArray_8016e1c4, (uint32_t a, uint32_t
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void GX__SetNumTexGens_8016e5a4(uint32_t n) {
|
||||
GX_DEFER_CALL(GX__SetNumTexGens_8016e5a4, n);
|
||||
GXSetNumTexGens((u8)n);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016e5a4, GX__SetNumTexGens_8016e5a4, (uint32_t n), (n));
|
||||
|
||||
extern "C" void GX__SetTexCoordGen2_8016e37c(uint32_t dc, uint32_t f, uint32_t sp, uint32_t m, uint32_t n, uint32_t pm) {
|
||||
GX_DEFER_CALL(GX__SetTexCoordGen2_8016e37c, dc, f, sp, m, n, pm);
|
||||
if (sp >= static_cast<uint32_t>(GX_MAX_TEXGENSRC)) {
|
||||
uint32_t pc = 0;
|
||||
uint32_t lr = 0;
|
||||
@@ -215,6 +248,7 @@ extern "C" void GX__SetTexCoordGen2_8016e37c(uint32_t dc, uint32_t f, uint32_t s
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016e37c, GX__SetTexCoordGen2_8016e37c, (uint32_t dc, uint32_t f, uint32_t sp, uint32_t m, uint32_t n, uint32_t pm), (dc, f, sp, m, n, pm));
|
||||
|
||||
extern "C" void GX__EnableTexOffsets_8016f37c(uint32_t coord, uint32_t lineEnable, uint32_t pointEnable) {
|
||||
GX_DEFER_CALL(GX__EnableTexOffsets_8016f37c, coord, lineEnable, pointEnable);
|
||||
GXEnableTexOffsets(static_cast<GXTexCoordID>(coord),
|
||||
lineEnable ? GX_TRUE : GX_FALSE,
|
||||
pointEnable ? GX_TRUE : GX_FALSE);
|
||||
@@ -222,11 +256,13 @@ extern "C" void GX__EnableTexOffsets_8016f37c(uint32_t coord, uint32_t lineEnabl
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f37c, GX__EnableTexOffsets_8016f37c, (uint32_t coord, uint32_t lineEnable, uint32_t pointEnable), (coord, lineEnable, pointEnable));
|
||||
|
||||
extern "C" void GX__SetLineWidth_8016f314(uint32_t width, uint32_t texOffsets) {
|
||||
GX_DEFER_CALL(GX__SetLineWidth_8016f314, width, texOffsets);
|
||||
GXSetLineWidth(static_cast<u8>(width), static_cast<GXTexOffset>(texOffsets));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f314, GX__SetLineWidth_8016f314, (uint32_t width, uint32_t texOffsets), (width, texOffsets));
|
||||
|
||||
extern "C" void GX__SetPointSize_8016f348(uint32_t pointSize, uint32_t texOffsets) {
|
||||
GX_DEFER_CALL(GX__SetPointSize_8016f348, pointSize, texOffsets);
|
||||
GXSetPointSize(static_cast<u8>(pointSize), static_cast<GXTexOffset>(texOffsets));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f348, GX__SetPointSize_8016f348, (uint32_t pointSize, uint32_t texOffsets), (pointSize, texOffsets));
|
||||
@@ -274,6 +310,8 @@ void ServiceDeferredTimingDuringGxWork() {
|
||||
}
|
||||
}
|
||||
|
||||
static void GxBeginImpl(uint32_t t, uint32_t vf, uint32_t nv);
|
||||
|
||||
extern "C" void GX__Begin_8016f0f0(uint32_t t, uint32_t vf, uint32_t nv) {
|
||||
if(IsDisplayListActive()){
|
||||
WriteDisplayListData((u8)(t|vf), 1);
|
||||
@@ -281,6 +319,11 @@ extern "C" void GX__Begin_8016f0f0(uint32_t t, uint32_t vf, uint32_t nv) {
|
||||
return;
|
||||
}
|
||||
ServiceDeferredTimingDuringGxWork();
|
||||
GX_DEFER_CALL(GxBeginImpl, t, vf, nv);
|
||||
GxBeginImpl(t, vf, nv);
|
||||
}
|
||||
|
||||
static void GxBeginImpl(uint32_t t, uint32_t vf, uint32_t nv) {
|
||||
ApplyAuroraVtxStateForBegin(static_cast<GXVtxFmt>(vf));
|
||||
g_hleGxState.currentVtxFmt=(GXVtxFmt)vf; g_hleGxState.currentPrim=(GXPrimitive)t;
|
||||
g_hleGxState.vertsRemaining=nv; g_hleGxState.inBegin=true; g_hleGxState.auroraBeginCalled=false;
|
||||
@@ -289,13 +332,19 @@ extern "C" void GX__Begin_8016f0f0(uint32_t t, uint32_t vf, uint32_t nv) {
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016f0f0, GX__Begin_8016f0f0, (uint32_t t, uint32_t vf, uint32_t nv), (t, vf, nv));
|
||||
|
||||
extern "C" void GX__End_80044b30() { g_hleGxState.inBegin=false; GXEnd(); }
|
||||
extern "C" void GX__End_80044b30() {
|
||||
GX_DEFER_CALL0(GX__End_80044b30); g_hleGxState.inBegin=false; GXEnd(); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(80044b30, GX__End_80044b30, (), ());
|
||||
|
||||
extern "C" void GX__End_80048c30() { GX__End_80044b30(); }
|
||||
PPC_NATIVE_OVERRIDE_VOID(80048c30, GX__End_80048c30, (), ());
|
||||
|
||||
extern "C" void GX__DrawSphere_80172a30(uint32_t numMajor, uint32_t numMinor) {
|
||||
if (GxWorker::ShouldDefer()) {
|
||||
EnsureAuroraFrameActive();
|
||||
GxWorker::Submit([=] { GX__DrawSphere_80172a30(numMajor, numMinor); });
|
||||
return;
|
||||
}
|
||||
constexpr uint32_t kAttrCount = 26;
|
||||
constexpr float kSphereRadius = 1.0f;
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
#include "gx_worker.h"
|
||||
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#if defined(__x86_64__) || defined(_M_X64)
|
||||
#include <immintrin.h>
|
||||
#endif
|
||||
|
||||
void GxWorkerReplayFifoWrite(uint32_t value, uint32_t sizeBytes);
|
||||
void GxWorkerReplayFifoBurst(const uint8_t* data, uint32_t sizeBytes);
|
||||
|
||||
namespace GxWorker {
|
||||
namespace detail {
|
||||
std::atomic<bool> g_active{false};
|
||||
constinit thread_local bool t_onWorker = false;
|
||||
constinit thread_local int t_inlineDepth = 0;
|
||||
bool g_fifoPending = false;
|
||||
} // namespace detail
|
||||
|
||||
namespace {
|
||||
struct Header {
|
||||
void (*run)(void* payload);
|
||||
uint32_t size;
|
||||
uint32_t reserved;
|
||||
};
|
||||
constexpr size_t kHeaderSize = 16;
|
||||
static_assert(sizeof(Header) == kHeaderSize);
|
||||
|
||||
constexpr size_t kCapacity = size_t{8} << 20;
|
||||
// How long the worker keeps polling before it sleeps; recording arrives in bursts and each wake-up
|
||||
// costs the game thread a lock and notify.
|
||||
constexpr auto kIdleSpin = std::chrono::microseconds{50};
|
||||
constexpr size_t kFifoFlushBytes = 8 * 1024;
|
||||
constexpr uint8_t kFifoBurstTag = 0xB0;
|
||||
|
||||
uint8_t* g_ring = nullptr;
|
||||
uint64_t g_write = 0;
|
||||
std::atomic<uint64_t> g_head{0};
|
||||
std::atomic<uint64_t> g_tail{0};
|
||||
std::atomic<bool> g_consumerWaiting{false};
|
||||
std::atomic<bool> g_producerWaiting{false};
|
||||
std::mutex g_headMutex;
|
||||
std::condition_variable g_headCv;
|
||||
std::mutex g_tailMutex;
|
||||
std::condition_variable g_tailCv;
|
||||
void (*g_waitCallback)() = nullptr;
|
||||
std::atomic<bool> g_stop{false};
|
||||
std::thread* g_thread = nullptr;
|
||||
std::vector<uint8_t> g_fifoBytes;
|
||||
|
||||
constexpr size_t AlignUp(size_t value) { return (value + detail::kAlign - 1) & ~(detail::kAlign - 1); }
|
||||
|
||||
inline void CpuRelax() {
|
||||
#if defined(__x86_64__) || defined(_M_X64)
|
||||
_mm_pause();
|
||||
#endif
|
||||
}
|
||||
|
||||
// libc++ on Windows truncates sub-millisecond condvar timeouts to zero (a spin), so the bounded
|
||||
// wait goes straight to the OS there.
|
||||
void WaitTailBriefly(std::unique_lock<std::mutex>& lock) {
|
||||
#if defined(_WIN32) && defined(_LIBCPP_VERSION)
|
||||
SleepConditionVariableSRW(reinterpret_cast<PCONDITION_VARIABLE>(g_tailCv.native_handle()),
|
||||
reinterpret_cast<PSRWLOCK>(lock.mutex()->native_handle()), 1, 0);
|
||||
#else
|
||||
g_tailCv.wait_for(lock, std::chrono::milliseconds{1});
|
||||
#endif
|
||||
}
|
||||
|
||||
void WaitForTail(uint64_t minTail, bool service) {
|
||||
for (int spin = 0; spin < 256 && g_tail.load(std::memory_order_acquire) < minTail; ++spin) {
|
||||
CpuRelax();
|
||||
}
|
||||
while (g_tail.load(std::memory_order_acquire) < minTail) {
|
||||
{
|
||||
std::unique_lock lock{g_tailMutex};
|
||||
g_producerWaiting.store(true, std::memory_order_seq_cst);
|
||||
if (g_tail.load(std::memory_order_seq_cst) < minTail) {
|
||||
WaitTailBriefly(lock);
|
||||
}
|
||||
g_producerWaiting.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
if (service && g_waitCallback != nullptr && g_tail.load(std::memory_order_acquire) < minTail) {
|
||||
g_waitCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WaitForSpace(size_t bytes) {
|
||||
if (g_write + bytes > kCapacity) {
|
||||
WaitForTail(g_write + bytes - kCapacity, false);
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayFifo(void* payload) {
|
||||
uint32_t size = 0;
|
||||
std::memcpy(&size, payload, sizeof(size));
|
||||
const uint8_t* data = static_cast<const uint8_t*>(payload) + sizeof(size);
|
||||
const uint8_t* end = data + size;
|
||||
while (data < end) {
|
||||
const uint8_t tag = *data++;
|
||||
if (tag == kFifoBurstTag) {
|
||||
uint32_t length = 0;
|
||||
std::memcpy(&length, data, sizeof(length));
|
||||
data += sizeof(length);
|
||||
try {
|
||||
GxWorkerReplayFifoBurst(data, length);
|
||||
} catch (...) {
|
||||
RT_LOGF(RT_TAG_GX, "GX worker FIFO burst failed\n");
|
||||
}
|
||||
data += length;
|
||||
} else {
|
||||
uint32_t value = 0;
|
||||
std::memcpy(&value, data, sizeof(value));
|
||||
data += sizeof(value);
|
||||
try {
|
||||
GxWorkerReplayFifoWrite(value, tag);
|
||||
} catch (...) {
|
||||
RT_LOGF(RT_TAG_GX, "GX worker FIFO write failed\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WorkerMain() {
|
||||
detail::t_onWorker = true;
|
||||
AuroraSetGxProducerThread();
|
||||
#if defined(_WIN32)
|
||||
SetThreadDescription(GetCurrentThread(), L"GX worker");
|
||||
#endif
|
||||
uint64_t tail = g_tail.load(std::memory_order_relaxed);
|
||||
for (;;) {
|
||||
uint64_t head = g_head.load(std::memory_order_acquire);
|
||||
if (head == tail) {
|
||||
const auto spinUntil = std::chrono::steady_clock::now() + kIdleSpin;
|
||||
while (head == tail) {
|
||||
for (int spin = 0; spin < 64 && head == tail; ++spin) {
|
||||
CpuRelax();
|
||||
head = g_head.load(std::memory_order_acquire);
|
||||
}
|
||||
if (head == tail && std::chrono::steady_clock::now() >= spinUntil) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (head == tail) {
|
||||
if (g_stop.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::unique_lock lock{g_headMutex};
|
||||
g_consumerWaiting.store(true, std::memory_order_seq_cst);
|
||||
while (g_head.load(std::memory_order_seq_cst) == tail && !g_stop.load(std::memory_order_acquire)) {
|
||||
g_headCv.wait(lock);
|
||||
}
|
||||
g_consumerWaiting.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
while (tail != head) {
|
||||
uint8_t* record = g_ring + tail % kCapacity;
|
||||
const Header* header = reinterpret_cast<const Header*>(record);
|
||||
if (header->run != nullptr) {
|
||||
try {
|
||||
header->run(record + kHeaderSize);
|
||||
} catch (const std::exception& error) {
|
||||
RT_LOGF(RT_TAG_GX, "GX worker command failed: %s\n", error.what());
|
||||
} catch (...) {
|
||||
RT_LOGF(RT_TAG_GX, "GX worker command failed\n");
|
||||
}
|
||||
}
|
||||
tail += header->size;
|
||||
g_tail.store(tail, std::memory_order_seq_cst);
|
||||
if (g_producerWaiting.load(std::memory_order_seq_cst)) {
|
||||
std::lock_guard lock{g_tailMutex};
|
||||
g_tailCv.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace detail {
|
||||
void* Reserve(size_t payloadSize, void (*run)(void*)) {
|
||||
const size_t total = kHeaderSize + AlignUp(payloadSize);
|
||||
if (total > kCapacity) {
|
||||
RT_LOGF(RT_TAG_GX, "GX worker command of %zu bytes exceeds the ring\n", total);
|
||||
std::abort();
|
||||
}
|
||||
size_t offset = static_cast<size_t>(g_write % kCapacity);
|
||||
const size_t remaining = kCapacity - offset;
|
||||
if (total > remaining) {
|
||||
WaitForSpace(remaining + total);
|
||||
auto* wrap = reinterpret_cast<Header*>(g_ring + offset);
|
||||
wrap->run = nullptr;
|
||||
wrap->size = static_cast<uint32_t>(remaining);
|
||||
g_write += remaining;
|
||||
offset = 0;
|
||||
} else {
|
||||
WaitForSpace(total);
|
||||
}
|
||||
auto* header = reinterpret_cast<Header*>(g_ring + offset);
|
||||
header->run = run;
|
||||
header->size = static_cast<uint32_t>(total);
|
||||
g_write += total;
|
||||
return g_ring + offset + kHeaderSize;
|
||||
}
|
||||
|
||||
void Commit() {
|
||||
g_head.store(g_write, std::memory_order_seq_cst);
|
||||
if (g_consumerWaiting.load(std::memory_order_seq_cst)) {
|
||||
std::lock_guard lock{g_headMutex};
|
||||
g_headCv.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
void FlushFifo() {
|
||||
g_fifoPending = false;
|
||||
if (g_fifoBytes.empty()) {
|
||||
return;
|
||||
}
|
||||
const uint32_t size = static_cast<uint32_t>(g_fifoBytes.size());
|
||||
void* payload = Reserve(sizeof(size) + size, &ReplayFifo);
|
||||
std::memcpy(payload, &size, sizeof(size));
|
||||
std::memcpy(static_cast<uint8_t*>(payload) + sizeof(size), g_fifoBytes.data(), size);
|
||||
g_fifoBytes.clear();
|
||||
Commit();
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
void FifoWrite(uint32_t value, uint32_t sizeBytes) {
|
||||
const size_t at = g_fifoBytes.size();
|
||||
g_fifoBytes.resize(at + 1 + sizeof(value));
|
||||
g_fifoBytes[at] = static_cast<uint8_t>(sizeBytes);
|
||||
std::memcpy(g_fifoBytes.data() + at + 1, &value, sizeof(value));
|
||||
detail::g_fifoPending = true;
|
||||
if (g_fifoBytes.size() >= kFifoFlushBytes) {
|
||||
detail::FlushFifo();
|
||||
}
|
||||
}
|
||||
|
||||
void FifoBurst(const uint8_t* data, uint32_t sizeBytes) {
|
||||
const size_t at = g_fifoBytes.size();
|
||||
g_fifoBytes.resize(at + 1 + sizeof(sizeBytes) + sizeBytes);
|
||||
g_fifoBytes[at] = kFifoBurstTag;
|
||||
std::memcpy(g_fifoBytes.data() + at + 1, &sizeBytes, sizeof(sizeBytes));
|
||||
std::memcpy(g_fifoBytes.data() + at + 1 + sizeof(sizeBytes), data, sizeBytes);
|
||||
detail::g_fifoPending = true;
|
||||
if (g_fifoBytes.size() >= kFifoFlushBytes) {
|
||||
detail::FlushFifo();
|
||||
}
|
||||
}
|
||||
|
||||
void Sync() {
|
||||
if (!Active() || detail::t_onWorker) {
|
||||
return;
|
||||
}
|
||||
// Guest callbacks serviced while waiting can record more work.
|
||||
do {
|
||||
if (detail::g_fifoPending) {
|
||||
detail::FlushFifo();
|
||||
}
|
||||
WaitForTail(g_write, true);
|
||||
} while (detail::g_fifoPending || g_tail.load(std::memory_order_acquire) != g_write);
|
||||
}
|
||||
|
||||
void SetWaitCallback(void (*callback)()) { g_waitCallback = callback; }
|
||||
|
||||
void BeginInline() {
|
||||
if (detail::t_inlineDepth == 0 && Active() && !detail::t_onWorker) {
|
||||
Sync();
|
||||
}
|
||||
++detail::t_inlineDepth;
|
||||
}
|
||||
|
||||
void EndInline() { --detail::t_inlineDepth; }
|
||||
|
||||
void Start() {
|
||||
if (Active()) {
|
||||
return;
|
||||
}
|
||||
const char* env = std::getenv("MKW_GX_WORKER");
|
||||
if (!RuntimeConfigFile::GxWorkerEnabled(true) || (env != nullptr && std::strcmp(env, "0") == 0)) {
|
||||
RT_LOGF(RT_TAG_GX, "GX worker disabled\n");
|
||||
return;
|
||||
}
|
||||
if (g_ring == nullptr) {
|
||||
g_ring = static_cast<uint8_t*>(::operator new(kCapacity, std::align_val_t{64}));
|
||||
}
|
||||
g_fifoBytes.reserve(kFifoFlushBytes + 4096);
|
||||
g_stop.store(false, std::memory_order_relaxed);
|
||||
g_thread = new std::thread(WorkerMain);
|
||||
detail::g_active.store(true, std::memory_order_release);
|
||||
RT_LOGF(RT_TAG_GX, "GX worker started\n");
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
if (!Active()) {
|
||||
return;
|
||||
}
|
||||
// Guest state may already be torn down; don't service it while draining.
|
||||
g_waitCallback = nullptr;
|
||||
Sync();
|
||||
detail::g_active.store(false, std::memory_order_release);
|
||||
g_stop.store(true, std::memory_order_release);
|
||||
Submit([] {});
|
||||
if (g_thread != nullptr) {
|
||||
g_thread->join();
|
||||
delete g_thread;
|
||||
g_thread = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace GxWorker
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "hle/hollywood_gpio.h"
|
||||
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace HollywoodGpio {
|
||||
|
||||
constexpr uint32_t kSensorBarPower = 0x100;
|
||||
|
||||
std::atomic<uint32_t> g_gpioBOut{0};
|
||||
|
||||
} // namespace HollywoodGpio
|
||||
|
||||
bool HollywoodGpio_Read32(uint32_t address, uint32_t& value) {
|
||||
if (address != kHollywoodGpioBOut) {
|
||||
return false;
|
||||
}
|
||||
value = HollywoodGpio::g_gpioBOut.load(std::memory_order_relaxed);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HollywoodGpio_Write32(uint32_t address, uint32_t value) {
|
||||
if (address != kHollywoodGpioBOut) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t previous = HollywoodGpio::g_gpioBOut.exchange(value, std::memory_order_relaxed);
|
||||
if ((previous ^ value) & HollywoodGpio::kSensorBarPower) {
|
||||
RT_LOG(RT_TAG_HLE) << "Sensor bar power " << ((value & HollywoodGpio::kSensorBarPower) ? "on" : "off") << std::endl;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "input_bindings.h"
|
||||
#include "memory.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
// KPAD HLE fed by a real Bluetooth Wii Remote. The game calls KPADRead once per
|
||||
// frame with room for 16 KPADStatus entries and only looks at entry 0; with a
|
||||
// Classic Controller it also calls KPADGetUnifiedWpadStatus for the raw
|
||||
// WPADCLStatus (buttons, sticks and triggers of the extension).
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kKpadStatusSize = 0x84;
|
||||
|
||||
// KPADStatus field offsets (RVL SDK).
|
||||
constexpr uint32_t kHold = 0x00, kTrig = 0x04, kRelease = 0x08, kAcc = 0x0C, kAccValue = 0x18,
|
||||
kAccSpeed = 0x1C, kPos = 0x20, kAccVertical = 0x54, kDevType = 0x5C, kWpadErr = 0x5D,
|
||||
kDpdValidFg = 0x5E, kDataFormat = 0x5F, kFsStick = 0x60, kFsAcc = 0x68, kFsAccValue = 0x74,
|
||||
kFsAccSpeed = 0x78;
|
||||
// KPADStatus.ex_status.cl (KPADEXStatus, Classic Controller view).
|
||||
constexpr uint32_t kClHold = 0x60, kClTrig = 0x64, kClRelease = 0x68, kClLStick = 0x6C, kClRStick = 0x74,
|
||||
kClLTrigger = 0x7C, kClRTrigger = 0x80;
|
||||
|
||||
// KPADUnifiedWpadStatus: WPADStatus / WPADFSStatus / WPADCLStatus union, then fmt.
|
||||
constexpr uint32_t kUnifiedSize = 0x38;
|
||||
constexpr uint32_t kUButton = 0x00, kUAccX = 0x02, kUAccY = 0x04, kUAccZ = 0x06, kUObj = 0x08, kUDev = 0x28,
|
||||
kUErr = 0x29, kUFsStickX = 0x2A, kUFsStickY = 0x2B, kUFsAccX = 0x2C, kUFsAccY = 0x2E,
|
||||
kUFsAccZ = 0x30, kUClButton = 0x2A, kUClLStickX = 0x2C, kUClLStickY = 0x2E, kUClRStickX = 0x30,
|
||||
kUClRStickY = 0x32, kUClTriggerL = 0x34, kUClTriggerR = 0x35, kUFmt = 0x36;
|
||||
|
||||
// WPAD device types (WPAD_DEV_*) and the data formats KPAD runs each of them
|
||||
// in (WPAD_FMT_*_ACC_DPD): the values KPADStatus.dev_type / data_format and
|
||||
// KPADUnifiedWpadStatus.dev / fmt carry on the console.
|
||||
constexpr uint8_t kDevCore = 0;
|
||||
constexpr uint8_t kDevFreestyle = 1;
|
||||
constexpr uint8_t kDevClassic = 2;
|
||||
constexpr uint8_t kFmtCoreAccDpd = 2;
|
||||
constexpr uint8_t kFmtFreestyleAccDpd = 5;
|
||||
constexpr uint8_t kFmtClassicAccDpd = 8;
|
||||
constexpr int8_t kWpadErrNone = 0;
|
||||
constexpr int8_t kWpadErrNoController = -1;
|
||||
|
||||
// Raw accelerometer as WPADStatus carries it: 10 bits, 0x200 at 0 g, 100 per g.
|
||||
constexpr float kRawAccZero = 512.0f;
|
||||
constexpr float kRawAccPerG = 100.0f;
|
||||
|
||||
struct ChannelState {
|
||||
uint32_t prevHold = 0;
|
||||
uint32_t prevClHold = 0;
|
||||
float prevAcc[3] = {0.0f, -1.0f, 0.0f};
|
||||
float prevFsAcc[3] = {0.0f, -1.0f, 0.0f};
|
||||
};
|
||||
|
||||
std::array<ChannelState, 4> g_channels{};
|
||||
|
||||
// Euclidean length of a 3-vector.
|
||||
float Length(const float* v) {
|
||||
return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
||||
}
|
||||
|
||||
// Euclidean distance between two 3-vectors.
|
||||
float Distance(const float* a, const float* b) {
|
||||
const float d[3] = {a[0] - b[0], a[1] - b[1], a[2] - b[2]};
|
||||
return Length(d);
|
||||
}
|
||||
|
||||
// Writes three big-endian floats to guest memory.
|
||||
void WriteVec3(uint32_t addr, const float* v) {
|
||||
Memory::WriteFloat32(addr, v[0]);
|
||||
Memory::WriteFloat32(addr + 4, v[1]);
|
||||
Memory::WriteFloat32(addr + 8, v[2]);
|
||||
}
|
||||
|
||||
// Zeroes `count` consecutive floats in guest memory.
|
||||
void WriteZeroFloats(uint32_t addr, uint32_t count) {
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
Memory::WriteFloat32(addr + i * 4, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Fills one KPADStatus at `addr` and returns the number of valid entries (1),
|
||||
// or writes an "unplugged" status and returns 0.
|
||||
int32_t WriteStatus(uint32_t chan, uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
|
||||
ChannelState& state = g_channels[chan];
|
||||
if (sample == nullptr) {
|
||||
state = {};
|
||||
Memory::Write32(addr + kHold, 0);
|
||||
Memory::Write32(addr + kTrig, 0);
|
||||
Memory::Write32(addr + kRelease, 0);
|
||||
Memory::Write8(addr + kDevType, kDevCore);
|
||||
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNoController));
|
||||
Memory::Write8(addr + kDpdValidFg, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t hold = sample->hold;
|
||||
Memory::Write32(addr + kHold, hold);
|
||||
Memory::Write32(addr + kTrig, hold & ~state.prevHold);
|
||||
Memory::Write32(addr + kRelease, state.prevHold & ~hold);
|
||||
state.prevHold = hold;
|
||||
|
||||
WriteVec3(addr + kAcc, sample->acc);
|
||||
Memory::WriteFloat32(addr + kAccValue, Length(sample->acc));
|
||||
Memory::WriteFloat32(addr + kAccSpeed, Distance(sample->acc, state.prevAcc));
|
||||
for (int i = 0; i < 3; ++i) state.prevAcc[i] = sample->acc[i];
|
||||
|
||||
// No IR pointer: pos .. acc_vertical zeroed and dpd_valid_fg clear, which
|
||||
// the game treats as "pointing away from the screen".
|
||||
WriteZeroFloats(addr + kPos, (kAccVertical + 8 - kPos) / 4);
|
||||
Memory::Write8(addr + kDpdValidFg, 0);
|
||||
|
||||
const uint8_t devType = sample->hasClassic ? kDevClassic : sample->hasNunchuk ? kDevFreestyle : kDevCore;
|
||||
const uint8_t dataFormat =
|
||||
sample->hasClassic ? kFmtClassicAccDpd : sample->hasNunchuk ? kFmtFreestyleAccDpd : kFmtCoreAccDpd;
|
||||
Memory::Write8(addr + kDevType, devType);
|
||||
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNone));
|
||||
Memory::Write8(addr + kDataFormat, dataFormat);
|
||||
|
||||
if (sample->hasClassic) {
|
||||
const uint32_t clHold = sample->clHold;
|
||||
Memory::Write32(addr + kClHold, clHold);
|
||||
Memory::Write32(addr + kClTrig, clHold & ~state.prevClHold);
|
||||
Memory::Write32(addr + kClRelease, state.prevClHold & ~clHold);
|
||||
state.prevClHold = clHold;
|
||||
Memory::WriteFloat32(addr + kClLStick, sample->clLStick[0]);
|
||||
Memory::WriteFloat32(addr + kClLStick + 4, sample->clLStick[1]);
|
||||
Memory::WriteFloat32(addr + kClRStick, sample->clRStick[0]);
|
||||
Memory::WriteFloat32(addr + kClRStick + 4, sample->clRStick[1]);
|
||||
Memory::WriteFloat32(addr + kClLTrigger, sample->clTriggerL / 255.0f);
|
||||
Memory::WriteFloat32(addr + kClRTrigger, sample->clTriggerR / 255.0f);
|
||||
} else if (sample->hasNunchuk) {
|
||||
state.prevClHold = 0;
|
||||
Memory::WriteFloat32(addr + kFsStick, sample->stick[0]);
|
||||
Memory::WriteFloat32(addr + kFsStick + 4, sample->stick[1]);
|
||||
WriteVec3(addr + kFsAcc, sample->nunchukAcc);
|
||||
Memory::WriteFloat32(addr + kFsAccValue, Length(sample->nunchukAcc));
|
||||
Memory::WriteFloat32(addr + kFsAccSpeed, Distance(sample->nunchukAcc, state.prevFsAcc));
|
||||
for (int i = 0; i < 3; ++i) state.prevFsAcc[i] = sample->nunchukAcc[i];
|
||||
} else {
|
||||
state.prevClHold = 0;
|
||||
WriteZeroFloats(addr + kFsStick, (kKpadStatusSize - kFsStick) / 4);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// One accelerometer axis of KPAD's g vector back to the 10-bit raw WPAD value.
|
||||
uint16_t RawAcc(float g) {
|
||||
const float raw = kRawAccZero + g * kRawAccPerG;
|
||||
return static_cast<uint16_t>(std::clamp(raw, 0.0f, 1023.0f));
|
||||
}
|
||||
|
||||
// Fills one KPADUnifiedWpadStatus at `addr` from the sample: the WPADStatus core
|
||||
// (remote buttons, raw accelerometer, no IR objects), then the Nunchuk or Classic
|
||||
// Controller tail, then the data format.
|
||||
void WriteUnifiedStatus(uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
|
||||
for (uint32_t offset = 0; offset < kUnifiedSize; offset += 4) {
|
||||
Memory::Write32(addr + offset, 0);
|
||||
}
|
||||
if (sample == nullptr) {
|
||||
Memory::Write8(addr + kUDev, kDevCore);
|
||||
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNoController));
|
||||
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
|
||||
return;
|
||||
}
|
||||
Memory::Write16(addr + kUButton, static_cast<uint16_t>(sample->hold & 0xFFFF));
|
||||
// KPAD's acc is (-wiiX, -wiiZ, wiiY); WPADStatus keeps the remote's own axes.
|
||||
Memory::Write16(addr + kUAccX, RawAcc(-sample->acc[0]));
|
||||
Memory::Write16(addr + kUAccY, RawAcc(sample->acc[2]));
|
||||
Memory::Write16(addr + kUAccZ, RawAcc(-sample->acc[1]));
|
||||
// No IR: every DPDObject invalid (x/y at the sensor's out-of-range value).
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
Memory::Write16(addr + kUObj + i * 8, 0x3FF);
|
||||
Memory::Write16(addr + kUObj + i * 8 + 2, 0x3FF);
|
||||
}
|
||||
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNone));
|
||||
if (sample->hasClassic) {
|
||||
Memory::Write8(addr + kUDev, kDevClassic);
|
||||
Memory::Write16(addr + kUClButton, static_cast<uint16_t>(sample->clHold & 0xFFFF));
|
||||
Memory::Write16(addr + kUClLStickX, static_cast<uint16_t>(sample->clLStickRaw[0]));
|
||||
Memory::Write16(addr + kUClLStickY, static_cast<uint16_t>(sample->clLStickRaw[1]));
|
||||
Memory::Write16(addr + kUClRStickX, static_cast<uint16_t>(sample->clRStickRaw[0]));
|
||||
Memory::Write16(addr + kUClRStickY, static_cast<uint16_t>(sample->clRStickRaw[1]));
|
||||
Memory::Write8(addr + kUClTriggerL, sample->clTriggerL);
|
||||
Memory::Write8(addr + kUClTriggerR, sample->clTriggerR);
|
||||
Memory::Write8(addr + kUFmt, kFmtClassicAccDpd);
|
||||
} else if (sample->hasNunchuk) {
|
||||
Memory::Write8(addr + kUDev, kDevFreestyle);
|
||||
// WPADFSStatus: 8-bit stick (centre 128) and 10-bit Nunchuk accelerometer.
|
||||
Memory::Write8(addr + kUFsStickX,
|
||||
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[0] * 100.0f, 0.0f, 255.0f)));
|
||||
Memory::Write8(addr + kUFsStickY,
|
||||
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[1] * 100.0f, 0.0f, 255.0f)));
|
||||
Memory::Write16(addr + kUFsAccX, RawAcc(-sample->nunchukAcc[0]));
|
||||
Memory::Write16(addr + kUFsAccY, RawAcc(sample->nunchukAcc[2]));
|
||||
Memory::Write16(addr + kUFsAccZ, RawAcc(-sample->nunchukAcc[1]));
|
||||
Memory::Write8(addr + kUFmt, kFmtFreestyleAccDpd);
|
||||
} else {
|
||||
Memory::Write8(addr + kUDev, kDevCore);
|
||||
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral sticks and no buttons while an overlay owns input; the remote stays connected.
|
||||
bool ReadSample(uint32_t chan, WiiRemoteInput::KpadSample& sample) {
|
||||
if (!WiiRemoteInput::ReadKpadSample(chan, sample)) {
|
||||
return false;
|
||||
}
|
||||
if (InputBindings::InputBlocked()) {
|
||||
sample.hold = 0;
|
||||
sample.clHold = 0;
|
||||
sample.stick[0] = sample.stick[1] = 0.0f;
|
||||
sample.clLStick[0] = sample.clLStick[1] = 0.0f;
|
||||
sample.clRStick[0] = sample.clRStick[1] = 0.0f;
|
||||
sample.clLStickRaw[0] = sample.clLStickRaw[1] = 0;
|
||||
sample.clRStickRaw[0] = sample.clRStickRaw[1] = 0;
|
||||
sample.clTriggerL = sample.clTriggerR = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// KPADRead: fills KPADStatus[0] for `chan` from the Bluetooth remote, returns the entry count.
|
||||
extern "C" int32_t KPAD__Read_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
|
||||
{
|
||||
if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
|
||||
return 0;
|
||||
}
|
||||
WiiRemoteInput::KpadSample sample;
|
||||
const bool have = ReadSample(chan, sample);
|
||||
try {
|
||||
return WriteStatus(chan, statusPtr, have ? &sample : nullptr);
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(80197380, KPAD__Read_HLE, int32_t, (uint32_t chan, uint32_t statusPtr, uint32_t count),
|
||||
(chan, statusPtr, count));
|
||||
|
||||
// KPADGetUnifiedWpadStatus: the raw WPAD status behind KPADStatus. The game
|
||||
// reads the Classic Controller's buttons, sticks and triggers from here. The
|
||||
// SDK fills `count` entries with the channel's recent samples (the game asks for
|
||||
// as many as it asked KPADRead for and looks at entry 0); with one sample per
|
||||
// frame here, every entry gets the current one.
|
||||
extern "C" int32_t KPAD__GetUnifiedWpadStatus_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
|
||||
{
|
||||
constexpr uint32_t kMaxEntries = 16; // KPAD_MAX_READ_BUFS
|
||||
if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
|
||||
return 0;
|
||||
}
|
||||
WiiRemoteInput::KpadSample sample;
|
||||
const bool have = ReadSample(chan, sample);
|
||||
try {
|
||||
const uint32_t entries = std::min(count, kMaxEntries);
|
||||
for (uint32_t i = 0; i < entries; ++i) {
|
||||
WriteUnifiedStatus(statusPtr + i * kUnifiedSize, have ? &sample : nullptr);
|
||||
}
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
return 0;
|
||||
}
|
||||
return have ? 1 : 0;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(8019812C, KPAD__GetUnifiedWpadStatus_HLE, int32_t,
|
||||
(uint32_t chan, uint32_t statusPtr, uint32_t count), (chan, statusPtr, count));
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "input_bindings.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
@@ -113,13 +112,7 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
|
||||
}
|
||||
|
||||
PADStatus statuses[PAD_CHANMAX]{};
|
||||
// Keep looking for a Bluetooth Wii Remote that dropped out (or was turned on late).
|
||||
WiiRemoteInput::Poll();
|
||||
uint32_t rumbleMask = PADRead(statuses);
|
||||
// Wii Remotes reach the game through KPAD, not as GameCube pads. This also
|
||||
// applies while input is blocked (overlay open) so the port does not flip
|
||||
// between "connected" and "no controller" every time the overlay toggles.
|
||||
WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX);
|
||||
|
||||
FillTriggersHeldByButtons(statuses);
|
||||
InputBindings::Apply(statuses);
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
extern "C" bool PAD_HLE_RumbleEnabled();
|
||||
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kDefaultWorkMemSize = 0x20000;
|
||||
constexpr uint8_t kDefaultDpdSensitivity = 3;
|
||||
constexpr int32_t kStatusOk = 0;
|
||||
|
||||
struct WpadStubState {
|
||||
bool initSubRan = false;
|
||||
bool simpleSyncActive = false;
|
||||
uint32_t syncDeviceCallback = 0;
|
||||
uint32_t workMemSize = kDefaultWorkMemSize;
|
||||
uint8_t dpdSensitivity = kDefaultDpdSensitivity;
|
||||
WpadContract::State contract{};
|
||||
};
|
||||
|
||||
WpadStubState g_state{};
|
||||
|
||||
void InvokeWpadCallback(uint32_t callback, uint32_t chan, int32_t result)
|
||||
{
|
||||
if (callback == 0) {
|
||||
return;
|
||||
}
|
||||
if (!TranslatedFunctionRegistry::FindByAddressPtr(callback)) {
|
||||
return;
|
||||
}
|
||||
auto& cpu = GetPersistentCpuContext();
|
||||
cpu.gpr[3] = chan;
|
||||
cpu.gpr[4] = static_cast<uint32_t>(result);
|
||||
InvokeIndirectCpu(callback, &cpu);
|
||||
}
|
||||
|
||||
int32_t InitializeWpadLibrary()
|
||||
{
|
||||
g_state.contract.Initialize();
|
||||
return kStatusOk;
|
||||
}
|
||||
|
||||
// The async WPAD entry points all report their outcome to the guest callback and
|
||||
// then return the same value.
|
||||
int32_t CompleteWpadRequest(uint32_t chan, uint32_t callback, int32_t result)
|
||||
{
|
||||
InvokeWpadCallback(callback, chan, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int32_t WPADGetStatus_HLE()
|
||||
{
|
||||
// RVL::WPADGetStatus takes no channel arg; it reports global WUD library state.
|
||||
// Reading r3 here would leak a caller's stale register value into the result.
|
||||
return g_state.contract.GetLibraryStatus();
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF64C, WPADGetStatus_HLE, int32_t, (), ());
|
||||
|
||||
extern "C" uint32_t WPADGetDpdSensitivity_HLE()
|
||||
{
|
||||
return static_cast<uint32_t>(g_state.dpdSensitivity);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C329C, WPADGetDpdSensitivity_HLE, uint32_t, (), ());
|
||||
|
||||
extern "C" int32_t WPADInitSub_HLE()
|
||||
{
|
||||
if (!g_state.initSubRan) {
|
||||
g_state.initSubRan = true;
|
||||
return InitializeWpadLibrary();
|
||||
}
|
||||
return kStatusOk;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF3B4, WPADInitSub_HLE, int32_t, (), ());
|
||||
|
||||
extern "C" int32_t WPADInit_HLE()
|
||||
{
|
||||
return InitializeWpadLibrary();
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF5C4, WPADInit_HLE, int32_t, (), ());
|
||||
|
||||
extern "C" int32_t WUDGetStatus_HLE()
|
||||
{
|
||||
return WpadContract::kStatusReady;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801CDB84, WUDGetStatus_HLE, int32_t, (), ());
|
||||
|
||||
// WPADGetDataFormat: the format KPAD runs the attached extension in (WPAD_FMT_*_ACC_DPD).
|
||||
// WPADClampStick keys off it. Can't reuse the translated SDK implementation: it dereferences
|
||||
// Bluetooth control blocks that HLE'd WPADInit never constructs.
|
||||
extern "C" int32_t WPADGetDataFormat_HLE(uint32_t chan)
|
||||
{
|
||||
const int32_t status = g_state.contract.GetDataFormat(chan);
|
||||
if (status < 0) {
|
||||
return status;
|
||||
}
|
||||
switch (WiiRemoteInput::EffectiveKind(chan)) {
|
||||
case WiiRemoteInput::Kind::RemoteWithNunchuk: return 5;
|
||||
case WiiRemoteInput::Kind::RemoteWithClassic: return 8;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan));
|
||||
|
||||
// WPADSetDataFormat: records the per-channel data format the game asked for.
|
||||
extern "C" int32_t WPADSetDataFormat_HLE(uint32_t chan, int32_t format)
|
||||
{
|
||||
return g_state.contract.SetDataFormat(chan, format);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0B9C, WPADSetDataFormat_HLE, int32_t, (uint32_t chan, int32_t format), (chan, format));
|
||||
|
||||
// WPADProbe: reports the extension type of a Bluetooth remote on `chan`, or no controller.
|
||||
extern "C" int32_t WPADProbe_HLE(uint32_t chan, uint32_t typePtr)
|
||||
{
|
||||
if (chan >= WpadContract::kChannelCount) {
|
||||
return WpadContract::kErrorBadChannel;
|
||||
}
|
||||
|
||||
// Drive the rescan state machine here too: a reconnect probe can arrive
|
||||
// before the next PADRead, and only Poll() brings a dropped remote back.
|
||||
WiiRemoteInput::Poll();
|
||||
|
||||
// A real Bluetooth remote: WPAD_DEV_CORE (0) for a bare remote,
|
||||
// WPAD_DEV_FREESTYLE (1) with a Nunchuk, WPAD_DEV_CLASSIC (2) with a Classic
|
||||
// Controller. The game reads the type from here (not from
|
||||
// KPADStatus.dev_type) to pick its control scheme, and re-reads it when it
|
||||
// changes, which is what makes an extension swap mid-game work like on the
|
||||
// console. EffectiveKind keeps the last type through SDL's re-creation of
|
||||
// the joystick after a swap.
|
||||
const WiiRemoteInput::Kind kind = WiiRemoteInput::EffectiveKind(chan);
|
||||
if (WiiRemoteInput::IsRemoteChannel(chan)) {
|
||||
if (typePtr != 0) {
|
||||
uint32_t type = WpadContract::kExtensionCore;
|
||||
if (kind == WiiRemoteInput::Kind::RemoteWithNunchuk) type = 1u;
|
||||
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) type = 2u;
|
||||
Memory::Write32(typePtr, type);
|
||||
}
|
||||
return kStatusOk;
|
||||
}
|
||||
if (typePtr != 0) {
|
||||
Memory::Write32(typePtr, WpadContract::kExtensionCore);
|
||||
}
|
||||
return WpadContract::kErrorNoController;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0990, WPADProbe_HLE, int32_t, (uint32_t chan, uint32_t typePtr), (chan, typePtr));
|
||||
|
||||
extern "C" void WPADControlMotor_HLE(uint32_t chan, uint32_t command)
|
||||
{
|
||||
const bool on = command != 0 && PAD_HLE_RumbleEnabled();
|
||||
PADControlMotor(chan, on ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0EC4, WPADControlMotor_HLE, void, (uint32_t chan, uint32_t command), (chan, command));
|
||||
|
||||
extern "C" int32_t WPADGetInfoAsync_HLE(uint32_t chan, uint32_t infoPtr, uint32_t callback)
|
||||
{
|
||||
if (chan >= WpadContract::kChannelCount) {
|
||||
return CompleteWpadRequest(chan, callback, WpadContract::kErrorBadChannel);
|
||||
}
|
||||
|
||||
(void)infoPtr;
|
||||
return CompleteWpadRequest(chan, callback, WpadContract::kErrorNoController);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0CA4, WPADGetInfoAsync_HLE, int32_t,
|
||||
(uint32_t chan, uint32_t infoPtr, uint32_t callback), (chan, infoPtr, callback));
|
||||
|
||||
extern "C" int32_t WPADControlLed_HLE(uint32_t chan, uint32_t ledMask, uint32_t callback)
|
||||
{
|
||||
if (chan >= WpadContract::kChannelCount) {
|
||||
return CompleteWpadRequest(chan, callback, WpadContract::kErrorBadChannel);
|
||||
}
|
||||
(void)ledMask;
|
||||
return CompleteWpadRequest(chan, callback, WpadContract::kErrorNoController);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0FF8, WPADControlLed_HLE, int32_t,
|
||||
(uint32_t chan, uint32_t ledMask, uint32_t callback), (chan, ledMask, callback));
|
||||
|
||||
extern "C" int32_t WPADStartSimpleSync_HLE()
|
||||
{
|
||||
if (g_state.simpleSyncActive) {
|
||||
return 0;
|
||||
}
|
||||
g_state.simpleSyncActive = true;
|
||||
return 1;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF634, WPADStartSimpleSync_HLE, int32_t, (), ()); // WUDStartSyncSimple
|
||||
PPC_NATIVE_OVERRIDE(801BF638, WPADStartSimpleSync_HLE, int32_t, (), ()); // WPADStartSimpleSync (HBM)
|
||||
|
||||
// due to multiplayer controller screen hle this to avoid startsyncdevice to return fail every frame
|
||||
// causing you to get softlocked in the game
|
||||
extern "C" int32_t WPADStopSimpleSync_HLE()
|
||||
{
|
||||
if (g_state.simpleSyncActive) {
|
||||
g_state.simpleSyncActive = false;
|
||||
const uint32_t callback = g_state.syncDeviceCallback;
|
||||
if (callback != 0 && TranslatedFunctionRegistry::FindByAddressPtr(callback)) {
|
||||
NandQueueIosCallback(callback, 1, 0); // callback(WUD_SYNC_DONE, devicesSynced=0)
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF63C, WPADStopSimpleSync_HLE, int32_t, (), ());
|
||||
|
||||
extern "C" uint32_t WPADSetSyncDeviceCallback_HLE(uint32_t callback)
|
||||
{
|
||||
const uint32_t previous = g_state.syncDeviceCallback;
|
||||
g_state.syncDeviceCallback = callback;
|
||||
return previous;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF640, WPADSetSyncDeviceCallback_HLE, uint32_t, (uint32_t callback), (callback));
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/bluetooth_hle.h"
|
||||
#include "hle/net/network.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -120,6 +121,10 @@ extern "C" void IOS_OpenAsync_HLE(CpuContext* ctx) {
|
||||
const uint32_t callbackArg = ctx->gpr[6];
|
||||
const char* path = pathPtr ? (const char*)Memory::GetPointer(pathPtr) : "(null)";
|
||||
if (pathPtr) {
|
||||
if (const int32_t btFd = Bluetooth_HLE_OpenDevice(path)) {
|
||||
CompleteAsync(ctx, callback, btFd, callbackArg);
|
||||
return;
|
||||
}
|
||||
if (const int32_t netFd = Network_HLE_OpenDevice(path, mode)) {
|
||||
CompleteAsync(ctx, callback, netFd, callbackArg);
|
||||
return;
|
||||
@@ -135,6 +140,11 @@ extern "C" void IOS_CloseAsync_HLE(CpuContext* ctx) {
|
||||
const uint32_t fd = ctx->gpr[3];
|
||||
const uint32_t callback = ctx->gpr[4];
|
||||
const uint32_t callbackArg = ctx->gpr[5];
|
||||
if (Bluetooth_HLE_IsFd(fd)) {
|
||||
const int32_t result = Bluetooth_HLE_Close(fd);
|
||||
CompleteAsync(ctx, callback, result, callbackArg);
|
||||
return;
|
||||
}
|
||||
if (Network_HLE_IsFd(fd)) {
|
||||
const int32_t result = Network_HLE_Close(fd);
|
||||
CompleteAsync(ctx, callback, result, callbackArg);
|
||||
@@ -179,6 +189,15 @@ extern "C" void IOS_IoctlvAsync_HLE(CpuContext* ctx)
|
||||
const uint32_t vectorPtr = ctx->gpr[7];
|
||||
const uint32_t callback = ctx->gpr[8];
|
||||
const uint32_t callbackArg = ctx->gpr[9];
|
||||
if (Bluetooth_HLE_IsFd(fd)) {
|
||||
int32_t result = 0;
|
||||
if (Bluetooth_HLE_IoctlvAsync(fd, cmd, numIn, numOut, vectorPtr, callback, callbackArg, result)) {
|
||||
CompleteAsync(ctx, callback, result, callbackArg);
|
||||
} else {
|
||||
SetIosReturn(ctx, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (TryDeferredNetworkAsync(
|
||||
ctx, fd, callback, callbackArg,
|
||||
[&] {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "ppc_runtime.h"
|
||||
#include "runtime_log.h"
|
||||
#include "hle/bluetooth_hle.h"
|
||||
#include "hle/net/network.h"
|
||||
#include "guest_small_data.h"
|
||||
#include "os_internal.h"
|
||||
@@ -237,6 +238,10 @@ bool ProcessAlarmQueue(CpuContext* cpu, int maxToProcess)
|
||||
handledAny = true;
|
||||
completionNeedsReschedule = true;
|
||||
}
|
||||
if (Bluetooth_HLE_ProcessCompletions(cpu)) {
|
||||
handledAny = true;
|
||||
completionNeedsReschedule = true;
|
||||
}
|
||||
|
||||
if (NandProcessPendingCallbacks(cpu, maxToProcess)) {
|
||||
handledAny = true;
|
||||
|
||||
@@ -80,33 +80,16 @@ extern "C" void OSFatal_HLE_801a4ec4(CpuContext* ctx) {
|
||||
|
||||
REGISTER_NATIVE_FUNCTION_AS(0x801A4EC4, OSFatal_HLE_801a4ec4, "OSFatal_HLE_801a4ec4");
|
||||
|
||||
extern "C" void GKI_delay_HLE_801301b4(CpuContext* ctx)
|
||||
extern "C" void GKI_delay_HLE_801301b4()
|
||||
{
|
||||
const uint32_t delayMs = ctx ? static_cast<uint32_t>(ctx->gpr[3]) : 0;
|
||||
const uint32_t sleepMs = delayMs == 0 ? 1u : std::min(delayMs, 10u);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
|
||||
// The guest's GKI_delay is empty: BTA_Init spins on it while the Bluetooth
|
||||
// module answers through IPC interrupts and the 2 ms BTU alarm. Service both
|
||||
// here and let the wall clock advance.
|
||||
OS_HLE_ProcessAlarms(32);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
|
||||
extern "C" uint32_t BTM_IsDeviceUp_HLE_8013a300(CpuContext* ctx)
|
||||
{
|
||||
// Force Bluetooth stack to "up" to avoid endless polling loops while we lack
|
||||
// real hardware bring-up.
|
||||
constexpr uint32_t kBtmCbBase = 0x80336278u;
|
||||
constexpr uint32_t kDevStateOffset = 0x64Eu;
|
||||
try {
|
||||
::Memory::Write8(kBtmCbBase + kDevStateOffset, 5u);
|
||||
} catch (const ::Memory::AccessViolation&) {
|
||||
// Ignore; best-effort write
|
||||
}
|
||||
|
||||
if (ctx) {
|
||||
ctx->gpr[3] = 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE_VOID(801301B4, GKI_delay_HLE_801301b4, (CpuContext* ctx), (ctx));
|
||||
PPC_NATIVE_OVERRIDE(8013A300, BTM_IsDeviceUp_HLE_8013a300, uint32_t, (CpuContext* ctx), (ctx));
|
||||
PPC_NATIVE_OVERRIDE_VOID(801301B4, GKI_delay_HLE_801301b4, (), ());
|
||||
|
||||
// Serial Interface (SI) - GameCube controller ports; stubbed since we don't emulate the MMIO.
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "memory.h"
|
||||
#include "hle/audio/ax_dsp.h"
|
||||
#include "gx_worker.h"
|
||||
#include "hle_stubs.h"
|
||||
#include "ppc_runtime.h"
|
||||
#include "runtime_log.h"
|
||||
@@ -124,6 +125,7 @@ extern "C" uint32_t OSResetSystem()
|
||||
std::fflush(stderr);
|
||||
// The AX mix worker holds resolved host pointers into the guest regions;
|
||||
// it must be stopped before those mappings are torn down.
|
||||
GxWorker::Stop();
|
||||
AxDspHle::ShutdownMixWorker();
|
||||
Memory::Reset();
|
||||
SetRuntimeExitCode(0);
|
||||
@@ -145,6 +147,7 @@ extern "C" uint32_t Exit_801AE58C(int status)
|
||||
ShowRuntimeFatalPopup("the game exited with an error status", details);
|
||||
MarkFatalErrorReported();
|
||||
}
|
||||
GxWorker::Stop();
|
||||
SetRuntimeExitCode(status);
|
||||
std::exit(status);
|
||||
return static_cast<uint32_t>(status);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "memory.h"
|
||||
#include "nand_path.h"
|
||||
#include "nand_save_probe.h"
|
||||
#include "hle/bluetooth_hle.h"
|
||||
#include "hle/net/network.h"
|
||||
#include "recomp_mod_loader.h"
|
||||
#include "runtime_config.h"
|
||||
|
||||
@@ -382,6 +382,9 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
|
||||
if (const int32_t netFd = Network_HLE_OpenDevice(path, mode)) {
|
||||
return netFd;
|
||||
}
|
||||
if (const int32_t btFd = Bluetooth_HLE_OpenDevice(path)) {
|
||||
return btFd;
|
||||
}
|
||||
if (std::strcmp(path, "/dev/dolphin") == 0) {
|
||||
return DOLPHIN_DEV_FD;
|
||||
}
|
||||
@@ -452,6 +455,9 @@ extern "C" int32_t NAND_IOS_Close_HLE(uint32_t fd) {
|
||||
if (Network_HLE_IsFd(fd)) {
|
||||
return Network_HLE_Close(fd);
|
||||
}
|
||||
if (Bluetooth_HLE_IsFd(fd)) {
|
||||
return Bluetooth_HLE_Close(fd);
|
||||
}
|
||||
|
||||
auto* handle = GetHandle(fd);
|
||||
if (!handle) {
|
||||
@@ -566,6 +572,10 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
||||
LogNandWarning("IOS_Ioctl", "/dev/sha does not support scalar ioctl cmd=%u", cmd);
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
if (Bluetooth_HLE_IsFd(fd)) {
|
||||
LogNandWarning("IOS_Ioctl", "/dev/usb/oh1 does not support scalar ioctl cmd=%u", cmd);
|
||||
return -4;
|
||||
}
|
||||
if (fd == DOLPHIN_DEV_FD) {
|
||||
LogNandWarning("IOS_Ioctl", "/dev/dolphin does not support scalar ioctl cmd=%u", cmd);
|
||||
return ISFS_EINVAL;
|
||||
@@ -1032,6 +1042,9 @@ extern "C" int32_t NAND_IOS_Ioctlv_HLE(
|
||||
if (Network_HLE_IsFd(fd)) {
|
||||
return Network_HLE_Ioctlv(fd, cmd, numIn, numOut, vectorPtr);
|
||||
}
|
||||
if (Bluetooth_HLE_IsFd(fd)) {
|
||||
return Bluetooth_HLE_Ioctlv(fd, cmd, numIn, numOut, vectorPtr);
|
||||
}
|
||||
|
||||
if (GetShaHandle(static_cast<int32_t>(fd))) {
|
||||
return HandleShaIoctlv(static_cast<int32_t>(fd), cmd, numIn, numOut, vectorPtr);
|
||||
|
||||
@@ -577,7 +577,7 @@ void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace) {
|
||||
aurora_set_present_schedule(0, 0);
|
||||
}
|
||||
|
||||
aurora_end_frame();
|
||||
EndAuroraFrame();
|
||||
if (paceThisFrame) {
|
||||
PaceToRetraceBoundary(paceDeadline);
|
||||
std::lock_guard<std::mutex> lock(g_viMutex);
|
||||
@@ -751,7 +751,10 @@ extern "C" void VIConfigure_HLE_801b9f6c(CpuContext* ctx)
|
||||
renderMode.xfbHeight != 0 ? renderMode.xfbHeight : renderMode.efbHeight;
|
||||
}
|
||||
|
||||
::VIConfigure(&renderMode);
|
||||
{
|
||||
GX_SYNC_INLINE();
|
||||
::VIConfigure(&renderMode);
|
||||
}
|
||||
|
||||
ViSetR3(ctx, 0);
|
||||
}
|
||||
|
||||
+17
-5
@@ -63,9 +63,10 @@
|
||||
#include "system_bridge.h"
|
||||
#include "ppc_runtime.h"
|
||||
#include "aurora_events.h"
|
||||
#include "wii_remote_input.h"
|
||||
#include "wiimote_host.h"
|
||||
#include "discord_presence.h"
|
||||
#include "fiber_manager.h"
|
||||
#include "gx_worker.h"
|
||||
#include "hle_stubs.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
@@ -88,6 +89,9 @@ namespace {
|
||||
std::string FormatHostStackTrace(unsigned framesToSkip = 0);
|
||||
|
||||
void ServiceGuestTimingDuringAuroraFrameWait() {
|
||||
if (GxWorker::OnWorker()) {
|
||||
return;
|
||||
}
|
||||
// Aurora can block inside FIFO drains before control returns to GX HLE, for as long as a
|
||||
// whole display period. Keep VI retraces, alarms and audio moving at wall-clock cadence here
|
||||
// while still suppressing guest rescheduling and recursive Aurora work.
|
||||
@@ -1430,10 +1434,9 @@ int RuntimeMain(int argc, char** argv) {
|
||||
}
|
||||
const AuroraBackend requestedBackend = auroraConfig.desiredBackend;
|
||||
|
||||
// SDL only reads its Wii driver hint when the joystick subsystem starts, which
|
||||
// aurora_initialize does; a Bluetooth Wii Remote paired before launch must be
|
||||
// visible on that first scan.
|
||||
WiiRemoteInput::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled(true));
|
||||
// SDL reads its device hints when the joystick subsystem starts, which
|
||||
// aurora_initialize does.
|
||||
Wiimote::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled());
|
||||
|
||||
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
|
||||
if (auroraInfo.initializationStatus != AURORA_INITIALIZATION_SUCCESS) {
|
||||
@@ -1450,6 +1453,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
<< std::endl;
|
||||
}
|
||||
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
||||
Wiimote::Initialize();
|
||||
GxGuestWrite::InstallAuroraHooks();
|
||||
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
|
||||
auroraInfo.windowSize.native_fb_height);
|
||||
@@ -1466,6 +1470,8 @@ int RuntimeMain(int argc, char** argv) {
|
||||
<< (forceAspect169 ? "16:9" : (configWidescreen ? "surface (dynamic EGG canvas)" : "4:3"))
|
||||
<< std::endl;
|
||||
g_auroraInitialized.store(true, std::memory_order_release);
|
||||
GxWorker::SetWaitCallback(ServiceGuestTimingDuringAuroraFrameWait);
|
||||
GxWorker::Start();
|
||||
|
||||
auto entry = ResolveEntry();
|
||||
InitializePersistentCpuContext();
|
||||
@@ -1493,6 +1499,8 @@ int RuntimeMain(int argc, char** argv) {
|
||||
// Shutdown fiber system
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
Wiimote::Shutdown();
|
||||
GxWorker::Stop();
|
||||
aurora_shutdown();
|
||||
DiscordPresence::Shutdown();
|
||||
SetRuntimeExitCodeImpl(0);
|
||||
@@ -1511,6 +1519,8 @@ int RuntimeMain(int argc, char** argv) {
|
||||
SetRuntimeExitCodeImpl(1);
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
Wiimote::Shutdown();
|
||||
GxWorker::Stop();
|
||||
aurora_shutdown();
|
||||
DiscordPresence::Shutdown();
|
||||
ShutdownProcessTranscript();
|
||||
@@ -1523,6 +1533,8 @@ int RuntimeMain(int argc, char** argv) {
|
||||
SetRuntimeExitCodeImpl(1);
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
Wiimote::Shutdown();
|
||||
GxWorker::Stop();
|
||||
aurora_shutdown();
|
||||
DiscordPresence::Shutdown();
|
||||
ShutdownProcessTranscript();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "memory.h"
|
||||
#include "hle/hollywood_gpio.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -643,6 +644,9 @@ uint16_t MemoryInline::Read16Slow(uint32_t addr) {
|
||||
|
||||
uint32_t MemoryInline::Read32Slow(uint32_t addr) {
|
||||
if (IsMmioAddress(addr)) {
|
||||
if (uint32_t value = 0; HollywoodGpio_Read32(addr, value)) {
|
||||
return value;
|
||||
}
|
||||
ThrowMmioReadBlocked(addr, sizeof(uint32_t));
|
||||
}
|
||||
ResolveDeferredReads(addr, sizeof(uint32_t));
|
||||
@@ -708,6 +712,9 @@ void MemoryInline::Write32Slow(uint32_t addr, uint32_t val) {
|
||||
return;
|
||||
}
|
||||
if (IsMmioAddress(addr)) {
|
||||
if (HollywoodGpio_Write32(addr, val)) {
|
||||
return;
|
||||
}
|
||||
throw Memory::AccessViolation(addr, sizeof(val), "MMIO write blocked (non-GPU)");
|
||||
}
|
||||
WriteScalar(addr, val);
|
||||
|
||||
+283
-258
@@ -1,4 +1,5 @@
|
||||
#include "settings_overlay.h"
|
||||
#include "gx_worker.h"
|
||||
#include "audio_backend.h"
|
||||
#include "aurora_events.h"
|
||||
#include "controller_button_names.h"
|
||||
@@ -8,7 +9,7 @@
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "wii_remote_input.h"
|
||||
#include "wiimote_host.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
@@ -188,6 +189,7 @@ bool IsHighFrameRateMode() {
|
||||
}
|
||||
|
||||
void SetResolutionScale(float scale) {
|
||||
GX_SYNC_INLINE();
|
||||
g_resolutionScale = scale;
|
||||
VISetFrameBufferScale(scale);
|
||||
RuntimeConfigFile::SetResolutionMultiplier(scale);
|
||||
@@ -259,9 +261,9 @@ void ApplyConfiguredMappings() {
|
||||
// they describe whatever pad the user set them up with (usually an Xbox
|
||||
// layout: a = south). A Wii U Pro Controller has a fixed, known layout
|
||||
// (A on the east position) that aurora already maps by name; applying
|
||||
// the shared bindings on top swaps A/B and X/Y. (Wii Remotes with any
|
||||
// extension never reach the PAD layer: the game reads them through KPAD.)
|
||||
if (WiiRemoteInput::KindForPort(port) == WiiRemoteInput::Kind::WiiUPro) {
|
||||
// the shared bindings on top swaps A/B and X/Y.
|
||||
if (const char* name = PADGetNameForControllerIndex(static_cast<uint32_t>(controllerIndex));
|
||||
name != nullptr && std::string_view(name).find("Wii U Pro") != std::string_view::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -295,139 +297,114 @@ void ApplyConfiguredMappings() {
|
||||
}
|
||||
}
|
||||
|
||||
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled(true);
|
||||
bool g_wiiContinuousScan = RuntimeConfigFile::WiiContinuousScanEnabled(false);
|
||||
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled();
|
||||
bool g_wiiAutoPair = RuntimeConfigFile::WiiAutoPairEnabled(true);
|
||||
bool g_wiiSensorBarAbove = RuntimeConfigFile::WiiSensorBarAbove(false);
|
||||
int g_wiiIrSensitivity = RuntimeConfigFile::WiiIrSensitivity(3);
|
||||
|
||||
// Accelerometer readout and zero-point calibration for a bare remote / remote + Nunchuk.
|
||||
void DrawWiiRemoteAccelerometer(uint32_t port) {
|
||||
ImGui::SeparatorText("Accelerometer");
|
||||
float sdlG[3] = {};
|
||||
float kpad[3] = {};
|
||||
if (WiiRemoteInput::ReadAccelDebug(port, sdlG, kpad)) {
|
||||
ImGui::Text("KPAD acc: x %+.2f y %+.2f z %+.2f g", kpad[0], kpad[1], kpad[2]);
|
||||
ImGui::TextDisabled("Flat, buttons up: (0, -1, 0). Sideways as a wheel: (1, 0, 0); z follows the turn.");
|
||||
} else {
|
||||
ImGui::TextDisabled("No accelerometer data yet.");
|
||||
}
|
||||
// Residual per-axis bias left after SDL's factory-calibrated decode, measured
|
||||
// here with the remote at rest.
|
||||
if (WiiRemoteInput::IsAccelCalibrating(port)) {
|
||||
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
|
||||
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
|
||||
WiiRemoteInput::StartAccelCalibration(port);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Put the remote down on a flat surface with the buttons facing up and do not touch it\n"
|
||||
"for about two seconds. Corrects the steering offset of a remote held sideways.");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
const auto stored = WiiRemoteInput::ReadAccelOffset(port);
|
||||
const std::array<double, 3> offset = stored.value_or(std::array<double, 3>{});
|
||||
const bool calibrated = offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
|
||||
ImGui::BeginDisabled(!calibrated || WiiRemoteInput::IsAccelCalibrating(port));
|
||||
if (ImGui::Button("Clear calibration")) {
|
||||
WiiRemoteInput::ClearAccelCalibration(port);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (const char* message = WiiRemoteInput::AccelCalibrationMessage(port)) {
|
||||
ImGui::TextWrapped("%s", message);
|
||||
} else if (calibrated) {
|
||||
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
|
||||
} else {
|
||||
ImGui::TextDisabled("No trim stored.");
|
||||
}
|
||||
|
||||
constexpr float kControllerMenuWidth = 460.0f;
|
||||
|
||||
void PushMenuTextWrap() {
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
}
|
||||
|
||||
// Wii Remotes (Bluetooth) menu: driver switch, pairing help, continuous scanning and the port's controller kind.
|
||||
void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
|
||||
if (!ImGui::BeginMenu("Wii Remotes (Bluetooth)")) {
|
||||
std::string WiiRemoteLabel(const Wiimote::SlotInfo& info) {
|
||||
std::string label = info.model;
|
||||
if (!info.extension.empty()) {
|
||||
label += " + " + info.extension;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
// Pairing, the console's Wii Remote settings and the driver switch; applies to every remote.
|
||||
void DrawWiiRemoteSetup() {
|
||||
if (!ImGui::CollapsingHeader("Wii Remote setup (Bluetooth)")) {
|
||||
return;
|
||||
}
|
||||
if (ImGui::Checkbox("Use Wii Remotes / Wii U Pro Controllers", &g_wiiRemotesEnabled)) {
|
||||
ImGui::Indent();
|
||||
PushMenuTextWrap();
|
||||
const std::string notice = Wiimote::PlatformNotice();
|
||||
if (!notice.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "%s", notice.c_str());
|
||||
}
|
||||
const Wiimote::PairingInfo pairing = Wiimote::Pairing();
|
||||
if (pairing.supported) {
|
||||
ImGui::TextUnformatted("Press the red SYNC button (or 1 and 2) on the Wii Remote, like on a Wii.");
|
||||
ImGui::TextDisabled("Paired before? Just press any button on it.");
|
||||
} else if (g_wiiRemotesEnabled) {
|
||||
ImGui::TextUnformatted("Pair the Wii Remote in your system's Bluetooth settings, then press any button on it.");
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::BeginDisabled(!pairing.supported);
|
||||
if (ImGui::Button("Pair a Wii Remote")) {
|
||||
Wiimote::StartPairing();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Searches for a minute, like pressing SYNC on the console.");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (pairing.searching) {
|
||||
ImGui::TextDisabled("Searching...");
|
||||
} else if (!pairing.status.empty()) {
|
||||
ImGui::TextDisabled("%s", pairing.status.c_str());
|
||||
}
|
||||
if (const size_t waiting = Wiimote::WaitingRemotes(); waiting != 0) {
|
||||
ImGui::TextDisabled("%zu more Wii Remote%s connected; joins when a player leaves.", waiting,
|
||||
waiting == 1 ? "" : "s");
|
||||
}
|
||||
if (ImGui::Checkbox("Look for Wii Remotes automatically", &g_wiiAutoPair)) {
|
||||
RuntimeConfigFile::SetWiiAutoPairEnabled(g_wiiAutoPair);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("While no Wii Remote is connected, keep searching for one in pairing mode.\n"
|
||||
"Searching can briefly disturb Bluetooth headphones.");
|
||||
}
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
int sensorBar = g_wiiSensorBarAbove ? 1 : 0;
|
||||
if (ImGui::Combo("Sensor bar", &sensorBar, "Below TV\0Above TV\0")) {
|
||||
g_wiiSensorBarAbove = sensorBar == 1;
|
||||
RuntimeConfigFile::SetWiiSensorBarAbove(g_wiiSensorBarAbove);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Takes effect the next time the game starts.");
|
||||
}
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
if (ImGui::SliderInt("Pointer sensitivity", &g_wiiIrSensitivity, 1, 5, "%d", ImGuiSliderFlags_AlwaysClamp)) {
|
||||
RuntimeConfigFile::SetWiiIrSensitivity(g_wiiIrSensitivity);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("The sensor bar sensitivity from the Wii settings.\nTakes effect the next time the game starts.");
|
||||
}
|
||||
if (ImGui::Checkbox("Use Wii Remotes", &g_wiiRemotesEnabled)) {
|
||||
RuntimeConfigFile::SetWiiRemotesEnabled(g_wiiRemotesEnabled);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Takes effect on the next launch. Turn this off if you use a Mayflash DolphinBar.");
|
||||
ImGui::SetTooltip("Takes effect on the next launch. Turn this off to use a Wii U Pro Controller as a gamepad.");
|
||||
}
|
||||
ImGui::TextDisabled("Pairing: Windows Settings > Bluetooth > Add device, then press 1+2");
|
||||
ImGui::TextDisabled("(or the red SYNC button) on the remote. Leave the PIN empty.");
|
||||
ImGui::TextDisabled("A remote that was paired before also needs to be turned on with 1+2/SYNC.");
|
||||
if (ImGui::Checkbox("Keep scanning for Wii Remotes (like Dolphin's Continuous Scanning)",
|
||||
&g_wiiContinuousScan)) {
|
||||
RuntimeConfigFile::SetWiiContinuousScanEnabled(g_wiiContinuousScan);
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
// The Wii Remote the game has on this player's channel.
|
||||
void DrawWiiRemoteDetails(uint32_t port, const Wiimote::SlotInfo& info) {
|
||||
ImGui::Text("%s", WiiRemoteLabel(info).c_str());
|
||||
if (info.battery >= 0) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Battery %d/4", info.battery);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("While no Wii controller is connected, re-check Bluetooth every 2 seconds so a\n"
|
||||
"remote that dropped out (\"Communications with the controller have been\n"
|
||||
"interrupted\") or was turned on after launch comes back by itself.");
|
||||
}
|
||||
// The driver hint is only read at launch, so a rescan after the user turned
|
||||
// the setting off would still re-enumerate Wii devices in this session.
|
||||
ImGui::BeginDisabled(!g_wiiRemotesEnabled);
|
||||
if (ImGui::Button("Rescan now")) {
|
||||
WiiRemoteInput::RescanNow();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (WiiRemoteInput::IsScanning()) {
|
||||
ImGui::TextDisabled("Scanning... (%u so far) - press 1+2 on the remote", WiiRemoteInput::ScanCount());
|
||||
PushMenuTextWrap();
|
||||
if (info.asleep) {
|
||||
ImGui::TextUnformatted("Disconnected by the game. Press any button on it to reconnect.");
|
||||
} else if (info.link == Bluetooth::LinkState::Ready) {
|
||||
ImGui::TextUnformatted("The game reads it like a real Wii Remote. Nothing to set up.");
|
||||
} else {
|
||||
ImGui::TextDisabled("Not scanning");
|
||||
ImGui::TextUnformatted("Connecting to the game...");
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
const WiiRemoteInput::Kind kind = WiiRemoteInput::KindForPort(selectedGamePort);
|
||||
ImGui::Text("Port %u: %s", static_cast<unsigned>(selectedGamePort + 1), WiiRemoteInput::KindLabel(kind));
|
||||
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) {
|
||||
WiiRemoteInput::KpadSample sample;
|
||||
if (WiiRemoteInput::ReadKpadSample(selectedGamePort, sample)) {
|
||||
// WPAD_CL_BUTTON_* bits, in the game's own layout (no mapping involved).
|
||||
const auto held = [&](uint32_t bit, const char* on, const char* off) { return (sample.clHold & bit) ? on : off; };
|
||||
ImGui::Text("Classic: %s %s %s %s %s %s %s %s %s %s %s %s %s %s", held(0x0010, "A", "a"),
|
||||
held(0x0040, "B", "b"), held(0x0008, "X", "x"), held(0x0020, "Y", "y"), held(0x2000, "L", "l"),
|
||||
held(0x0200, "R", "r"), held(0x0080, "ZL", "zl"), held(0x0004, "ZR", "zr"),
|
||||
held(0x0400, "PLUS", "plus"), held(0x1000, "MINUS", "minus"), held(0x0001, "UP", "up"),
|
||||
held(0x4000, "DOWN", "down"), held(0x0002, "LEFT", "left"), held(0x8000, "RIGHT", "right"));
|
||||
ImGui::Text("Sticks: L %+.2f %+.2f (WPAD %+d %+d) R %+.2f %+.2f (WPAD %+d %+d)", sample.clLStick[0],
|
||||
sample.clLStick[1], static_cast<int>(sample.clLStickRaw[0]),
|
||||
static_cast<int>(sample.clLStickRaw[1]), sample.clRStick[0], sample.clRStick[1],
|
||||
static_cast<int>(sample.clRStickRaw[0]), static_cast<int>(sample.clRStickRaw[1]));
|
||||
ImGui::TextDisabled("Capitals = held. The game reads this Classic Controller through KPAD, as on the");
|
||||
ImGui::TextDisabled("console: its buttons mean what the game says they mean, no mapping applies.");
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
if (!info.asleep && ImGui::Button("Disconnect")) {
|
||||
Wiimote::Disconnect(port);
|
||||
}
|
||||
if (kind == WiiRemoteInput::Kind::WiiUPro) {
|
||||
if (SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(selectedGamePort))) {
|
||||
// SDL's Wii driver posts the D-pad as joystick buttons 11-14 (the
|
||||
// SDL_GAMEPAD_BUTTON_DPAD_* values) while its default HIDAPI mapping
|
||||
// expects a hat, so SDL_GetGamepadButton never sees them; read the
|
||||
// joystick directly, like the fallback in aurora's PADRead does.
|
||||
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
|
||||
const auto rawButton = [&](int index) {
|
||||
return joystick != nullptr && SDL_GetJoystickButton(joystick, index);
|
||||
};
|
||||
ImGui::Text("Raw D-pad: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_DPAD_UP) ? "UP" : "up",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN) ? "DOWN" : "down",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT) ? "LEFT" : "left",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT) ? "RIGHT" : "right");
|
||||
ImGui::Text("Raw face buttons: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_EAST) ? "A" : "a",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_SOUTH) ? "B" : "b", rawButton(SDL_GAMEPAD_BUTTON_NORTH) ? "X" : "x",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_WEST) ? "Y" : "y");
|
||||
ImGui::Text("Raw ZL/ZR: %d / %d (pressed above 0)",
|
||||
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
|
||||
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER));
|
||||
ImGui::TextDisabled("Capitals = held. If a button never turns to capitals while physically held,");
|
||||
ImGui::TextDisabled("that press is not reaching SDL at all (a driver-level issue, not a mapping one).");
|
||||
ImGui::TextDisabled("This pad uses Nintendo's own layout (a/b/x/y as labelled); the shared");
|
||||
ImGui::TextDisabled("button mapping above does not apply to it.");
|
||||
}
|
||||
}
|
||||
if (kind == WiiRemoteInput::Kind::Remote || kind == WiiRemoteInput::Kind::RemoteWithNunchuk ||
|
||||
kind == WiiRemoteInput::Kind::RemoteWithClassic) {
|
||||
DrawWiiRemoteAccelerometer(selectedGamePort);
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
const char* KeyBindingName(int scancode) {
|
||||
@@ -594,65 +571,74 @@ void DrawKeyBinding(const char* label, int scancode, RebindKind kind, uint16_t t
|
||||
|
||||
}
|
||||
|
||||
bool DrawKeyboardSettings(uint32_t port) {
|
||||
void ApplyKeyboardPreset(uint32_t port) {
|
||||
const std::array<int, PAD_BUTTON_COUNT> keys = {
|
||||
PAD_KEY_MOUSE_LEFT, SDL_SCANCODE_SPACE, SDL_SCANCODE_E, SDL_SCANCODE_Q,
|
||||
SDL_SCANCODE_RETURN, PAD_KEY_MOUSE_MIDDLE, SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT,
|
||||
SDL_SCANCODE_UP, SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT,
|
||||
};
|
||||
for (size_t i = 0; i < keys.size(); ++i)
|
||||
PADSetKeyButtonBinding(port, {keys[i], kControllerButtons[i].padButton});
|
||||
const std::array<int, PAD_AXIS_COUNT> axes = {
|
||||
SDL_SCANCODE_D, SDL_SCANCODE_A, SDL_SCANCODE_W, SDL_SCANCODE_S,
|
||||
SDL_SCANCODE_L, SDL_SCANCODE_J, SDL_SCANCODE_I, SDL_SCANCODE_K,
|
||||
SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT,
|
||||
};
|
||||
uint32_t axisCount = 0;
|
||||
auto* mappings = PADGetKeyAxisBindings(port, &axisCount);
|
||||
for (uint32_t i = 0; i < axisCount; ++i)
|
||||
PADSetKeyAxisBinding(port, {axes[i], mappings[i].padAxis, 1});
|
||||
PADSerializeMappings();
|
||||
}
|
||||
|
||||
bool KeyboardActive(uint32_t port) {
|
||||
uint32_t count = 0;
|
||||
return PADGetKeyButtonBindings(port, &count) != nullptr;
|
||||
}
|
||||
|
||||
void SetKeyboardActive(uint32_t port, bool active) {
|
||||
if (KeyboardActive(port) == active) return;
|
||||
PADSetKeyboardActive(port, active);
|
||||
PADSerializeMappings();
|
||||
uint32_t count = 0;
|
||||
auto* buttons = PADGetKeyButtonBindings(port, &count);
|
||||
bool enabled = buttons != nullptr;
|
||||
bool usePreset = false;
|
||||
if (ImGui::Checkbox("Keyboard and mouse", &enabled)) {
|
||||
PADSetKeyboardActive(port, enabled);
|
||||
PADSerializeMappings();
|
||||
buttons = PADGetKeyButtonBindings(port, &count);
|
||||
usePreset = enabled && std::all_of(buttons, buttons + count, [](const auto& binding) {
|
||||
if (active && buttons != nullptr && std::all_of(buttons, buttons + count, [](const auto& binding) {
|
||||
return binding.scancode == PAD_KEY_INVALID;
|
||||
});
|
||||
})) {
|
||||
ApplyKeyboardPreset(port);
|
||||
}
|
||||
if (!enabled) return false;
|
||||
ImGui::TextDisabled("Replaces the gamepad on this port. F10 opens settings.");
|
||||
if (ImGui::Button("Use WASD + mouse preset") || usePreset) {
|
||||
const std::array<int, PAD_BUTTON_COUNT> keys = {
|
||||
PAD_KEY_MOUSE_LEFT, SDL_SCANCODE_SPACE, SDL_SCANCODE_E, SDL_SCANCODE_Q,
|
||||
SDL_SCANCODE_RETURN, PAD_KEY_MOUSE_MIDDLE, SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT,
|
||||
SDL_SCANCODE_UP, SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT,
|
||||
};
|
||||
for (size_t i = 0; i < keys.size(); ++i)
|
||||
PADSetKeyButtonBinding(port, {keys[i], kControllerButtons[i].padButton});
|
||||
const std::array<int, PAD_AXIS_COUNT> axes = {
|
||||
SDL_SCANCODE_D, SDL_SCANCODE_A, SDL_SCANCODE_W, SDL_SCANCODE_S,
|
||||
SDL_SCANCODE_L, SDL_SCANCODE_J, SDL_SCANCODE_I, SDL_SCANCODE_K,
|
||||
SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT,
|
||||
};
|
||||
uint32_t axisCount = 0;
|
||||
auto* mappings = PADGetKeyAxisBindings(port, &axisCount);
|
||||
for (uint32_t i = 0; i < axisCount; ++i)
|
||||
PADSetKeyAxisBinding(port, {axes[i], mappings[i].padAxis, 1});
|
||||
PADSerializeMappings();
|
||||
}
|
||||
ImGui::SeparatorText("Button mapping");
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
int key = buttons[i].scancode;
|
||||
}
|
||||
|
||||
void DrawKeyboardSettings(uint32_t port) {
|
||||
PushMenuTextWrap();
|
||||
ImGui::TextUnformatted("Default: WASD to steer, mouse left to accelerate, Space to brake.");
|
||||
ImGui::PopTextWrapPos();
|
||||
if (!ImGui::CollapsingHeader("Customize keys")) return;
|
||||
ImGui::Indent();
|
||||
if (ImGui::Button("Reset to WASD + mouse")) ApplyKeyboardPreset(port);
|
||||
uint32_t count = 0;
|
||||
auto* buttons = PADGetKeyButtonBindings(port, &count);
|
||||
ImGui::SeparatorText("Buttons");
|
||||
for (uint32_t i = 0; buttons != nullptr && i < count; ++i) {
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
ImGui::SetNextItemWidth(220.0f);
|
||||
DrawKeyBinding(PADGetButtonName(buttons[i].padButton), key, RebindKind::KeyboardButton, buttons[i].padButton);
|
||||
DrawKeyBinding(PADGetButtonName(buttons[i].padButton), buttons[i].scancode, RebindKind::KeyboardButton,
|
||||
buttons[i].padButton);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::SeparatorText("Stick and trigger mapping");
|
||||
ImGui::SeparatorText("Sticks and triggers");
|
||||
uint32_t axisCount = 0;
|
||||
auto* axes = PADGetKeyAxisBindings(port, &axisCount);
|
||||
for (uint32_t i = 0; i < axisCount; ++i) {
|
||||
int key = axes[i].scancode;
|
||||
for (uint32_t i = 0; axes != nullptr && i < axisCount; ++i) {
|
||||
ImGui::PushID(static_cast<int>(count + i));
|
||||
const char* direction = PADGetAxisDirectionLabel(axes[i].padAxis);
|
||||
const std::string label = std::string(PADGetAxisName(axes[i].padAxis)) + " " +
|
||||
(direction != nullptr ? direction : "");
|
||||
ImGui::SetNextItemWidth(220.0f);
|
||||
DrawKeyBinding(label.c_str(), key, RebindKind::KeyboardAxis, axes[i].padAxis);
|
||||
DrawKeyBinding(label.c_str(), axes[i].scancode, RebindKind::KeyboardAxis, axes[i].padAxis);
|
||||
ImGui::PopID();
|
||||
}
|
||||
return true;
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
// Controller settings menu: port selection, controller assignment and button mapping.
|
||||
int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
|
||||
auto* text = static_cast<std::string*>(data->UserData);
|
||||
@@ -663,7 +649,6 @@ int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
|
||||
}
|
||||
|
||||
void DrawExpressionSettings() {
|
||||
ImGui::SeparatorText("Expressions (Dolphin syntax)");
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 440.0f);
|
||||
ImGui::TextDisabled(
|
||||
"Optional. An expression overrides nothing: its result is combined with the "
|
||||
@@ -729,7 +714,7 @@ void DrawExpressionSettings() {
|
||||
}
|
||||
|
||||
void DrawRumbleSettings() {
|
||||
ImGui::SeparatorText("Vibration");
|
||||
ImGui::Separator();
|
||||
if (ImGui::Checkbox("Controller vibration", &g_rumbleEnabled)) {
|
||||
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
|
||||
RuntimeConfigFile::SetRumbleEnabled(g_rumbleEnabled);
|
||||
@@ -747,73 +732,15 @@ void DrawRumbleSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
void DrawControllerSettings() {
|
||||
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
const std::string label = "Port " + std::to_string(port + 1);
|
||||
ImGui::RadioButton(label.c_str(), &g_controllerPort, port);
|
||||
if (port + 1 < PAD_MAX_CONTROLLERS) {
|
||||
ImGui::SameLine();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
|
||||
if (DrawKeyboardSettings(selectedGamePort)) {
|
||||
return;
|
||||
}
|
||||
ImGui::Separator();
|
||||
const char* currentName = PADGetName(selectedGamePort);
|
||||
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
|
||||
if (ImGui::MenuItem("Unassign controller")) {
|
||||
PADClearPort(selectedGamePort);
|
||||
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||
}
|
||||
ImGui::Separator();
|
||||
controller_mapping_wizard::DrawSetupList();
|
||||
DrawWiiRemoteSettings(selectedGamePort);
|
||||
const uint32_t controllerCount = PADCount();
|
||||
if (controllerCount == 0) {
|
||||
ImGui::TextDisabled("No controller connected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Assign connected controller")) {
|
||||
for (uint32_t index = 0; index < controllerCount; ++index) {
|
||||
const char* name = PADGetNameForControllerIndex(index);
|
||||
std::string label = name != nullptr ? name : "Unknown controller";
|
||||
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
if (PADGetIndexForPort(port) == static_cast<s32>(index)) {
|
||||
label += " (Port " + std::to_string(port + 1) + ")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
if (ImGui::MenuItem(label.c_str())) {
|
||||
PADSetPortForIndex(index, selectedGamePort);
|
||||
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||
ApplyConfiguredMappings();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (WiiRemoteInput::IsRemoteChannel(selectedGamePort)) {
|
||||
ImGui::TextDisabled("The game reads this controller as a Wii Remote, so the button mapping does not apply.");
|
||||
DrawRumbleSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
void DrawGamepadMapping(uint32_t port) {
|
||||
uint32_t mappingCount = 0;
|
||||
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
|
||||
PADButtonMapping* mappings = PADGetButtonMappings(port, &mappingCount);
|
||||
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
|
||||
ImGui::TextDisabled("Assign a controller to edit its buttons");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t altMappingCount = 0;
|
||||
PADButtonMapping* altMappings =
|
||||
PADGetAltButtonMappings(static_cast<uint32_t>(g_controllerPort), &altMappingCount);
|
||||
PADButtonMapping* altMappings = PADGetAltButtonMappings(port, &altMappingCount);
|
||||
|
||||
const auto writeBinding = [](size_t index, uint32_t primaryNative, uint32_t altNative) {
|
||||
std::string value = NativeBindingConfig(primaryNative);
|
||||
@@ -824,18 +751,18 @@ void DrawControllerSettings() {
|
||||
RuntimeConfigFile::SetControllerButton(index, value);
|
||||
};
|
||||
|
||||
// Which rows show the second-binding combo without one being bound yet;
|
||||
// Which rows show the second-binding button without one being bound yet;
|
||||
// reset when the user switches ports so a stale "+" click doesn't linger.
|
||||
static std::array<bool, PAD_BUTTON_COUNT> altRowExpanded{};
|
||||
static int altRowExpandedPort = -1;
|
||||
if (altRowExpandedPort != g_controllerPort) {
|
||||
altRowExpandedPort = g_controllerPort;
|
||||
if (altRowExpandedPort != static_cast<int>(port)) {
|
||||
altRowExpandedPort = static_cast<int>(port);
|
||||
altRowExpanded.fill(false);
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Presets");
|
||||
if (ImGui::Button("GameCube")) {
|
||||
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
||||
ImGui::TextUnformatted("Layout:");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("GameCube")) {
|
||||
PADRestoreDefaultMapping(port);
|
||||
uint32_t restoredCount = 0;
|
||||
if (PADButtonMapping* restored = PADGetButtonMappings(port, &restoredCount)) {
|
||||
@@ -853,7 +780,6 @@ void DrawControllerSettings() {
|
||||
mappings = PADGetButtonMappings(port, &mappingCount);
|
||||
}
|
||||
const auto applyPreset = [&](const std::array<const char*, PAD_BUTTON_COUNT>& preset) {
|
||||
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
||||
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
||||
if (const NativeButtonItem* native = FindNativeButton(preset[i])) {
|
||||
PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton});
|
||||
@@ -866,22 +792,20 @@ void DrawControllerSettings() {
|
||||
PADSerializeMappings();
|
||||
mappings = PADGetButtonMappings(port, &mappingCount);
|
||||
};
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Classic Controller Pro")) {
|
||||
if (ImGui::SmallButton("Classic Controller Pro")) {
|
||||
applyPreset(kClassicProPreset);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("PlayStation")) {
|
||||
if (ImGui::SmallButton("PlayStation")) {
|
||||
applyPreset(kPlayStationPreset);
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Button mapping");
|
||||
ImGui::TextDisabled("LT / L2 = left trigger. RT / R2 = right trigger.");
|
||||
ImGui::TextDisabled("LB / L1 = left shoulder. RB / R1 = right shoulder.");
|
||||
ImGui::TextDisabled("Click a binding, then press an input. No input for 10 seconds clears it.");
|
||||
ImGui::TextDisabled("Click a binding, then press the input to use. Triggers: LT/L2, RT/R2.");
|
||||
const float bindingWidth = ImGui::CalcTextSize("Right shoulder (RB / R1)").x +
|
||||
ImGui::GetFrameHeight() + ImGui::GetStyle().FramePadding.x * 2.0f;
|
||||
const float bindingX = ImGui::GetCursorPosX() + ImGui::CalcTextSize("D-pad Right").x +
|
||||
ImGui::GetStyle().ItemSpacing.x * 2.0f;
|
||||
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
||||
auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) {
|
||||
return mapping.padButton == kControllerButtons[i].padButton;
|
||||
@@ -911,8 +835,8 @@ void DrawControllerSettings() {
|
||||
PADAxisButtonIdentity(mapping->nativeButton) | (static_cast<uint32_t>(threshold) << 8),
|
||||
mapping->padButton,
|
||||
};
|
||||
if (secondary) PADSetAltButtonMapping(selectedGamePort, updated);
|
||||
else PADSetButtonMapping(selectedGamePort, updated);
|
||||
if (secondary) PADSetAltButtonMapping(port, updated);
|
||||
else PADSetButtonMapping(port, updated);
|
||||
}
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||||
writeBinding(i, mappingIt->nativeButton,
|
||||
@@ -920,8 +844,10 @@ void DrawControllerSettings() {
|
||||
PADSerializeMappings();
|
||||
}
|
||||
};
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(kControllerButtons[i].label);
|
||||
ImGui::SameLine(bindingX);
|
||||
ImGui::BeginGroup();
|
||||
ImGui::SetNextItemWidth(bindingWidth);
|
||||
const std::string primaryCaption = std::string(current.label) + "##primary";
|
||||
if (ImGui::Button(primaryCaption.c_str(), ImVec2(bindingWidth, 0.0f))) {
|
||||
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label);
|
||||
@@ -945,7 +871,6 @@ void DrawControllerSettings() {
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None";
|
||||
ImGui::SetNextItemWidth(bindingWidth);
|
||||
const std::string altCaption = std::string(altLabel) + "##alt";
|
||||
if (ImGui::Button(altCaption.c_str(), ImVec2(bindingWidth, 0.0f))) {
|
||||
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true);
|
||||
@@ -954,11 +879,113 @@ void DrawControllerSettings() {
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(kControllerButtons[i].label);
|
||||
ImGui::PopID();
|
||||
}
|
||||
DrawExpressionSettings();
|
||||
}
|
||||
|
||||
std::string ControllerLabel(uint32_t index) {
|
||||
const char* name = PADGetNameForControllerIndex(index);
|
||||
return name != nullptr ? name : "Unknown controller";
|
||||
}
|
||||
|
||||
// One row per player: the device combo replaces separate assign/unassign/keyboard controls.
|
||||
void DrawPlayerRow(uint32_t port) {
|
||||
const bool keyboard = KeyboardActive(port);
|
||||
const int32_t assigned = PADGetIndexForPort(port);
|
||||
std::string preview = "None";
|
||||
if (keyboard) {
|
||||
preview = "Keyboard and mouse";
|
||||
} else if (assigned >= 0) {
|
||||
preview = ControllerLabel(static_cast<uint32_t>(assigned));
|
||||
} else if (const Wiimote::SlotInfo remote = Wiimote::Slot(port); remote.attached) {
|
||||
preview = WiiRemoteLabel(remote);
|
||||
}
|
||||
|
||||
ImGui::PushID(static_cast<int>(port));
|
||||
const std::string label = "Player " + std::to_string(port + 1);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
if (ImGui::RadioButton(label.c_str(), g_controllerPort == static_cast<int>(port))) {
|
||||
g_controllerPort = static_cast<int>(port);
|
||||
}
|
||||
ImGui::SameLine(ImGui::CalcTextSize("Player 0").x + ImGui::GetFrameHeight() + 16.0f);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (ImGui::BeginCombo("##device", preview.c_str())) {
|
||||
g_controllerPort = static_cast<int>(port);
|
||||
if (ImGui::Selectable("None", !keyboard && assigned < 0)) {
|
||||
SetKeyboardActive(port, false);
|
||||
PADClearPort(port);
|
||||
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||
}
|
||||
const uint32_t controllerCount = PADCount();
|
||||
for (uint32_t index = 0; index < controllerCount; ++index) {
|
||||
std::string item = ControllerLabel(index);
|
||||
for (uint32_t other = 0; other < PAD_MAX_CONTROLLERS; ++other) {
|
||||
if (other != port && PADGetIndexForPort(other) == static_cast<s32>(index)) {
|
||||
item += " (Player " + std::to_string(other + 1) + ")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
if (ImGui::Selectable(item.c_str(), !keyboard && assigned == static_cast<int32_t>(index))) {
|
||||
SetKeyboardActive(port, false);
|
||||
PADSetPortForIndex(index, port);
|
||||
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||
ApplyConfiguredMappings();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (ImGui::Selectable("Keyboard and mouse", keyboard)) {
|
||||
SetKeyboardActive(port, true);
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
void DrawControllerSettings() {
|
||||
ImGui::Dummy(ImVec2(kControllerMenuWidth, 0.0f));
|
||||
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
DrawPlayerRow(port);
|
||||
}
|
||||
bool anyRemote = false;
|
||||
for (uint32_t player = 0; player < PAD_MAX_CONTROLLERS; ++player) {
|
||||
anyRemote |= Wiimote::Slot(player).attached;
|
||||
}
|
||||
if (PADCount() == 0 && !anyRemote) {
|
||||
ImGui::TextDisabled("No controller detected. Plug one in, or pair a Wii Remote below.");
|
||||
}
|
||||
|
||||
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
||||
ImGui::SeparatorText(("Player " + std::to_string(port + 1)).c_str());
|
||||
const Wiimote::SlotInfo remote = Wiimote::Slot(port);
|
||||
if (remote.attached) {
|
||||
DrawWiiRemoteDetails(port, remote);
|
||||
if (KeyboardActive(port) || PADGetIndexForPort(port) >= 0) {
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
if (KeyboardActive(port)) {
|
||||
DrawKeyboardSettings(port);
|
||||
} else if (PADGetIndexForPort(port) < 0) {
|
||||
if (!remote.attached) {
|
||||
ImGui::TextDisabled("No device. Pick one in the list above.");
|
||||
}
|
||||
} else {
|
||||
ImGui::TextUnformatted("Ready to play.");
|
||||
if (ImGui::CollapsingHeader("Customize buttons")) {
|
||||
ImGui::Indent();
|
||||
DrawGamepadMapping(port);
|
||||
if (ImGui::TreeNode("Advanced: Dolphin expressions")) {
|
||||
DrawExpressionSettings();
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::Unindent();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
controller_mapping_wizard::DrawSetupList();
|
||||
DrawWiiRemoteSetup();
|
||||
DrawRumbleSettings();
|
||||
}
|
||||
|
||||
@@ -1109,6 +1136,7 @@ void DrawGraphicsSettings() {
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (frameInterpolationModeChanged) {
|
||||
GX_SYNC_INLINE();
|
||||
const uint32_t targetFps = kFrameInterpolationTargetFps[static_cast<size_t>(g_frameInterpolationMode)];
|
||||
aurora_set_frame_interpolation_fps(targetFps);
|
||||
RuntimeConfigFile::SetFrameInterpolationFps(targetFps);
|
||||
@@ -1152,10 +1180,12 @@ void DrawGraphicsSettings() {
|
||||
}
|
||||
#endif
|
||||
if (ImGui::Checkbox("Disable copy filter", &g_disableCopyFilter)) {
|
||||
GX_SYNC_INLINE();
|
||||
aurora_set_disable_copy_filter(g_disableCopyFilter);
|
||||
RuntimeConfigFile::SetDisableCopyFilter(g_disableCopyFilter);
|
||||
}
|
||||
if (ImGui::Checkbox("Skip draws while shaders compile", &g_skipUnreadyPipelines)) {
|
||||
GX_SYNC_INLINE();
|
||||
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
|
||||
RuntimeConfigFile::SetSkipUnreadyPipelines(g_skipUnreadyPipelines);
|
||||
}
|
||||
@@ -1359,6 +1389,7 @@ void DrawTopBar() {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, 0.0f), ImVec2(FLT_MAX, viewport->Size.y * 0.75f));
|
||||
if (ImGui::BeginMenu("Controller settings")) {
|
||||
DrawControllerSettings();
|
||||
// Nest capture under this menu so opening/closing the modal preserves
|
||||
@@ -1539,6 +1570,7 @@ void ReleaseControllers() noexcept {
|
||||
constexpr std::array<uint32_t, PAD_MAX_CONTROLLERS> stopAll{
|
||||
PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD};
|
||||
PADControlAllMotors(stopAll.data());
|
||||
Wiimote::Shutdown();
|
||||
// SDL hands LED and rumble reports to its own HIDAPI sender thread rather
|
||||
// than writing them here, so without this the process dies before the
|
||||
// controller ever receives them.
|
||||
@@ -1546,14 +1578,7 @@ void ReleaseControllers() noexcept {
|
||||
}
|
||||
|
||||
void Draw() noexcept {
|
||||
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
|
||||
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
|
||||
aurora_wait_for_frame_worker();
|
||||
// Also drive the Wii Remote rescan from here: PADRead runs it too, but this
|
||||
// runs once per presented frame whatever the game is doing (e.g. sitting in
|
||||
// its "communications interrupted" prompt without polling pads). Same guest
|
||||
// thread as PADRead, so no concurrent access to the scanner's state.
|
||||
WiiRemoteInput::Poll();
|
||||
// ImGui is only touched on this thread; the render worker replays a frozen copy.
|
||||
ApplyConfiguredMappings();
|
||||
PersistDisplayModeIfChanged();
|
||||
UpdateCursorAutoHide();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,260 @@
|
||||
// Wii Remote extension encryption (first-party key schedule). Ported from
|
||||
// Dolphin (Source/Core/Core/HW/WiimoteEmu/Encryption.cpp, GPL-2.0-or-later),
|
||||
// based on Hector Martin's reverse engineering.
|
||||
|
||||
#include "wiimote_ext_crypto.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
|
||||
namespace Wiimote::ExtCrypto {
|
||||
namespace Detail {
|
||||
|
||||
using SBox = std::array<uint8_t, 256>;
|
||||
|
||||
constexpr SBox kKeygenSbox = {
|
||||
0x70, 0x51, 0x03, 0x86, 0x40, 0x0d, 0x4f, 0xeb, 0x3e, 0xcc, 0xd1, 0x87, 0x35, 0xbd, 0xf5, 0x0b,
|
||||
0x5e, 0xd0, 0xf8, 0xf2, 0xd5, 0xe2, 0x6c, 0x31, 0x0c, 0xad, 0xfc, 0x21, 0xc3, 0x78, 0xc1, 0x06,
|
||||
0xc2, 0x4c, 0x55, 0xe6, 0x4a, 0x34, 0x48, 0x11, 0x1e, 0xda, 0xe7, 0x1a, 0x84, 0xa0, 0x96, 0xa7,
|
||||
0xe3, 0x7f, 0xaf, 0x63, 0x9c, 0xfa, 0x23, 0x5b, 0x79, 0xc8, 0x9e, 0xba, 0xb2, 0xc9, 0x22, 0x12,
|
||||
0x4b, 0xb3, 0xa1, 0xb6, 0x32, 0x49, 0xa2, 0xe1, 0x89, 0x39, 0x10, 0x66, 0xc5, 0x07, 0x8f, 0x54,
|
||||
0xea, 0x91, 0xca, 0x3f, 0xf9, 0x19, 0xf0, 0xd7, 0x46, 0xbc, 0x28, 0x1b, 0x61, 0xe8, 0x2f, 0x6a,
|
||||
0xae, 0x9d, 0xf6, 0x4e, 0x09, 0x14, 0x77, 0x4d, 0xdb, 0x1f, 0x2e, 0x7b, 0x7c, 0xf1, 0x43, 0xa3,
|
||||
0x00, 0xb8, 0x13, 0x8c, 0x85, 0xb9, 0x29, 0x75, 0x88, 0xfd, 0xd2, 0x56, 0x1c, 0x50, 0x97, 0x41,
|
||||
0xe5, 0x3b, 0x60, 0xb5, 0xc0, 0x64, 0xee, 0x98, 0xd6, 0x2d, 0x25, 0xa4, 0xaa, 0xcd, 0x7d, 0xa8,
|
||||
0x83, 0xc6, 0xab, 0xbe, 0x44, 0x99, 0x26, 0x3c, 0xce, 0x9f, 0xbf, 0xd3, 0xcb, 0x76, 0x7a, 0x7e,
|
||||
0x82, 0x01, 0x8a, 0x9a, 0x80, 0x1d, 0x0e, 0xb0, 0x5c, 0xd4, 0x38, 0x62, 0xf4, 0x30, 0xe0, 0x8e,
|
||||
0x53, 0xb7, 0x02, 0x57, 0xac, 0xa6, 0x52, 0x0a, 0x6d, 0x92, 0x65, 0x17, 0x24, 0x33, 0x45, 0x72,
|
||||
0x74, 0xb1, 0xb4, 0xf7, 0x5d, 0xed, 0x2c, 0xff, 0x47, 0x37, 0x5a, 0x90, 0xbb, 0xdf, 0x2a, 0x16,
|
||||
0x59, 0x95, 0xd9, 0xc4, 0x27, 0x67, 0x73, 0xc7, 0x68, 0xfe, 0xa5, 0xdd, 0x6b, 0x5f, 0x93, 0xd8,
|
||||
0xec, 0x05, 0x3a, 0x8d, 0x6e, 0xfb, 0x3d, 0xa9, 0x69, 0x36, 0xf3, 0x94, 0xde, 0xef, 0x15, 0x6f,
|
||||
0x8b, 0x9b, 0x08, 0x0f, 0xdc, 0x81, 0x18, 0x20, 0x04, 0xe4, 0x71, 0xcf, 0xe9, 0x2b, 0x42, 0x58,
|
||||
};
|
||||
|
||||
constexpr std::array<SBox, 8> kSboxes = {{
|
||||
{
|
||||
0x01, 0xa0, 0xa9, 0x62, 0xd6, 0x3f, 0x85, 0xa7, 0xb6, 0xd4, 0xfa, 0x15, 0x66, 0x17, 0x09,
|
||||
0xbd, 0x5d, 0x14, 0x34, 0x26, 0x59, 0x72, 0x91, 0x54, 0x06, 0x4f, 0xf8, 0xb0, 0x5b, 0x74,
|
||||
0x93, 0x99, 0x8c, 0xf2, 0x45, 0xcd, 0xea, 0x4e, 0xad, 0x10, 0x4a, 0xe5, 0xca, 0xee, 0xdf,
|
||||
0xc6, 0x6f, 0x9f, 0x88, 0x8e, 0x02, 0xcc, 0x08, 0xa8, 0x77, 0x94, 0x6d, 0x21, 0xb1, 0x28,
|
||||
0xe4, 0x39, 0x79, 0x96, 0x60, 0x71, 0x81, 0x16, 0x2e, 0xe6, 0x78, 0xb9, 0xc4, 0x46, 0x9a,
|
||||
0x42, 0xae, 0xb7, 0x7c, 0x43, 0xb3, 0x22, 0x1a, 0x86, 0xc2, 0x32, 0x3d, 0x2d, 0x9c, 0xd2,
|
||||
0x29, 0xe9, 0x63, 0x9b, 0xd1, 0x31, 0x38, 0x5e, 0x1e, 0x36, 0x41, 0xbb, 0x03, 0x18, 0x2b,
|
||||
0x3e, 0xbf, 0x68, 0x61, 0xfc, 0x52, 0xc0, 0xde, 0xe0, 0x0a, 0x58, 0x13, 0x5a, 0x00, 0xbe,
|
||||
0x1c, 0x90, 0x0e, 0x53, 0x12, 0xfd, 0xe2, 0x6e, 0xba, 0xce, 0x24, 0x27, 0x44, 0x7f, 0x87,
|
||||
0xa3, 0xa1, 0xd5, 0x50, 0x40, 0xe3, 0xf9, 0x83, 0xf7, 0xc7, 0xa2, 0x35, 0xc8, 0xdb, 0x19,
|
||||
0xab, 0x2f, 0x11, 0x25, 0xed, 0x33, 0x9e, 0x55, 0xe1, 0x48, 0xaf, 0x73, 0x84, 0xda, 0x2a,
|
||||
0xaa, 0x51, 0xeb, 0x9d, 0x95, 0xb2, 0xcb, 0xe7, 0x70, 0x80, 0xfe, 0x4c, 0x65, 0x04, 0xef,
|
||||
0xc5, 0xf1, 0xc3, 0x3a, 0xb4, 0xf5, 0x5f, 0x23, 0x89, 0xdd, 0x30, 0xa5, 0x8b, 0xd3, 0xf6,
|
||||
0xdc, 0x4d, 0x64, 0xd7, 0xf0, 0x8f, 0xec, 0x56, 0x37, 0x5c, 0xa4, 0x0d, 0x07, 0x76, 0x8a,
|
||||
0x2c, 0x0b, 0xb5, 0xd8, 0xc1, 0x1f, 0xe8, 0x3b, 0xf4, 0x4b, 0x1b, 0x47, 0x6c, 0x49, 0x67,
|
||||
0x7b, 0x92, 0xcf, 0x75, 0x7e, 0x20, 0xd9, 0x7d, 0x3c, 0x97, 0x7a, 0xd0, 0x05, 0x6b, 0x0f,
|
||||
0x1d, 0xfb, 0x82, 0x98, 0x57, 0x8d, 0xf3, 0x6a, 0xbc, 0xac, 0xc9, 0xa6, 0xff, 0xb8, 0x69,
|
||||
0x0c,
|
||||
},
|
||||
{
|
||||
0x4c, 0x4d, 0x72, 0x07, 0x5a, 0x49, 0x33, 0x8d, 0xa2, 0xab, 0x46, 0x3d, 0x63, 0x0d, 0xa0,
|
||||
0x97, 0xff, 0xf0, 0xf5, 0xfa, 0xc0, 0xe9, 0xdb, 0x62, 0xe4, 0xe1, 0x74, 0x43, 0xdc, 0x86,
|
||||
0x18, 0x29, 0x37, 0xf4, 0x06, 0xe2, 0xed, 0x6f, 0x90, 0x48, 0x1e, 0x2d, 0x1d, 0xea, 0x73,
|
||||
0x94, 0x54, 0xdf, 0x25, 0xf6, 0x47, 0x27, 0xd9, 0x11, 0x77, 0xc9, 0x84, 0x1c, 0x5b, 0x5c,
|
||||
0x51, 0x81, 0xa6, 0x22, 0x3e, 0x24, 0x96, 0xc8, 0x8a, 0xec, 0x82, 0x7c, 0x09, 0xb8, 0x45,
|
||||
0x4a, 0x57, 0xbb, 0x2f, 0x50, 0x75, 0x8e, 0x61, 0x70, 0x8c, 0x6c, 0xaf, 0xd0, 0xfd, 0xb4,
|
||||
0x1b, 0xae, 0xde, 0xfe, 0x3b, 0xb5, 0x36, 0xbd, 0x55, 0x01, 0x0e, 0x9c, 0x41, 0x56, 0x5f,
|
||||
0xb3, 0x26, 0x03, 0x83, 0xba, 0x13, 0x4b, 0xca, 0xc5, 0x0a, 0xf8, 0x60, 0xa5, 0xb9, 0xc7,
|
||||
0xc3, 0x98, 0x32, 0xfb, 0x12, 0xf9, 0xa7, 0x92, 0xaa, 0x68, 0xf3, 0x78, 0x7e, 0x05, 0x20,
|
||||
0x21, 0x02, 0xe8, 0xbf, 0xf2, 0xb0, 0x59, 0x8f, 0xd2, 0xcb, 0x87, 0x65, 0x15, 0xf1, 0x1a,
|
||||
0xb2, 0x30, 0xad, 0xee, 0x58, 0xa3, 0x8b, 0x66, 0x1f, 0x2c, 0xd7, 0x5d, 0x19, 0x85, 0xa8,
|
||||
0xe6, 0xd3, 0x6b, 0xa1, 0x0c, 0x91, 0x93, 0x6a, 0x5e, 0x0b, 0x79, 0xe3, 0xdd, 0x00, 0x4f,
|
||||
0x3c, 0x89, 0x6e, 0x71, 0x69, 0xa9, 0xac, 0x40, 0xe5, 0x99, 0x28, 0xc6, 0x31, 0x4e, 0x7a,
|
||||
0xcd, 0x08, 0x9e, 0x7d, 0xef, 0x17, 0xfc, 0x88, 0xd8, 0xa4, 0x6d, 0x44, 0x95, 0xd1, 0xb7,
|
||||
0xd4, 0x9b, 0xbe, 0x2a, 0x34, 0x64, 0x2b, 0xcf, 0x2e, 0xeb, 0x38, 0xce, 0x23, 0xe0, 0x3a,
|
||||
0x3f, 0xf7, 0x7b, 0x9f, 0x10, 0x53, 0xbc, 0x52, 0x67, 0x16, 0xe7, 0x80, 0x76, 0x04, 0xc4,
|
||||
0xb6, 0xc1, 0xc2, 0x7f, 0x9a, 0xda, 0xd5, 0x39, 0x42, 0x14, 0x9d, 0xb1, 0x0f, 0x35, 0xd6,
|
||||
0xcc,
|
||||
},
|
||||
{
|
||||
0xb9, 0xda, 0x38, 0x0c, 0xa2, 0x9c, 0x09, 0x1f, 0x06, 0xb1, 0xb6, 0xfd, 0x1a, 0x69, 0x23,
|
||||
0x30, 0xc4, 0xde, 0x01, 0xd1, 0xf4, 0x58, 0x29, 0x37, 0x1c, 0x7d, 0xd5, 0xbf, 0xff, 0xbd,
|
||||
0xc8, 0xc9, 0xcf, 0x65, 0xbe, 0x7b, 0x78, 0x97, 0x98, 0x67, 0x08, 0xb3, 0x26, 0x57, 0xf7,
|
||||
0xfa, 0x40, 0xad, 0x8e, 0x75, 0xa6, 0x7c, 0xdb, 0x91, 0x8b, 0x51, 0x99, 0xd4, 0x17, 0x7a,
|
||||
0x90, 0x8d, 0xce, 0x63, 0xcb, 0x4e, 0xa0, 0xab, 0x18, 0x3a, 0x5b, 0x50, 0x7f, 0x21, 0x74,
|
||||
0xc1, 0xbb, 0xb8, 0xb7, 0xba, 0x0b, 0x35, 0x95, 0x31, 0x59, 0x9a, 0x4d, 0x04, 0x07, 0x1e,
|
||||
0x5a, 0x76, 0x13, 0xf3, 0x71, 0x83, 0xf0, 0x86, 0x03, 0xa8, 0x39, 0x42, 0xaa, 0x28, 0xe6,
|
||||
0xe4, 0xd8, 0x5d, 0xd3, 0xd0, 0x6e, 0x6f, 0x96, 0xfb, 0x5e, 0xbc, 0x56, 0xc2, 0x5f, 0x85,
|
||||
0x9b, 0xe7, 0xaf, 0xd2, 0x3b, 0x84, 0x6a, 0xa7, 0x53, 0xc5, 0x44, 0x49, 0xa5, 0xf9, 0x36,
|
||||
0x72, 0x3d, 0x2c, 0xd9, 0x1b, 0xa1, 0xf5, 0x4f, 0x93, 0x9d, 0x68, 0x47, 0x41, 0x16, 0xca,
|
||||
0x2a, 0x4c, 0xa3, 0x87, 0xd6, 0xe5, 0x19, 0x2e, 0x77, 0x15, 0x6d, 0x70, 0xc0, 0xdf, 0xb2,
|
||||
0x00, 0x46, 0xed, 0xc6, 0x6c, 0x43, 0x60, 0x92, 0x2d, 0xa9, 0x22, 0x45, 0x8f, 0x34, 0x55,
|
||||
0xae, 0xa4, 0x0a, 0x66, 0x32, 0xe0, 0xdc, 0x02, 0xac, 0xe8, 0x20, 0x8c, 0x89, 0x62, 0x4a,
|
||||
0xfe, 0xee, 0xc3, 0xe3, 0x3c, 0xf1, 0x79, 0x05, 0xe9, 0xf6, 0x27, 0x33, 0xcc, 0xf2, 0x9e,
|
||||
0x11, 0x81, 0x7e, 0x80, 0x10, 0x8a, 0x82, 0x9f, 0x48, 0x0d, 0xd7, 0xb4, 0xfc, 0x2f, 0xb5,
|
||||
0xc7, 0xdd, 0x88, 0x14, 0x6b, 0x2b, 0x54, 0xea, 0x1d, 0x94, 0x5c, 0xb0, 0xef, 0x12, 0x24,
|
||||
0xcd, 0xeb, 0xe1, 0xe2, 0x64, 0x73, 0x3f, 0x0e, 0x52, 0x61, 0x25, 0x3e, 0xf8, 0x0f, 0x4b,
|
||||
0xec,
|
||||
},
|
||||
{
|
||||
0xc0, 0x00, 0x30, 0xf6, 0x02, 0x49, 0x3d, 0x10, 0x6e, 0x20, 0xc9, 0xa6, 0x2f, 0xfe, 0x2c,
|
||||
0x2b, 0x75, 0x2e, 0x45, 0x26, 0xab, 0x48, 0xa9, 0x80, 0xfc, 0x04, 0xcc, 0xd3, 0xb5, 0xba,
|
||||
0xa3, 0x38, 0x31, 0x7d, 0x01, 0xd9, 0xa7, 0x7b, 0x96, 0xb6, 0x63, 0x69, 0x4e, 0xf7, 0xde,
|
||||
0xe0, 0x78, 0xca, 0x50, 0xaa, 0x41, 0x91, 0x65, 0x88, 0xe4, 0x21, 0x85, 0xda, 0x3a, 0x27,
|
||||
0xbe, 0x1c, 0x3e, 0x42, 0x5e, 0x17, 0x52, 0x7f, 0x1f, 0x89, 0x24, 0x6f, 0x8f, 0x5c, 0x67,
|
||||
0x74, 0x0e, 0x12, 0x87, 0x8d, 0xe9, 0x34, 0xed, 0x73, 0xc4, 0xf8, 0x61, 0x5b, 0x05, 0xdf,
|
||||
0x59, 0x4c, 0x97, 0x79, 0x83, 0x18, 0xa4, 0x55, 0x95, 0xeb, 0xbd, 0x53, 0xf5, 0xf1, 0x57,
|
||||
0x66, 0x46, 0x9f, 0xb2, 0x81, 0x09, 0x51, 0x86, 0x22, 0x16, 0xdd, 0x23, 0x93, 0x76, 0x29,
|
||||
0xc2, 0xd7, 0x1d, 0xd4, 0xbf, 0x36, 0x3f, 0xea, 0x4b, 0x11, 0x32, 0xb9, 0x62, 0x54, 0x60,
|
||||
0xd6, 0x6d, 0x43, 0x9a, 0x0d, 0x92, 0x9c, 0xb0, 0xef, 0x58, 0x6c, 0x9d, 0x77, 0x2d, 0x70,
|
||||
0xfa, 0xf3, 0xb3, 0x0b, 0xe2, 0x40, 0x7e, 0xf4, 0x8a, 0xe5, 0x8c, 0x3c, 0x56, 0x71, 0xd1,
|
||||
0x64, 0xe1, 0x82, 0x0a, 0xcb, 0x13, 0x15, 0x90, 0xec, 0x03, 0x99, 0xaf, 0x14, 0x5d, 0x0f,
|
||||
0x33, 0x4a, 0x94, 0xa5, 0xa8, 0x35, 0x1b, 0xe3, 0x6a, 0xc6, 0x28, 0xff, 0x4d, 0xe7, 0x25,
|
||||
0x84, 0xac, 0x08, 0xae, 0xc5, 0xa2, 0x2a, 0xb8, 0x37, 0x0c, 0x7a, 0xa0, 0xc3, 0xce, 0xad,
|
||||
0x06, 0x1a, 0x9e, 0x8b, 0xfb, 0xd5, 0xf0, 0xc1, 0x1e, 0xd0, 0xb4, 0x9b, 0xb1, 0x44, 0xf2,
|
||||
0x47, 0xc7, 0x68, 0xcf, 0x72, 0xbb, 0x4f, 0x5a, 0xf9, 0xdc, 0x6b, 0xdb, 0xd2, 0xe8, 0x7c,
|
||||
0xc8, 0xee, 0x98, 0xa1, 0xe6, 0xd8, 0x39, 0x07, 0x5f, 0xfd, 0x8e, 0x19, 0xb7, 0x3b, 0xbc,
|
||||
0xcd,
|
||||
},
|
||||
{
|
||||
0x7c, 0xe3, 0x81, 0x73, 0xb2, 0x11, 0xbf, 0x6f, 0x20, 0x98, 0xfe, 0x75, 0x96, 0xef, 0x6c,
|
||||
0xda, 0x50, 0xe1, 0x09, 0x72, 0x54, 0x45, 0xba, 0x34, 0x80, 0x5b, 0xed, 0x3e, 0x53, 0x2c,
|
||||
0x87, 0xa4, 0x57, 0xf3, 0x33, 0x3f, 0x3c, 0xb7, 0x67, 0xb4, 0xa3, 0x25, 0x60, 0x4f, 0x07,
|
||||
0x6b, 0x1b, 0x47, 0x15, 0x0f, 0xe4, 0x0a, 0xea, 0xd1, 0x32, 0x78, 0x36, 0x49, 0x8d, 0x4b,
|
||||
0xd2, 0xbc, 0xa5, 0xdc, 0x1d, 0x0d, 0x4d, 0xcd, 0x9a, 0x82, 0x5f, 0xfc, 0x94, 0x65, 0xbe,
|
||||
0xe2, 0xf4, 0xc9, 0x1e, 0x44, 0xcb, 0x9e, 0x0c, 0x64, 0x71, 0x26, 0x63, 0xb3, 0x14, 0xe8,
|
||||
0x40, 0x70, 0x8a, 0x0e, 0x19, 0x42, 0x6d, 0xac, 0x88, 0x10, 0x5c, 0xdf, 0x41, 0xa9, 0xad,
|
||||
0xe5, 0xfb, 0x74, 0xcc, 0xd5, 0x06, 0x8e, 0x59, 0x86, 0xce, 0x1f, 0x3d, 0x76, 0xe0, 0x8f,
|
||||
0xb9, 0x77, 0x27, 0x7b, 0xa6, 0xd8, 0x29, 0xd3, 0xec, 0xb8, 0x13, 0xf7, 0xfa, 0xc3, 0x51,
|
||||
0x6a, 0xde, 0x4a, 0x5a, 0xeb, 0xc2, 0x8b, 0x23, 0x48, 0x92, 0xcf, 0x62, 0xa8, 0x99, 0xf8,
|
||||
0xd0, 0x2e, 0x85, 0x61, 0x43, 0xc8, 0xbd, 0xf0, 0x05, 0x93, 0xca, 0x4e, 0xf1, 0x7d, 0x30,
|
||||
0xfd, 0xc4, 0x69, 0x66, 0x2f, 0x08, 0xb1, 0x52, 0xf9, 0x21, 0xe6, 0x7a, 0x2b, 0xdd, 0x39,
|
||||
0x84, 0xff, 0xc0, 0x91, 0xd6, 0x37, 0xd4, 0x7f, 0x2d, 0x9b, 0x5d, 0xa1, 0x3b, 0x6e, 0xb5,
|
||||
0xc5, 0x46, 0x04, 0xf5, 0x90, 0xee, 0x7e, 0x83, 0x1c, 0x03, 0x56, 0xb6, 0xaa, 0x00, 0x17,
|
||||
0x01, 0x35, 0x55, 0x79, 0x0b, 0x12, 0xbb, 0x1a, 0x31, 0xe7, 0x02, 0x28, 0x16, 0xc1, 0xf6,
|
||||
0xa2, 0xdb, 0x18, 0x9c, 0x89, 0x68, 0x38, 0x97, 0xab, 0xc7, 0x2a, 0xd7, 0x3a, 0xf2, 0xc6,
|
||||
0x24, 0x4c, 0xb0, 0x58, 0xa0, 0x22, 0x5e, 0x9d, 0xd9, 0xa7, 0xe9, 0xae, 0xaf, 0x8c, 0x95,
|
||||
0x9f,
|
||||
},
|
||||
{
|
||||
0x28, 0xb7, 0x20, 0xd7, 0xb0, 0x30, 0xc3, 0x09, 0x19, 0xc0, 0x67, 0xd6, 0x00, 0x3c, 0x7e,
|
||||
0xe7, 0xe9, 0xf4, 0x08, 0x5a, 0xf8, 0xb8, 0x2e, 0x05, 0xa6, 0x25, 0x9e, 0x5c, 0xd8, 0x15,
|
||||
0x0d, 0xe1, 0xf6, 0x11, 0x54, 0x6b, 0xcd, 0x21, 0x46, 0x66, 0x5e, 0x84, 0xad, 0x06, 0x38,
|
||||
0x29, 0x44, 0xc5, 0xa2, 0xce, 0xf1, 0xaa, 0xc1, 0x40, 0x71, 0x86, 0xb5, 0xef, 0xfc, 0x36,
|
||||
0xa8, 0xcb, 0x0a, 0x48, 0x27, 0x45, 0x64, 0xa3, 0xaf, 0x8c, 0xb2, 0xc6, 0x9f, 0x07, 0x89,
|
||||
0xdc, 0x17, 0xd3, 0x49, 0x79, 0xfb, 0xfe, 0x1d, 0xd0, 0xb9, 0x88, 0x43, 0x52, 0xbc, 0x01,
|
||||
0x78, 0x2b, 0x7d, 0x94, 0xc7, 0x0e, 0xde, 0xa5, 0xd5, 0x9b, 0xcc, 0xf7, 0x61, 0x7a, 0xc2,
|
||||
0x74, 0x81, 0x39, 0x03, 0xab, 0x96, 0xa0, 0x37, 0xbd, 0x2d, 0x72, 0x75, 0x3f, 0xc9, 0xd4,
|
||||
0x8e, 0x6f, 0xf9, 0x8d, 0xed, 0x62, 0xdb, 0x1c, 0xdf, 0x04, 0xac, 0x1b, 0x6c, 0x14, 0x4b,
|
||||
0x63, 0xf0, 0xbf, 0xb4, 0x82, 0xec, 0x7b, 0x1a, 0x59, 0x92, 0xd2, 0x10, 0x60, 0xb6, 0x3d,
|
||||
0x5f, 0xe6, 0x80, 0x6e, 0x70, 0xc4, 0xf2, 0x35, 0xd9, 0x7c, 0xee, 0xe5, 0x41, 0xa4, 0x5b,
|
||||
0x50, 0xdd, 0xbb, 0x4c, 0xf3, 0x1f, 0x9d, 0x5d, 0x57, 0x55, 0x51, 0x97, 0xe3, 0x58, 0x42,
|
||||
0x4d, 0x9c, 0x73, 0xba, 0xc8, 0x77, 0x31, 0x69, 0x26, 0xae, 0xea, 0x8a, 0xda, 0x22, 0xb3,
|
||||
0x87, 0x56, 0xfa, 0x93, 0x0b, 0x34, 0x16, 0x33, 0xe8, 0xe4, 0x53, 0xbe, 0xa9, 0xb1, 0x3a,
|
||||
0x3e, 0xf5, 0x90, 0x6a, 0xcf, 0x3b, 0x12, 0xfd, 0x8f, 0x9a, 0xa7, 0x47, 0x91, 0x99, 0xeb,
|
||||
0x0f, 0x24, 0xff, 0x23, 0x18, 0x85, 0x4e, 0x7f, 0x0c, 0xe0, 0xa1, 0xe2, 0xd1, 0x2c, 0x2a,
|
||||
0x4a, 0x02, 0x4f, 0x1e, 0x95, 0x68, 0x8b, 0x98, 0x83, 0x6d, 0x76, 0xca, 0x65, 0x32, 0x13,
|
||||
0x2f,
|
||||
},
|
||||
{
|
||||
0xc3, 0x82, 0x9a, 0xa4, 0xba, 0x81, 0x60, 0x37, 0x34, 0x35, 0xfc, 0x80, 0xa8, 0x51, 0x65,
|
||||
0x67, 0xed, 0x30, 0x5f, 0x10, 0xd3, 0x4a, 0x27, 0x2f, 0x13, 0xb9, 0x2a, 0xd2, 0xcc, 0xe1,
|
||||
0xef, 0xae, 0xeb, 0xbe, 0xf4, 0xbd, 0xcf, 0x43, 0xb3, 0xc5, 0x88, 0x84, 0xb7, 0xdd, 0x39,
|
||||
0x40, 0xce, 0x48, 0x6d, 0x9b, 0x72, 0x61, 0x7e, 0xe7, 0xa1, 0x4e, 0x53, 0x2e, 0x77, 0x3b,
|
||||
0xe2, 0xc9, 0x36, 0x22, 0x1b, 0x6e, 0x73, 0xb1, 0x03, 0xb2, 0x4c, 0x87, 0xa9, 0xd4, 0x4d,
|
||||
0x0f, 0xd8, 0x15, 0x6c, 0xaa, 0x18, 0xf6, 0x49, 0x57, 0x5d, 0xfb, 0x7a, 0x14, 0x94, 0x63,
|
||||
0xa0, 0x11, 0xb0, 0x9e, 0xde, 0x05, 0x46, 0xc8, 0xee, 0x47, 0xdb, 0xdc, 0x24, 0x89, 0x9c,
|
||||
0x91, 0x97, 0x29, 0xe9, 0x7b, 0xc1, 0x07, 0x1e, 0xb8, 0xfd, 0xfe, 0xac, 0xc6, 0x62, 0x98,
|
||||
0x4f, 0xf1, 0x79, 0xe0, 0xe8, 0x6b, 0x78, 0x56, 0xb6, 0x8d, 0x04, 0x50, 0x86, 0xca, 0x6f,
|
||||
0x20, 0xe6, 0xea, 0xe5, 0x76, 0x17, 0x1c, 0x74, 0x7f, 0xbc, 0x0d, 0x2c, 0x85, 0xf7, 0x66,
|
||||
0x96, 0xe4, 0x8b, 0x75, 0x3f, 0x4b, 0xd9, 0x38, 0xaf, 0x7c, 0xda, 0x0b, 0x83, 0x2d, 0x31,
|
||||
0x32, 0xa2, 0xf5, 0x1d, 0x59, 0x41, 0x45, 0xbf, 0x3c, 0x1f, 0xf8, 0xf9, 0x8a, 0xd0, 0x16,
|
||||
0x25, 0x69, 0x12, 0x99, 0x9d, 0x21, 0x95, 0xab, 0x01, 0xa6, 0xd7, 0xb5, 0xc0, 0x7d, 0xff,
|
||||
0x58, 0x0e, 0x3a, 0x92, 0xd1, 0x55, 0xe3, 0x08, 0x9f, 0xd6, 0x3e, 0x52, 0x8e, 0xfa, 0xa3,
|
||||
0xc7, 0x02, 0xcd, 0xdf, 0x8f, 0x64, 0x19, 0x8c, 0xf3, 0xa7, 0x0c, 0x5e, 0x0a, 0x6a, 0x09,
|
||||
0xf0, 0x93, 0x5b, 0x42, 0xc2, 0x06, 0x23, 0xec, 0x71, 0xad, 0xb4, 0xcb, 0xbb, 0x70, 0x28,
|
||||
0xd5, 0x1a, 0x5c, 0x33, 0x68, 0x5a, 0x00, 0x44, 0x90, 0xa5, 0xc4, 0x26, 0x3d, 0x2b, 0xf2,
|
||||
0x54,
|
||||
},
|
||||
{
|
||||
0x96, 0xad, 0xda, 0x1f, 0xed, 0x33, 0xe1, 0x81, 0x69, 0x08, 0x0d, 0x0a, 0xdb, 0x35, 0x77,
|
||||
0x9a, 0x64, 0xd1, 0xfc, 0x78, 0xaa, 0x1b, 0xd0, 0x67, 0xa0, 0xdd, 0xfa, 0x6c, 0x63, 0x71,
|
||||
0x05, 0x84, 0x17, 0x6a, 0x89, 0x4f, 0x66, 0x7f, 0xc6, 0x50, 0x55, 0x92, 0x6f, 0xbd, 0xe7,
|
||||
0xd2, 0x40, 0x72, 0x8d, 0xbb, 0xec, 0x06, 0x42, 0x8a, 0xe4, 0x88, 0x9d, 0x7e, 0x7a, 0x82,
|
||||
0x27, 0x13, 0x41, 0x1a, 0xaf, 0xc8, 0xa4, 0x76, 0xb4, 0xc2, 0xfe, 0x6d, 0x1c, 0xd9, 0x61,
|
||||
0x30, 0xb3, 0x7c, 0xea, 0xf7, 0x29, 0x0f, 0xf2, 0x3b, 0x51, 0xc1, 0xde, 0x5f, 0xe5, 0x2a,
|
||||
0x2f, 0x99, 0x0b, 0x5d, 0xa3, 0x2b, 0x4a, 0xab, 0x95, 0xa5, 0xd3, 0x58, 0x56, 0xee, 0x28,
|
||||
0x31, 0x00, 0xcc, 0x15, 0x46, 0xca, 0xe6, 0x86, 0x38, 0x3c, 0x65, 0xf5, 0xe3, 0x9f, 0xd6,
|
||||
0x5b, 0x09, 0x49, 0x83, 0x70, 0x2d, 0x53, 0xa9, 0x7d, 0xe2, 0xc4, 0xac, 0x8e, 0x5e, 0xb8,
|
||||
0x25, 0xf4, 0xb9, 0x57, 0xf3, 0xf1, 0x68, 0x47, 0xb2, 0xa2, 0x59, 0x20, 0xce, 0x34, 0x79,
|
||||
0x5c, 0x90, 0x0e, 0x1e, 0xbe, 0xd5, 0x22, 0x23, 0xb1, 0xc9, 0x18, 0x62, 0x16, 0x2e, 0x91,
|
||||
0x3e, 0x07, 0x8f, 0xd8, 0x3f, 0x93, 0x3d, 0xd4, 0x9b, 0xdf, 0x85, 0x21, 0xfb, 0x11, 0x74,
|
||||
0x97, 0xc7, 0xd7, 0xdc, 0x4c, 0x19, 0x45, 0x98, 0xe9, 0x43, 0x02, 0x4b, 0xbc, 0xc3, 0x04,
|
||||
0x9c, 0x6b, 0xf0, 0x75, 0x52, 0xa7, 0x26, 0xf6, 0xc5, 0xba, 0xcf, 0xb0, 0xb7, 0xae, 0x5a,
|
||||
0xa1, 0xbf, 0x03, 0x8b, 0x80, 0x12, 0x6e, 0x0c, 0xeb, 0xf9, 0xc0, 0x44, 0x24, 0xef, 0x10,
|
||||
0xf8, 0xa8, 0x8c, 0xe8, 0x7b, 0xff, 0x9e, 0x2c, 0xcd, 0x60, 0x36, 0x87, 0xb5, 0x94, 0xa6,
|
||||
0x54, 0x73, 0x3a, 0x14, 0x4e, 0x01, 0x1d, 0xb6, 0xfd, 0x37, 0x48, 0x4d, 0x39, 0xcb, 0xe0,
|
||||
0x32,
|
||||
},
|
||||
}};
|
||||
|
||||
constexpr uint8_t kAnswers[7][6] = {
|
||||
{0xa8, 0x77, 0xa6, 0xe0, 0xf7, 0x43}, {0x5a, 0x35, 0x85, 0xe2, 0x72, 0x97},
|
||||
{0x8f, 0xb7, 0x1a, 0x62, 0x87, 0x38}, {0x0d, 0x67, 0xc7, 0xbe, 0x4f, 0x3e},
|
||||
{0x20, 0x76, 0x37, 0x8f, 0x68, 0xb7}, {0xa9, 0x26, 0x3f, 0x2b, 0x10, 0xe3},
|
||||
{0x30, 0x7e, 0x90, 0x0e, 0x85, 0x0a},
|
||||
};
|
||||
|
||||
using Rand = std::array<uint8_t, 10>;
|
||||
using Key = std::array<uint8_t, 6>;
|
||||
|
||||
Key KeyFor(const Rand& rand, size_t idx) {
|
||||
const uint8_t* ans = kAnswers[idx];
|
||||
Rand t{};
|
||||
for (size_t i = 0; i < t.size(); ++i) t[i] = kKeygenSbox[rand[i]];
|
||||
return {
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[0] ^ t[5], t[2]) - t[9]) ^ t[4]),
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[1] ^ t[1], t[0]) - t[5]) ^ t[7]),
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[2] ^ t[6], t[8]) - t[2]) ^ t[0]),
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[3] ^ t[4], t[7]) - t[3]) ^ t[2]),
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[4] ^ t[1], t[6]) - t[3]) ^ t[4]),
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[5] ^ t[7], t[8]) - t[5]) ^ t[9]),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
std::optional<Tables> FromKey(const std::array<uint8_t, 16>& keyData) {
|
||||
using namespace Detail;
|
||||
Rand rand{};
|
||||
std::copy_n(keyData.begin(), rand.size(), rand.rbegin());
|
||||
Key key{};
|
||||
std::copy_n(keyData.begin() + rand.size(), key.size(), key.rbegin());
|
||||
// The key was derived from "rand" and an index the extension is not told.
|
||||
for (size_t idx = 0; idx < 7; ++idx) {
|
||||
if (KeyFor(rand, idx) != key) continue;
|
||||
const SBox& a = kSboxes[idx];
|
||||
const SBox& b = kSboxes[idx + 1];
|
||||
Tables t;
|
||||
t.ft = {static_cast<uint8_t>(a[key[4]] ^ b[rand[3]]), static_cast<uint8_t>(a[key[2]] ^ b[rand[5]]),
|
||||
static_cast<uint8_t>(a[key[5]] ^ b[rand[7]]), static_cast<uint8_t>(a[key[0]] ^ b[rand[2]]),
|
||||
static_cast<uint8_t>(a[key[1]] ^ b[rand[4]]), static_cast<uint8_t>(a[key[3]] ^ b[rand[9]]),
|
||||
static_cast<uint8_t>(a[rand[0]] ^ b[rand[6]]), static_cast<uint8_t>(a[rand[1]] ^ b[rand[8]])};
|
||||
t.sb = {static_cast<uint8_t>(a[key[0]] ^ b[rand[1]]), static_cast<uint8_t>(a[key[5]] ^ b[rand[4]]),
|
||||
static_cast<uint8_t>(a[key[3]] ^ b[rand[0]]), static_cast<uint8_t>(a[key[2]] ^ b[rand[9]]),
|
||||
static_cast<uint8_t>(a[key[4]] ^ b[rand[7]]), static_cast<uint8_t>(a[key[1]] ^ b[rand[8]]),
|
||||
static_cast<uint8_t>(a[rand[3]] ^ b[rand[5]]), static_cast<uint8_t>(a[rand[2]] ^ b[rand[6]])};
|
||||
return t;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void Tables::Encrypt(uint8_t* data, uint32_t address, size_t length) const {
|
||||
for (size_t i = 0; i < length; ++i, ++address) {
|
||||
data[i] = static_cast<uint8_t>((data[i] - ft[address % 8]) ^ sb[address % 8]);
|
||||
}
|
||||
}
|
||||
|
||||
void Tables::Decrypt(uint8_t* data, uint32_t address, size_t length) const {
|
||||
for (size_t i = 0; i < length; ++i, ++address) {
|
||||
data[i] = static_cast<uint8_t>((data[i] ^ sb[address % 8]) + ft[address % 8]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Wiimote::ExtCrypto
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "wiimote_extension.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Wiimote {
|
||||
namespace ExtensionDetail {
|
||||
|
||||
constexpr uint8_t kReportStatus = 0x20;
|
||||
constexpr uint8_t kReportReadReply = 0x21;
|
||||
constexpr uint8_t kReportWriteMemory = 0x16;
|
||||
constexpr uint8_t kReportReadMemory = 0x17;
|
||||
constexpr uint32_t kExtensionRegisters = 0xA40000;
|
||||
constexpr uint8_t kRegisterCalibration = 0x20;
|
||||
constexpr uint8_t kRegisterKey = 0x40;
|
||||
constexpr uint8_t kRegisterEncryption = 0xF0;
|
||||
constexpr uint8_t kRegisterIdentifier = 0xFA;
|
||||
constexpr uint8_t kEncryptionOn = 0xAA;
|
||||
constexpr uint8_t kEncryptionOff = 0x55;
|
||||
|
||||
struct ExtensionSpan {
|
||||
size_t offset;
|
||||
size_t length;
|
||||
};
|
||||
|
||||
// Where the extension bytes sit in each data report.
|
||||
std::optional<ExtensionSpan> SpanFor(uint8_t id) {
|
||||
switch (id) {
|
||||
case 0x32: return ExtensionSpan{3, 8};
|
||||
case 0x34: return ExtensionSpan{3, 19};
|
||||
case 0x35: return ExtensionSpan{6, 16};
|
||||
case 0x36: return ExtensionSpan{13, 9};
|
||||
case 0x37: return ExtensionSpan{16, 6};
|
||||
case 0x3D: return ExtensionSpan{1, 21};
|
||||
default: return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool InExtensionSpace(uint32_t address) {
|
||||
return (address & 0xFF0000) == kExtensionRegisters;
|
||||
}
|
||||
|
||||
} // namespace ExtensionDetail
|
||||
|
||||
void ExtensionState::Reset() {
|
||||
m_type = Type::Unknown;
|
||||
m_format = 0;
|
||||
m_encrypted = false;
|
||||
m_tables.reset();
|
||||
m_haveCalibration = false;
|
||||
}
|
||||
|
||||
void ExtensionState::ObserveOutput(const uint8_t* report, size_t size) {
|
||||
using namespace ExtensionDetail;
|
||||
if (size < 6) return;
|
||||
const bool registerSpace = (report[1] & 0x04) != 0;
|
||||
const uint32_t address = (report[2] << 16) | (report[3] << 8) | report[4];
|
||||
if (report[0] == kReportReadMemory) {
|
||||
m_readAddress = registerSpace ? address : 0;
|
||||
return;
|
||||
}
|
||||
if (!registerSpace || report[0] != kReportWriteMemory || !InExtensionSpace(address)) return;
|
||||
const size_t length = std::min<size_t>(report[5], size - 6);
|
||||
bool keyChanged = false;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
const size_t reg = (address & 0xFF) + i;
|
||||
const uint8_t value = report[6 + i];
|
||||
if (reg >= kRegisterKey && reg < kRegisterKey + m_key.size()) {
|
||||
m_key[reg - kRegisterKey] = value;
|
||||
keyChanged = true;
|
||||
} else if (reg == kRegisterEncryption) {
|
||||
if (value == kEncryptionOn) m_encrypted = true;
|
||||
if (value == kEncryptionOff) Reset();
|
||||
}
|
||||
}
|
||||
if (keyChanged) {
|
||||
m_tables = ExtCrypto::FromKey(m_key);
|
||||
}
|
||||
}
|
||||
|
||||
void ExtensionState::ObserveInput(const uint8_t* report, size_t size) {
|
||||
using namespace ExtensionDetail;
|
||||
if (size >= 4 && report[0] == kReportStatus) {
|
||||
if ((report[3] & 0x02) == 0) Reset();
|
||||
return;
|
||||
}
|
||||
if (size < 7 || report[0] != kReportReadReply || (report[3] & 0x0F) != 0 || !InExtensionSpace(m_readAddress)) {
|
||||
return;
|
||||
}
|
||||
const size_t length = std::min<size_t>((report[3] >> 4) + 1, size - 6);
|
||||
const uint32_t first = ((report[4] << 8) | report[5]) & 0xFF;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
const uint32_t reg = first + static_cast<uint32_t>(i);
|
||||
uint8_t value = report[6 + i];
|
||||
if (reg >= kRegisterIdentifier) {
|
||||
// The identifier is read before the game turns encryption on.
|
||||
if (reg == 0xFE) m_format = value;
|
||||
if (reg == 0xFF) m_type = value == 0x00 ? Type::Nunchuk : value == 0x01 ? Type::Classic : Type::Unknown;
|
||||
} else if (reg >= kRegisterCalibration && reg < kRegisterCalibration + m_calibration.size()) {
|
||||
if (m_encrypted) {
|
||||
if (!m_tables) continue;
|
||||
m_tables->Decrypt(&value, reg, 1);
|
||||
}
|
||||
m_calibration[reg - kRegisterCalibration] = value;
|
||||
m_haveCalibration = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t ExtensionState::Calibration(size_t index, uint8_t fallback) const {
|
||||
return m_haveCalibration ? m_calibration[index] : fallback;
|
||||
}
|
||||
|
||||
void ExtensionState::Neutralize(std::vector<uint8_t>& report) const {
|
||||
using namespace ExtensionDetail;
|
||||
if (report.empty() || m_type == Type::Unknown || (m_encrypted && !m_tables)) return;
|
||||
const auto span = SpanFor(report[0]);
|
||||
if (!span || report.size() < span->offset + 6) return;
|
||||
uint8_t* data = report.data() + span->offset;
|
||||
std::array<uint8_t, 6> plain{};
|
||||
std::copy_n(data, plain.size(), plain.begin());
|
||||
if (m_encrypted) m_tables->Decrypt(plain.data(), 0, plain.size());
|
||||
|
||||
if (m_type == Type::Nunchuk) {
|
||||
plain[0] = Calibration(10, 0x80);
|
||||
plain[1] = Calibration(13, 0x80);
|
||||
plain[5] |= 0x03; // C and Z are active low
|
||||
} else if (m_format == 0x01) {
|
||||
const uint8_t lx = Calibration(2, 0x80) >> 2;
|
||||
const uint8_t ly = Calibration(5, 0x80) >> 2;
|
||||
const uint8_t rx = Calibration(8, 0x80) >> 3;
|
||||
const uint8_t ry = Calibration(11, 0x80) >> 3;
|
||||
plain[0] = static_cast<uint8_t>(((rx >> 3) & 0x03) << 6 | (lx & 0x3F));
|
||||
plain[1] = static_cast<uint8_t>(((rx >> 1) & 0x03) << 6 | (ly & 0x3F));
|
||||
plain[2] = static_cast<uint8_t>((rx & 0x01) << 7 | (ry & 0x1F));
|
||||
plain[3] = 0x00;
|
||||
plain[4] = 0xFF; // buttons are active low
|
||||
plain[5] = 0xFF;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_encrypted) m_tables->Encrypt(plain.data(), 0, plain.size());
|
||||
std::copy(plain.begin(), plain.end(), data);
|
||||
}
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -0,0 +1,828 @@
|
||||
#include "wiimote_host.h"
|
||||
|
||||
#include "input_bindings.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "wiimote_extension.h"
|
||||
#include "wiimote_platform.h"
|
||||
|
||||
#include <SDL3/SDL_hidapi.h>
|
||||
#include <SDL3/SDL_hints.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
extern "C" bool PAD_HLE_RumbleEnabled();
|
||||
|
||||
namespace Wiimote {
|
||||
namespace HostDetail {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Report = std::vector<uint8_t>;
|
||||
|
||||
constexpr uint16_t kNintendoVendor = 0x057E;
|
||||
constexpr uint16_t kProductRemote = 0x0306;
|
||||
constexpr uint16_t kProductRemotePlus = 0x0330;
|
||||
|
||||
constexpr auto kScanInterval = std::chrono::milliseconds(500);
|
||||
constexpr auto kEnumerateInterval = std::chrono::seconds(1);
|
||||
constexpr auto kProbeCooldown = std::chrono::seconds(3);
|
||||
constexpr auto kKeepAlive = std::chrono::seconds(1);
|
||||
constexpr auto kBlinkInterval = std::chrono::milliseconds(500);
|
||||
constexpr auto kPairingWindow = std::chrono::seconds(60);
|
||||
constexpr auto kGuestSearchTail = std::chrono::seconds(10);
|
||||
constexpr auto kAutoPairPause = std::chrono::seconds(4);
|
||||
constexpr int kReadTimeoutMs = 200;
|
||||
constexpr size_t kInputQueueLimit = 32;
|
||||
constexpr uint16_t kButtonMask = 0x9F1F;
|
||||
constexpr uint8_t kReportSpeakerData = 0x18;
|
||||
|
||||
enum class Phase : uint8_t {
|
||||
Waiting, // connected to the PC, offered to the game
|
||||
Linked, // the game's HID channels are open
|
||||
Asleep, // the game let it go and it could not be switched off
|
||||
PoweringOff,
|
||||
};
|
||||
|
||||
// Input report sizes including the id: Windows always reads the longest report.
|
||||
size_t ReportSize(uint8_t id, size_t read) {
|
||||
size_t size = 0;
|
||||
switch (id) {
|
||||
case 0x20: size = 7; break;
|
||||
case 0x21: size = 22; break;
|
||||
case 0x22: size = 5; break;
|
||||
case 0x30: size = 3; break;
|
||||
case 0x31: size = 6; break;
|
||||
case 0x32: size = 11; break;
|
||||
case 0x33: size = 18; break;
|
||||
case 0x34: case 0x35: case 0x36: case 0x37: case 0x3D: case 0x3E: case 0x3F: size = 22; break;
|
||||
default: return 0;
|
||||
}
|
||||
return size <= read ? size : 0;
|
||||
}
|
||||
|
||||
uint16_t Buttons(const Report& report) {
|
||||
if (report.size() < 3 || report[0] < 0x20 || report[0] == 0x3D) return 0;
|
||||
return static_cast<uint16_t>(((report[1] << 8) | report[2]) & kButtonMask);
|
||||
}
|
||||
|
||||
// The game's own battery bars (WPAD's status report thresholds).
|
||||
int BatteryBars(uint8_t level) {
|
||||
if (level < 0x03) return 0;
|
||||
if (level < 0x33) return 1;
|
||||
if (level < 0x44) return 2;
|
||||
if (level < 0x55) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
struct Remote {
|
||||
std::string path;
|
||||
std::string address;
|
||||
std::string name;
|
||||
bool plus = false;
|
||||
SDL_hid_device* device = nullptr;
|
||||
std::thread reader;
|
||||
std::thread writer;
|
||||
std::atomic<bool> alive{true};
|
||||
std::atomic<Phase> phase{Phase::Waiting};
|
||||
std::atomic<bool> wake{true};
|
||||
std::atomic<int> battery{-1};
|
||||
std::atomic<int> extension{-1}; // -1 none, else the extension id byte
|
||||
std::atomic<bool> extensionAttached{false};
|
||||
// Rumble bit of the game's last report, before the vibration setting.
|
||||
std::atomic<bool> gameRumble{false};
|
||||
|
||||
std::mutex writeMutex;
|
||||
std::condition_variable writeCv;
|
||||
std::deque<Report> writes;
|
||||
std::mutex inputMutex;
|
||||
std::deque<Report> input;
|
||||
// Emulation thread only.
|
||||
ExtensionState extensionState;
|
||||
};
|
||||
|
||||
class Manager final : public Bluetooth::RemoteHost {
|
||||
public:
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
bool IsAttached(size_t slot) const override;
|
||||
bool ConsumeWake(size_t slot) override;
|
||||
bool PopInputReport(size_t slot, std::vector<uint8_t>& report) override;
|
||||
void SendOutputReport(size_t slot, const uint8_t* report, size_t size) override;
|
||||
void OnLinkStateChanged(size_t slot, Bluetooth::LinkState state) override;
|
||||
void OnGuestDisconnect(size_t slot) override;
|
||||
void OnGuestInquiry(bool active) override;
|
||||
const char* DeviceName(size_t slot) const override;
|
||||
|
||||
SlotInfo Slot(size_t slot);
|
||||
PairingInfo Pairing();
|
||||
void StartPairing();
|
||||
void Disconnect(size_t slot);
|
||||
size_t Waiting();
|
||||
|
||||
private:
|
||||
std::shared_ptr<Remote> At(size_t slot) const;
|
||||
void ScanLoop();
|
||||
void DiscoveryLoop();
|
||||
void Enumerate();
|
||||
void Reap();
|
||||
void AssignWaiting();
|
||||
void Open(const SDL_hid_device_info& info);
|
||||
void SetPhase(const std::shared_ptr<Remote>& remote, Phase phase);
|
||||
void PowerOff(const std::shared_ptr<Remote>& remote);
|
||||
static void ReadLoop(std::shared_ptr<Remote> remote);
|
||||
static void WriteLoop(std::shared_ptr<Remote> remote);
|
||||
static void Queue(Remote& remote, Report report);
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::array<std::shared_ptr<Remote>, Bluetooth::kRemoteSlots> m_slots;
|
||||
std::array<Bluetooth::LinkState, Bluetooth::kRemoteSlots> m_links{};
|
||||
std::vector<std::shared_ptr<Remote>> m_waiting;
|
||||
std::map<std::string, size_t> m_lastSlot;
|
||||
std::map<std::string, Clock::time_point> m_cooldown;
|
||||
std::set<std::string> m_refusedLogged;
|
||||
// Remotes the OS knows but that were off when last seen, by path.
|
||||
std::map<std::string, std::string> m_dormant;
|
||||
Clock::time_point m_lastEnumerate{};
|
||||
uint32_t m_deviceChanges = 0;
|
||||
|
||||
std::atomic<bool> m_running{false};
|
||||
std::thread m_scanner;
|
||||
std::mutex m_scanMutex;
|
||||
std::condition_variable m_scanCv;
|
||||
|
||||
std::thread m_discovery;
|
||||
std::mutex m_discoveryMutex;
|
||||
std::condition_variable m_discoveryCv;
|
||||
std::atomic<bool> m_cancelDiscovery{false};
|
||||
std::atomic<bool> m_searching{false};
|
||||
std::atomic<bool> m_guestSearching{false};
|
||||
Clock::time_point m_guestSearchUntil{};
|
||||
Clock::time_point m_pairingUntil{};
|
||||
std::string m_pairingStatus;
|
||||
};
|
||||
|
||||
// Never destroyed: its threads may outlive static destruction at exit.
|
||||
Manager& TheManager() {
|
||||
static Manager* manager = new Manager();
|
||||
return *manager;
|
||||
}
|
||||
|
||||
std::shared_ptr<Remote> Manager::At(size_t slot) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return slot < m_slots.size() ? m_slots[slot] : nullptr;
|
||||
}
|
||||
|
||||
bool Manager::IsAttached(size_t slot) const {
|
||||
const auto remote = At(slot);
|
||||
if (!remote || !remote->alive) return false;
|
||||
const Phase phase = remote->phase;
|
||||
return phase == Phase::Waiting || phase == Phase::Linked;
|
||||
}
|
||||
|
||||
bool Manager::ConsumeWake(size_t slot) {
|
||||
const auto remote = At(slot);
|
||||
return remote && remote->phase == Phase::Waiting && remote->wake.exchange(false);
|
||||
}
|
||||
|
||||
bool Manager::PopInputReport(size_t slot, std::vector<uint8_t>& report) {
|
||||
const auto remote = At(slot);
|
||||
if (!remote) return false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote->inputMutex);
|
||||
if (remote->input.empty()) return false;
|
||||
report = std::move(remote->input.front());
|
||||
remote->input.pop_front();
|
||||
}
|
||||
remote->extensionState.ObserveInput(report.data(), report.size());
|
||||
if (InputBindings::InputBlocked()) {
|
||||
// The overlay owns input: the game sees the controller at rest.
|
||||
if (Buttons(report) != 0) {
|
||||
report[1] &= static_cast<uint8_t>(~(kButtonMask >> 8));
|
||||
report[2] &= static_cast<uint8_t>(~kButtonMask);
|
||||
}
|
||||
remote->extensionState.Neutralize(report);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Manager::Queue(Remote& remote, Report report) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote.writeMutex);
|
||||
remote.writes.push_back(std::move(report));
|
||||
}
|
||||
remote.writeCv.notify_one();
|
||||
}
|
||||
|
||||
void Manager::SendOutputReport(size_t slot, const uint8_t* report, size_t size) {
|
||||
const auto remote = At(slot);
|
||||
if (!remote || remote->phase != Phase::Linked || size == 0) return;
|
||||
remote->extensionState.ObserveOutput(report, size);
|
||||
const bool rumble = size >= 2 && (report[1] & 0x01) != 0;
|
||||
const bool rumbleChanged = size >= 2 && remote->gameRumble.exchange(rumble) != rumble;
|
||||
// PC Bluetooth cannot feed the speaker smoothly enough to sound right, but
|
||||
// WPAD may carry a rumble change only on speaker data.
|
||||
if (report[0] == kReportSpeakerData) {
|
||||
if (rumbleChanged) Queue(*remote, {0x10, static_cast<uint8_t>(rumble ? 0x01 : 0x00)});
|
||||
return;
|
||||
}
|
||||
Queue(*remote, Report(report, report + size));
|
||||
}
|
||||
|
||||
void Manager::OnLinkStateChanged(size_t slot, Bluetooth::LinkState state) {
|
||||
std::shared_ptr<Remote> remote;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_links[slot] = state;
|
||||
remote = m_slots[slot];
|
||||
}
|
||||
if (!remote) return;
|
||||
if (state == Bluetooth::LinkState::Ready) {
|
||||
if (remote->phase == Phase::PoweringOff) return;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote->inputMutex);
|
||||
remote->input.clear();
|
||||
}
|
||||
SetPhase(remote, Phase::Linked);
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << (slot + 1) << " connected to the game" << std::endl;
|
||||
} else if (state == Bluetooth::LinkState::Down && remote->phase == Phase::Linked) {
|
||||
// Lost without the game asking (e.g. the controller was reset): offer it again.
|
||||
SetPhase(remote, Phase::Waiting);
|
||||
remote->wake = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::OnGuestDisconnect(size_t slot) {
|
||||
const auto remote = At(slot);
|
||||
if (!remote) return;
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << (slot + 1) << " disconnected by the game" << std::endl;
|
||||
PowerOff(remote);
|
||||
}
|
||||
|
||||
void Manager::PowerOff(const std::shared_ptr<Remote>& remote) {
|
||||
remote->wake = false;
|
||||
if (Platform::Query().powerOff && !remote->address.empty()) {
|
||||
SetPhase(remote, Phase::PoweringOff);
|
||||
std::thread([remote] {
|
||||
const bool requested = Platform::PowerOff(remote->address);
|
||||
// The HID device goes away once the remote is off; if it does not,
|
||||
// keep it asleep so a button press brings it back.
|
||||
const auto deadline = Clock::now() + std::chrono::seconds(5);
|
||||
while (requested && remote->alive && Clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
if (remote->alive && remote->phase == Phase::PoweringOff) {
|
||||
remote->phase = Phase::Asleep;
|
||||
remote->writeCv.notify_one();
|
||||
}
|
||||
}).detach();
|
||||
} else {
|
||||
SetPhase(remote, Phase::Asleep);
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::SetPhase(const std::shared_ptr<Remote>& remote, Phase phase) {
|
||||
remote->phase = phase;
|
||||
remote->gameRumble = false;
|
||||
if (phase != Phase::Linked) {
|
||||
std::lock_guard<std::mutex> lock(remote->writeMutex);
|
||||
remote->writes.clear();
|
||||
}
|
||||
remote->writeCv.notify_one();
|
||||
}
|
||||
|
||||
void Manager::OnGuestInquiry(bool active) {
|
||||
if (active) {
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
m_guestSearching = true;
|
||||
} else {
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
m_guestSearching = false;
|
||||
m_guestSearchUntil = Clock::now() + kGuestSearchTail;
|
||||
}
|
||||
m_discoveryCv.notify_all();
|
||||
}
|
||||
|
||||
const char* Manager::DeviceName(size_t slot) const {
|
||||
const auto remote = At(slot);
|
||||
return remote && remote->plus ? "Nintendo RVL-CNT-01-TR" : "Nintendo RVL-CNT-01";
|
||||
}
|
||||
|
||||
void Manager::Start() {
|
||||
if (m_running.exchange(true)) return;
|
||||
if (SDL_hid_init() != 0) {
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remotes: HID access unavailable: " << SDL_GetError() << std::endl;
|
||||
m_running = false;
|
||||
return;
|
||||
}
|
||||
m_scanner = std::thread([this] { ScanLoop(); });
|
||||
if (Platform::Query().pairing) {
|
||||
m_discovery = std::thread([this] { DiscoveryLoop(); });
|
||||
}
|
||||
const std::string notice = Platform::Notice();
|
||||
if (!notice.empty()) {
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remotes: " << notice << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::Stop() {
|
||||
if (!m_running.exchange(false)) return;
|
||||
m_cancelDiscovery = true;
|
||||
m_scanCv.notify_all();
|
||||
m_discoveryCv.notify_all();
|
||||
if (m_scanner.joinable()) m_scanner.join();
|
||||
// An inquiry in progress cannot be interrupted; let it finish on its own.
|
||||
if (m_discovery.joinable()) m_discovery.detach();
|
||||
|
||||
std::vector<std::shared_ptr<Remote>> remotes;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto& remote : m_slots) {
|
||||
if (remote) remotes.push_back(remote);
|
||||
remote.reset();
|
||||
}
|
||||
remotes.insert(remotes.end(), m_waiting.begin(), m_waiting.end());
|
||||
m_waiting.clear();
|
||||
}
|
||||
for (const auto& remote : remotes) {
|
||||
remote->alive = false;
|
||||
remote->writeCv.notify_all();
|
||||
}
|
||||
// A console switches its remotes off when it shuts down.
|
||||
for (const auto& remote : remotes) {
|
||||
if (remote->writer.joinable()) remote->writer.join();
|
||||
if (remote->reader.joinable()) remote->reader.join();
|
||||
if (remote->device != nullptr) {
|
||||
const uint8_t rumbleOff[] = {0x10, 0x00};
|
||||
const uint8_t ledsOff[] = {0x11, 0x00};
|
||||
SDL_hid_write(remote->device, rumbleOff, sizeof(rumbleOff));
|
||||
SDL_hid_write(remote->device, ledsOff, sizeof(ledsOff));
|
||||
SDL_hid_close(remote->device);
|
||||
remote->device = nullptr;
|
||||
}
|
||||
if (Platform::Query().powerOff && !remote->address.empty()) {
|
||||
Platform::PowerOff(remote->address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::ScanLoop() {
|
||||
while (m_running) {
|
||||
Reap();
|
||||
Enumerate();
|
||||
AssignWaiting();
|
||||
std::unique_lock<std::mutex> lock(m_scanMutex);
|
||||
m_scanCv.wait_for(lock, kScanInterval, [this] { return !m_running; });
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::Reap() {
|
||||
std::vector<std::shared_ptr<Remote>> dead;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto& remote : m_slots) {
|
||||
if (remote && !remote->alive) {
|
||||
dead.push_back(remote);
|
||||
remote.reset();
|
||||
}
|
||||
}
|
||||
for (auto it = m_waiting.begin(); it != m_waiting.end();) {
|
||||
if (!(*it)->alive) {
|
||||
dead.push_back(*it);
|
||||
it = m_waiting.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& remote : dead) {
|
||||
remote->writeCv.notify_all();
|
||||
if (remote->writer.joinable()) remote->writer.join();
|
||||
if (remote->reader.joinable()) remote->reader.join();
|
||||
if (remote->device != nullptr) SDL_hid_close(remote->device);
|
||||
remote->device = nullptr;
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << (remote->address.empty() ? remote->path : remote->address)
|
||||
<< " disconnected from the PC" << std::endl;
|
||||
m_cooldown[remote->path] = Clock::now() + std::chrono::seconds(1);
|
||||
if (!remote->address.empty()) m_dormant[remote->path] = remote->address;
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::Enumerate() {
|
||||
const Uint32 changes = SDL_hid_device_change_count();
|
||||
const auto now = Clock::now();
|
||||
bool fullSlots = false;
|
||||
bool anyRemote = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
fullSlots = std::all_of(m_slots.begin(), m_slots.end(), [](const auto& remote) { return remote != nullptr; });
|
||||
anyRemote = !m_waiting.empty() ||
|
||||
std::any_of(m_slots.begin(), m_slots.end(), [](const auto& remote) { return remote != nullptr; });
|
||||
}
|
||||
bool pairing = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
pairing = m_guestSearching || now < m_guestSearchUntil || now < m_pairingUntil;
|
||||
}
|
||||
if (fullSlots) return;
|
||||
if (changes == m_deviceChanges) {
|
||||
if (now - m_lastEnumerate < kEnumerateInterval) return;
|
||||
// Once a remote is connected, skip HID enumeration unless pairing or a known remote came back.
|
||||
if (anyRemote && !pairing &&
|
||||
std::none_of(m_dormant.begin(), m_dormant.end(),
|
||||
[](const auto& entry) { return Platform::MaybeConnected(entry.second); })) {
|
||||
m_lastEnumerate = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_deviceChanges = changes;
|
||||
m_lastEnumerate = now;
|
||||
|
||||
std::set<std::string> open;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (const auto& remote : m_slots) {
|
||||
if (remote) open.insert(remote->path);
|
||||
}
|
||||
for (const auto& remote : m_waiting) open.insert(remote->path);
|
||||
}
|
||||
std::set<std::string> listed;
|
||||
SDL_hid_device_info* list = SDL_hid_enumerate(kNintendoVendor, 0);
|
||||
for (const SDL_hid_device_info* info = list; info != nullptr; info = info->next) {
|
||||
if (info->path == nullptr || (info->product_id != kProductRemote && info->product_id != kProductRemotePlus)) {
|
||||
continue;
|
||||
}
|
||||
listed.insert(info->path);
|
||||
if (open.count(info->path) != 0) continue;
|
||||
const auto cooldown = m_cooldown.find(info->path);
|
||||
if (cooldown != m_cooldown.end() && now < cooldown->second) continue;
|
||||
open.insert(info->path);
|
||||
Open(*info);
|
||||
}
|
||||
SDL_hid_free_enumeration(list);
|
||||
for (auto it = m_dormant.begin(); it != m_dormant.end();) {
|
||||
it = listed.count(it->first) != 0 ? std::next(it) : m_dormant.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::Open(const SDL_hid_device_info& info) {
|
||||
const std::string path = info.path;
|
||||
const std::string address = Platform::AddressFor(info);
|
||||
const std::string name = Platform::NameFor(address, info);
|
||||
// Wii U Pro Controllers share the Wii Remote Plus id; balance boards are not used by the game.
|
||||
if (name.find("-UC") != std::string::npos || name.find("RVL-WBC") != std::string::npos) {
|
||||
m_cooldown[path] = Clock::time_point::max();
|
||||
return;
|
||||
}
|
||||
if (!address.empty() && !Platform::MaybeConnected(address)) {
|
||||
// Known to the OS but switched off: check again soon, a button press
|
||||
// reconnects it.
|
||||
m_cooldown[path] = Clock::now() + kScanInterval;
|
||||
m_dormant[path] = address;
|
||||
return;
|
||||
}
|
||||
if (!Platform::Usable(info)) {
|
||||
if (m_refusedLogged.insert(path).second) {
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << address << " is held by the system driver: " << Platform::Notice()
|
||||
<< std::endl;
|
||||
}
|
||||
m_cooldown[path] = Clock::now() + std::chrono::seconds(10);
|
||||
return;
|
||||
}
|
||||
SDL_hid_device* device = SDL_hid_open_path(path.c_str());
|
||||
if (device == nullptr) {
|
||||
m_cooldown[path] = Clock::now() + kProbeCooldown;
|
||||
return;
|
||||
}
|
||||
// A remote the OS remembers but that is switched off still has a HID device;
|
||||
// only a write tells.
|
||||
const uint8_t status[] = {0x15, 0x00};
|
||||
if (SDL_hid_write(device, status, sizeof(status)) < 0) {
|
||||
SDL_hid_close(device);
|
||||
m_cooldown[path] = Clock::now() + kProbeCooldown;
|
||||
if (!address.empty()) m_dormant[path] = address;
|
||||
return;
|
||||
}
|
||||
m_dormant.erase(path);
|
||||
|
||||
auto remote = std::make_shared<Remote>();
|
||||
remote->path = path;
|
||||
remote->address = address;
|
||||
remote->name = name;
|
||||
remote->plus = info.product_id == kProductRemotePlus;
|
||||
remote->device = device;
|
||||
remote->reader = std::thread(ReadLoop, remote);
|
||||
remote->writer = std::thread(WriteLoop, remote);
|
||||
RT_LOG(RT_TAG_HLE) << (remote->plus ? "Wii Remote Plus " : "Wii Remote ")
|
||||
<< (address.empty() ? path : address) << " connected to the PC" << std::endl;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_waiting.push_back(remote);
|
||||
}
|
||||
AssignWaiting();
|
||||
}
|
||||
|
||||
void Manager::AssignWaiting() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_waiting.begin(); it != m_waiting.end();) {
|
||||
const std::shared_ptr<Remote>& remote = *it;
|
||||
const std::string key = remote->address.empty() ? remote->path : remote->address;
|
||||
size_t slot = Bluetooth::kRemoteSlots;
|
||||
const auto last = m_lastSlot.find(key);
|
||||
// A slot is free once the game has seen its previous remote go.
|
||||
const auto free = [this](size_t i) { return !m_slots[i] && m_links[i] == Bluetooth::LinkState::Down; };
|
||||
if (last != m_lastSlot.end() && free(last->second)) {
|
||||
slot = last->second;
|
||||
} else {
|
||||
for (size_t i = 0; i < m_slots.size(); ++i) {
|
||||
if (free(i)) {
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (slot == Bluetooth::kRemoteSlots) return;
|
||||
m_slots[slot] = remote;
|
||||
m_lastSlot[key] = slot;
|
||||
it = m_waiting.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::ReadLoop(std::shared_ptr<Remote> remote) {
|
||||
std::array<uint8_t, 32> buffer{};
|
||||
while (remote->alive) {
|
||||
const int read = SDL_hid_read_timeout(remote->device, buffer.data(), buffer.size(), kReadTimeoutMs);
|
||||
if (read < 0) {
|
||||
remote->alive = false;
|
||||
remote->writeCv.notify_all();
|
||||
break;
|
||||
}
|
||||
if (read == 0) continue;
|
||||
const size_t size = ReportSize(buffer[0], static_cast<size_t>(read));
|
||||
if (size == 0) continue;
|
||||
Report report(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(size));
|
||||
|
||||
if (report[0] == 0x20) {
|
||||
remote->battery = BatteryBars(report[6]);
|
||||
const bool attached = (report[3] & 0x02) != 0;
|
||||
remote->extensionAttached = attached;
|
||||
if (!attached) remote->extension = -1;
|
||||
} else if (report[0] == 0x21 && (report[3] & 0x0F) == 0 && report[4] == 0x00 && report[5] == 0xFA &&
|
||||
(report[3] >> 4) >= 5) {
|
||||
// The game reading the extension id at 0xA400FA (before encryption).
|
||||
remote->extension = report[11];
|
||||
}
|
||||
|
||||
const Phase phase = remote->phase;
|
||||
if (phase == Phase::Linked) {
|
||||
std::lock_guard<std::mutex> lock(remote->inputMutex);
|
||||
if (remote->input.size() >= kInputQueueLimit) {
|
||||
auto stale = std::find_if(remote->input.begin(), remote->input.end(),
|
||||
[](const Report& r) { return r[0] >= 0x30; });
|
||||
remote->input.erase(stale != remote->input.end() ? stale : remote->input.begin());
|
||||
}
|
||||
remote->input.push_back(std::move(report));
|
||||
} else if (Buttons(report) != 0) {
|
||||
if (phase == Phase::Asleep) {
|
||||
remote->phase = Phase::Waiting;
|
||||
remote->writeCv.notify_one();
|
||||
}
|
||||
if (phase == Phase::Asleep || phase == Phase::Waiting) {
|
||||
remote->wake = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::WriteLoop(std::shared_ptr<Remote> remote) {
|
||||
auto lastWrite = Clock::now();
|
||||
auto lastBlink = Clock::time_point{};
|
||||
bool blinkOn = false;
|
||||
bool appliedRumble = false;
|
||||
Phase lastPhase = Phase::Linked; // forces the quiet setup on the first pass
|
||||
const auto write = [&](const Report& report) {
|
||||
if (SDL_hid_write(remote->device, report.data(), report.size()) < 0) {
|
||||
remote->alive = false;
|
||||
return false;
|
||||
}
|
||||
lastWrite = Clock::now();
|
||||
return true;
|
||||
};
|
||||
while (remote->alive) {
|
||||
std::deque<Report> batch;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(remote->writeMutex);
|
||||
remote->writeCv.wait_for(lock, std::chrono::milliseconds(100),
|
||||
[&] { return !remote->alive || !remote->writes.empty(); });
|
||||
batch.swap(remote->writes);
|
||||
}
|
||||
const bool rumbleAllowed = PAD_HLE_RumbleEnabled();
|
||||
const Phase phase = remote->phase;
|
||||
// Queued just before the game let go of the remote.
|
||||
if (phase != Phase::Linked) batch.clear();
|
||||
for (Report& report : batch) {
|
||||
if (report.size() >= 2 && !rumbleAllowed) report[1] &= 0xFE;
|
||||
if (!write(report)) break;
|
||||
if (report.size() >= 2) appliedRumble = (report[1] & 0x01) != 0;
|
||||
}
|
||||
if (!remote->alive) break;
|
||||
|
||||
const auto now = Clock::now();
|
||||
if (phase != lastPhase && phase != Phase::Linked && phase != Phase::PoweringOff) {
|
||||
// Stop streaming and rumbling while the game does not own the remote.
|
||||
if (!write({0x12, 0x00, 0x30}) || !write({0x11, 0x00})) break;
|
||||
appliedRumble = false;
|
||||
blinkOn = false;
|
||||
lastBlink = now;
|
||||
}
|
||||
lastPhase = phase;
|
||||
if (phase == Phase::Waiting && now - lastBlink >= kBlinkInterval) {
|
||||
// Like a remote looking for its console.
|
||||
blinkOn = !blinkOn;
|
||||
lastBlink = now;
|
||||
if (!write({0x11, static_cast<uint8_t>(blinkOn ? 0xF0 : 0x00)})) break;
|
||||
} else if (phase == Phase::Linked) {
|
||||
const bool wanted = remote->gameRumble && rumbleAllowed;
|
||||
if (wanted != appliedRumble || now - lastWrite >= kKeepAlive) {
|
||||
// Also how Windows notices a remote that went away.
|
||||
if (!write({0x10, static_cast<uint8_t>(wanted ? 0x01 : 0x00)})) break;
|
||||
appliedRumble = wanted;
|
||||
}
|
||||
} else if (phase == Phase::Asleep && now - lastWrite >= kKeepAlive) {
|
||||
if (!write({0x10, 0x00})) break;
|
||||
}
|
||||
}
|
||||
remote->writeCv.notify_all();
|
||||
}
|
||||
|
||||
void Manager::DiscoveryLoop() {
|
||||
while (m_running) {
|
||||
bool search = false;
|
||||
bool automatic = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_discoveryMutex);
|
||||
const auto now = Clock::now();
|
||||
bool anyRemote = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> slotLock(m_mutex);
|
||||
for (const auto& remote : m_slots) anyRemote |= remote != nullptr;
|
||||
anyRemote |= !m_waiting.empty();
|
||||
}
|
||||
const bool requested = m_guestSearching || now < m_guestSearchUntil || now < m_pairingUntil;
|
||||
automatic = !requested && !anyRemote && RuntimeConfigFile::WiiAutoPairEnabled();
|
||||
search = requested || automatic;
|
||||
if (!search) {
|
||||
m_searching = false;
|
||||
m_discoveryCv.wait_for(lock, std::chrono::seconds(1));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
m_searching = true;
|
||||
std::string status;
|
||||
const int connected = Platform::DiscoverAndPair(m_cancelDiscovery, status);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
if (!status.empty()) m_pairingStatus = status;
|
||||
if (connected > 0) {
|
||||
m_scanCv.notify_all();
|
||||
}
|
||||
}
|
||||
if (automatic && connected == 0) {
|
||||
std::unique_lock<std::mutex> lock(m_discoveryMutex);
|
||||
m_discoveryCv.wait_for(lock, kAutoPairPause, [this] {
|
||||
return !m_running || m_guestSearching || Clock::now() < m_pairingUntil;
|
||||
});
|
||||
}
|
||||
}
|
||||
m_searching = false;
|
||||
}
|
||||
|
||||
SlotInfo Manager::Slot(size_t slot) {
|
||||
SlotInfo info;
|
||||
std::shared_ptr<Remote> remote;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (slot >= m_slots.size()) return info;
|
||||
remote = m_slots[slot];
|
||||
info.link = m_links[slot];
|
||||
}
|
||||
if (!remote || !remote->alive || remote->phase == Phase::PoweringOff) {
|
||||
info.link = Bluetooth::LinkState::Down;
|
||||
return info;
|
||||
}
|
||||
info.attached = true;
|
||||
info.asleep = remote->phase == Phase::Asleep;
|
||||
info.model = remote->plus ? "Wii Remote Plus" : "Wii Remote";
|
||||
if (remote->extensionAttached) {
|
||||
switch (remote->extension.load()) {
|
||||
case 0x00: info.extension = "Nunchuk"; break;
|
||||
case 0x01: info.extension = "Classic Controller"; break;
|
||||
case 0x03: info.extension = "Classic Controller Pro"; break;
|
||||
default: info.extension = "Extension"; break;
|
||||
}
|
||||
}
|
||||
info.battery = remote->battery;
|
||||
return info;
|
||||
}
|
||||
|
||||
PairingInfo Manager::Pairing() {
|
||||
PairingInfo info;
|
||||
info.supported = Platform::Query().pairing && m_running;
|
||||
info.searching = m_searching;
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
info.status = m_pairingStatus;
|
||||
return info;
|
||||
}
|
||||
|
||||
void Manager::StartPairing() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
m_pairingUntil = Clock::now() + kPairingWindow;
|
||||
m_pairingStatus = "Press the red SYNC button (or 1 and 2) on the Wii Remote.";
|
||||
}
|
||||
m_discoveryCv.notify_all();
|
||||
}
|
||||
|
||||
void Manager::Disconnect(size_t slot) {
|
||||
if (const auto remote = At(slot)) {
|
||||
PowerOff(remote);
|
||||
}
|
||||
}
|
||||
|
||||
size_t Manager::Waiting() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_waiting.size();
|
||||
}
|
||||
|
||||
bool g_enabled = false;
|
||||
|
||||
} // namespace HostDetail
|
||||
|
||||
using namespace HostDetail;
|
||||
|
||||
void ConfigureSdlHints(bool enabled) {
|
||||
g_enabled = enabled;
|
||||
// The remotes belong to the game's Bluetooth stack: SDL must not open them
|
||||
// as gamepads (its own Wii driver, or the Linux kernel driver's evdev nodes).
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, enabled ? "0" : "1");
|
||||
if (enabled) {
|
||||
std::string ignored = "0x057e/0x0306,0x057e/0x0330";
|
||||
if (const char* existing = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES); existing && *existing) {
|
||||
ignored = std::string(existing) + "," + ignored;
|
||||
}
|
||||
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, ignored.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Initialize() {
|
||||
if (g_enabled) {
|
||||
TheManager().Start();
|
||||
}
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
TheManager().Stop();
|
||||
}
|
||||
|
||||
Bluetooth::RemoteHost& Host() {
|
||||
return TheManager();
|
||||
}
|
||||
|
||||
SlotInfo Slot(size_t slot) {
|
||||
return TheManager().Slot(slot);
|
||||
}
|
||||
|
||||
PairingInfo Pairing() {
|
||||
return TheManager().Pairing();
|
||||
}
|
||||
|
||||
void StartPairing() {
|
||||
TheManager().StartPairing();
|
||||
}
|
||||
|
||||
void Disconnect(size_t slot) {
|
||||
TheManager().Disconnect(slot);
|
||||
}
|
||||
|
||||
std::string PlatformNotice() {
|
||||
return g_enabled ? Platform::Notice() : std::string();
|
||||
}
|
||||
|
||||
size_t WaitingRemotes() {
|
||||
return TheManager().Waiting();
|
||||
}
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -0,0 +1,496 @@
|
||||
#if !defined(_WIN32)
|
||||
|
||||
#include "wiimote_platform.h"
|
||||
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <SDL3/SDL_hidapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <filesystem>
|
||||
#endif
|
||||
|
||||
namespace Wiimote::Platform {
|
||||
namespace Detail {
|
||||
|
||||
std::string Narrow(const wchar_t* text) {
|
||||
std::string out;
|
||||
if (text != nullptr) {
|
||||
for (const wchar_t* c = text; *c != L'\0'; ++c) out += static_cast<char>(*c < 128 ? *c : '?');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string HexDigits(const std::string& text) {
|
||||
std::string hex;
|
||||
for (const char c : text) {
|
||||
if (std::isxdigit(static_cast<unsigned char>(c))) hex += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
std::mutex g_noticeMutex;
|
||||
std::string g_notice;
|
||||
|
||||
void SetNotice(const std::string& notice) {
|
||||
std::lock_guard<std::mutex> lock(g_noticeMutex);
|
||||
g_notice = notice;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
|
||||
// BlueZ over the system bus. libdbus-1 is loaded at run time so a system
|
||||
// without it still runs (pairing then falls back to the desktop's tools).
|
||||
extern "C" {
|
||||
struct BzConnection;
|
||||
struct BzMessage;
|
||||
struct BzError {
|
||||
const char* name;
|
||||
const char* message;
|
||||
unsigned int dummy1 : 1;
|
||||
unsigned int dummy2 : 1;
|
||||
unsigned int dummy3 : 1;
|
||||
unsigned int dummy4 : 1;
|
||||
unsigned int dummy5 : 1;
|
||||
void* padding1;
|
||||
};
|
||||
// Mirrors DBusMessageIter.
|
||||
struct BzIter {
|
||||
void* dummy1;
|
||||
void* dummy2;
|
||||
uint32_t dummy3;
|
||||
int dummy4;
|
||||
int dummy5;
|
||||
int dummy6;
|
||||
int dummy7;
|
||||
int dummy8;
|
||||
int dummy9;
|
||||
int dummy10;
|
||||
int dummy11;
|
||||
int pad1;
|
||||
int pad2;
|
||||
void* pad3;
|
||||
};
|
||||
}
|
||||
|
||||
constexpr int kBusSystem = 1;
|
||||
constexpr int kTypeInvalid = 0;
|
||||
constexpr int kTypeString = 's';
|
||||
constexpr int kTypeObjectPath = 'o';
|
||||
constexpr int kTypeBoolean = 'b';
|
||||
constexpr int kTypeInt16 = 'n';
|
||||
constexpr int kTypeArray = 'a';
|
||||
constexpr int kTypeVariant = 'v';
|
||||
constexpr int kTypeDictEntry = 'e';
|
||||
constexpr int kShortTimeoutMs = 2000;
|
||||
constexpr int kPairTimeoutMs = 30000;
|
||||
constexpr auto kInquiryTime = std::chrono::milliseconds(3840);
|
||||
|
||||
struct Device {
|
||||
std::string path;
|
||||
std::string address;
|
||||
std::string name;
|
||||
bool paired = false;
|
||||
bool connected = false;
|
||||
bool trusted = false;
|
||||
bool discovered = false; // has an RSSI: answered the current inquiry
|
||||
};
|
||||
|
||||
class BlueZ {
|
||||
public:
|
||||
bool Load() {
|
||||
if (m_loaded) return m_conn != nullptr;
|
||||
m_loaded = true;
|
||||
m_handle = dlopen("libdbus-1.so.3", RTLD_NOW | RTLD_LOCAL);
|
||||
if (m_handle == nullptr) return false;
|
||||
const bool ok = Bind(bus_get_private, "dbus_bus_get_private") &&
|
||||
Bind(connection_set_exit_on_disconnect, "dbus_connection_set_exit_on_disconnect") &&
|
||||
Bind(error_init, "dbus_error_init") && Bind(error_free, "dbus_error_free") &&
|
||||
Bind(message_new_method_call, "dbus_message_new_method_call") &&
|
||||
Bind(message_append_args, "dbus_message_append_args") &&
|
||||
Bind(send_with_reply_and_block, "dbus_connection_send_with_reply_and_block") &&
|
||||
Bind(message_unref, "dbus_message_unref") &&
|
||||
Bind(message_iter_init, "dbus_message_iter_init") &&
|
||||
Bind(message_iter_init_append, "dbus_message_iter_init_append") &&
|
||||
Bind(message_iter_open_container, "dbus_message_iter_open_container") &&
|
||||
Bind(message_iter_close_container, "dbus_message_iter_close_container") &&
|
||||
Bind(message_iter_append_basic, "dbus_message_iter_append_basic") &&
|
||||
Bind(message_iter_next, "dbus_message_iter_next") &&
|
||||
Bind(message_iter_recurse, "dbus_message_iter_recurse") &&
|
||||
Bind(message_iter_get_arg_type, "dbus_message_iter_get_arg_type") &&
|
||||
Bind(message_iter_get_basic, "dbus_message_iter_get_basic");
|
||||
if (!ok) return false;
|
||||
BzError error;
|
||||
error_init(&error);
|
||||
m_conn = bus_get_private(kBusSystem, &error);
|
||||
error_free(&error);
|
||||
if (m_conn != nullptr) connection_set_exit_on_disconnect(m_conn, 0);
|
||||
return m_conn != nullptr;
|
||||
}
|
||||
|
||||
// org.freedesktop.DBus.ObjectManager.GetManagedObjects on org.bluez.
|
||||
bool Objects(std::string& adapter, std::vector<Device>& devices) {
|
||||
BzMessage* reply = Call("/", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", kShortTimeoutMs);
|
||||
if (reply == nullptr) return false;
|
||||
BzIter root;
|
||||
BzIter objects;
|
||||
if (message_iter_init(reply, &root) && message_iter_get_arg_type(&root) == kTypeArray) {
|
||||
message_iter_recurse(&root, &objects);
|
||||
while (message_iter_get_arg_type(&objects) == kTypeDictEntry) {
|
||||
BzIter entry;
|
||||
message_iter_recurse(&objects, &entry);
|
||||
const char* path = nullptr;
|
||||
message_iter_get_basic(&entry, &path);
|
||||
message_iter_next(&entry);
|
||||
ReadObject(path != nullptr ? path : "", entry, adapter, devices);
|
||||
message_iter_next(&objects);
|
||||
}
|
||||
}
|
||||
message_unref(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Simple(const std::string& path, const char* interface, const char* method, int timeoutMs,
|
||||
std::string* error = nullptr) {
|
||||
BzMessage* reply = Call(path.c_str(), interface, method, timeoutMs, nullptr, error);
|
||||
if (reply == nullptr) return false;
|
||||
message_unref(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RemoveDevice(const std::string& adapter, const std::string& device) {
|
||||
const char* path = device.c_str();
|
||||
BzMessage* reply = Call(adapter.c_str(), "org.bluez.Adapter1", "RemoveDevice", kShortTimeoutMs,
|
||||
[&](BzMessage* message) {
|
||||
return message_append_args(message, kTypeObjectPath, &path, kTypeInvalid) != 0;
|
||||
});
|
||||
if (reply == nullptr) return false;
|
||||
message_unref(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SetTrusted(const std::string& device) {
|
||||
BzMessage* reply = Call(device.c_str(), "org.freedesktop.DBus.Properties", "Set", kShortTimeoutMs,
|
||||
[&](BzMessage* message) {
|
||||
const char* interface = "org.bluez.Device1";
|
||||
const char* property = "Trusted";
|
||||
if (!message_append_args(message, kTypeString, &interface, kTypeString, &property,
|
||||
kTypeInvalid)) {
|
||||
return false;
|
||||
}
|
||||
BzIter args;
|
||||
BzIter variant;
|
||||
message_iter_init_append(message, &args);
|
||||
if (!message_iter_open_container(&args, kTypeVariant, "b", &variant)) return false;
|
||||
const uint32_t value = 1;
|
||||
message_iter_append_basic(&variant, kTypeBoolean, &value);
|
||||
return message_iter_close_container(&args, &variant) != 0;
|
||||
});
|
||||
if (reply == nullptr) return false;
|
||||
message_unref(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Fn>
|
||||
bool Bind(Fn& fn, const char* symbol) {
|
||||
fn = reinterpret_cast<Fn>(dlsym(m_handle, symbol));
|
||||
return fn != nullptr;
|
||||
}
|
||||
|
||||
template <typename Append = std::nullptr_t>
|
||||
BzMessage* Call(const char* path, const char* interface, const char* method, int timeoutMs,
|
||||
Append append = nullptr, std::string* errorText = nullptr) {
|
||||
BzMessage* message = message_new_method_call("org.bluez", path, interface, method);
|
||||
if (message == nullptr) return nullptr;
|
||||
if constexpr (!std::is_same_v<Append, std::nullptr_t>) {
|
||||
if (!append(message)) {
|
||||
message_unref(message);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
BzError error;
|
||||
error_init(&error);
|
||||
BzMessage* reply = send_with_reply_and_block(m_conn, message, timeoutMs, &error);
|
||||
message_unref(message);
|
||||
if (reply == nullptr && errorText != nullptr && error.name != nullptr) {
|
||||
*errorText = error.name;
|
||||
}
|
||||
error_free(&error);
|
||||
return reply;
|
||||
}
|
||||
|
||||
// One object's a{sa{sv}} interface map.
|
||||
void ReadObject(const std::string& path, BzIter& interfaces, std::string& adapter, std::vector<Device>& devices) {
|
||||
if (message_iter_get_arg_type(&interfaces) != kTypeArray) return;
|
||||
BzIter list;
|
||||
message_iter_recurse(&interfaces, &list);
|
||||
while (message_iter_get_arg_type(&list) == kTypeDictEntry) {
|
||||
BzIter entry;
|
||||
message_iter_recurse(&list, &entry);
|
||||
const char* interface = nullptr;
|
||||
message_iter_get_basic(&entry, &interface);
|
||||
message_iter_next(&entry);
|
||||
const std::string name = interface != nullptr ? interface : "";
|
||||
if (name == "org.bluez.Adapter1" && adapter.empty()) {
|
||||
adapter = path;
|
||||
} else if (name == "org.bluez.Device1") {
|
||||
Device device;
|
||||
device.path = path;
|
||||
ReadDevice(entry, device);
|
||||
devices.push_back(device);
|
||||
}
|
||||
message_iter_next(&list);
|
||||
}
|
||||
}
|
||||
|
||||
void ReadDevice(BzIter& properties, Device& device) {
|
||||
if (message_iter_get_arg_type(&properties) != kTypeArray) return;
|
||||
BzIter list;
|
||||
message_iter_recurse(&properties, &list);
|
||||
while (message_iter_get_arg_type(&list) == kTypeDictEntry) {
|
||||
BzIter entry;
|
||||
message_iter_recurse(&list, &entry);
|
||||
const char* key = nullptr;
|
||||
message_iter_get_basic(&entry, &key);
|
||||
message_iter_next(&entry);
|
||||
BzIter value;
|
||||
message_iter_recurse(&entry, &value);
|
||||
const std::string name = key != nullptr ? key : "";
|
||||
const int type = message_iter_get_arg_type(&value);
|
||||
if (type == kTypeString && (name == "Address" || name == "Name")) {
|
||||
const char* text = nullptr;
|
||||
message_iter_get_basic(&value, &text);
|
||||
(name == "Address" ? device.address : device.name) = text != nullptr ? text : "";
|
||||
} else if (type == kTypeBoolean) {
|
||||
uint32_t flag = 0;
|
||||
message_iter_get_basic(&value, &flag);
|
||||
if (name == "Paired") device.paired = flag != 0;
|
||||
if (name == "Connected") device.connected = flag != 0;
|
||||
if (name == "Trusted") device.trusted = flag != 0;
|
||||
} else if (type == kTypeInt16 && name == "RSSI") {
|
||||
device.discovered = true;
|
||||
}
|
||||
message_iter_next(&list);
|
||||
}
|
||||
}
|
||||
|
||||
bool m_loaded = false;
|
||||
void* m_handle = nullptr;
|
||||
BzConnection* m_conn = nullptr;
|
||||
BzConnection* (*bus_get_private)(int, BzError*) = nullptr;
|
||||
void (*connection_set_exit_on_disconnect)(BzConnection*, unsigned int) = nullptr;
|
||||
void (*error_init)(BzError*) = nullptr;
|
||||
void (*error_free)(BzError*) = nullptr;
|
||||
BzMessage* (*message_new_method_call)(const char*, const char*, const char*, const char*) = nullptr;
|
||||
unsigned int (*message_append_args)(BzMessage*, int, ...) = nullptr;
|
||||
BzMessage* (*send_with_reply_and_block)(BzConnection*, BzMessage*, int, BzError*) = nullptr;
|
||||
void (*message_unref)(BzMessage*) = nullptr;
|
||||
unsigned int (*message_iter_init)(BzMessage*, BzIter*) = nullptr;
|
||||
void (*message_iter_init_append)(BzMessage*, BzIter*) = nullptr;
|
||||
unsigned int (*message_iter_open_container)(BzIter*, int, const char*, BzIter*) = nullptr;
|
||||
unsigned int (*message_iter_close_container)(BzIter*, BzIter*) = nullptr;
|
||||
unsigned int (*message_iter_append_basic)(BzIter*, int, const void*) = nullptr;
|
||||
unsigned int (*message_iter_next)(BzIter*) = nullptr;
|
||||
void (*message_iter_recurse)(BzIter*, BzIter*) = nullptr;
|
||||
int (*message_iter_get_arg_type)(BzIter*) = nullptr;
|
||||
void (*message_iter_get_basic)(BzIter*, void*) = nullptr;
|
||||
};
|
||||
|
||||
// Every call happens on the discovery thread or the power-off worker.
|
||||
std::timed_mutex g_busMutex;
|
||||
|
||||
BlueZ* Bus() {
|
||||
static BlueZ bus;
|
||||
static const bool ok = bus.Load();
|
||||
return ok ? &bus : nullptr;
|
||||
}
|
||||
|
||||
// A remote that was just paired needs a few seconds to connect; until then it
|
||||
// must not be taken for a stale pairing.
|
||||
std::vector<std::pair<std::string, std::chrono::steady_clock::time_point>> g_recent;
|
||||
|
||||
bool RecentlyPaired(const std::string& path) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
g_recent.erase(std::remove_if(g_recent.begin(), g_recent.end(),
|
||||
[&](const auto& entry) { return now - entry.second > std::chrono::seconds(30); }),
|
||||
g_recent.end());
|
||||
return std::any_of(g_recent.begin(), g_recent.end(), [&](const auto& entry) { return entry.first == path; });
|
||||
}
|
||||
|
||||
bool IsRemoteName(const std::string& name) {
|
||||
return name.rfind("Nintendo RVL-CNT-01", 0) == 0 && name.find("-UC") == std::string::npos;
|
||||
}
|
||||
|
||||
std::string DriverOf(const std::string& hidrawPath) {
|
||||
const std::filesystem::path node(hidrawPath);
|
||||
std::error_code error;
|
||||
const auto driver =
|
||||
std::filesystem::read_symlink(std::filesystem::path("/sys/class/hidraw") / node.filename() / "device" / "driver",
|
||||
error);
|
||||
return error ? std::string() : driver.filename().string();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
using namespace Detail;
|
||||
|
||||
Capabilities Query() {
|
||||
#if defined(__linux__)
|
||||
// Cached: adapters rarely change and this is asked often.
|
||||
static std::mutex mutex;
|
||||
static Capabilities cached;
|
||||
static std::chrono::steady_clock::time_point checked{};
|
||||
std::lock_guard<std::mutex> cacheLock(mutex);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (checked == std::chrono::steady_clock::time_point{} || now - checked > std::chrono::seconds(10)) {
|
||||
// A search holds the bus for seconds; keep the cached answer rather than wait.
|
||||
std::unique_lock<std::timed_mutex> lock(g_busMutex, std::try_to_lock);
|
||||
if (!lock.owns_lock()) return cached;
|
||||
checked = now;
|
||||
std::string adapter;
|
||||
std::vector<Device> devices;
|
||||
BlueZ* bus = Bus();
|
||||
cached.pairing = cached.powerOff = bus != nullptr && bus->Objects(adapter, devices) && !adapter.empty();
|
||||
}
|
||||
return cached;
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
int DiscoverAndPair(const std::atomic<bool>& cancel, std::string& status) {
|
||||
#if defined(__linux__)
|
||||
std::lock_guard<std::timed_mutex> lock(g_busMutex);
|
||||
BlueZ* bus = Bus();
|
||||
std::string adapter;
|
||||
std::vector<Device> devices;
|
||||
if (bus == nullptr || !bus->Objects(adapter, devices) || adapter.empty()) {
|
||||
status = "No Bluetooth adapter found.";
|
||||
return 0;
|
||||
}
|
||||
bus->Simple(adapter, "org.bluez.Adapter1", "StartDiscovery", kShortTimeoutMs);
|
||||
int connected = 0;
|
||||
const auto deadline = std::chrono::steady_clock::now() + kInquiryTime;
|
||||
std::vector<std::string> handled;
|
||||
while (!cancel && std::chrono::steady_clock::now() < deadline) {
|
||||
devices.clear();
|
||||
bus->Objects(adapter, devices);
|
||||
for (const Device& device : devices) {
|
||||
if (!IsRemoteName(device.name) || device.connected || !device.discovered) continue;
|
||||
if (std::find(handled.begin(), handled.end(), device.path) != handled.end() || RecentlyPaired(device.path)) {
|
||||
continue;
|
||||
}
|
||||
if (device.paired) {
|
||||
// In pairing mode while paired: the remote forgot this PC.
|
||||
bus->RemoveDevice(adapter, device.path);
|
||||
continue;
|
||||
}
|
||||
handled.push_back(device.path);
|
||||
// BlueZ's wiimote plugin answers the PIN (SYNC or 1+2).
|
||||
std::string error;
|
||||
if (bus->Simple(device.path, "org.bluez.Device1", "Pair", kPairTimeoutMs, &error)) {
|
||||
bus->SetTrusted(device.path);
|
||||
g_recent.emplace_back(device.path, std::chrono::steady_clock::now());
|
||||
if (bus->Simple(device.path, "org.bluez.Device1", "Connect", kPairTimeoutMs, &error)) {
|
||||
++connected;
|
||||
status = "Wii Remote paired. It reconnects with any button from now on.";
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << device.address << " paired" << std::endl;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
status = "Could not pair the Wii Remote. Press its red SYNC button and try again.";
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << device.address << ": pairing failed (" << error << ")" << std::endl;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
}
|
||||
bus->Simple(adapter, "org.bluez.Adapter1", "StopDiscovery", kShortTimeoutMs);
|
||||
return connected;
|
||||
#else
|
||||
(void)cancel;
|
||||
status = "Pair the Wii Remote in the system's Bluetooth settings.";
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PowerOff(const std::string& address) {
|
||||
#if defined(__linux__)
|
||||
std::unique_lock<std::timed_mutex> lock(g_busMutex, std::chrono::milliseconds(kShortTimeoutMs));
|
||||
if (!lock.owns_lock()) return false;
|
||||
BlueZ* bus = Bus();
|
||||
std::string adapter;
|
||||
std::vector<Device> devices;
|
||||
if (bus == nullptr || !bus->Objects(adapter, devices)) return false;
|
||||
for (const Device& device : devices) {
|
||||
if (HexDigits(device.address) == address) {
|
||||
return bus->Simple(device.path, "org.bluez.Device1", "Disconnect", kShortTimeoutMs);
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)address;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string AddressFor(const SDL_hid_device_info& info) {
|
||||
const std::string hex = HexDigits(Narrow(info.serial_number));
|
||||
return hex.size() == 12 && hex != "000000000000" ? hex : std::string();
|
||||
}
|
||||
|
||||
std::string NameFor(const std::string& address, const SDL_hid_device_info& info) {
|
||||
(void)address;
|
||||
return Narrow(info.product_string);
|
||||
}
|
||||
|
||||
bool MaybeConnected(const std::string& address) {
|
||||
(void)address;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Usable(const SDL_hid_device_info& info) {
|
||||
#if defined(__linux__)
|
||||
if (info.path == nullptr) return false;
|
||||
if (DriverOf(info.path) == "wiimote") {
|
||||
SetNotice("The Linux hid-wiimote driver holds the Wii Remote, so the game cannot use it. Run "
|
||||
"\"sudo modprobe -r hid_wiimote\" and add \"blacklist hid_wiimote\" to /etc/modprobe.d/wiimote.conf.");
|
||||
return false;
|
||||
}
|
||||
if (access(info.path, R_OK | W_OK) != 0) {
|
||||
SetNotice("No permission to open the Wii Remote. Add the udev rule "
|
||||
"KERNEL==\"hidraw*\", KERNELS==\"0005:057E:*\", MODE=\"0666\" and reconnect it.");
|
||||
return false;
|
||||
}
|
||||
SetNotice({});
|
||||
#else
|
||||
(void)info;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Notice() {
|
||||
std::lock_guard<std::mutex> lock(g_noticeMutex);
|
||||
#if defined(__APPLE__)
|
||||
if (g_notice.empty()) return "Pair Wii Remotes in System Settings > Bluetooth, then press a button on them.";
|
||||
#endif
|
||||
return g_notice;
|
||||
}
|
||||
|
||||
} // namespace Wiimote::Platform
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,300 @@
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include "wiimote_platform.h"
|
||||
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <SDL3/SDL_hidapi.h>
|
||||
|
||||
#include <windows.h>
|
||||
#include <bluetoothapis.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Wiimote::Platform {
|
||||
namespace Detail {
|
||||
|
||||
// bthioctl.h IOCTL_BTH_DISCONNECT_DEVICE: CTL_CODE(FILE_DEVICE_BLUETOOTH, 0x03, METHOD_BUFFERED, FILE_ANY_ACCESS).
|
||||
constexpr DWORD kIoctlBthDisconnectDevice = 0x0041000C;
|
||||
const GUID kHidServiceClass = {0x00001124, 0x0000, 0x1000, {0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB}};
|
||||
// Each inquiry lasts this many 1.28 s units.
|
||||
constexpr UCHAR kInquiryUnits = 3;
|
||||
|
||||
struct Radio {
|
||||
HANDLE handle = nullptr;
|
||||
BLUETOOTH_RADIO_INFO info{};
|
||||
};
|
||||
|
||||
class Radios {
|
||||
public:
|
||||
Radios() {
|
||||
BLUETOOTH_FIND_RADIO_PARAMS params{sizeof(params)};
|
||||
HANDLE radio = nullptr;
|
||||
HBLUETOOTH_RADIO_FIND find = BluetoothFindFirstRadio(¶ms, &radio);
|
||||
if (find == nullptr) return;
|
||||
do {
|
||||
Radio entry;
|
||||
entry.handle = radio;
|
||||
entry.info.dwSize = sizeof(entry.info);
|
||||
if (BluetoothGetRadioInfo(radio, &entry.info) == ERROR_SUCCESS) {
|
||||
m_radios.push_back(entry);
|
||||
} else {
|
||||
CloseHandle(radio);
|
||||
}
|
||||
} while (BluetoothFindNextRadio(find, &radio));
|
||||
BluetoothFindRadioClose(find);
|
||||
}
|
||||
~Radios() {
|
||||
for (const Radio& radio : m_radios) CloseHandle(radio.handle);
|
||||
}
|
||||
Radios(const Radios&) = delete;
|
||||
Radios& operator=(const Radios&) = delete;
|
||||
const std::vector<Radio>& All() const { return m_radios; }
|
||||
|
||||
private:
|
||||
std::vector<Radio> m_radios;
|
||||
};
|
||||
|
||||
bool IsRemoteName(const WCHAR* name) {
|
||||
const std::wstring text = name;
|
||||
return text.rfind(L"Nintendo RVL-CNT-01", 0) == 0 && text.find(L"-UC") == std::wstring::npos;
|
||||
}
|
||||
|
||||
std::string Hex(const BLUETOOTH_ADDRESS& address) {
|
||||
char text[13];
|
||||
std::snprintf(text, sizeof(text), "%012llx", static_cast<unsigned long long>(address.ullLong & 0xFFFFFFFFFFFFull));
|
||||
return text;
|
||||
}
|
||||
|
||||
bool ParseHex(const std::string& text, BLUETOOTH_ADDRESS& address) {
|
||||
if (text.size() != 12) return false;
|
||||
address.ullLong = std::stoull(text, nullptr, 16);
|
||||
return true;
|
||||
}
|
||||
|
||||
ULONGLONG FileTimeOf(const SYSTEMTIME& time) {
|
||||
FILETIME file{};
|
||||
if (!SystemTimeToFileTime(&time, &file)) return 0;
|
||||
return (static_cast<ULONGLONG>(file.dwHighDateTime) << 32) | file.dwLowDateTime;
|
||||
}
|
||||
|
||||
ULONGLONG Now() {
|
||||
SYSTEMTIME now{};
|
||||
GetSystemTime(&now);
|
||||
return FileTimeOf(now);
|
||||
}
|
||||
|
||||
// The remote takes the host's address as PIN after its SYNC button, its own
|
||||
// address after 1+2; both as raw bytes, least significant first.
|
||||
DWORD Authenticate(const Radio& radio, BLUETOOTH_DEVICE_INFO& device, const BLUETOOTH_ADDRESS& pinAddress) {
|
||||
WCHAR pin[6];
|
||||
for (int i = 0; i < 6; ++i) pin[i] = pinAddress.rgBytes[i];
|
||||
return BluetoothAuthenticateDevice(nullptr, radio.handle, &device, pin, 6);
|
||||
}
|
||||
|
||||
// A remote that was just paired needs a few seconds to connect its HID service;
|
||||
// until then it must not be taken for a stale pairing.
|
||||
constexpr auto kPairingSettleTime = std::chrono::seconds(30);
|
||||
std::mutex g_recentMutex;
|
||||
std::vector<std::pair<std::string, std::chrono::steady_clock::time_point>> g_recent;
|
||||
|
||||
bool RecentlyPaired(const std::string& address) {
|
||||
std::lock_guard<std::mutex> lock(g_recentMutex);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
g_recent.erase(std::remove_if(g_recent.begin(), g_recent.end(),
|
||||
[&](const auto& entry) { return now - entry.second > kPairingSettleTime; }),
|
||||
g_recent.end());
|
||||
return std::any_of(g_recent.begin(), g_recent.end(), [&](const auto& entry) { return entry.first == address; });
|
||||
}
|
||||
|
||||
void MarkPaired(const std::string& address) {
|
||||
std::lock_guard<std::mutex> lock(g_recentMutex);
|
||||
g_recent.emplace_back(address, std::chrono::steady_clock::now());
|
||||
}
|
||||
|
||||
std::vector<BLUETOOTH_DEVICE_INFO> Devices(const Radio& radio, bool inquiry) {
|
||||
std::vector<BLUETOOTH_DEVICE_INFO> devices;
|
||||
BLUETOOTH_DEVICE_SEARCH_PARAMS params{sizeof(params)};
|
||||
params.fReturnAuthenticated = TRUE;
|
||||
params.fReturnRemembered = TRUE;
|
||||
params.fReturnUnknown = TRUE;
|
||||
params.fReturnConnected = TRUE;
|
||||
params.fIssueInquiry = inquiry ? TRUE : FALSE;
|
||||
params.cTimeoutMultiplier = inquiry ? kInquiryUnits : 0;
|
||||
params.hRadio = radio.handle;
|
||||
BLUETOOTH_DEVICE_INFO device{sizeof(device)};
|
||||
HBLUETOOTH_DEVICE_FIND find = BluetoothFindFirstDevice(¶ms, &device);
|
||||
if (find == nullptr) return devices;
|
||||
do {
|
||||
devices.push_back(device);
|
||||
device = BLUETOOTH_DEVICE_INFO{sizeof(device)};
|
||||
} while (BluetoothFindNextDevice(find, &device));
|
||||
BluetoothFindDeviceClose(find);
|
||||
return devices;
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
using namespace Detail;
|
||||
|
||||
Capabilities Query() {
|
||||
// Cached: radios rarely change and this is asked often.
|
||||
static std::mutex mutex;
|
||||
static Capabilities cached;
|
||||
static std::chrono::steady_clock::time_point checked{};
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (checked == std::chrono::steady_clock::time_point{} || now - checked > std::chrono::seconds(10)) {
|
||||
checked = now;
|
||||
const Radios radios;
|
||||
cached.pairing = cached.powerOff = !radios.All().empty();
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
int DiscoverAndPair(const std::atomic<bool>& cancel, std::string& status) {
|
||||
const Radios radios;
|
||||
if (radios.All().empty()) {
|
||||
status = "No Bluetooth adapter found.";
|
||||
return 0;
|
||||
}
|
||||
int connected = 0;
|
||||
for (const Radio& radio : radios.All()) {
|
||||
// A remote Windows remembers without a bond can never reconnect.
|
||||
for (BLUETOOTH_DEVICE_INFO& device : Devices(radio, false)) {
|
||||
if (IsRemoteName(device.szName) && device.fRemembered && !device.fConnected && !device.fAuthenticated) {
|
||||
BluetoothRemoveDevice(&device.Address);
|
||||
}
|
||||
}
|
||||
if (cancel) return connected;
|
||||
const ULONGLONG started = Now();
|
||||
std::vector<ULONGLONG> seen;
|
||||
for (BLUETOOTH_DEVICE_INFO& found : Devices(radio, true)) {
|
||||
if (cancel) return connected;
|
||||
if (!IsRemoteName(found.szName) || std::find(seen.begin(), seen.end(), found.Address.ullLong) != seen.end()) {
|
||||
continue;
|
||||
}
|
||||
seen.push_back(found.Address.ullLong);
|
||||
const std::string address = Hex(found.Address);
|
||||
if (RecentlyPaired(address)) continue;
|
||||
// The inquiry results can be seconds old: act on the current state.
|
||||
BLUETOOTH_DEVICE_INFO device = found;
|
||||
BluetoothGetDeviceInfo(radio.handle, &device);
|
||||
if (device.fConnected) continue;
|
||||
// Only a remote in pairing mode answers an inquiry; remembered ones
|
||||
// that did not answer keep their old timestamp.
|
||||
const bool answered = !device.fRemembered || FileTimeOf(found.stLastSeen) >= started;
|
||||
if (!answered) continue;
|
||||
if (device.fRemembered) {
|
||||
// Paired before, but in pairing mode now: it forgot this PC.
|
||||
BluetoothRemoveDevice(&device.Address);
|
||||
device.fAuthenticated = FALSE;
|
||||
device.fRemembered = FALSE;
|
||||
}
|
||||
DWORD result = Authenticate(radio, device, radio.info.address);
|
||||
const bool permanent = result == ERROR_SUCCESS;
|
||||
if (!permanent) {
|
||||
result = Authenticate(radio, device, device.Address);
|
||||
}
|
||||
if (result == ERROR_SUCCESS) {
|
||||
// Windows only keeps the pairing once the services were read.
|
||||
DWORD count = 0;
|
||||
BluetoothEnumerateInstalledServices(radio.handle, &device, &count, nullptr);
|
||||
}
|
||||
const DWORD service =
|
||||
BluetoothSetServiceState(radio.handle, &device, const_cast<GUID*>(&kHidServiceClass), BLUETOOTH_SERVICE_ENABLE);
|
||||
if (result == ERROR_SUCCESS || service == ERROR_SUCCESS) {
|
||||
++connected;
|
||||
MarkPaired(address);
|
||||
status = permanent ? "Wii Remote paired. It reconnects with any button from now on."
|
||||
: "Wii Remote connected for this session (press SYNC to pair it for good).";
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << address << (permanent ? " paired (SYNC)" : " connected (1+2)")
|
||||
<< std::endl;
|
||||
} else {
|
||||
status = "Could not pair the Wii Remote. Press its red SYNC button and try again.";
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << address << ": pairing failed (" << result << ", " << service
|
||||
<< ")" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
bool PowerOff(const std::string& address) {
|
||||
BLUETOOTH_ADDRESS target{};
|
||||
if (!ParseHex(address, target)) return false;
|
||||
const Radios radios;
|
||||
for (const Radio& radio : radios.All()) {
|
||||
ULONGLONG raw = target.ullLong;
|
||||
DWORD returned = 0;
|
||||
if (DeviceIoControl(radio.handle, kIoctlBthDisconnectDevice, &raw, sizeof(raw), nullptr, 0, &returned,
|
||||
nullptr)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string AddressFor(const SDL_hid_device_info& info) {
|
||||
// Bluetooth HID devices report the remote's address as their serial number.
|
||||
std::string hex;
|
||||
if (info.serial_number != nullptr) {
|
||||
for (const wchar_t* c = info.serial_number; *c != L'\0'; ++c) {
|
||||
if (*c < 128 && std::isxdigit(static_cast<int>(*c))) hex += static_cast<char>(std::tolower(*c));
|
||||
}
|
||||
}
|
||||
return hex.size() == 12 && hex != "000000000000" ? hex : std::string();
|
||||
}
|
||||
|
||||
std::string NameFor(const std::string& address, const SDL_hid_device_info& info) {
|
||||
BLUETOOTH_ADDRESS target{};
|
||||
if (ParseHex(address, target)) {
|
||||
const Radios radios;
|
||||
for (const Radio& radio : radios.All()) {
|
||||
BLUETOOTH_DEVICE_INFO device{sizeof(device)};
|
||||
device.Address = target;
|
||||
if (BluetoothGetDeviceInfo(radio.handle, &device) == ERROR_SUCCESS && device.szName[0] != L'\0') {
|
||||
std::string name;
|
||||
for (const WCHAR* c = device.szName; *c != L'\0'; ++c) name += static_cast<char>(*c < 128 ? *c : '?');
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string name;
|
||||
if (info.product_string != nullptr) {
|
||||
for (const wchar_t* c = info.product_string; *c != L'\0'; ++c) name += static_cast<char>(*c < 128 ? *c : '?');
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
bool MaybeConnected(const std::string& address) {
|
||||
BLUETOOTH_ADDRESS target{};
|
||||
if (!ParseHex(address, target)) return true;
|
||||
const Radios radios;
|
||||
for (const Radio& radio : radios.All()) {
|
||||
BLUETOOTH_DEVICE_INFO device{sizeof(device)};
|
||||
device.Address = target;
|
||||
if (BluetoothGetDeviceInfo(radio.handle, &device) == ERROR_SUCCESS) {
|
||||
return device.fConnected != FALSE;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Usable(const SDL_hid_device_info&) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Notice() {
|
||||
return Query().pairing ? std::string() : std::string("No Bluetooth adapter found.");
|
||||
}
|
||||
|
||||
} // namespace Wiimote::Platform
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,368 @@
|
||||
#include "bluetooth/bt_controller.h"
|
||||
#include "bluetooth/bt_sdp.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
using Bluetooth::BdAddr;
|
||||
using Bluetooth::Controller;
|
||||
using Bluetooth::LinkState;
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
std::fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \
|
||||
++g_failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
struct FakeHost : Bluetooth::RemoteHost {
|
||||
std::array<bool, Bluetooth::kRemoteSlots> attached{};
|
||||
std::array<bool, Bluetooth::kRemoteSlots> wake{};
|
||||
std::array<std::deque<Bytes>, Bluetooth::kRemoteSlots> input;
|
||||
std::vector<std::pair<size_t, Bytes>> output;
|
||||
std::array<LinkState, Bluetooth::kRemoteSlots> states{};
|
||||
std::vector<size_t> guestDisconnects;
|
||||
bool inquiry = false;
|
||||
|
||||
bool IsAttached(size_t slot) const override { return attached[slot]; }
|
||||
bool ConsumeWake(size_t slot) override {
|
||||
const bool w = wake[slot];
|
||||
wake[slot] = false;
|
||||
return w;
|
||||
}
|
||||
bool PopInputReport(size_t slot, Bytes& report) override {
|
||||
if (input[slot].empty()) return false;
|
||||
report = input[slot].front();
|
||||
input[slot].pop_front();
|
||||
return true;
|
||||
}
|
||||
void SendOutputReport(size_t slot, const uint8_t* report, size_t size) override {
|
||||
output.emplace_back(slot, Bytes(report, report + size));
|
||||
}
|
||||
void OnLinkStateChanged(size_t slot, LinkState state) override { states[slot] = state; }
|
||||
void OnGuestDisconnect(size_t slot) override { guestDisconnects.push_back(slot); }
|
||||
void OnGuestInquiry(bool active) override { inquiry = active; }
|
||||
const char* DeviceName(size_t) const override { return "Nintendo RVL-CNT-01"; }
|
||||
};
|
||||
|
||||
uint16_t Le16(const Bytes& b, size_t i) {
|
||||
return static_cast<uint16_t>(b[i] | (b[i + 1] << 8));
|
||||
}
|
||||
|
||||
void PutLe16(Bytes& b, uint16_t v) {
|
||||
b.push_back(static_cast<uint8_t>(v));
|
||||
b.push_back(static_cast<uint8_t>(v >> 8));
|
||||
}
|
||||
|
||||
Bytes Command(uint16_t opcode, Bytes params) {
|
||||
Bytes packet;
|
||||
PutLe16(packet, opcode);
|
||||
packet.push_back(static_cast<uint8_t>(params.size()));
|
||||
packet.insert(packet.end(), params.begin(), params.end());
|
||||
return packet;
|
||||
}
|
||||
|
||||
struct Guest {
|
||||
Controller& controller;
|
||||
uint64_t now = 1000;
|
||||
|
||||
std::vector<Bytes> Run(uint16_t opcode, Bytes params = {}) {
|
||||
const Bytes packet = Command(opcode, std::move(params));
|
||||
controller.ExecuteCommand(packet.data(), packet.size(), now);
|
||||
return Events();
|
||||
}
|
||||
std::vector<Bytes> Events() {
|
||||
std::vector<Bytes> events;
|
||||
Bytes event;
|
||||
while (controller.PopEvent(event)) events.push_back(event);
|
||||
return events;
|
||||
}
|
||||
std::vector<Bytes> Acl() {
|
||||
std::vector<Bytes> packets;
|
||||
Bytes packet;
|
||||
while (controller.PopAcl(packet)) packets.push_back(packet);
|
||||
return packets;
|
||||
}
|
||||
void Tick() {
|
||||
now += 1000;
|
||||
controller.Tick(now);
|
||||
}
|
||||
void SendL2cap(uint16_t handle, uint16_t cid, const Bytes& payload) {
|
||||
Bytes packet;
|
||||
PutLe16(packet, static_cast<uint16_t>(handle | 0x2000));
|
||||
PutLe16(packet, static_cast<uint16_t>(payload.size() + 4));
|
||||
PutLe16(packet, static_cast<uint16_t>(payload.size()));
|
||||
PutLe16(packet, cid);
|
||||
packet.insert(packet.end(), payload.begin(), payload.end());
|
||||
controller.ReceiveAcl(packet.data(), packet.size(), now);
|
||||
}
|
||||
void Signal(uint16_t handle, uint8_t code, uint8_t ident, const Bytes& data) {
|
||||
Bytes payload = {code, ident};
|
||||
PutLe16(payload, static_cast<uint16_t>(data.size()));
|
||||
payload.insert(payload.end(), data.begin(), data.end());
|
||||
SendL2cap(handle, 0x0001, payload);
|
||||
}
|
||||
};
|
||||
|
||||
bool IsCommandComplete(const Bytes& e, uint16_t opcode) {
|
||||
return e.size() >= 6 && e[0] == 0x0E && Le16(e, 3) == opcode && e[5] == 0x00;
|
||||
}
|
||||
|
||||
bool IsCommandStatusOk(const Bytes& e, uint16_t opcode) {
|
||||
return e.size() == 6 && e[0] == 0x0F && e[2] == 0x00 && Le16(e, 4) == opcode;
|
||||
}
|
||||
|
||||
// Splits a signalling ACL packet from the remote into (code, ident, data).
|
||||
struct Signal {
|
||||
uint8_t code = 0;
|
||||
uint8_t ident = 0;
|
||||
Bytes data;
|
||||
};
|
||||
|
||||
std::vector<Signal> Signals(const std::vector<Bytes>& acl) {
|
||||
std::vector<Signal> out;
|
||||
for (const Bytes& packet : acl) {
|
||||
if (packet.size() < 8 || Le16(packet, 6) != 0x0001) continue;
|
||||
size_t i = 8;
|
||||
while (i + 4 <= packet.size()) {
|
||||
Signal s;
|
||||
s.code = packet[i];
|
||||
s.ident = packet[i + 1];
|
||||
const uint16_t length = Le16(packet, i + 2);
|
||||
s.data.assign(packet.begin() + static_cast<std::ptrdiff_t>(i + 4),
|
||||
packet.begin() + static_cast<std::ptrdiff_t>(i + 4 + length));
|
||||
out.push_back(s);
|
||||
i += 4 + length;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Opens one HID channel the remote requested and completes both configurations.
|
||||
uint16_t AcceptRemoteChannel(Guest& guest, uint16_t handle, uint16_t psm, uint16_t guestCid) {
|
||||
guest.Events();
|
||||
const auto requests = Signals(guest.Acl());
|
||||
CHECK(requests.size() == 1);
|
||||
if (requests.size() != 1) return 0;
|
||||
CHECK(requests[0].code == 0x02 && Le16(requests[0].data, 0) == psm);
|
||||
const uint16_t remoteCid = Le16(requests[0].data, 2);
|
||||
Bytes rsp;
|
||||
PutLe16(rsp, guestCid);
|
||||
PutLe16(rsp, remoteCid);
|
||||
PutLe16(rsp, 0);
|
||||
PutLe16(rsp, 0);
|
||||
guest.Signal(handle, 0x03, requests[0].ident, rsp);
|
||||
const auto config = Signals(guest.Acl());
|
||||
CHECK(config.size() == 1 && config[0].code == 0x04 && Le16(config[0].data, 0) == guestCid);
|
||||
Bytes cfg;
|
||||
PutLe16(cfg, remoteCid);
|
||||
PutLe16(cfg, 0);
|
||||
cfg.insert(cfg.end(), {0x01, 0x02, 0xA0, 0x02});
|
||||
guest.Signal(handle, 0x04, 0x40, cfg);
|
||||
const auto cfgRsp = Signals(guest.Acl());
|
||||
CHECK(cfgRsp.size() == 1 && cfgRsp[0].code == 0x05 && cfgRsp[0].ident == 0x40 &&
|
||||
Le16(cfgRsp[0].data, 0) == guestCid && Le16(cfgRsp[0].data, 4) == 0);
|
||||
Bytes ourCfgRsp;
|
||||
PutLe16(ourCfgRsp, remoteCid);
|
||||
PutLe16(ourCfgRsp, 0);
|
||||
PutLe16(ourCfgRsp, 0);
|
||||
if (!config.empty()) guest.Signal(handle, 0x05, config[0].ident, ourCfgRsp);
|
||||
return remoteCid;
|
||||
}
|
||||
|
||||
void TestBootAndConnection() {
|
||||
FakeHost host;
|
||||
Controller controller(host);
|
||||
Guest guest{controller};
|
||||
controller.Open(guest.now);
|
||||
|
||||
auto e = guest.Run(0x0C03);
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x0C03));
|
||||
e = guest.Run(0x1005);
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1005) && Le16(e[0], 6) == 339 && Le16(e[0], 9) == 10);
|
||||
e = guest.Run(0x1009);
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1009) && e[0][11] == 0xFF);
|
||||
e = guest.Run(0x1001);
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1001) && Le16(e[0], 7) == 0x40A7);
|
||||
e = guest.Run(0x1003);
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1003));
|
||||
e = guest.Run(0x0C0D, {0, 0, 0, 0, 0, 0, 1});
|
||||
CHECK(e.size() == 2 && e[0][0] == 0x15 && e[0][2] == 4 && IsCommandComplete(e[1], 0x0C0D) &&
|
||||
Le16(e[1], 8) == 4);
|
||||
Bytes slot0Key;
|
||||
if (!e.empty() && e[0].size() >= 25) slot0Key.assign(e[0].begin() + 9, e[0].begin() + 25);
|
||||
e = guest.Run(0xFC4F, {0x00});
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0xFC4F));
|
||||
|
||||
// Attached remote, page scan off: nothing yet.
|
||||
host.attached[0] = true;
|
||||
host.wake[0] = true;
|
||||
guest.Tick();
|
||||
CHECK(guest.Events().empty());
|
||||
|
||||
e = guest.Run(0x0C1A, {0x02});
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x0C1A));
|
||||
guest.Tick();
|
||||
e = guest.Events();
|
||||
CHECK(e.size() == 1 && e[0][0] == 0x04 && e[0][7] == 0x00);
|
||||
const BdAddr addr = Controller::RemoteAddress(0);
|
||||
|
||||
Bytes accept(addr.begin(), addr.end());
|
||||
accept.push_back(0x00);
|
||||
e = guest.Run(0x0409, accept);
|
||||
CHECK(e.size() == 3 && IsCommandStatusOk(e[0], 0x0409) && e[1][0] == 0x12 && e[2][0] == 0x03 &&
|
||||
e[2][2] == 0x00 && Le16(e[2], 3) == 0x0100);
|
||||
CHECK(host.states[0] == LinkState::Connecting);
|
||||
|
||||
// Authentication with the stored key.
|
||||
e = guest.Run(0x0411, {0x00, 0x01});
|
||||
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0411) && e[1][0] == 0x17);
|
||||
Bytes reply(addr.begin(), addr.end());
|
||||
reply.insert(reply.end(), slot0Key.begin(), slot0Key.end());
|
||||
e = guest.Run(0x040B, reply);
|
||||
CHECK(e.size() == 2 && IsCommandComplete(e[0], 0x040B) && e[1][0] == 0x06 && e[1][2] == 0x00);
|
||||
|
||||
guest.Tick();
|
||||
const uint16_t controlCid = AcceptRemoteChannel(guest, 0x0100, 0x0011, 0x0041);
|
||||
CHECK(host.states[0] == LinkState::Connecting);
|
||||
guest.Tick();
|
||||
const uint16_t interruptCid = AcceptRemoteChannel(guest, 0x0100, 0x0013, 0x0042);
|
||||
CHECK(controlCid != 0 && interruptCid != 0);
|
||||
CHECK(host.states[0] == LinkState::Ready);
|
||||
|
||||
// Input passthrough.
|
||||
host.input[0].push_back({0x30, 0x00, 0x08});
|
||||
guest.Tick();
|
||||
guest.Events();
|
||||
auto acl = guest.Acl();
|
||||
CHECK(acl.size() == 1);
|
||||
if (acl.size() == 1) {
|
||||
const Bytes& p = acl[0];
|
||||
CHECK(Le16(p, 0) == 0x2100 && Le16(p, 6) == 0x0042 && p[8] == 0xA1 && p[9] == 0x30 && p[11] == 0x08);
|
||||
}
|
||||
|
||||
// Output passthrough on the interrupt channel, and a SET_REPORT on control.
|
||||
guest.SendL2cap(0x0100, interruptCid, {0xA2, 0x11, 0x10});
|
||||
guest.SendL2cap(0x0100, controlCid, {0x52, 0x15, 0x00});
|
||||
CHECK(host.output.size() == 2 && host.output[0].second == Bytes({0x11, 0x10}) &&
|
||||
host.output[1].second == Bytes({0x15, 0x00}));
|
||||
acl = guest.Acl();
|
||||
CHECK(acl.size() == 1 && acl[0].size() == 9 && acl[0][8] == 0x00);
|
||||
guest.Tick();
|
||||
e = guest.Events();
|
||||
CHECK(e.size() == 1 && e[0][0] == 0x13 && e[0][2] == 1 && Le16(e[0], 3) == 0x0100 && Le16(e[0], 5) == 2);
|
||||
|
||||
// The game drops the remote: a real one powers off.
|
||||
e = guest.Run(0x0406, {0x00, 0x01, 0x13});
|
||||
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0406) && e[1][0] == 0x05 && e[1][5] == 0x16);
|
||||
CHECK(host.guestDisconnects.size() == 1 && host.states[0] == LinkState::Down);
|
||||
guest.Tick();
|
||||
CHECK(guest.Events().empty());
|
||||
}
|
||||
|
||||
void TestInquiryAndPaging() {
|
||||
FakeHost host;
|
||||
Controller controller(host);
|
||||
Guest guest{controller};
|
||||
controller.Open(guest.now);
|
||||
guest.Run(0x0C45, {0x01});
|
||||
host.attached[1] = true;
|
||||
host.wake[1] = true;
|
||||
guest.Tick(); // consumes the wake; page scan is off
|
||||
auto e = guest.Run(0x0401, {0x00, 0x8B, 0x9E, 0x03, 0x01});
|
||||
CHECK(e.size() == 1 && IsCommandStatusOk(e[0], 0x0401) && host.inquiry);
|
||||
guest.Tick();
|
||||
e = guest.Events();
|
||||
CHECK(e.size() == 1 && e[0][0] == 0x22 && e[0][8] == 0x01);
|
||||
guest.Tick();
|
||||
e = guest.Events();
|
||||
CHECK(e.size() == 1 && e[0][0] == 0x01);
|
||||
CHECK(!host.inquiry);
|
||||
Bytes create;
|
||||
const BdAddr addr = Controller::RemoteAddress(1);
|
||||
create.insert(create.end(), addr.begin(), addr.end());
|
||||
create.insert(create.end(), {0x18, 0xCC, 0x01, 0x00, 0x00, 0x00, 0x00});
|
||||
e = guest.Run(0x0405, create);
|
||||
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0405) && e[1][0] == 0x03 && e[1][2] == 0x00 &&
|
||||
Le16(e[1], 3) == 0x0101);
|
||||
|
||||
// Paging a remote that is not there times out.
|
||||
const BdAddr missing = Controller::RemoteAddress(2);
|
||||
Bytes create2(missing.begin(), missing.end());
|
||||
create2.insert(create2.end(), {0x18, 0xCC, 0x01, 0x00, 0x00, 0x00, 0x00});
|
||||
e = guest.Run(0x0405, create2);
|
||||
CHECK(e.size() == 1);
|
||||
guest.now += 6'000'000;
|
||||
guest.Tick();
|
||||
e = guest.Events();
|
||||
CHECK(e.size() == 1 && e[0][0] == 0x03 && e[0][2] == 0x04);
|
||||
|
||||
// The guest opens SDP itself on a paged remote.
|
||||
Bytes sdpReq;
|
||||
PutLe16(sdpReq, 0x0001);
|
||||
PutLe16(sdpReq, 0x0050);
|
||||
guest.Signal(0x0101, 0x02, 0x07, sdpReq);
|
||||
const auto rsp = Signals(guest.Acl());
|
||||
CHECK(rsp.size() == 2 && rsp[0].code == 0x03 && Le16(rsp[0].data, 4) == 0 && rsp[1].code == 0x04);
|
||||
}
|
||||
|
||||
void TestSdp() {
|
||||
// ServiceAttributeRequest for the whole HID record.
|
||||
const Bytes request = {0x04, 0x00, 0x01, 0x00, 0x0E, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xFF,
|
||||
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF, 0x00};
|
||||
const Bytes response = Bluetooth::Sdp::HandleRequest(request.data(), request.size(), 4096);
|
||||
CHECK(response.size() > 10 && response[0] == 0x05);
|
||||
if (response.size() > 10) {
|
||||
const uint16_t count = static_cast<uint16_t>((response[5] << 8) | response[6]);
|
||||
CHECK(count == 0x01CC + 3 && response[7] == 0x36 && response[8] == 0x01 && response[9] == 0xCC);
|
||||
CHECK(response.back() == 0x00);
|
||||
}
|
||||
CHECK(Bluetooth::Sdp::WiiRemoteHidDescriptor().size() == 0xD9);
|
||||
|
||||
// Continuation with a small MTU reassembles to the same list.
|
||||
Bytes assembled;
|
||||
Bytes cont = {0x00};
|
||||
for (int guard = 0; guard < 64; ++guard) {
|
||||
Bytes req = {0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40,
|
||||
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF};
|
||||
req.insert(req.end(), cont.begin(), cont.end());
|
||||
req[4] = static_cast<uint8_t>(req.size() - 5);
|
||||
const Bytes part = Bluetooth::Sdp::HandleRequest(req.data(), req.size(), 48);
|
||||
CHECK(part.size() <= 48 && part[0] == 0x05);
|
||||
const uint16_t count = static_cast<uint16_t>((part[5] << 8) | part[6]);
|
||||
assembled.insert(assembled.end(), part.begin() + 7, part.begin() + 7 + count);
|
||||
cont.assign(part.begin() + 7 + count, part.end());
|
||||
if (cont.size() == 1 && cont[0] == 0) break;
|
||||
}
|
||||
CHECK(assembled.size() == 0x01CC + 3);
|
||||
|
||||
// Device ID record via ServiceSearchAttribute.
|
||||
const Bytes search = {0x06, 0x00, 0x03, 0x00, 0x0F, 0x35, 0x03, 0x19, 0x12, 0x00, 0xFF, 0xFF,
|
||||
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF, 0x00};
|
||||
const Bytes found = Bluetooth::Sdp::HandleRequest(search.data(), search.size(), 4096);
|
||||
CHECK(found.size() > 12 && found[0] == 0x07 && found[7] == 0x35 && found[9] == 0x36 && found[10] == 0x00 &&
|
||||
found[11] == 0x5A);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestBootAndConnection();
|
||||
TestInquiryAndPaging();
|
||||
TestSdp();
|
||||
if (g_failures != 0) {
|
||||
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::printf("bluetooth controller tests passed\n");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -93,6 +93,13 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
sb.AppendLine($"{pad}{dest} = {GuestLoadHelper(8, isFloat: true)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
|
||||
}
|
||||
else if (TryGetKnownEffectiveAddress(load.Address, knownConstants, out var knownLoadAddress) &&
|
||||
FlatWriteNeedsPolicy(knownLoadAddress))
|
||||
{
|
||||
// The flat view maps the MMIO window no-access, so a hardware register read would fault
|
||||
// without knowing its width. The checked read reaches the runtime's register HLE.
|
||||
sb.AppendLine($"{pad}{dest} = {CheckedMmioLoadHelper(load.SizeBytes)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"{pad}{dest} = {GuestLoadHelper(load.SizeBytes, isFloat: false)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
|
||||
|
||||
@@ -84,6 +84,14 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
private static string GuestLoadHelper(int sizeBytes, bool isFloat) =>
|
||||
FlatReadHelper(Bits(sizeBytes), isFloat);
|
||||
|
||||
private static string CheckedMmioLoadHelper(int sizeBytes) => Bits(sizeBytes) switch
|
||||
{
|
||||
8 => "Memory::Read8",
|
||||
16 => "Memory::Read16",
|
||||
64 => "Memory::Read64",
|
||||
_ => "Memory::Read32"
|
||||
};
|
||||
|
||||
private static string GuestStoreHelper(int sizeBytes, bool isFloat) =>
|
||||
FlatWriteHelper(Bits(sizeBytes), isFloat);
|
||||
|
||||
|
||||
@@ -480,6 +480,41 @@ public class CodeGenTests
|
||||
Assert.Contains("MemoryInline::FlatWrite8((ctx->gpr[6] + -32768), static_cast<uint32_t>(ctx->gpr[3]));", code, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CodeGenerator_ReadsKnownMmioAddressesThroughCheckedHelper()
|
||||
{
|
||||
var function = new IrFunction(
|
||||
"known_mmio_load",
|
||||
"entry",
|
||||
new[]
|
||||
{
|
||||
new IrBasicBlock("entry", new IrInstruction[]
|
||||
{
|
||||
new IrAssign("r4", IrValue.Imm(unchecked((int)0xCD000000))),
|
||||
new IrLoad("r0", new IrAddress("r4", 0xC0), 4),
|
||||
new IrLoad("r5", new IrAddress("r3", 8), 4),
|
||||
new IrReturn(null)
|
||||
})
|
||||
});
|
||||
|
||||
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
|
||||
{
|
||||
["r0"] = ValueRepresentation.UInt32,
|
||||
["r3"] = ValueRepresentation.UInt32,
|
||||
["r4"] = ValueRepresentation.UInt32,
|
||||
["r5"] = ValueRepresentation.UInt32
|
||||
});
|
||||
var signature = new FunctionAbiClassification(
|
||||
"known_mmio_load",
|
||||
ValueRepresentation.Void);
|
||||
|
||||
var ssa = new SsaTransformer().Convert(function);
|
||||
var code = new CxxLinearCodeGenerator().Emit(0x80008020, ssa, signature, types);
|
||||
|
||||
Assert.Contains("= Memory::Read32(", code, StringComparison.Ordinal);
|
||||
Assert.Contains("= MemoryInline::FlatRead32((ctx->gpr[3] + 8));", code, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CodeGenerator_ReusesFctiwzStackLowWordForMatchingLoad()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user