mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-23 11:31:51 +01:00
Implement Rumble Support for Controllers and Device Vibrators
This commit is contained in:
parent
d8ccdd723e
commit
1a58a2e967
@ -98,6 +98,7 @@ add_library(skyline SHARED
|
||||
${source_DIR}/skyline/services/am/applet/ILibraryAppletAccessor.cpp
|
||||
${source_DIR}/skyline/services/hid/IHidServer.cpp
|
||||
${source_DIR}/skyline/services/hid/IAppletResource.cpp
|
||||
${source_DIR}/skyline/services/hid/IActiveVibrationDeviceList.cpp
|
||||
${source_DIR}/skyline/services/timesrv/IStaticService.cpp
|
||||
${source_DIR}/skyline/services/timesrv/ISystemClock.cpp
|
||||
${source_DIR}/skyline/services/timesrv/ISteadyClock.cpp
|
||||
|
@ -116,7 +116,7 @@ extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setButtonSt
|
||||
auto device = input->npad.controllers[index].device;
|
||||
if (device)
|
||||
device->SetButtonState(skyline::input::NpadButton{.raw = static_cast<skyline::u64>(mask)}, pressed);
|
||||
} catch (const std::bad_weak_ptr&) {
|
||||
} catch (const std::bad_weak_ptr &) {
|
||||
// We don't mind if we miss button updates while input hasn't been initialized
|
||||
}
|
||||
}
|
||||
@ -127,7 +127,7 @@ extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setAxisValu
|
||||
auto device = input->npad.controllers[index].device;
|
||||
if (device)
|
||||
device->SetAxisValue(static_cast<skyline::input::NpadAxisId>(axis), value);
|
||||
} catch (const std::bad_weak_ptr&) {
|
||||
} catch (const std::bad_weak_ptr &) {
|
||||
// We don't mind if we miss axis updates while input hasn't been initialized
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include <fstream>
|
||||
#include <syslog.h>
|
||||
|
@ -4,7 +4,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <common.h>
|
||||
#include <span>
|
||||
#include <queue>
|
||||
#include "engines/engine.h"
|
||||
#include "engines/gpfifo.h"
|
||||
@ -177,4 +176,4 @@ namespace skyline::gpu {
|
||||
void Push(std::span<GpEntry> entries);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <common.h>
|
||||
|
||||
namespace skyline {
|
||||
|
@ -50,9 +50,13 @@ namespace skyline::input {
|
||||
if (style.raw) {
|
||||
if (style.proController || style.joyconHandheld || style.joyconLeft || style.joyconRight) {
|
||||
device.Connect(controller.type);
|
||||
device.index = static_cast<size_t>(&controller - controllers.data());
|
||||
device.partnerIndex = -1;
|
||||
controller.device = &device;
|
||||
} else if (style.joyconDual && orientation == NpadJoyOrientation::Vertical && device.GetAssignment() == NpadJoyAssignment::Dual) {
|
||||
device.Connect(NpadControllerType::JoyconDual);
|
||||
device.index = static_cast<size_t>(&controller - controllers.data());
|
||||
device.partnerIndex = controller.partnerIndex;
|
||||
controller.device = &device;
|
||||
controllers.at(controller.partnerIndex).device = &device;
|
||||
} else {
|
||||
|
@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include <jvm.h>
|
||||
#include "npad_device.h"
|
||||
#include "npad.h"
|
||||
|
||||
@ -155,6 +156,9 @@ namespace skyline::input {
|
||||
section = {};
|
||||
globalTimestamp = 0;
|
||||
|
||||
index = -1;
|
||||
partnerIndex = -1;
|
||||
|
||||
type = NpadControllerType::None;
|
||||
controllerInfo = nullptr;
|
||||
|
||||
@ -347,4 +351,91 @@ namespace skyline::input {
|
||||
|
||||
globalTimestamp++;
|
||||
}
|
||||
|
||||
void NpadDevice::VibrateDevice(i8 vibrateIndex, const NpadVibrationValue &value) {
|
||||
std::array<jlong, 3> timings;
|
||||
std::array<jint, 3> amplitudes;
|
||||
|
||||
jlong periodLow = 1000 / value.frequencyLow;
|
||||
jlong periodHigh = 1000 / value.frequencyHigh;
|
||||
|
||||
jint amplitudeLow = value.amplitudeLow * 127;
|
||||
jint amplitudeHigh = value.amplitudeHigh * 127;
|
||||
|
||||
if (amplitudeLow + amplitudeHigh == 0 || periodLow + periodHigh == 0) {
|
||||
manager.state.jvm->ClearVibrationDevice(vibrateIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (periodLow == periodHigh) {
|
||||
timings = {periodLow, periodHigh, 0};
|
||||
amplitudes = {std::min(amplitudeLow + amplitudeHigh, 255), 0, 0};
|
||||
} else if (periodLow < periodHigh) {
|
||||
timings = {periodLow, periodHigh - periodLow, periodHigh};
|
||||
amplitudes = {std::min(amplitudeLow + amplitudeHigh, 255), amplitudeHigh, 0};
|
||||
} else if (periodHigh < periodLow) {
|
||||
timings = {periodHigh, periodLow - periodHigh, periodLow};
|
||||
amplitudes = {std::min(amplitudeHigh + amplitudeLow, 255), amplitudeLow, 0};
|
||||
}
|
||||
|
||||
manager.state.jvm->VibrateDevice(vibrateIndex, timings, amplitudes);
|
||||
}
|
||||
|
||||
void NpadDevice::Vibrate(bool isRight, const NpadVibrationValue &value) {
|
||||
if (isRight)
|
||||
vibrationRight = value;
|
||||
else
|
||||
vibrationLeft = value;
|
||||
|
||||
if (vibrationRight)
|
||||
Vibrate(vibrationLeft, *vibrationRight);
|
||||
else
|
||||
VibrateDevice(index, value);
|
||||
}
|
||||
|
||||
void NpadDevice::Vibrate(const NpadVibrationValue &left, const NpadVibrationValue &right) {
|
||||
if (partnerIndex == -1) {
|
||||
std::array<jlong, 5> timings;
|
||||
std::array<jint, 5> amplitudes;
|
||||
|
||||
std::array<std::pair<jlong, jint>, 4> vibrations{std::pair<jlong, jint>{1000 / left.frequencyLow, left.amplitudeLow * 64},
|
||||
{1000 / left.frequencyHigh, left.amplitudeHigh * 64},
|
||||
{1000 / right.frequencyLow, right.amplitudeLow * 64},
|
||||
{1000 / right.frequencyHigh, right.amplitudeHigh * 64},
|
||||
};
|
||||
|
||||
jlong totalTime{};
|
||||
std::sort(vibrations.begin(), vibrations.end(), [](const std::pair<jlong, jint> &a, const std::pair<jlong, jint> &b) {
|
||||
return a.first < b.first;
|
||||
});
|
||||
|
||||
jint totalAmplitude{};
|
||||
for (const auto &vibration : vibrations)
|
||||
totalAmplitude += vibration.second;
|
||||
|
||||
if (totalAmplitude == 0 || vibrations[3].first == 0) {
|
||||
manager.state.jvm->ClearVibrationDevice(index);
|
||||
return;
|
||||
}
|
||||
|
||||
for (u8 i{0}; i < vibrations.size(); i++) {
|
||||
const auto &vibration = vibrations[i];
|
||||
|
||||
auto time = vibration.first - totalTime;
|
||||
timings[i] = time;
|
||||
totalTime += time;
|
||||
|
||||
amplitudes[i] = std::min(totalAmplitude, 255);
|
||||
totalAmplitude -= vibration.second;
|
||||
}
|
||||
|
||||
timings[4] = totalTime;
|
||||
amplitudes[4] = 0;
|
||||
|
||||
manager.state.jvm->VibrateDevice(index, timings, amplitudes);
|
||||
} else {
|
||||
VibrateDevice(index, left);
|
||||
VibrateDevice(partnerIndex, right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,50 @@ namespace skyline::input {
|
||||
Handheld = 0x20,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A handle to a specific device addressed by it's ID and type
|
||||
* @note This is used by both Six-Axis and Vibration
|
||||
*/
|
||||
union __attribute__((__packed__)) NpadDeviceHandle {
|
||||
u32 raw;
|
||||
struct {
|
||||
u8 type;
|
||||
NpadId id : 8;
|
||||
bool isRight : 1; //!< If this is a right Joy-Con (Both) or right LRA in the Pro-Controller (Vibration)
|
||||
bool isSixAxisSingle : 1; //!< If the Six-Axis device is a single unit, either Handheld or Pro-Controller
|
||||
};
|
||||
|
||||
constexpr NpadControllerType GetType() {
|
||||
switch (type) {
|
||||
case 3:
|
||||
return NpadControllerType::ProController;
|
||||
case 4:
|
||||
return NpadControllerType::Handheld;
|
||||
case 5:
|
||||
return NpadControllerType::JoyconDual;
|
||||
case 6:
|
||||
return NpadControllerType::JoyconLeft;
|
||||
case 7:
|
||||
return NpadControllerType::JoyconRight;
|
||||
}
|
||||
return NpadControllerType::None;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The parameters to produce a vibration using an LRA
|
||||
* @note The vibration is broken into a frequency band with the lower and high range supplied
|
||||
* @note Amplitude is in arbitrary units from 0f to 1f
|
||||
* @note Frequency is in Hertz
|
||||
*/
|
||||
struct NpadVibrationValue {
|
||||
float amplitudeLow;
|
||||
float frequencyLow;
|
||||
float amplitudeHigh;
|
||||
float frequencyHigh;
|
||||
};
|
||||
static_assert(sizeof(NpadVibrationValue) == 0x10);
|
||||
|
||||
class NpadManager;
|
||||
|
||||
/**
|
||||
@ -85,8 +129,14 @@ namespace skyline::input {
|
||||
*/
|
||||
NpadControllerInfo &GetControllerInfo();
|
||||
|
||||
void VibrateDevice(i8 index, const NpadVibrationValue &value);
|
||||
|
||||
public:
|
||||
NpadId id;
|
||||
i8 index{-1}; //!< The index of the device assigned to this player
|
||||
i8 partnerIndex{-1}; //!< The index of a partner device, if present
|
||||
NpadVibrationValue vibrationLeft; //!< Vibration for the left Joy-Con (Handheld/Pair), left LRA in a Pro-Controller or individual Joy-Cons
|
||||
std::optional<NpadVibrationValue> vibrationRight; //!< Vibration for the right Joy-Con (Handheld/Pair) or right LRA in a Pro-Controller
|
||||
NpadControllerType type{};
|
||||
NpadConnectionState connectionState{};
|
||||
std::shared_ptr<kernel::type::KEvent> updateEvent; //!< This event is triggered on the controller's style changing
|
||||
@ -132,5 +182,9 @@ namespace skyline::input {
|
||||
* @param value The value to set
|
||||
*/
|
||||
void SetAxisValue(NpadAxisId axis, i32 value);
|
||||
|
||||
void Vibrate(bool isRight, const NpadVibrationValue &value);
|
||||
|
||||
void Vibrate(const NpadVibrationValue &left, const NpadVibrationValue &right);
|
||||
};
|
||||
}
|
||||
|
@ -147,38 +147,38 @@ namespace skyline::input {
|
||||
/**
|
||||
* @brief This structure is used to hold a single sample of 3D data from the IMU
|
||||
*/
|
||||
struct SixaxisVector {
|
||||
struct SixAxisVector {
|
||||
float x; //!< The data in the X-axis
|
||||
float y; //!< The data in the Y-axis
|
||||
float z; //!< The data in the Z-axis
|
||||
};
|
||||
static_assert(sizeof(SixaxisVector) == 0xC);
|
||||
static_assert(sizeof(SixAxisVector) == 0xC);
|
||||
|
||||
/**
|
||||
* @brief This structure contains data about the state of the controller's IMU (Sixaxis) (https://switchbrew.org/wiki/HID_Shared_Memory#NpadSixAxisSensorHandheldState)
|
||||
* @brief This structure contains data about the state of the controller's IMU (Six-Axis) (https://switchbrew.org/wiki/HID_Shared_Memory#NpadSixAxisSensorHandheldState)
|
||||
*/
|
||||
struct NpadSixaxisState {
|
||||
struct NpadSixAxisState {
|
||||
u64 globalTimestamp; //!< The global timestamp in samples
|
||||
u64 _unk0_;
|
||||
u64 localTimestamp; //!< The local timestamp in samples
|
||||
|
||||
SixaxisVector accelerometer;
|
||||
SixaxisVector gyroscope;
|
||||
SixaxisVector rotation;
|
||||
std::array<SixaxisVector, 3> orientation; //!< The orientation basis data as a matrix
|
||||
SixAxisVector accelerometer;
|
||||
SixAxisVector gyroscope;
|
||||
SixAxisVector rotation;
|
||||
std::array<SixAxisVector, 3> orientation; //!< The orientation basis data as a matrix
|
||||
|
||||
u64 _unk2_; //!< This is always 1
|
||||
};
|
||||
static_assert(sizeof(NpadSixaxisState) == 0x68);
|
||||
static_assert(sizeof(NpadSixAxisState) == 0x68);
|
||||
|
||||
/**
|
||||
* @brief This structure contains header and entries for the IMU (Sixaxis) data
|
||||
* @brief This structure contains header and entries for the IMU (Six-Axis) data
|
||||
*/
|
||||
struct NpadSixaxisInfo {
|
||||
struct NpadSixAxisInfo {
|
||||
CommonHeader header;
|
||||
std::array<NpadSixaxisState, constant::HidEntryCount> state;
|
||||
std::array<NpadSixAxisState, constant::HidEntryCount> state;
|
||||
};
|
||||
static_assert(sizeof(NpadSixaxisInfo) == 0x708);
|
||||
static_assert(sizeof(NpadSixAxisInfo) == 0x708);
|
||||
|
||||
/**
|
||||
* @brief This is a bit-field of all the device types (https://switchbrew.org/wiki/HID_services#DeviceType)
|
||||
@ -268,12 +268,12 @@ namespace skyline::input {
|
||||
NpadControllerInfo palmaController; //!< The Poké Ball Plus controller data
|
||||
NpadControllerInfo defaultController; //!< The Default controller data (Inputs are rotated based on orientation and SL/SR are mapped to L/R incase it is a single JC)
|
||||
|
||||
NpadSixaxisInfo fullKeySixaxis; //!< The Pro/GC IMU data
|
||||
NpadSixaxisInfo handheldSixaxis; //!< The Handheld IMU data
|
||||
NpadSixaxisInfo dualLeftSixaxis; //!< The Left Joy-Con in dual mode's IMU data
|
||||
NpadSixaxisInfo dualRightSixaxis; //!< The Left Joy-Con in dual mode's IMU data
|
||||
NpadSixaxisInfo leftSixaxis; //!< The Left Joy-Con IMU data
|
||||
NpadSixaxisInfo rightSixaxis; //!< The Right Joy-Con IMU data
|
||||
NpadSixAxisInfo fullKeySixAxis; //!< The Pro/GC IMU data
|
||||
NpadSixAxisInfo handheldSixAxis; //!< The Handheld IMU data
|
||||
NpadSixAxisInfo dualLeftSixAxis; //!< The Left Joy-Con in dual mode's IMU data
|
||||
NpadSixAxisInfo dualRightSixAxis; //!< The Left Joy-Con in dual mode's IMU data
|
||||
NpadSixAxisInfo leftSixAxis; //!< The Left Joy-Con IMU data
|
||||
NpadSixAxisInfo rightSixAxis; //!< The Right Joy-Con IMU data
|
||||
|
||||
NpadDeviceType deviceType;
|
||||
|
||||
|
@ -6,12 +6,17 @@
|
||||
thread_local JNIEnv *env;
|
||||
|
||||
namespace skyline {
|
||||
JvmManager::JvmManager(JNIEnv *environ, jobject instance) : instance(instance), instanceClass(reinterpret_cast<jclass>(environ->NewGlobalRef(environ->GetObjectClass(instance)))), initializeControllersId(environ->GetMethodID(instanceClass, "initializeControllers", "()V")) {
|
||||
JvmManager::JvmManager(JNIEnv *environ, jobject instance) : instance(environ->NewGlobalRef(instance)), instanceClass(reinterpret_cast<jclass>(environ->NewGlobalRef(environ->GetObjectClass(instance)))), initializeControllersId(environ->GetMethodID(instanceClass, "initializeControllers", "()V")), vibrateDeviceId(environ->GetMethodID(instanceClass, "vibrateDevice", "(I[J[I)V")), clearVibrationDeviceId(environ->GetMethodID(instanceClass, "clearVibrationDevice", "(I)V")) {
|
||||
env = environ;
|
||||
if (env->GetJavaVM(&vm) < 0)
|
||||
throw exception("Cannot get JavaVM from environment");
|
||||
}
|
||||
|
||||
JvmManager::~JvmManager() {
|
||||
env->DeleteGlobalRef(instanceClass);
|
||||
env->DeleteGlobalRef(instance);
|
||||
}
|
||||
|
||||
void JvmManager::AttachThread() {
|
||||
if (!env)
|
||||
vm->AttachCurrentThread(&env, nullptr);
|
||||
@ -41,4 +46,20 @@ namespace skyline {
|
||||
void JvmManager::InitializeControllers() {
|
||||
env->CallVoidMethod(instance, initializeControllersId);
|
||||
}
|
||||
|
||||
void JvmManager::VibrateDevice(jint index, const std::span<jlong> &timings, const std::span<jint> &litudes) {
|
||||
auto jTimings = env->NewLongArray(timings.size());
|
||||
env->SetLongArrayRegion(jTimings, 0, timings.size(), timings.data());
|
||||
auto jAmplitudes = env->NewIntArray(amplitudes.size());
|
||||
env->SetIntArrayRegion(jAmplitudes, 0, amplitudes.size(), amplitudes.data());
|
||||
|
||||
env->CallVoidMethod(instance, vibrateDeviceId, index, jTimings, jAmplitudes);
|
||||
|
||||
env->DeleteLocalRef(jTimings);
|
||||
env->DeleteLocalRef(jAmplitudes);
|
||||
}
|
||||
|
||||
void JvmManager::ClearVibrationDevice(jint index) {
|
||||
env->CallVoidMethod(instance, clearVibrationDeviceId, index);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ namespace skyline {
|
||||
*/
|
||||
JvmManager(JNIEnv *env, jobject instance);
|
||||
|
||||
~JvmManager();
|
||||
|
||||
/**
|
||||
* @brief Attach the current thread to the Java VM
|
||||
*/
|
||||
@ -92,7 +94,19 @@ namespace skyline {
|
||||
*/
|
||||
void InitializeControllers();
|
||||
|
||||
/**
|
||||
* @brief A call to EmulationActivity.vibrateDevice in Kotlin
|
||||
*/
|
||||
void VibrateDevice(jint index, const std::span<jlong> &timings, const std::span<jint> &litudes);
|
||||
|
||||
/**
|
||||
* @brief A call to EmulationActivity.clearVibrationDevice in Kotlin
|
||||
*/
|
||||
void ClearVibrationDevice(jint index);
|
||||
|
||||
private:
|
||||
jmethodID initializeControllersId;
|
||||
jmethodID vibrateDeviceId;
|
||||
jmethodID clearVibrationDeviceId;
|
||||
};
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ extern skyline::GroupMutex JniMtx;
|
||||
|
||||
namespace skyline {
|
||||
void NCE::KernelThread(pid_t thread) {
|
||||
state.jvm->AttachThread();
|
||||
try {
|
||||
state.thread = state.process->threads.at(thread);
|
||||
state.ctx = reinterpret_cast<ThreadContext *>(state.thread->ctxMemory->kernel.address);
|
||||
@ -76,6 +77,8 @@ namespace skyline {
|
||||
state.os->KillThread(thread);
|
||||
}
|
||||
}
|
||||
|
||||
state.jvm->DetachThread();
|
||||
}
|
||||
|
||||
NCE::NCE(DeviceState &state) : state(state) {}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include <span>
|
||||
#include <kernel/types/KProcess.h>
|
||||
#include "IManagerForApplication.h"
|
||||
#include "IProfile.h"
|
||||
|
@ -52,6 +52,7 @@ namespace skyline::service {
|
||||
audio_IAudioDevice,
|
||||
hid_IHidServer,
|
||||
hid_IAppletResource,
|
||||
hid_IActiveVibrationDeviceList,
|
||||
timesrv_IStaticService,
|
||||
timesrv_ISystemClock,
|
||||
timesrv_ITimeZoneService,
|
||||
|
@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include <input.h>
|
||||
#include "IActiveVibrationDeviceList.h"
|
||||
|
||||
using namespace skyline::input;
|
||||
|
||||
namespace skyline::service::hid {
|
||||
IActiveVibrationDeviceList::IActiveVibrationDeviceList(const DeviceState &state, ServiceManager &manager) : BaseService(state, manager, Service::hid_IActiveVibrationDeviceList, "hid:IActiveVibrationDeviceList", {
|
||||
{0x0, SFUNC(IActiveVibrationDeviceList::ActivateVibrationDevice)}
|
||||
}) {}
|
||||
|
||||
void IActiveVibrationDeviceList::ActivateVibrationDevice(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
|
||||
auto handle = request.Pop<NpadDeviceHandle>();
|
||||
|
||||
if (!handle.isRight)
|
||||
state.input->npad.at(handle.id).vibrationRight = NpadVibrationValue{};
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <kernel/types/KProcess.h>
|
||||
#include <services/base_service.h>
|
||||
#include <services/serviceman.h>
|
||||
|
||||
namespace skyline::service::hid {
|
||||
/**
|
||||
* @brief IActiveVibrationDeviceList is used to activate vibration on certain HID devices (https://switchbrew.org/wiki/HID_services#IActiveVibrationDeviceList)
|
||||
*/
|
||||
class IActiveVibrationDeviceList : public BaseService {
|
||||
public:
|
||||
IActiveVibrationDeviceList(const DeviceState &state, ServiceManager &manager);
|
||||
|
||||
/**
|
||||
* @brief Activates a vibration device with the specified #VibrationDeviceHandle (https://switchbrew.org/wiki/HID_services#ActivateVibrationDevice)
|
||||
*/
|
||||
void ActivateVibrationDevice(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
|
||||
};
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
#include <input.h>
|
||||
#include "IHidServer.h"
|
||||
#include "IActiveVibrationDeviceList.h"
|
||||
|
||||
using namespace skyline::input;
|
||||
|
||||
@ -20,7 +21,9 @@ namespace skyline::service::hid {
|
||||
{0x79, SFUNC(IHidServer::GetNpadJoyHoldType)},
|
||||
{0x7A, SFUNC(IHidServer::SetNpadJoyAssignmentModeSingleByDefault)},
|
||||
{0x7B, SFUNC(IHidServer::SetNpadJoyAssignmentModeSingle)},
|
||||
{0x7C, SFUNC(IHidServer::SetNpadJoyAssignmentModeDual)}
|
||||
{0x7C, SFUNC(IHidServer::SetNpadJoyAssignmentModeDual)},
|
||||
{0xCB, SFUNC(IHidServer::CreateActiveVibrationDeviceList)},
|
||||
{0xCE, SFUNC(IHidServer::SendVibrationValues)}
|
||||
}) {}
|
||||
|
||||
void IHidServer::CreateAppletResource(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
|
||||
@ -105,4 +108,34 @@ namespace skyline::service::hid {
|
||||
state.input->npad.at(id).SetAssignment(NpadJoyAssignment::Dual);
|
||||
state.input->npad.Update();
|
||||
}
|
||||
|
||||
void IHidServer::CreateActiveVibrationDeviceList(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
|
||||
manager.RegisterService(SRVREG(IActiveVibrationDeviceList), session, response);
|
||||
}
|
||||
|
||||
void IHidServer::SendVibrationValues(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
|
||||
request.Skip<u64>(); // appletResourceUserId
|
||||
|
||||
auto &handleBuf = request.inputBuf.at(0);
|
||||
std::span handles(reinterpret_cast<NpadDeviceHandle *>(handleBuf.address), handleBuf.size / sizeof(NpadDeviceHandle));
|
||||
auto &valueBuf = request.inputBuf.at(1);
|
||||
std::span values(reinterpret_cast<NpadVibrationValue *>(valueBuf.address), valueBuf.size / sizeof(NpadVibrationValue));
|
||||
|
||||
for (int i = 0; i < handles.size(); ++i) {
|
||||
auto &handle = handles[i];
|
||||
|
||||
auto &device = state.input->npad.at(handle.id);
|
||||
if (device.type == handle.GetType()) {
|
||||
if (i + 1 != handles.size() && handles[i + 1].id == handle.id && handles[i + 1].isRight && !handle.isRight) {
|
||||
state.logger->Info("Vibration #{}&{} - Handle: 0x{:02X} (0b{:05b}), Vibration: {:.2f}@{:.2f}Hz, {:.2f}@{:.2f}Hz - {:.2f}@{:.2f}Hz, {:.2f}@{:.2f}Hz", i, i + 1, u8(handle.id), u8(handle.type), values[i].amplitudeLow, values[i].frequencyLow, values[i].amplitudeHigh, values[i].frequencyHigh, values[i + 1].amplitudeLow, values[i + 1].frequencyLow, values[i + 1].amplitudeHigh, values[i + 1].frequencyHigh);
|
||||
device.Vibrate(values[i], values[i + 1]);
|
||||
i++;
|
||||
} else {
|
||||
auto &value = values[i];
|
||||
state.logger->Info("Vibration #{} - Handle: 0x{:02X} (0b{:05b}), Vibration: {:.2f}@{:.2f}Hz, {:.2f}@{:.2f}Hz", i, u8(handle.id), u8(handle.type), value.amplitudeLow, value.frequencyLow, value.amplitudeHigh, value.frequencyHigh);
|
||||
device.Vibrate(handle.isRight, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,5 +79,15 @@ namespace skyline::service::hid {
|
||||
* @brief Sets the Joy-Con assignment mode to Dual (https://switchbrew.org/wiki/HID_services#SetNpadJoyAssignmentModeDual)
|
||||
*/
|
||||
void SetNpadJoyAssignmentModeDual(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
|
||||
|
||||
/**
|
||||
* @brief Returns an instance of #IActiveVibrationDeviceList (https://switchbrew.org/wiki/HID_services#CreateActiveVibrationDeviceList)
|
||||
*/
|
||||
void CreateActiveVibrationDeviceList(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
|
||||
|
||||
/**
|
||||
* @brief Send vibration values to an NPad (https://switchbrew.org/wiki/HID_services#SendVibrationValues)
|
||||
*/
|
||||
void SendVibrationValues(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include <span>
|
||||
#include <os.h>
|
||||
#include <gpu/gpfifo.h>
|
||||
#include <kernel/types/KProcess.h>
|
||||
|
@ -6,12 +6,10 @@
|
||||
package emu.skyline
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.ConditionVariable
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@ -42,10 +40,15 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
|
||||
*/
|
||||
private lateinit var input : InputManager
|
||||
|
||||
/**
|
||||
* A map of [Vibrator]s that correspond to [InputManager.controllers]
|
||||
*/
|
||||
private var vibrators = HashMap<Int, Vibrator>()
|
||||
|
||||
/**
|
||||
* A boolean flag denoting the current operation mode of the emulator (Docked = true/Handheld = false)
|
||||
*/
|
||||
private var operationMode : Boolean = true
|
||||
private var operationMode = true
|
||||
|
||||
/**
|
||||
* The surface object used for displaying frames
|
||||
@ -260,7 +263,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
|
||||
shouldFinish = false
|
||||
|
||||
setHalt(true)
|
||||
emulationThread.join()
|
||||
emulationThread.join(1000)
|
||||
|
||||
vibrators.forEach { (_, vibrator) -> vibrator.cancel() }
|
||||
vibrators.clear()
|
||||
|
||||
romFd.close()
|
||||
preferenceFd.close()
|
||||
@ -383,4 +389,35 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
|
||||
|
||||
return super.onGenericMotionEvent(event)
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
fun vibrateDevice(index : Int, timing : LongArray, amplitude : IntArray) {
|
||||
val vibrator = if (vibrators[index] != null) {
|
||||
vibrators[index]!!
|
||||
} else {
|
||||
input.controllers[index]?.rumbleDeviceDescriptor?.let {
|
||||
if (it == "builtin") {
|
||||
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
vibrators[index] = vibrator
|
||||
vibrator
|
||||
} else {
|
||||
for (id in InputDevice.getDeviceIds()) {
|
||||
val device = InputDevice.getDevice(id)
|
||||
if (device.descriptor == input.controllers[index]?.rumbleDeviceDescriptor) {
|
||||
vibrators[index] = device.vibrator
|
||||
device.vibrator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val effect = VibrationEffect.createWaveform(timing, amplitude, 0)
|
||||
vibrator.vibrate(effect)
|
||||
}
|
||||
|
||||
fun clearVibrationDevice(index : Int) {
|
||||
vibrators[index]?.cancel()
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +83,8 @@ class ControllerGeneralItem(val context : ControllerActivity, val type : General
|
||||
else
|
||||
context.getString(R.string.none)
|
||||
}
|
||||
GeneralType.RumbleDevice -> controller.rumbleDevice?.second ?: context.getString(R.string.none)
|
||||
|
||||
GeneralType.RumbleDevice -> controller.rumbleDeviceName ?: context.getString(R.string.none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import java.io.Serializable
|
||||
* @param firstController If the type only applies to the first controller
|
||||
*/
|
||||
enum class ControllerType(val stringRes : Int, val firstController : Boolean, val sticks : Array<StickId> = arrayOf(), val buttons : Array<ButtonId> = arrayOf(), val id : Int) {
|
||||
None(R.string.none, false, id=0b0),
|
||||
None(R.string.none, false, id = 0b0),
|
||||
ProController(R.string.procon, false, arrayOf(StickId.Left, StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR, ButtonId.Plus, ButtonId.Minus), 0b1),
|
||||
HandheldProController(R.string.handheld_procon, true, arrayOf(StickId.Left, StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR, ButtonId.Plus, ButtonId.Minus), 0b10),
|
||||
JoyConLeft(R.string.ljoycon, false, arrayOf(StickId.Left), arrayOf(ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.ZL, ButtonId.Minus, ButtonId.LeftSL, ButtonId.LeftSR), 0b1000),
|
||||
@ -38,9 +38,10 @@ enum class GeneralType(val stringRes : Int, val compatibleControllers : Array<Co
|
||||
*
|
||||
* @param id The ID of the controller
|
||||
* @param type The type of the controller
|
||||
* @param rumbleDevice The device descriptor and the name of the device rumble/force-feedback will be passed onto
|
||||
* @param rumbleDeviceDescriptor The device descriptor of the device rumble/force-feedback will be passed onto
|
||||
* @param rumbleDeviceName The name of the device rumble/force-feedback will be passed onto
|
||||
*/
|
||||
open class Controller(val id : Int, var type : ControllerType, var rumbleDevice : Pair<String, String>? = null) : Serializable {
|
||||
open class Controller(val id : Int, var type : ControllerType, var rumbleDeviceDescriptor : String? = null, var rumbleDeviceName : String? = null) : Serializable {
|
||||
/**
|
||||
* The current version of this class so that different versions won't be deserialized mistakenly
|
||||
*/
|
||||
|
@ -6,8 +6,10 @@
|
||||
package emu.skyline.input.dialog
|
||||
|
||||
import android.animation.LayoutTransition
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.view.*
|
||||
import android.view.animation.LinearInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
@ -50,12 +52,26 @@ class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment
|
||||
|
||||
// Set up the reset button to clear out [Controller.rumbleDevice] when pressed
|
||||
rumble_reset.setOnClickListener {
|
||||
controller.rumbleDevice = null
|
||||
controller.rumbleDeviceDescriptor = null
|
||||
controller.rumbleDeviceName = null
|
||||
item.update()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
if (context.id == 0) {
|
||||
rumble_builtin.visibility = View.VISIBLE
|
||||
if (!(context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).hasVibrator())
|
||||
rumble_builtin.isEnabled = false
|
||||
rumble_builtin.setOnClickListener {
|
||||
controller.rumbleDeviceDescriptor = "builtin"
|
||||
controller.rumbleDeviceName = getString(R.string.builtin_vibrator)
|
||||
item.update()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that layout animations are proper
|
||||
rumble_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
rumble_controller.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
@ -80,7 +96,8 @@ class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
} else {
|
||||
rumble_controller_supported.text = getString(R.string.not_supported)
|
||||
rumble_title.text = getString(R.string.press_any_button)
|
||||
dialog?.setOnKeyListener { _, _, _ -> false }
|
||||
rumble_reset.requestFocus()
|
||||
}
|
||||
|
||||
rumble_controller_icon.animate().apply {
|
||||
@ -97,16 +114,16 @@ class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment
|
||||
vibrator.hasVibrator() -> {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
|
||||
controller.rumbleDevice = Pair(event.device.descriptor, event.device.name)
|
||||
controller.rumbleDeviceDescriptor = event.device.descriptor
|
||||
controller.rumbleDeviceName = event.device.name
|
||||
|
||||
item.update()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// If the currently selected device doesn't have a vibrator then dismiss the dialog entirely
|
||||
else -> {
|
||||
dismiss()
|
||||
return@setOnKeyListener false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,13 +63,29 @@
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/rumble_reset"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/reset" />
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/rumble_builtin"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/builtin"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/rumble_reset"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/reset" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -62,6 +62,8 @@
|
||||
<string name="not_supported">Not Supported</string>
|
||||
<string name="press_any_button">Press any button on a controller</string>
|
||||
<string name="confirm_button_again">Confirm choice by pressing a button again</string>
|
||||
<string name="builtin">Built-in</string>
|
||||
<string name="builtin_vibrator">Built-in Vibrator</string>
|
||||
<string name="reset">Reset</string>
|
||||
<string name="buttons">Buttons</string>
|
||||
<string name="use_button_axis">Use any button or axis on a controller</string>
|
||||
|
@ -8,7 +8,7 @@ buildscript {
|
||||
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
Loading…
Reference in New Issue
Block a user