Files
lcs-recomp/lcs/host/display_window.cpp
2026-09-24 19:34:17 -03:00

581 lines
23 KiB
C++

#include "display_window.hpp"
#include <span>
#include "ge_gpu_backend.hpp"
#include "lcs_mouse.hpp"
#include "lcs_controls.hpp"
#include "lcs_render_config.hpp"
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <mutex>
#include <vector>
namespace lcs {
namespace {
HWND g_window{};
bool g_closed{};
bool g_fullscreen{};
WINDOWPLACEMENT g_windowed_placement{};
std::vector<std::uint32_t> g_pixels;
std::uint32_t g_pixel_width{};
std::uint32_t g_pixel_height{};
std::atomic<int> g_wheel{0};
std::atomic<std::int32_t> g_mouse_dx{0};
std::atomic<std::int32_t> g_mouse_dy{0};
constexpr std::uint32_t kPspSelect = 0x000001u;
constexpr std::uint32_t kPspStart = 0x000008u;
constexpr std::uint32_t kPspUp = 0x000010u;
constexpr std::uint32_t kPspRight = 0x000020u;
constexpr std::uint32_t kPspDown = 0x000040u;
constexpr std::uint32_t kPspLeft = 0x000080u;
constexpr std::uint32_t kPspLTrigger = 0x000100u;
constexpr std::uint32_t kPspRTrigger = 0x000200u;
constexpr std::uint32_t kPspTriangle = 0x001000u;
constexpr std::uint32_t kPspCircle = 0x002000u;
constexpr std::uint32_t kPspCross = 0x004000u;
constexpr std::uint32_t kPspSquare = 0x008000u;
struct KeyBinding {
int virtual_key;
std::uint32_t psp_button;
};
constexpr KeyBinding kKeyBindings[] = {
{VK_SPACE, kPspCross},
{VK_LSHIFT, kPspSquare},
{VK_RSHIFT, kPspSquare},
{'F', kPspTriangle},
{VK_RETURN, kPspTriangle},
{'Q', kPspLeft},
{'E', kPspRight},
{'H', kPspLTrigger},
{VK_UP, kPspUp},
{VK_DOWN, kPspDown},
{VK_LEFT, kPspLeft},
{VK_RIGHT, kPspRight},
{VK_ESCAPE, kPspStart},
{'V', kPspSelect},
};
constexpr KeyBinding kMouseBindings[] = {
{VK_LBUTTON, kPspCircle},
{VK_RBUTTON, kPspRTrigger},
{VK_MBUTTON, kPspLTrigger},
};
constexpr int kMoveForward = 'W';
constexpr int kMoveBack = 'S';
constexpr int kMoveLeft = 'A';
constexpr int kMoveRight = 'D';
bool key_down(int virtual_key) noexcept {
return (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
}
bool cursor_in_client(HWND hwnd) noexcept {
POINT cursor{};
if (!GetCursorPos(&cursor) || WindowFromPoint(cursor) != hwnd) return false;
RECT client{};
if (!GetClientRect(hwnd, &client) || !ScreenToClient(hwnd, &cursor)) return false;
return PtInRect(&client, cursor) != FALSE;
}
struct XInputGamepad {
std::uint16_t buttons;
std::uint8_t left_trigger;
std::uint8_t right_trigger;
std::int16_t lx, ly, rx, ry;
};
struct XInputStatePacket {
std::uint32_t packet;
XInputGamepad gamepad;
};
using PfnXInputGetState = DWORD(WINAPI *)(DWORD, XInputStatePacket *);
constexpr std::uint16_t kPadDpadUp = 0x0001u;
constexpr std::uint16_t kPadDpadDown = 0x0002u;
constexpr std::uint16_t kPadDpadLeft = 0x0004u;
constexpr std::uint16_t kPadDpadRight = 0x0008u;
constexpr std::uint16_t kPadStart = 0x0010u;
constexpr std::uint16_t kPadBack = 0x0020u;
constexpr std::uint16_t kPadLeftShoulder = 0x0100u;
constexpr std::uint16_t kPadRightShoulder = 0x0200u;
constexpr std::uint16_t kPadA = 0x1000u;
constexpr std::uint16_t kPadB = 0x2000u;
constexpr std::uint16_t kPadX = 0x4000u;
constexpr std::uint16_t kPadY = 0x8000u;
PfnXInputGetState xinput_get_state() noexcept {
static PfnXInputGetState resolved = []() -> PfnXInputGetState {
for (const wchar_t *name : {L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll"}) {
if (HMODULE module = LoadLibraryW(name)) {
if (auto function = reinterpret_cast<PfnXInputGetState>(
reinterpret_cast<void *>(GetProcAddress(module, "XInputGetState"))))
return function;
}
}
return nullptr;
}();
return resolved;
}
std::uint8_t stick_to_psp(std::int16_t value, bool invert) noexcept {
constexpr int kDeadZone = 7849;
int magnitude = std::abs(static_cast<int>(value));
if (magnitude <= kDeadZone) return 128u;
magnitude = (magnitude - kDeadZone) * 32767 / (32767 - kDeadZone);
int signed_value = value < 0 ? -magnitude : magnitude;
if (invert) signed_value = -signed_value;
return static_cast<std::uint8_t>(std::clamp(128 + signed_value * 127 / 32767, 0, 255));
}
void update_cursor_clip(HWND hwnd, bool capture) noexcept {
if (!capture || hwnd == nullptr || !lcs_camera_hook_enabled() || !lcs_camera_in_use()) {
ClipCursor(nullptr);
return;
}
RECT client{};
if (!GetClientRect(hwnd, &client)) return;
POINT top_left{client.left, client.top};
POINT bottom_right{client.right, client.bottom};
ClientToScreen(hwnd, &top_left);
ClientToScreen(hwnd, &bottom_right);
const RECT wanted{top_left.x, top_left.y, bottom_right.x, bottom_right.y};
RECT current{};
if (GetClipCursor(&current) && EqualRect(&current, &wanted)) return;
ClipCursor(&wanted);
}
void set_fullscreen(HWND hwnd, bool fullscreen) noexcept {
if (hwnd == nullptr || fullscreen == g_fullscreen) return;
const LONG_PTR style = GetWindowLongPtrW(hwnd, GWL_STYLE);
if (fullscreen) {
MONITORINFO monitor{};
monitor.cbSize = sizeof(monitor);
g_windowed_placement.length = sizeof(g_windowed_placement);
if (!GetWindowPlacement(hwnd, &g_windowed_placement) ||
!GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &monitor))
return;
SetWindowLongPtrW(hwnd, GWL_STYLE, (style & ~WS_OVERLAPPEDWINDOW) | WS_POPUP);
const RECT &area = monitor.rcMonitor;
SetWindowPos(hwnd, HWND_TOP, area.left, area.top, area.right - area.left,
area.bottom - area.top, SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
} else {
SetWindowLongPtrW(hwnd, GWL_STYLE, (style & ~WS_POPUP) | WS_OVERLAPPEDWINDOW);
SetWindowPlacement(hwnd, &g_windowed_placement);
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
}
g_fullscreen = fullscreen;
}
LRESULT CALLBACK window_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
if (message == WM_KEYDOWN && wparam == VK_F11 && (lparam & (1 << 30)) == 0) {
set_fullscreen(hwnd, !g_fullscreen);
return 0;
}
if (message == WM_ACTIVATE) {
update_cursor_clip(hwnd, LOWORD(wparam) != WA_INACTIVE && HIWORD(wparam) == 0);
return DefWindowProcW(hwnd, message, wparam, lparam);
}
if (message == WM_SIZE || message == WM_MOVE || message == WM_EXITSIZEMOVE) {
if (GetForegroundWindow() == hwnd) update_cursor_clip(hwnd, true);
return DefWindowProcW(hwnd, message, wparam, lparam);
}
if (message == WM_ENTERSIZEMOVE) {
update_cursor_clip(hwnd, false);
return DefWindowProcW(hwnd, message, wparam, lparam);
}
if (message == WM_INPUT) {
UINT size = 0u;
GetRawInputData(reinterpret_cast<HRAWINPUT>(lparam), RID_INPUT, nullptr, &size,
sizeof(RAWINPUTHEADER));
if (size != 0u && size <= 256u) {
alignas(8) std::byte buffer[256];
if (GetRawInputData(reinterpret_cast<HRAWINPUT>(lparam), RID_INPUT, buffer, &size,
sizeof(RAWINPUTHEADER)) == size) {
const RAWINPUT *raw = reinterpret_cast<const RAWINPUT *>(buffer);
if (raw->header.dwType == RIM_TYPEMOUSE &&
(raw->data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) == 0) {
g_mouse_dx.fetch_add(raw->data.mouse.lLastX, std::memory_order_relaxed);
g_mouse_dy.fetch_add(raw->data.mouse.lLastY, std::memory_order_relaxed);
}
}
}
return DefWindowProcW(hwnd, message, wparam, lparam);
}
if (message == WM_SETCURSOR && LOWORD(lparam) == HTCLIENT && lcs_camera_hook_enabled() &&
lcs_camera_in_use()) {
SetCursor(nullptr);
return TRUE;
}
if (message == WM_MOUSEWHEEL) {
g_wheel.fetch_add(GET_WHEEL_DELTA_WPARAM(wparam) / WHEEL_DELTA, std::memory_order_relaxed);
return 0;
}
if (message == WM_DESTROY || message == WM_CLOSE) {
ClipCursor(nullptr);
g_closed = true;
if (message == WM_DESTROY) {
g_window = nullptr;
return 0;
}
}
return DefWindowProcW(hwnd, message, wparam, lparam);
}
std::uint32_t unpack_pixel(const std::uint8_t *src, std::uint32_t format) {
switch (format) {
case 0u: { // GU_PSM_5650
const std::uint16_t value = static_cast<std::uint16_t>(src[0] | (src[1] << 8));
const std::uint32_t r = (value & 0x1Fu) * 255u / 31u;
const std::uint32_t g = ((value >> 5u) & 0x3Fu) * 255u / 63u;
const std::uint32_t b = ((value >> 11u) & 0x1Fu) * 255u / 31u;
return 0xFF000000u | (r << 16u) | (g << 8u) | b;
}
case 1u: { // GU_PSM_5551
const std::uint16_t value = static_cast<std::uint16_t>(src[0] | (src[1] << 8));
const std::uint32_t r = (value & 0x1Fu) * 255u / 31u;
const std::uint32_t g = ((value >> 5u) & 0x1Fu) * 255u / 31u;
const std::uint32_t b = ((value >> 10u) & 0x1Fu) * 255u / 31u;
return 0xFF000000u | (r << 16u) | (g << 8u) | b;
}
case 2u: { // GU_PSM_4444
const std::uint16_t value = static_cast<std::uint16_t>(src[0] | (src[1] << 8));
const std::uint32_t r = (value & 0xFu) * 17u;
const std::uint32_t g = ((value >> 4u) & 0xFu) * 17u;
const std::uint32_t b = ((value >> 8u) & 0xFu) * 17u;
return 0xFF000000u | (r << 16u) | (g << 8u) | b;
}
default: { // GU_PSM_8888
return 0xFF000000u | (static_cast<std::uint32_t>(src[0]) << 16u) |
(static_cast<std::uint32_t>(src[1]) << 8u) | src[2];
}
}
}
std::uint32_t bytes_per_pixel(std::uint32_t format) { return format == 3u ? 4u : 2u; }
} // namespace
void display_window_init() {
if (g_window != nullptr) return;
WNDCLASSW wc{};
wc.lpfnWndProc = window_proc;
wc.hInstance = GetModuleHandleW(nullptr);
wc.lpszClassName = L"LCSNativeWindow";
wc.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512)); // IDC_ARROW
wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(1));
RegisterClassW(&wc);
int scale = 2;
if (const char *text = std::getenv("LCS_WINDOW_SCALE")) {
const int parsed = std::atoi(text);
if (parsed >= 1 && parsed <= 8) scale = parsed;
}
g_window = CreateWindowExW(0, wc.lpszClassName, L"LCSNative - GTA: Liberty City Stories",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
480 * scale, 272 * scale,
nullptr, nullptr, wc.hInstance, nullptr);
if (g_window == nullptr) return;
RECT outer{};
RECT client{};
GetWindowRect(g_window, &outer);
GetClientRect(g_window, &client);
const int frame_width = (outer.right - outer.left) - client.right;
const int frame_height = (outer.bottom - outer.top) - client.bottom;
RECT work{0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)};
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
const int work_width = static_cast<int>(work.right - work.left);
const int work_height = static_cast<int>(work.bottom - work.top);
const int client_width =
std::clamp(480 * scale, 1, std::max(1, work_width - frame_width));
const int client_height =
std::clamp(272 * scale, 1, std::max(1, work_height - frame_height));
const int window_width = client_width + frame_width;
const int window_height = client_height + frame_height;
SetWindowPos(g_window, nullptr,
static_cast<int>(work.left) + (work_width - window_width) / 2,
static_cast<int>(work.top) + (work_height - window_height) / 2,
window_width, window_height, SWP_NOZORDER | SWP_NOACTIVATE);
set_fullscreen(g_window, lcs_render_configuration().display.fullscreen);
ge_gpu_backend_set_native_window(g_window);
ShowWindow(g_window, SW_SHOW);
const RAWINPUTDEVICE mouse{0x01u, 0x02u, 0u, g_window};
RegisterRawInputDevices(&mouse, 1u, sizeof(mouse));
}
void display_window_attach_gpu_backend() {
if (g_window != nullptr) ge_gpu_backend_set_native_window(g_window);
}
bool display_window_profile_key_pressed() {
static bool was_down = false;
const bool down = g_window != nullptr && GetForegroundWindow() == g_window && key_down('P');
const bool pressed = down && !was_down;
was_down = down;
return pressed;
}
bool display_window_closed() { return g_closed; }
void display_window_pump() {
if (g_window == nullptr) return;
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
void display_window_present(psprecomp::Runtime &runtime, std::uint32_t frame_buffer,
std::uint32_t buffer_width, std::uint32_t pixel_format,
std::uint32_t width, std::uint32_t height) {
if (g_window == nullptr) return;
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
if (g_closed || g_window == nullptr) return;
// Guest calls WaitVblankStart way faster than 60Hz (~90k times per 500k
// dispatches), so cap the actual blit to ~30Hz real time.
static auto last_present = std::chrono::steady_clock::time_point{};
const auto now = std::chrono::steady_clock::now();
if (now - last_present < std::chrono::milliseconds(33)) return;
last_present = now;
if (frame_buffer == 0u || width == 0u || height == 0u || buffer_width == 0u) return;
const std::uint32_t stride_bytes = buffer_width * bytes_per_pixel(pixel_format);
const std::size_t total_bytes = static_cast<std::size_t>(stride_bytes) * height;
const std::uint8_t *source = runtime.memory().raw_pointer(frame_buffer, total_bytes);
if (source == nullptr) return;
g_pixels.resize(static_cast<std::size_t>(width) * height);
g_pixel_width = width;
g_pixel_height = height;
const std::uint32_t bpp = bytes_per_pixel(pixel_format);
for (std::uint32_t y = 0; y < height; ++y) {
const std::uint8_t *row = source + static_cast<std::size_t>(y) * stride_bytes;
for (std::uint32_t x = 0; x < width; ++x) {
g_pixels[static_cast<std::size_t>(y) * width + x] = unpack_pixel(row + static_cast<std::size_t>(x) * bpp, pixel_format);
}
}
HDC hdc = GetDC(g_window);
BITMAPINFO info{};
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = static_cast<LONG>(width);
info.bmiHeader.biHeight = -static_cast<LONG>(height);
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
RECT client{};
GetClientRect(g_window, &client);
SetStretchBltMode(hdc, HALFTONE);
SetBrushOrgEx(hdc, 0, 0, nullptr);
StretchDIBits(hdc, 0, 0, client.right - client.left, client.bottom - client.top,
0, 0, static_cast<int>(width), static_cast<int>(height),
g_pixels.data(), &info, DIB_RGB_COLORS, SRCCOPY);
ReleaseDC(g_window, hdc);
}
void display_window_present_rgba(std::span<const std::byte> rgba, std::uint32_t width,
std::uint32_t height) {
if (g_window == nullptr || width == 0u || height == 0u) return;
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
if (g_window == nullptr) return;
const std::size_t pixel_count = static_cast<std::size_t>(width) * height;
if (rgba.size() < pixel_count * 4u) return;
g_pixels.resize(pixel_count);
g_pixel_width = width;
g_pixel_height = height;
const auto *source = reinterpret_cast<const std::uint8_t *>(rgba.data());
for (std::size_t index = 0u; index < pixel_count; ++index) {
const std::uint8_t *pixel = source + index * 4u;
g_pixels[index] = (static_cast<std::uint32_t>(pixel[0]) << 16u) |
(static_cast<std::uint32_t>(pixel[1]) << 8u) |
static_cast<std::uint32_t>(pixel[2]);
}
HDC hdc = GetDC(g_window);
BITMAPINFO info{};
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = static_cast<LONG>(width);
info.bmiHeader.biHeight = -static_cast<LONG>(height);
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
RECT client{};
GetClientRect(g_window, &client);
SetStretchBltMode(hdc, HALFTONE);
SetBrushOrgEx(hdc, 0, 0, nullptr);
StretchDIBits(hdc, 0, 0, client.right - client.left, client.bottom - client.top,
0, 0, static_cast<int>(width), static_cast<int>(height),
g_pixels.data(), &info, DIB_RGB_COLORS, SRCCOPY);
ReleaseDC(g_window, hdc);
}
void display_window_shutdown() {
ClipCursor(nullptr);
if (g_window != nullptr) {
DestroyWindow(g_window);
g_window = nullptr;
}
}
HostInputState display_window_input() {
static std::mutex cache_mutex;
static HostInputState cached{};
static std::chrono::steady_clock::time_point cached_at{};
const std::lock_guard<std::mutex> guard(cache_mutex);
const auto poll_time = std::chrono::steady_clock::now();
if (cached_at.time_since_epoch().count() != 0 &&
poll_time - cached_at < std::chrono::milliseconds(4))
return cached;
cached_at = poll_time;
HostInputState input{};
const auto publish = [&]() -> HostInputState {
lcs_camera_set_axes(input.camera_x, input.camera_y);
lcs_set_host_drive_inputs(input.accelerate, input.brake);
cached = input;
return cached;
};
const int wheel = g_wheel.exchange(0, std::memory_order_relaxed);
const std::int32_t mouse_dx = g_mouse_dx.exchange(0, std::memory_order_relaxed);
const std::int32_t mouse_dy = g_mouse_dy.exchange(0, std::memory_order_relaxed);
const bool focused = g_window != nullptr && GetForegroundWindow() == g_window;
static bool was_captured = false;
const bool in_game = lcs_camera_in_use();
const bool moving_window = focused && (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0 &&
GetCapture() != nullptr;
const bool capture = focused && in_game && !moving_window && !IsIconic(g_window);
if (capture || was_captured) update_cursor_clip(g_window, capture);
if (capture != was_captured && focused) {
POINT cursor{};
GetCursorPos(&cursor);
SetCursorPos(cursor.x, cursor.y);
}
was_captured = capture;
const bool driving = lcs_player_in_vehicle();
const ControlsConfiguration &controls = lcs_render_configuration().controls;
if (focused) {
for (const KeyBinding &binding : kKeyBindings)
if (key_down(binding.virtual_key)) input.buttons |= binding.psp_button;
if (cursor_in_client(g_window))
for (const KeyBinding &binding : kMouseBindings)
if (key_down(binding.virtual_key)) input.buttons |= binding.psp_button;
if (driving) {
input.buttons &= ~(kPspCross | kPspRTrigger);
if (key_down(VK_SPACE)) input.buttons |= kPspRTrigger;
}
int move_x = 0;
int move_y = 0;
if (key_down(kMoveLeft)) move_x -= 1;
if (key_down(kMoveRight)) move_x += 1;
if (!driving) {
if (key_down(kMoveForward)) move_y -= 1;
if (key_down(kMoveBack)) move_y += 1;
} else {
if (key_down(VK_UP)) move_y -= 1;
if (key_down(VK_DOWN)) move_y += 1;
}
input.accelerate = key_down(kMoveForward);
input.brake = key_down(kMoveBack);
const int reach = key_down(VK_LMENU) ? 60 : 127;
input.analog_x = static_cast<std::uint8_t>(std::clamp(128 + move_x * reach, 0, 255));
input.analog_y = static_cast<std::uint8_t>(std::clamp(128 + move_y * reach, 0, 255));
const int sensitivity = static_cast<int>(controls.mouse_sensitivity);
const auto camera_response = [sensitivity](std::int32_t delta) {
const double scaled = std::abs(delta) * (sensitivity / 12.0);
const double magnitude = 127.0 * scaled / (scaled + 12.0);
return static_cast<int>(std::lround(delta < 0 ? -magnitude : magnitude));
};
input.camera_x = camera_response(mouse_dx);
input.camera_y = camera_response(-mouse_dy);
if (capture) lcs_add_mouse_camera_delta(mouse_dx, mouse_dy);
if (controls.invert_camera_y) input.camera_y = -input.camera_y;
}
static int wheel_hold = 0;
static std::uint32_t wheel_button = 0u;
if (focused && wheel != 0) {
wheel_button = wheel > 0 ? kPspLeft : kPspRight;
wheel_hold = 4;
}
if (wheel_hold > 0) {
--wheel_hold;
input.buttons |= wheel_button;
}
if (!focused) return publish();
if (const PfnXInputGetState get_state = xinput_get_state()) {
XInputStatePacket pad{};
if (get_state(0u, &pad) == ERROR_SUCCESS) {
const std::uint16_t b = pad.gamepad.buttons;
if (b & kPadA) input.buttons |= kPspCross;
if (b & kPadX) input.buttons |= kPspSquare;
if (b & kPadY) input.buttons |= kPspTriangle;
if (b & kPadB) input.buttons |= kPspCircle;
if (b & kPadLeftShoulder) input.buttons |= kPspLTrigger;
if (b & kPadRightShoulder) input.buttons |= kPspRTrigger;
if (b & kPadStart) input.buttons |= kPspStart;
if (b & kPadBack) input.buttons |= kPspSelect;
if (b & kPadDpadUp) input.buttons |= kPspUp;
if (b & kPadDpadDown) input.buttons |= kPspDown;
if (b & kPadDpadLeft) input.buttons |= kPspLeft;
if (b & kPadDpadRight) input.buttons |= kPspRight;
if (!driving) {
if (pad.gamepad.left_trigger > 64u) input.buttons |= kPspLTrigger;
if (pad.gamepad.right_trigger > 64u) input.buttons |= kPspRTrigger;
}
if (pad.gamepad.right_trigger > 64u) input.accelerate = true;
if (pad.gamepad.left_trigger > 64u) input.brake = true;
const std::uint8_t pad_x = stick_to_psp(pad.gamepad.lx, false);
const std::uint8_t pad_y = stick_to_psp(pad.gamepad.ly, true);
if (pad_x != 128u || pad_y != 128u) {
input.analog_x = pad_x;
input.analog_y = pad_y;
}
const int camera_x = stick_to_psp(pad.gamepad.rx, false) - 128;
int camera_y = stick_to_psp(pad.gamepad.ry, false) - 128;
if (controls.invert_camera_y) camera_y = -camera_y;
if (camera_x != 0 || camera_y != 0) {
input.camera_x = std::clamp(camera_x, -127, 127);
input.camera_y = std::clamp(camera_y, -127, 127);
}
}
}
return publish();
}
} // namespace lcs