Write HID LIFO entries at fixed intervals

Certain titles depend on HID LIFO entries being written out at a fixed frequency rather than on actual state change, not doing this can lead to applications freezing till the LIFO is filled up to maximum size, this behavior is seen in Super Mario Odyssey. In other cases such as Metroid Dread, the game can run into race conditions that would lead to crashes, these were worked around by smashing a button during loading prior.

This commit introduces a thread which sleeps and wakes up occasionally to write LIFO entries into HID shared memory at the desired frequencies. This alleviates any issues as it fills up the LIFO instantly and correctly emulates HID Shared Memory behavior expected by the guest.

Co-authored-by: Narr the Reg <juangerman-13@hotmail.com>
This commit is contained in:
PixelyIon 2022-08-31 22:33:42 +05:30
parent 015d633aae
commit 70ad4498a2
7 changed files with 189 additions and 103 deletions

View File

@ -198,6 +198,7 @@ add_library(skyline SHARED
${source_DIR}/skyline/soc/gm20b/engines/maxwell_dma.cpp
${source_DIR}/skyline/soc/gm20b/engines/maxwell/initialization.cpp
${source_DIR}/skyline/soc/gm20b/engines/fermi_2d.cpp
${source_DIR}/skyline/input.cpp
${source_DIR}/skyline/input/npad.cpp
${source_DIR}/skyline/input/npad_device.cpp
${source_DIR}/skyline/input/touch.cpp

View File

@ -0,0 +1,77 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
#include <common/signal.h>
#include <loader/loader.h>
#include <kernel/types/KProcess.h>
#include "input.h"
namespace skyline::input {
Input::Input(const DeviceState &state)
: state{state},
kHid{std::make_shared<kernel::type::KSharedMemory>(state, sizeof(HidSharedMemory))},
hid{reinterpret_cast<HidSharedMemory *>(kHid->host.data())},
npad{state, hid},
touch{state, hid},
updateThread{&Input::UpdateThread, this} {}
void Input::UpdateThread() {
if (int result{pthread_setname_np(pthread_self(), "Sky-Input")})
Logger::Warn("Failed to set the thread name: {}", strerror(result));
try {
signal::SetSignalHandler({SIGINT, SIGILL, SIGTRAP, SIGBUS, SIGFPE, SIGSEGV}, signal::ExceptionalSignalHandler);
struct UpdateCallback {
std::chrono::milliseconds period;
std::chrono::steady_clock::time_point next;
std::function<void(UpdateCallback &)> callback;
UpdateCallback(std::chrono::milliseconds period, std::function<void(UpdateCallback &)> callback)
: period{period}, next{std::chrono::steady_clock::now() + period}, callback{std::move(callback)} {}
void operator()() {
callback(*this);
next += period;
}
};
constexpr std::chrono::milliseconds NPadUpdatePeriod{4}; //!< The period at which a Joy-Con is updated (250Hz)
constexpr std::chrono::milliseconds TouchUpdatePeriod{4}; //!< The period at which the touch screen is updated (250Hz)
std::array<UpdateCallback, 2> updateCallbacks{
UpdateCallback{NPadUpdatePeriod, [&](UpdateCallback &callback) {
for (auto &pad : npad.npads)
pad.UpdateSharedMemory();
}},
UpdateCallback{TouchUpdatePeriod, [&](UpdateCallback &callback) {
touch.UpdateSharedMemory();
}},
};
while (true) {
auto now{std::chrono::steady_clock::now()}, next{updateCallbacks[0].next};
for (auto &callback : updateCallbacks) {
if (now >= callback.next)
callback();
if (callback.next < next)
next = callback.next;
}
std::this_thread::sleep_until(next);
}
} catch (const signal::SignalException &e) {
Logger::Error("{}\nStack Trace:{}", e.what(), state.loader->GetStackTrace(e.frames));
if (state.process)
state.process->Kill(false);
else
std::rethrow_exception(std::current_exception());
} catch (const std::exception &e) {
Logger::Error(e.what());
if (state.process)
state.process->Kill(false);
else
std::rethrow_exception(std::current_exception());
}
}
}

View File

@ -24,11 +24,14 @@ namespace skyline::input {
NpadManager npad;
TouchManager touch;
Input(const DeviceState &state)
: state(state),
kHid(std::make_shared<kernel::type::KSharedMemory>(state, sizeof(HidSharedMemory))),
hid(reinterpret_cast<HidSharedMemory *>(kHid->host.data())),
npad(state, hid),
touch(state, hid) {}
Input(const DeviceState &state);
private:
std::thread updateThread; //!< A thread that handles delivering HID shared memory updates at a fixed rate
/**
* @brief The entry point for the update thread, this handles timing and delegation to the shared memory managers
*/
void UpdateThread();
};
}

