Files

503 lines
26 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#include "devices.h"
#include "log_pipe.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <numbers>
#include <stdexcept>
#include <time.h>
#include "common/input.h"
#include "common/logging.h"
#include "input_common/input_poller.h"
namespace {
// Signed-in users; unused entries are -1.
struct LoginUserIdList {
std::int32_t user_id[4];
};
}
extern "C" int sceUserServiceGetForegroundUser(int* user);
extern "C" int sceUserServiceGetLoginUserIdList(LoginUserIdList* list);
namespace Eden {
namespace Performance {
void ReportInputThreadGpuLiveness();
}
// Look for newly signed-in (or signed-out) users about once a second at the 4 ms poll interval.
constexpr unsigned kPollsPerScan = 250;
// The touchpad is Select (the guest's Minus) as well as the shortcut key. So that a shortcut
// never reaches the game as a press of Minus, a tap presses Minus when the touchpad is released
// (for about 100 ms), and a press held this long (about a quarter of a second) holds Minus.
constexpr unsigned kSelectTapPolls = 25;
constexpr unsigned kSelectHoldPolls = 60;
// Eden's virtual gamepad numbers the right Joy-Con's SL and SR after its named buttons
// (hid_core, emulated_controller.cpp: virtual_button_params).
constexpr int kRightSL = 20;
constexpr int kRightSR = 21;
PadEngine::PadEngine(std::string name) : InputEngine(std::move(name)) {
// 2 means no value has been published yet, so even the first neutral
// state propagates to Eden; later identical 4 ms samples are suppressed.
for (auto& buttons : button_cache)
buttons.fill(2);
for (std::size_t player = 0; player < kPlayers; ++player)
PreSetController(Identifier(player));
}
PadIdentifier PadEngine::Identifier(std::size_t player) const {
return {.guid = Common::UUID{}, .port = player, .pad = 0};
}
void PadEngine::SetButtonState(std::size_t player, int button, bool value) {
if (player >= kPlayers) return;
if (button >= 0 && button < kTrackedButtons) {
const auto state = static_cast<std::uint8_t>(value ? 1 : 0);
auto& previous = button_cache[player][static_cast<std::size_t>(button)];
if (previous == state) return;
previous = state;
// Count only actual rising edges delivered to virtual input, not
// repeated scePadRead samples. Never count a publishing attempt as
// proof that FC27 read the Switch Npad shared-memory event.
if (player == 0 && value && NativeLogs::Detailed()) {
if (button == static_cast<int>(VirtualButton::ButtonPlus))
published_plus_edges.fetch_add(1, std::memory_order_relaxed);
else if (button == static_cast<int>(VirtualButton::ButtonHome))
published_home_edges.fetch_add(1, std::memory_order_relaxed);
else if (button == static_cast<int>(VirtualButton::ButtonCapture))
published_capture_edges.fetch_add(1, std::memory_order_relaxed);
}
}
// Inputs outside the known virtual-button subset remain visible to
// the underlying engine rather than silently losing a future mapping.
SetButton(Identifier(player), button, value);
}
void PadEngine::SetButtonState(std::size_t player, VirtualButton button, bool value) {
SetButtonState(player, static_cast<int>(button), value);
}
void PadEngine::SetStickPosition(std::size_t player, int axis, float x, float y) {
if (player >= kPlayers) return;
const int base = axis * 2;
const auto publish = [&](int physical_axis, float value) {
if (physical_axis >= 0 && physical_axis < 4) {
const auto index = static_cast<std::size_t>(physical_axis);
if (axis_known[player][index] && axis_cache[player][index] == value)
return;
axis_cache[player][index] = value;
axis_known[player][index] = true;
}
SetAxis(Identifier(player), physical_axis, value);
};
publish(base, x);
publish(base + 1, y);
}
void PadEngine::SetMotionState(std::size_t player, u64 delta_us, float gyro_x, float gyro_y, float gyro_z,
float accel_x, float accel_y, float accel_z) {
if (player >= kPlayers) return;
SetMotion(Identifier(player), 0, {.gyro_x = gyro_x, .gyro_y = gyro_y, .gyro_z = gyro_z,
.accel_x = accel_x, .accel_y = accel_y, .accel_z = accel_z,
.delta_timestamp = delta_us});
}
void PadEngine::ResetControllers() {
for (std::size_t player = 0; player < kPlayers; ++player) {
SetStickPosition(player, 0, 0.0f, 0.0f);
SetStickPosition(player, 1, 0.0f, 0.0f);
for (int button = 0; button <= kRightSR; ++button)
SetButtonState(player, button, false);
SetMotionAtRest(player);
}
}
void PadEngine::SetMotionAtRest(std::size_t player) {
// Level and still: gravity along -Z.
SetMotionState(player, 0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f);
}
Common::Input::DriverResult PadEngine::SetVibration(const PadIdentifier& identifier,
const Common::Input::VibrationStatus& vibration) {
// The handheld controller (index 8) is played on player 1's DualSense.
const std::size_t player = identifier.port == 8 ? 0 : identifier.port;
if (player >= kPlayers) return Common::Input::DriverResult::InvalidParameters;
std::scoped_lock lock(rumble_mutex);
auto& sides = rumble[player];
(identifier.pad == 2 ? sides.right : sides.left) = vibration;
sides.changed = true;
rumble_pending[player].store(true, std::memory_order_release);
return Common::Input::DriverResult::Success;
}
bool PadEngine::TakeRumble(std::size_t player, Rumble& out) {
if (player >= kPlayers) return false;
// A guest rumble request may originate on a CPU/HID worker. This acquire
// pairs with its release store, avoiding the mutex on silent polls.
if (!rumble_pending[player].load(std::memory_order_acquire))
return false;
std::scoped_lock lock(rumble_mutex);
auto& sides = rumble[player];
if (!sides.changed) return false;
sides.changed = false;
// Protected by the same mutex as SetVibration; a concurrent new command
// cannot be lost between examining 'changed' and clearing the hint.
rumble_pending[player].store(false, std::memory_order_release);
// The console's HD rumble per side -> one DualSense: the low band drives the large motor and the
// high band the small one, with the amplitude curves of Eden's SDL driver.
const auto level = [](float amplitude, Common::Input::VibrationAmplificationType type) {
const float factor = type == Common::Input::VibrationAmplificationType::Linear ? 0.5f : 0.35f;
amplitude = std::clamp(amplitude, 0.0f, 1.0f);
return static_cast<u8>(std::lround((amplitude + std::pow(amplitude, factor)) * 0.5f * 255.0f));
};
out.large = std::max(level(sides.left.low_amplitude, sides.left.type),
level(sides.right.low_amplitude, sides.right.type));
out.small = std::max(level(sides.left.high_amplitude, sides.left.type),
level(sides.right.high_amplitude, sides.right.type));
return true;
}
Pad::Pad(float deadzone_, float threshold)
: engine{std::make_shared<PadEngine>("virtual_gamepad")},
deadzone{deadzone_}, trigger_threshold{threshold} {
if (!std::isfinite(deadzone) || deadzone < 0 || deadzone >= 1 ||
!std::isfinite(threshold) || threshold <= 0 || threshold > 1)
throw std::invalid_argument("Invalid pad calibration");
Common::Input::RegisterInputFactory("virtual_gamepad",
std::make_shared<InputCommon::InputFactory>(engine));
Common::Input::RegisterOutputFactory("virtual_gamepad",
std::make_shared<InputCommon::OutputFactory>(engine));
}
Pad::~Pad() {
Close();
Common::Input::UnregisterInputFactory("virtual_gamepad");
Common::Input::UnregisterOutputFactory("virtual_gamepad");
}
bool Pad::Open() {
if (slots[0].handle >= 0) return true;
owns_user_service = sceUserServiceInitialize(nullptr) == 0;
int user = -1;
// Player 1 is the application user, as for the title-aware launch controller.
if (sceUserServiceGetForegroundUser(&user) < 0 || user < 0 || scePadInit() < 0) {
Close();
return false;
}
OpenSlot(0, user);
if (slots[0].handle < 0) { Close(); return false; }
int initial_user = -1;
const int initial_result = sceUserServiceGetInitialUser(&initial_user);
LOG_INFO(Input, "EDEN_PAD_USERS foreground={} initial={} initial_rc={}", user, initial_user, initial_result);
Rescan();
return true;
}
void Pad::OpenSlot(std::size_t player, int user) {
int handle = scePadOpen(user, ps5::pad::kPortTypeStandard, 0, nullptr);
// A handle the process already holds for this user (e.g. from the launcher) is reused.
if (handle < 0) handle = scePadGetHandle(user, ps5::pad::kPortTypeStandard, 0);
if (handle < 0) {
LOG_WARNING(Input, "EDEN_PAD_OPEN_FAILED player={} user={} result={:#x}", player + 1, user,
static_cast<unsigned>(handle));
return;
}
slots[player] = {user, handle, 0, 0};
// Classic two-motor rumble, and motion reports (on by default; failures only lose the feature).
const int vibration_mode = scePadSetVibrationMode(handle, ps5::pad::kVibrationModeCompatible);
const int motion = scePadSetMotionSensorState(handle, true);
if (vibration_mode < 0 || motion < 0)
LOG_INFO(Input, "EDEN_PAD_FEEDBACK player={} vibration_mode={:#x} motion={:#x}", player + 1,
static_cast<unsigned>(vibration_mode), static_cast<unsigned>(motion));
connected_players |= 1u << player;
connection_changes |= 1u << player;
LOG_INFO(Input, "EDEN_PAD_OPEN player={} user={} handle={}", player + 1, user, handle);
}
void Pad::CloseSlot(std::size_t player) {
auto& slot = slots[player];
if (slot.handle < 0) return;
const auto neutral = ps5::pad::neutral_data();
Consume(player, {&neutral, 1});
const ps5::pad::Vibration stop{};
(void)scePadSetVibration(slot.handle, &stop);
const int closed = scePadClose(slot.handle);
LOG_INFO(Input, "EDEN_PAD_CLOSE player={} user={} close={}", player + 1, slot.user, closed);
slot = {};
connected_players &= ~(1u << player);
connection_changes |= 1u << player;
}
void Pad::Rescan() {
LoginUserIdList list;
std::fill(std::begin(list.user_id), std::end(list.user_id), -1);
if (sceUserServiceGetLoginUserIdList(&list) < 0) return;
const auto signed_in = [&](int user) {
return std::find(std::begin(list.user_id), std::end(list.user_id), user) != std::end(list.user_id);
};
// Player 1 stays with the launching user; other players leave when their user signs out.
for (std::size_t player = 1; player < kMaxPlayers; ++player) {
if (slots[player].handle >= 0 && !signed_in(slots[player].user)) CloseSlot(player);
}
for (const int user : list.user_id) {
if (user < 0 || std::any_of(slots.begin(), slots.end(), [user](const Slot& slot) {
return slot.handle >= 0 && slot.user == user; }))
continue;
for (std::size_t player = 1; player < kMaxPlayers; ++player) {
if (slots[player].handle < 0) {
OpenSlot(player, user);
break;
}
}
}
}
void Pad::Close() {
engine->ResetControllers();
for (std::size_t player = 0; player < kMaxPlayers; ++player) {
if (slots[player].handle < 0) continue;
const ps5::pad::Vibration stop{};
(void)scePadSetVibration(slots[player].handle, &stop);
const int closed = scePadClose(slots[player].handle);
if (player == 0) {
LOG_INFO(Input, "EDEN_PAD_CLOSED polls={} samples={} usable={} intercepted={} circle={} errors={} last={} close={} "
"rumble={} rumble_errors={} rumble_last={:#x}",
polls, samples_read, usable_samples, intercepted_samples, circle_samples, read_errors, last_result, closed,
rumble_updates, rumble_errors, static_cast<unsigned>(rumble_last_error));
}
slots[player] = {};
}
connected_players = 0;
connection_changes = 0;
if (owns_user_service) { sceUserServiceTerminate(); owns_user_service = false; }
}
bool Pad::Poll() {
if (slots[0].handle >= 0 && ++polls_since_scan >= kPollsPerScan) {
polls_since_scan = 0;
Rescan();
}
++polls;
bool primary = false;
std::array<ps5::pad::Data, ps5::pad::kMaxSamples> samples{};
for (std::size_t player = 0; player < kMaxPlayers; ++player) {
const int handle = slots[player].handle;
if (handle < 0) continue;
PadEngine::Rumble rumble;
if (engine->TakeRumble(player, rumble)) {
const ps5::pad::Vibration vibration{.large_motor = rumble.large, .small_motor = rumble.small};
const int result = scePadSetVibration(handle, &vibration);
++rumble_updates;
if (result < 0) { ++rumble_errors; rumble_last_error = result; }
}
const int count = scePadRead(handle, samples.data(), samples.size());
if (player == 0) last_result = count;
if (count < 0 || count > static_cast<int>(samples.size())) {
++read_errors;
const auto neutral = ps5::pad::neutral_data();
Consume(player, {&neutral, 1});
continue;
}
for (int i = 0; i < count; ++i) {
++samples_read;
usable_samples += ps5::pad::is_usable(samples[i]);
intercepted_samples += (samples[i].buttons & ps5::pad::kButtonIntercepted) != 0;
circle_samples += ps5::pad::is_usable(samples[i]) && (samples[i].buttons & ps5::pad::kButtonCircle);
}
Consume(player, {samples.data(), static_cast<std::size_t>(count)});
primary |= player == 0;
}
// R332: a single low-frequency heartbeat proves the input worker is alive
// even when scePadRead returns zero samples. Never log on every 4 ms poll;
// never flush the log pipe from the input thread.
// Always check the shipping GPU heartbeat, even without verbose pad logs.
// No locks, per-frame probes, or logging unless its report has stalled.
if (polls % 1250 == 0) Performance::ReportInputThreadGpuLiveness();
if (NativeLogs::Detailed() && polls % 1250 == 0) {
// One timestamp pair per heartbeat, never on the 4ms pad hot path.
// The two logs share this clock anchor for frame/pad correlation.
timespec utc{}, monotonic{};
const int utc_rc = clock_gettime(CLOCK_REALTIME, &utc);
const int mono_rc = clock_gettime(CLOCK_MONOTONIC, &monotonic);
const long long epoch_ms = utc_rc == 0 ?
static_cast<long long>(utc.tv_sec) * 1000 + utc.tv_nsec / 1000000 : -1;
const long long mono_ns = mono_rc == 0 ?
static_cast<long long>(monotonic.tv_sec) * 1000000000LL + monotonic.tv_nsec : -1;
std::fprintf(stderr,
"EDEN_PAD_HEARTBEAT polls=%llu samples=%llu usable=%llu intercepted=%llu "
"errors=%llu last_read=%d connected=%u last_buttons=%08x "
"edges=%llu cross=%llu circle=%llu square=%llu triangle=%llu map_x=%d map_y=%d "
"epoch_ms=%lld mono_ns=%lld\n",
static_cast<unsigned long long>(polls),
static_cast<unsigned long long>(samples_read),
static_cast<unsigned long long>(usable_samples),
static_cast<unsigned long long>(intercepted_samples),
static_cast<unsigned long long>(read_errors), last_result,
connected_players.load(std::memory_order_relaxed), slots[0].last_buttons,
static_cast<unsigned long long>(face_edges.changes),
static_cast<unsigned long long>(face_edges.cross),
static_cast<unsigned long long>(face_edges.circle),
static_cast<unsigned long long>(face_edges.square),
static_cast<unsigned long long>(face_edges.triangle),
ActiveMapping()[game_x], ActiveMapping()[game_y], epoch_ms, mono_ns);
// Compact extra read-only diagnostic: physical Options -> virtual
// Plus publication. The missing final Npad consumption remains a
// separate layer and must not be inferred from these counters.
std::fprintf(stderr,
"EDEN_PAD_INPUT_CHAIN polls=%llu options_edges=%llu published_plus_edges=%llu "
"sony_intercepted_samples=%llu create_edges=%llu "
"published_home_edges=%llu published_capture_edges=%llu "
"epoch_ms=%lld mono_ns=%lld\n",
static_cast<unsigned long long>(polls),
static_cast<unsigned long long>(options_edges),
static_cast<unsigned long long>(engine->PublishedPlusEdges()),
static_cast<unsigned long long>(intercepted_samples),
static_cast<unsigned long long>(create_edges),
static_cast<unsigned long long>(engine->PublishedHomeEdges()),
static_cast<unsigned long long>(engine->PublishedCaptureEdges()),
epoch_ms, mono_ns);
}
return primary;
}
void Pad::Consume(std::size_t player, std::span<const ps5::pad::Data> samples) {
using namespace ps5::pad;
using Button = InputCommon::VirtualGamepad::VirtualButton;
// The game's buttons and the DualSense buttons, in the order of button_mapping.h.
static constexpr Button kGame[kGameButtons] = {
Button::ButtonA, Button::ButtonB, Button::ButtonX, Button::ButtonY, Button::TriggerL,
Button::TriggerR, Button::TriggerZL, Button::TriggerZR, Button::ButtonPlus, Button::ButtonMinus,
Button::StickL, Button::StickR};
static constexpr ButtonMask kPad[kPadButtons] = {
kButtonCross, kButtonCircle, kButtonSquare, kButtonTriangle, kButtonL1, kButtonR1, kButtonL2,
kButtonR2, kButtonL3, kButtonR3, kButtonOptions, kButtonCreate, kButtonTouchPad};
static constexpr std::pair<ButtonMask, Button> fixed[] = {
{kButtonLeft, Button::ButtonLeft}, {kButtonUp, Button::ButtonUp},
{kButtonRight, Button::ButtonRight}, {kButtonDown, Button::ButtonDown},
// SL and SR, the shoulder buttons of a single Joy-Con held sideways: L1 and R1 press them
// too. A game that takes single Joy-Cons asks for SL + SR on its controller screen, and
// nothing pressed them before. Eden passes them to the game only for that controller
// style (left Joy-Con: virtual buttons 16 and 17, right Joy-Con: kRightSL and kRightSR).
{kButtonL1, Button::ButtonSL}, {kButtonR1, Button::ButtonSR},
{kButtonL1, static_cast<Button>(kRightSL)}, {kButtonR1, static_cast<Button>(kRightSR)},
};
auto& slot = slots[player];
// Only the touchpad presses Minus (tap or hold). Physical Create now
// provides virtual Switch Capture, not a second, surprise Minus press.
const auto set_touch = [&](u32 pressed) {
const int touch_button = MappedTo(ActiveMapping(), pad_touchpad);
if (touch_button < 0) return;
engine->SetButtonState(player, kGame[touch_button],
!slot.guest_home.BlockChordButtons() &&
(slot.select_held || slot.select_pulse > 0));
};
const auto axis = [this](u8 raw) {
const float x = (static_cast<int>(raw) - 128) / (raw < 128 ? 128.0f : 127.0f);
return std::abs(x) <= deadzone ? 0.0f :
std::copysign((std::abs(x) - deadzone) / (1.0f - deadzone), x);
};
auto& last_buttons = slots[player].last_buttons;
for (const auto& raw : samples) {
auto sample = is_usable(raw) ? raw : neutral_data(raw.timestamp_us);
// The guest title owns its saved gameplay assignments. Publish the
// same physical-to-guest buttons in match, pause, and cinematics.
constexpr ButtonMask menu_chord = kButtonTouchPad | kButtonL1;
constexpr ButtonMask hud_chord = kButtonTouchPad | kButtonR1;
const auto pressed = sample.buttons;
// The launcher/menu and HUD shortcuts still work from every controller.
if ((pressed & menu_chord) == menu_chord &&
(last_buttons & menu_chord) != menu_chord)
return_to_menu = true;
if ((pressed & hud_chord) == hud_chord &&
(last_buttons & hud_chord) != hud_chord)
hud_toggle = true;
if ((pressed & menu_chord) == menu_chord || (pressed & hud_chord) == hud_chord)
sample.buttons &= ~(kButtonTouchPad | kButtonL1 | kButtonR1);
// libScePad does not expose the physical Sony PS/Home button.
// Touchpad + Options is a local guest HOME substitute, NEVER a
// Nintendo system menu shortcut. It suppresses guest Plus and Minus
// until both underlying controls are released.
const bool guest_home = slot.guest_home.Observe(
(pressed & kButtonTouchPad) != 0,
(pressed & kButtonOptions) != 0,
(pressed & (kButtonL1 | kButtonR1)) != 0);
if (slot.guest_home.BlockChordButtons()) {
sample.buttons &= ~(kButtonTouchPad | kButtonOptions);
// A tap pulse from an earlier interaction must not leak through
// during the guest HOME chord or immediately after its release.
slot.select_pulse = 0;
slot.select_held = false;
}
engine->SetButtonState(player, Button::ButtonHome, guest_home);
// The touchpad on its own: see kSelectTapPolls.
const bool touch = (pressed & kButtonTouchPad) != 0;
const bool touched = (last_buttons & kButtonTouchPad) != 0;
if (touch && !touched) {
slot.touch_chord = false;
slot.touch_polls = 0;
}
if (touch && (pressed & (kButtonL1 | kButtonR1 | kButtonOptions)) != 0) {
slot.touch_chord = true;
slot.select_held = false;
}
if (!touch && touched) {
if (!slot.touch_chord && !slot.select_held) slot.select_pulse = kSelectTapPolls + 1;
slot.select_held = false;
}
if (!is_usable(raw)) {
// A controller that went away neither tapped nor holds anything.
slot.select_pulse = 0;
slot.select_held = false;
}
#ifdef EDEN_DEV_PROFILE
// Diagnostic input trace only. Per-button fprintf in a shipping 4 ms
// poll loop adds avoidable pipe/log pressure during a match.
if (player == 0 && pressed != last_buttons) {
std::fprintf(stderr,
"EDEN_PAD_EVENT player=1 previous=%08x buttons=%08x timestamp_us=%llu usable=%d\n",
last_buttons, pressed, static_cast<unsigned long long>(raw.timestamp_us),
is_usable(raw) ? 1 : 0);
}
#endif
// R332: record physical rising edges, not guest button labels, so a
// missing Square/Triangle can be isolated from in-game remapping.
// Quiet sessions never pay for or publish these counters.
if (player == 0 && is_usable(raw) && NativeLogs::Detailed()) {
face_edges.Observe(last_buttons, pressed, kButtonCross, kButtonCircle,
kButtonSquare, kButtonTriangle);
if ((pressed & kButtonOptions) && !(last_buttons & kButtonOptions))
++options_edges;
if ((pressed & kButtonCreate) && !(last_buttons & kButtonCreate))
++create_edges;
}
last_buttons = pressed;
for (const auto [mask, button] : fixed)
engine->SetButtonState(player, button, (sample.buttons & mask) != 0);
// The game's buttons are digital: the analog triggers count from the threshold.
const bool left = sample.triggers.l2 / 255.0f >= trigger_threshold;
const bool right = sample.triggers.r2 / 255.0f >= trigger_threshold;
const auto& active_mapping = ActiveMapping();
for (int game = 0; game < kGameButtons; ++game) {
const int pad = active_mapping[game];
if (pad == pad_touchpad) continue; // set_touch
engine->SetButtonState(player, kGame[game], (sample.buttons & kPad[pad]) != 0 ||
(pad == pad_l2 && left) || (pad == pad_r2 && right));
}
set_touch(sample.buttons);
// Respect custom layouts that explicitly map Create to another guest
// action. Only otherwise does physical Create become Switch Capture.
engine->SetButtonState(player, Button::ButtonCapture,
MappedTo(active_mapping, pad_create) < 0 && (sample.buttons & kButtonCreate) != 0);
engine->SetStickPosition(player, 0, axis(sample.left_stick.x), -axis(sample.left_stick.y));
engine->SetStickPosition(player, 1, axis(sample.right_stick.x), -axis(sample.right_stick.y));
// Motion: acceleration in G and angular velocity in rad/s, mapped to Eden's axes and units
// the way its SDL driver maps a DualSense.
auto& last_motion = slots[player].last_motion_us;
if (!is_usable(raw)) {
// Away (disconnected, or input taken by the system): stop, rather than keep turning.
if (last_motion) engine->SetMotionAtRest(player);
last_motion = 0;
} else if (raw.timestamp_us > last_motion) {
const u64 delta = last_motion ? raw.timestamp_us - last_motion : 0;
last_motion = raw.timestamp_us;
if (delta > 0 && delta < 1'000'000) {
constexpr float turn = 2.0f * std::numbers::pi_v<float>;
const auto& a = raw.acceleration;
const auto& w = raw.angular_velocity;
engine->SetMotionState(player, delta, w.x / turn, -w.z / turn, w.y / turn, -a.x, a.z, -a.y);
}
}
}
// Once per poll: a touchpad held long enough becomes a held press, and a tap's press runs out.
if ((last_buttons & kButtonTouchPad) != 0 && !slot.touch_chord && !slot.select_held &&
++slot.touch_polls >= kSelectHoldPolls)
slot.select_held = true;
if (slot.select_pulse > 0) --slot.select_pulse;
set_touch(last_buttons);
}
}