View File

@ -146,10 +146,7 @@ namespace skyline::input {
type = newType;
controllerInfo = &GetControllerInfo();
GetNextEntry(*controllerInfo);
GetNextEntry(section.defaultController);
globalTimestamp++;
UpdateSharedMemory();
updateEvent->Signal();
}
@ -186,7 +183,7 @@ namespace skyline::input {
}
}
NpadControllerState &NpadDevice::GetNextEntry(NpadControllerInfo &info) {
void NpadDevice::WriteNextEntry(NpadControllerInfo &info, NpadControllerState entry) {
auto &lastEntry{info.state.at(info.header.currentEntry)};
info.header.timestamp = util::GetTimeTicks();
@ -194,30 +191,33 @@ namespace skyline::input {
info.header.maxEntry = info.header.entryCount;
info.header.currentEntry = (info.header.currentEntry != constant::HidEntryCount - 1) ? info.header.currentEntry + 1 : 0;
auto &entry{info.state.at(info.header.currentEntry)};
auto &nextEntry{info.state.at(info.header.currentEntry)};
entry.globalTimestamp = globalTimestamp;
entry.localTimestamp = lastEntry.localTimestamp + 1;
entry.buttons = lastEntry.buttons;
entry.leftX = lastEntry.leftX;
entry.leftY = lastEntry.leftY;
entry.rightX = lastEntry.rightX;
entry.rightY = lastEntry.rightY;
entry.status.raw = connectionState.raw;
return entry;
nextEntry.globalTimestamp = globalTimestamp;
nextEntry.localTimestamp = lastEntry.localTimestamp + 1;
nextEntry.buttons = entry.buttons;
nextEntry.leftX = entry.leftX;
nextEntry.leftY = entry.leftY;
nextEntry.rightX = entry.rightX;
nextEntry.rightY = entry.rightY;
nextEntry.status.raw = connectionState.raw;
}
void NpadDevice::SetButtonState(NpadButton mask, bool pressed) {
void NpadDevice::UpdateSharedMemory() {
if (!connectionState.connected)
return;
auto &entry{GetNextEntry(*controllerInfo)};
WriteNextEntry(*controllerInfo, controllerState);
WriteNextEntry(section.defaultController, defaultState);
globalTimestamp++;
}
void NpadDevice::SetButtonState(NpadButton mask, bool pressed) {
if (pressed)
entry.buttons.raw |= mask.raw;
controllerState.buttons.raw |= mask.raw;
else
entry.buttons.raw &= ~mask.raw;
controllerState.buttons.raw &= ~mask.raw;
if (manager.orientation == NpadJoyOrientation::Horizontal && (type == NpadControllerType::JoyconLeft || type == NpadControllerType::JoyconRight)) {
NpadButton orientedMask{};
@ -252,109 +252,98 @@ namespace skyline::input {
mask = orientedMask;
}
auto &defaultEntry{GetNextEntry(section.defaultController)};
if (pressed)
defaultEntry.buttons.raw |= mask.raw;
defaultState.buttons.raw |= mask.raw;
else
defaultEntry.buttons.raw &= ~mask.raw;
globalTimestamp++;
defaultState.buttons.raw &= ~mask.raw;
}
void NpadDevice::SetAxisValue(NpadAxisId axis, i32 value) {
if (!connectionState.connected)
return;
auto &controllerEntry{GetNextEntry(*controllerInfo)};
auto &defaultEntry{GetNextEntry(section.defaultController)};
constexpr i16 threshold{std::numeric_limits<i16>::max() / 2}; // A 50% deadzone for the stick buttons
if (manager.orientation == NpadJoyOrientation::Vertical || (type != NpadControllerType::JoyconLeft && type != NpadControllerType::JoyconRight)) {
switch (axis) {
case NpadAxisId::LX:
controllerEntry.leftX = value;
defaultEntry.leftX = value;
controllerState.leftX = value;
defaultState.leftX = value;
controllerEntry.buttons.leftStickLeft = controllerEntry.leftX <= -threshold;
defaultEntry.buttons.leftStickLeft = controllerEntry.buttons.leftStickLeft;
controllerState.buttons.leftStickLeft = controllerState.leftX <= -threshold;
defaultState.buttons.leftStickLeft = controllerState.buttons.leftStickLeft;
controllerEntry.buttons.leftStickRight = controllerEntry.leftX >= threshold;
defaultEntry.buttons.leftStickRight = controllerEntry.buttons.leftStickRight;
controllerState.buttons.leftStickRight = controllerState.leftX >= threshold;
defaultState.buttons.leftStickRight = controllerState.buttons.leftStickRight;
break;
case NpadAxisId::LY:
controllerEntry.leftY = value;
defaultEntry.leftY = value;
controllerState.leftY = value;
defaultState.leftY = value;
defaultEntry.buttons.leftStickUp = controllerEntry.buttons.leftStickUp;
controllerEntry.buttons.leftStickUp = controllerEntry.leftY >= threshold;
defaultState.buttons.leftStickUp = controllerState.buttons.leftStickUp;
controllerState.buttons.leftStickUp = controllerState.leftY >= threshold;
controllerEntry.buttons.leftStickDown = controllerEntry.leftY <= -threshold;
defaultEntry.buttons.leftStickDown = controllerEntry.buttons.leftStickDown;
controllerState.buttons.leftStickDown = controllerState.leftY <= -threshold;
defaultState.buttons.leftStickDown = controllerState.buttons.leftStickDown;
break;
case NpadAxisId::RX:
controllerEntry.rightX = value;
defaultEntry.rightX = value;
controllerState.rightX = value;
defaultState.rightX = value;
controllerEntry.buttons.rightStickLeft = controllerEntry.rightX <= -threshold;
defaultEntry.buttons.rightStickLeft = controllerEntry.buttons.rightStickLeft;
controllerState.buttons.rightStickLeft = controllerState.rightX <= -threshold;
defaultState.buttons.rightStickLeft = controllerState.buttons.rightStickLeft;
controllerEntry.buttons.rightStickRight = controllerEntry.rightX >= threshold;
defaultEntry.buttons.rightStickRight = controllerEntry.buttons.rightStickRight;
controllerState.buttons.rightStickRight = controllerState.rightX >= threshold;
defaultState.buttons.rightStickRight = controllerState.buttons.rightStickRight;
break;
case NpadAxisId::RY:
controllerEntry.rightY = value;
defaultEntry.rightY = value;
controllerState.rightY = value;
defaultState.rightY = value;
controllerEntry.buttons.rightStickUp = controllerEntry.rightY >= threshold;
defaultEntry.buttons.rightStickUp = controllerEntry.buttons.rightStickUp;
controllerState.buttons.rightStickUp = controllerState.rightY >= threshold;
defaultState.buttons.rightStickUp = controllerState.buttons.rightStickUp;
controllerEntry.buttons.rightStickDown = controllerEntry.rightY <= -threshold;
defaultEntry.buttons.rightStickDown = controllerEntry.buttons.rightStickDown;
controllerState.buttons.rightStickDown = controllerState.rightY <= -threshold;
defaultState.buttons.rightStickDown = controllerState.buttons.rightStickDown;
break;
}
} else {
switch (axis) {
case NpadAxisId::LX:
controllerEntry.leftY = value;
controllerEntry.buttons.leftStickUp = controllerEntry.leftY >= threshold;
controllerEntry.buttons.leftStickDown = controllerEntry.leftY <= -threshold;
controllerState.leftY = value;
controllerState.buttons.leftStickUp = controllerState.leftY >= threshold;
controllerState.buttons.leftStickDown = controllerState.leftY <= -threshold;
defaultEntry.leftX = value;
defaultEntry.buttons.leftStickLeft = defaultEntry.leftX <= -threshold;
defaultEntry.buttons.leftStickRight = defaultEntry.leftX >= threshold;
defaultState.leftX = value;
defaultState.buttons.leftStickLeft = defaultState.leftX <= -threshold;
defaultState.buttons.leftStickRight = defaultState.leftX >= threshold;
break;
case NpadAxisId::LY:
controllerEntry.leftX = -value;
controllerEntry.buttons.leftStickLeft = controllerEntry.leftX <= -threshold;
controllerEntry.buttons.leftStickRight = controllerEntry.leftX >= threshold;
controllerState.leftX = -value;
controllerState.buttons.leftStickLeft = controllerState.leftX <= -threshold;
controllerState.buttons.leftStickRight = controllerState.leftX >= threshold;
defaultEntry.leftY = value;
defaultEntry.buttons.leftStickUp = defaultEntry.leftY >= threshold;
defaultEntry.buttons.leftStickDown = defaultEntry.leftY <= -threshold;
defaultState.leftY = value;
defaultState.buttons.leftStickUp = defaultState.leftY >= threshold;
defaultState.buttons.leftStickDown = defaultState.leftY <= -threshold;
break;
case NpadAxisId::RX:
controllerEntry.rightY = value;
controllerEntry.buttons.rightStickUp = controllerEntry.rightY >= threshold;
controllerEntry.buttons.rightStickDown = controllerEntry.rightY <= -threshold;
controllerState.rightY = value;
controllerState.buttons.rightStickUp = controllerState.rightY >= threshold;
controllerState.buttons.rightStickDown = controllerState.rightY <= -threshold;
defaultEntry.rightX = value;
defaultEntry.buttons.rightStickLeft = defaultEntry.rightX <= -threshold;
defaultEntry.buttons.rightStickRight = defaultEntry.rightX >= threshold;
defaultState.rightX = value;
defaultState.buttons.rightStickLeft = defaultState.rightX <= -threshold;
defaultState.buttons.rightStickRight = defaultState.rightX >= threshold;
break;
case NpadAxisId::RY:
controllerEntry.rightX = -value;
controllerEntry.buttons.rightStickLeft = controllerEntry.rightX <= -threshold;
controllerEntry.buttons.rightStickRight = controllerEntry.rightX >= threshold;
controllerState.rightX = -value;
controllerState.buttons.rightStickLeft = controllerState.rightX <= -threshold;
controllerState.buttons.rightStickRight = controllerState.rightX >= threshold;
defaultEntry.rightY = value;
defaultEntry.buttons.rightStickUp = defaultEntry.rightY >= threshold;
defaultEntry.buttons.rightStickDown = defaultEntry.rightY <= -threshold;
defaultState.rightY = value;
defaultState.buttons.rightStickUp = defaultState.rightY >= threshold;
defaultState.buttons.rightStickDown = defaultState.rightY <= -threshold;
break;
}
}
globalTimestamp++;
}
constexpr jlong MsInSecond{1000}; //!< The amount of milliseconds in a single second of time

View File

@ -136,13 +136,14 @@ namespace skyline::input {
NpadSection &section; //!< The section in HID shared memory for this controller
NpadControllerInfo *controllerInfo{}; //!< The NpadControllerInfo for this controller's type
u64 globalTimestamp{}; //!< An incrementing timestamp that's common across all sections
NpadControllerState controllerState{}, defaultState{}; //!< The current state of the controller (normal and default)
/**
* @brief Updates the headers and creates a new entry in HID Shared Memory
* @brief Updates the headers and writes a new entry in HID Shared Memory
* @param info The controller info of the NPad that needs to be updated
* @return The next entry that has been created with values from the last entry
* @param entry An entry with the state of the controller
*/
NpadControllerState &GetNextEntry(NpadControllerInfo &info);
void WriteNextEntry(NpadControllerInfo &info, NpadControllerState entry);
/**
* @return The NpadControllerInfo for this controller based on its type
@ -180,6 +181,11 @@ namespace skyline::input {
*/
void Disconnect();
/**
* @brief Writes the current state of the controller to HID shared memory
*/
void UpdateSharedMemory();
/**
* @brief Changes the state of buttons to the specified state
* @param mask A bit-field mask of all the buttons to change

View File

@ -16,20 +16,12 @@ namespace skyline::input {
}
void TouchManager::SetState(span<TouchScreenPoint> touchPoints) {
if (!activated)
return;
const auto &lastEntry{section.entries[section.header.currentEntry]};
auto entryIndex{(section.header.currentEntry != constant::HidEntryCount - 1) ? section.header.currentEntry + 1 : 0};
auto &entry{section.entries[entryIndex]};
touchPoints = touchPoints.first(std::min(touchPoints.size(), entry.data.size()));
entry.globalTimestamp = lastEntry.globalTimestamp + 1;
entry.localTimestamp = lastEntry.localTimestamp + 1;
entry.touchCount = touchPoints.size();
touchPoints = touchPoints.first(std::min(touchPoints.size(), screenState.data.size()));
screenState.touchCount = touchPoints.size();
for (size_t i{}; i < touchPoints.size(); i++) {
const auto &host{touchPoints[i]};
auto &guest{entry.data[i]};
auto &guest{screenState.data[i]};
guest.attribute.raw = static_cast<u32>(host.attribute);
guest.index = static_cast<u32>(host.id);
guest.positionX = static_cast<u32>(host.x);
@ -40,8 +32,20 @@ namespace skyline::input {
}
// Clear unused touch points
for (size_t i{touchPoints.size()}; i < entry.data.size(); i++)
entry.data[i] = {};
for (size_t i{touchPoints.size()}; i < screenState.data.size(); i++)
screenState.data[i] = {};
}
void TouchManager::UpdateSharedMemory() {
if (!activated)
return;
const auto &lastEntry{section.entries[section.header.currentEntry]};
auto entryIndex{(section.header.currentEntry != constant::HidEntryCount - 1) ? section.header.currentEntry + 1 : 0};
auto &entry{section.entries[entryIndex]};
entry = screenState;
entry.globalTimestamp = lastEntry.globalTimestamp + 1;
entry.localTimestamp = lastEntry.localTimestamp + 1;
section.header.timestamp = util::GetTimeTicks();
section.header.entryCount = std::min(static_cast<u8>(section.header.entryCount + 1), constant::HidEntryCount);

View File

@ -30,6 +30,7 @@ namespace skyline::input {
const DeviceState &state;
bool activated{};
TouchScreenSection &section;
TouchScreenState screenState{}; //!< The current state of the touch screen
public:
/**
@ -40,5 +41,10 @@ namespace skyline::input {
void Activate();
void SetState(span<TouchScreenPoint> touchPoints);
/**
* @brief Writes the current state of the touch screen to HID shared memory
*/
void UpdateSharedMemory();
};
}