feat(steam): M4 complete — C-ABI send path, Decky UX, Apple/Android parity
Finish the client side of the Steam Controller / Steam Deck pipeline. - C-ABI (core abi.rs): PunktfunkRichInputEx — a size-prefixed superset of PunktfunkRichInput that can express the second trackpad (surface), a distinct click vs touch, signed coords + pressure — plus punktfunk_connection_send_rich_input2 (the struct_size ABI-skew-guard precedent). The only way a C client (Apple/embedders) can emit a TouchpadEx; the legacy struct + send_rich_input stay byte-for-byte. punktfunk_core.h regenerated. - Decky (clients/decky): a "Steam Deck" gamepad type in Settings + an unmissable Disable-Steam-Input instruction shown when it's selected (in Game Mode Steam Input holds 0x1205, so the SDL HIDAPI Steam driver can't open the Deck's controls until the user disables Steam Input for the shortcut). Plus a best-effort, feature-detected disableSteamInputForShortcut() in launchStream — never blocks/throws; the manual toggle is the documented source of truth. - Apple parity (PunktfunkConnection.swift): GamepadType.steamController/steamDeck (wire 5/6) + name parsing, so the resolved type round-trips. Capture is blocked (GameController never surfaces a 0x28DE HID device). - Android parity (Gamepad.kt): PREF_STEAMCONTROLLER/STEAMDECK + the Valve 0x28DE PIDs in prefFor(). Rich-input capture stays out of scope (no rich-input plane yet) — standard buttons/sticks resolve to the host's Steam Deck pad. Rust workspace clippy/fmt/test green; Decky src/ typechecks clean (only a pre-existing @decky/api dep resolution error remains); Swift/Kotlin compile on their CI. The full pipeline is now BUILT; what remains is validation that needs hardware we don't have (a running Steam on the host, a live Deck client, the Moonlight paddle regression). Not pushed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,15 +50,25 @@ object Gamepad {
|
|||||||
const val PREF_DUALSENSE = 2
|
const val PREF_DUALSENSE = 2
|
||||||
const val PREF_XBOXONE = 3
|
const val PREF_XBOXONE = 3
|
||||||
const val PREF_DUALSHOCK4 = 4
|
const val PREF_DUALSHOCK4 = 4
|
||||||
|
const val PREF_STEAMCONTROLLER = 5
|
||||||
|
const val PREF_STEAMDECK = 6
|
||||||
|
|
||||||
// USB vendor ids of the controllers we can identify by VID/PID.
|
// USB vendor ids of the controllers we can identify by VID/PID.
|
||||||
private const val VID_SONY = 0x054C
|
private const val VID_SONY = 0x054C
|
||||||
private const val VID_MICROSOFT = 0x045E
|
private const val VID_MICROSOFT = 0x045E
|
||||||
|
private const val VID_VALVE = 0x28DE
|
||||||
|
|
||||||
// Sony product ids. DualSense (PS5) and DualShock 4 (PS4) map to distinct host pad types.
|
// Sony product ids. DualSense (PS5) and DualShock 4 (PS4) map to distinct host pad types.
|
||||||
private val PID_DUALSENSE = setOf(0x0CE6, 0x0DF2)
|
private val PID_DUALSENSE = setOf(0x0CE6, 0x0DF2)
|
||||||
private val PID_DUALSHOCK4 = setOf(0x05C4, 0x09CC)
|
private val PID_DUALSHOCK4 = setOf(0x05C4, 0x09CC)
|
||||||
|
|
||||||
|
// Valve: Steam Deck built-in controller (0x1205); classic Steam Controller wired (0x1102) /
|
||||||
|
// dongle (0x1142). The host builds the virtual hid-steam pad; rich-input capture (paddles /
|
||||||
|
// trackpads / gyro) is out of scope on Android (no rich-input plane yet), so only the standard
|
||||||
|
// buttons + sticks reach the host for now — parity with the desktop type resolution.
|
||||||
|
private val PID_STEAMDECK = setOf(0x1205)
|
||||||
|
private val PID_STEAMCONTROLLER = setOf(0x1102, 0x1142)
|
||||||
|
|
||||||
// Microsoft Xbox One / Series product ids (wired + the common Bluetooth/dongle revisions). All
|
// Microsoft Xbox One / Series product ids (wired + the common Bluetooth/dongle revisions). All
|
||||||
// behave like Xbox 360 on the host minus the glyph identity, so they share one pref byte.
|
// behave like Xbox 360 on the host minus the glyph identity, so they share one pref byte.
|
||||||
private val PID_XBOXONE = setOf(
|
private val PID_XBOXONE = setOf(
|
||||||
@@ -82,6 +92,8 @@ object Gamepad {
|
|||||||
vid == VID_SONY && pid in PID_DUALSENSE -> PREF_DUALSENSE
|
vid == VID_SONY && pid in PID_DUALSENSE -> PREF_DUALSENSE
|
||||||
vid == VID_SONY && pid in PID_DUALSHOCK4 -> PREF_DUALSHOCK4
|
vid == VID_SONY && pid in PID_DUALSHOCK4 -> PREF_DUALSHOCK4
|
||||||
vid == VID_MICROSOFT && pid in PID_XBOXONE -> PREF_XBOXONE
|
vid == VID_MICROSOFT && pid in PID_XBOXONE -> PREF_XBOXONE
|
||||||
|
vid == VID_VALVE && pid in PID_STEAMDECK -> PREF_STEAMDECK
|
||||||
|
vid == VID_VALVE && pid in PID_STEAMCONTROLLER -> PREF_STEAMCONTROLLER
|
||||||
else -> PREF_XBOX360
|
else -> PREF_XBOX360
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,11 @@ public final class PunktfunkConnection {
|
|||||||
case dualSense = 2
|
case dualSense = 2
|
||||||
case xboxOne = 3
|
case xboxOne = 3
|
||||||
case dualShock4 = 4
|
case dualShock4 = 4
|
||||||
|
// Valve Steam Controller / Steam Deck (Linux UHID hid-steam hosts). Parity only on Apple —
|
||||||
|
// GameController never surfaces a 0x28DE HID device, so the client can't capture one; these
|
||||||
|
// exist so the resolved type round-trips and name parsing matches the host.
|
||||||
|
case steamController = 5
|
||||||
|
case steamDeck = 6
|
||||||
|
|
||||||
/// Loose name parsing for env/dev hooks, mirroring the host's
|
/// Loose name parsing for env/dev hooks, mirroring the host's
|
||||||
/// `GamepadPref::from_name`.
|
/// `GamepadPref::from_name`.
|
||||||
@@ -192,6 +197,8 @@ public final class PunktfunkConnection {
|
|||||||
case "dualsense", "ds", "ds5", "ps5": self = .dualSense
|
case "dualsense", "ds", "ds5", "ps5": self = .dualSense
|
||||||
case "xboxone", "xbox-one", "xboxseries", "series": self = .xboxOne
|
case "xboxone", "xbox-one", "xboxseries", "series": self = .xboxOne
|
||||||
case "dualshock4", "dualshock", "ds4", "ps4": self = .dualShock4
|
case "dualshock4", "dualshock", "ds4", "ps4": self = .dualShock4
|
||||||
|
case "steamdeck", "steam-deck", "deck": self = .steamDeck
|
||||||
|
case "steamcontroller", "steam-controller", "steamcon": self = .steamController
|
||||||
default: return nil
|
default: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,7 +294,13 @@ const RESOLUTIONS: [number, number, string][] = [
|
|||||||
[2560, 1440, "2560 × 1440"],
|
[2560, 1440, "2560 × 1440"],
|
||||||
];
|
];
|
||||||
const REFRESH = [0, 30, 60, 90, 120];
|
const REFRESH = [0, 30, 60, 90, 120];
|
||||||
const GAMEPADS = ["auto", "xbox360", "dualsense"];
|
const GAMEPADS = ["auto", "xbox360", "dualsense", "steamdeck"];
|
||||||
|
const GAMEPAD_LABELS: Record<string, string> = {
|
||||||
|
auto: "Automatic",
|
||||||
|
xbox360: "Xbox 360",
|
||||||
|
dualsense: "DualSense",
|
||||||
|
steamdeck: "Steam Deck",
|
||||||
|
};
|
||||||
|
|
||||||
const SettingsSection: FC = () => {
|
const SettingsSection: FC = () => {
|
||||||
const [s, setS] = useState<StreamSettings | null>(null);
|
const [s, setS] = useState<StreamSettings | null>(null);
|
||||||
@@ -355,14 +361,17 @@ const SettingsSection: FC = () => {
|
|||||||
/>
|
/>
|
||||||
<Field label="Gamepad type" childrenContainerWidth="max">
|
<Field label="Gamepad type" childrenContainerWidth="max">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
rgOptions={GAMEPADS.map((g) => ({
|
rgOptions={GAMEPADS.map((g) => ({ data: g, label: GAMEPAD_LABELS[g] ?? g }))}
|
||||||
data: g,
|
|
||||||
label: g === "auto" ? "Automatic" : g === "xbox360" ? "Xbox 360" : "DualSense",
|
|
||||||
}))}
|
|
||||||
selectedOption={s.gamepad}
|
selectedOption={s.gamepad}
|
||||||
onChange={(o) => patch({ gamepad: o.data as string })}
|
onChange={(o) => patch({ gamepad: o.data as string })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
{s.gamepad === "steamdeck" && (
|
||||||
|
<Field
|
||||||
|
label="⚠ Disable Steam Input"
|
||||||
|
description="Steam Deck mode forwards the paddles, both trackpads, and gyro to the host. For that, Steam Input must be OFF for punktfunk: on the game page tap ⚙ → Controller Settings → set Steam Input to Off. Otherwise Steam keeps the Deck's controls and only the sticks + buttons reach the host."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ToggleField
|
<ToggleField
|
||||||
label="Stream microphone"
|
label="Stream microphone"
|
||||||
checked={s.mic_enabled}
|
checked={s.mic_enabled}
|
||||||
|
|||||||
@@ -113,12 +113,35 @@ async function ensureShortcut(): Promise<number> {
|
|||||||
return appId;
|
return appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort: turn Steam Input OFF for our shortcut so SDL's HIDAPI Steam Deck driver can open the
|
||||||
|
* Deck's controls (paddles · trackpads · gyro) directly. There is no confirmed-stable SteamClient
|
||||||
|
* API for this, so it is feature-detected and MUST never block or throw into the launch — the manual
|
||||||
|
* toggle (game page → ⚙ → Controller Settings → Steam Input Off, surfaced in the plugin Settings) is
|
||||||
|
* the documented source of truth. No-op when the optional API is absent.
|
||||||
|
*/
|
||||||
|
function disableSteamInputForShortcut(appId: number): void {
|
||||||
|
try {
|
||||||
|
const input = (
|
||||||
|
SteamClient as unknown as {
|
||||||
|
Input?: { SetSteamInputEnabledForApp?: (appId: number, enabled: boolean) => void };
|
||||||
|
}
|
||||||
|
).Input;
|
||||||
|
input?.SetSteamInputEnabledForApp?.(appId, false);
|
||||||
|
} catch {
|
||||||
|
/* a controller tweak must never break the launch */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch a stream to `host:port` fullscreen in Gaming Mode. Encodes the target into the
|
* Launch a stream to `host:port` fullscreen in Gaming Mode. Encodes the target into the
|
||||||
* shortcut's launch options (so one generic shortcut serves every host), then RunGame.
|
* shortcut's launch options (so one generic shortcut serves every host), then RunGame.
|
||||||
*/
|
*/
|
||||||
export async function launchStream(host: string, port: number): Promise<void> {
|
export async function launchStream(host: string, port: number): Promise<void> {
|
||||||
const appId = await ensureShortcut();
|
const appId = await ensureShortcut();
|
||||||
|
// Best-effort so the Deck's rich controls reach the client; no-op if the API is absent (the user
|
||||||
|
// disables Steam Input manually — see the Settings instruction).
|
||||||
|
disableSteamInputForShortcut(appId);
|
||||||
const target = port && port !== 9777 ? `${host}:${port}` : host;
|
const target = port && port !== 9777 ? `${host}:${port}` : host;
|
||||||
// KEY=value ... %command% — the wrapper reads PF_HOST from the environment.
|
// KEY=value ... %command% — the wrapper reads PF_HOST from the environment.
|
||||||
SteamClient.Apps.SetAppLaunchOptions(appId, `PF_HOST=${target} %command%`);
|
SteamClient.Apps.SetAppLaunchOptions(appId, `PF_HOST=${target} %command%`);
|
||||||
|
|||||||
@@ -692,6 +692,77 @@ impl PunktfunkRichInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forward-compatible superset of [`PunktfunkRichInput`] that can also express the rich Steam
|
||||||
|
/// surfaces: a *second* trackpad (`surface`), a distinct `click` vs touch, signed coordinates, and
|
||||||
|
/// pressure. Sent via [`punktfunk_connection_send_rich_input2`] — the only way a C client can emit a
|
||||||
|
/// `TouchpadEx`. The caller MUST set `struct_size = sizeof(PunktfunkRichInputEx)` (the ABI-skew
|
||||||
|
/// guard, like [`PunktfunkConfig`]); the legacy [`PunktfunkRichInput`] +
|
||||||
|
/// [`punktfunk_connection_send_rich_input`] stay byte-for-byte for existing callers.
|
||||||
|
#[cfg(feature = "quic")]
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct PunktfunkRichInputEx {
|
||||||
|
/// MUST equal `sizeof(PunktfunkRichInputEx)`.
|
||||||
|
pub struct_size: u32,
|
||||||
|
/// One of `PUNKTFUNK_RICH_*` (`TOUCHPAD` / `MOTION` / `TOUCHPAD_EX`).
|
||||||
|
pub kind: u8,
|
||||||
|
/// Gamepad index.
|
||||||
|
pub pad: u8,
|
||||||
|
/// Touchpad/TouchpadEx: contact id.
|
||||||
|
pub finger: u8,
|
||||||
|
/// Touchpad/TouchpadEx: 1 = finger down / touching, 0 = lifted.
|
||||||
|
pub active: u8,
|
||||||
|
/// TouchpadEx: which surface — 0 = single/DualSense, 1 = Steam left pad, 2 = Steam right pad.
|
||||||
|
pub surface: u8,
|
||||||
|
/// TouchpadEx: 1 = the pad is physically clicked (depressed), distinct from a touch contact.
|
||||||
|
pub click: u8,
|
||||||
|
/// Reserved for alignment; set to 0.
|
||||||
|
pub _reserved: [u8; 2],
|
||||||
|
/// TouchpadEx: x coordinate — **signed**, centred at 0 (the real Steam report convention). For a
|
||||||
|
/// legacy `TOUCHPAD` kind sent through this struct, store the unsigned `0..=65535` value's bits.
|
||||||
|
pub x: i16,
|
||||||
|
/// TouchpadEx: y coordinate — signed, centred at 0.
|
||||||
|
pub y: i16,
|
||||||
|
/// TouchpadEx: contact pressure (`0` if the surface has no force sensor).
|
||||||
|
pub pressure: u16,
|
||||||
|
/// Motion: gyro (pitch, yaw, roll), raw signed-16.
|
||||||
|
pub gyro: [i16; 3],
|
||||||
|
/// Motion: accelerometer (x, y, z), raw signed-16.
|
||||||
|
pub accel: [i16; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "quic")]
|
||||||
|
impl PunktfunkRichInputEx {
|
||||||
|
fn to_rich(self) -> Option<crate::quic::RichInput> {
|
||||||
|
use crate::quic::RichInput;
|
||||||
|
match self.kind {
|
||||||
|
PUNKTFUNK_RICH_TOUCHPAD_EX => Some(RichInput::TouchpadEx {
|
||||||
|
pad: self.pad,
|
||||||
|
surface: self.surface,
|
||||||
|
finger: self.finger,
|
||||||
|
touch: self.active != 0,
|
||||||
|
click: self.click != 0,
|
||||||
|
x: self.x,
|
||||||
|
y: self.y,
|
||||||
|
pressure: self.pressure,
|
||||||
|
}),
|
||||||
|
PUNKTFUNK_RICH_MOTION => Some(RichInput::Motion {
|
||||||
|
pad: self.pad,
|
||||||
|
gyro: self.gyro,
|
||||||
|
accel: self.accel,
|
||||||
|
}),
|
||||||
|
PUNKTFUNK_RICH_TOUCHPAD => Some(RichInput::Touchpad {
|
||||||
|
pad: self.pad,
|
||||||
|
finger: self.finger,
|
||||||
|
active: self.active != 0,
|
||||||
|
x: self.x as u16,
|
||||||
|
y: self.y as u16,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Read an optional NUL-terminated UTF-8 string parameter; `Err` = invalid pointer/UTF-8.
|
/// Read an optional NUL-terminated UTF-8 string parameter; `Err` = invalid pointer/UTF-8.
|
||||||
#[cfg(feature = "quic")]
|
#[cfg(feature = "quic")]
|
||||||
unsafe fn opt_cstr<'a>(p: *const std::os::raw::c_char) -> std::result::Result<Option<&'a str>, ()> {
|
unsafe fn opt_cstr<'a>(p: *const std::os::raw::c_char) -> std::result::Result<Option<&'a str>, ()> {
|
||||||
@@ -1786,6 +1857,43 @@ pub unsafe extern "C" fn punktfunk_connection_send_rich_input(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a rich client→host input via the forward-compatible [`PunktfunkRichInputEx`] — the only way
|
||||||
|
/// a C client can emit a `TouchpadEx` (a second trackpad / signed coords / pressure). Set
|
||||||
|
/// `rich->struct_size = sizeof(PunktfunkRichInputEx)`; a smaller (older-layout) value is rejected.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `c` is a valid connection handle; `rich` is null or points to at least its declared
|
||||||
|
/// `struct_size` bytes.
|
||||||
|
#[cfg(feature = "quic")]
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn punktfunk_connection_send_rich_input2(
|
||||||
|
c: *mut PunktfunkConnection,
|
||||||
|
rich: *const PunktfunkRichInputEx,
|
||||||
|
) -> PunktfunkStatus {
|
||||||
|
guard(|| {
|
||||||
|
let c = match unsafe { c.as_ref() } {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return PunktfunkStatus::NullPointer,
|
||||||
|
};
|
||||||
|
if rich.is_null() {
|
||||||
|
return PunktfunkStatus::NullPointer;
|
||||||
|
}
|
||||||
|
// Read only the 4-byte size prefix first to bound the subsequent full read (the
|
||||||
|
// `PunktfunkConfig` ABI-skew precedent).
|
||||||
|
let declared = unsafe { std::ptr::addr_of!((*rich).struct_size).read_unaligned() } as usize;
|
||||||
|
if declared < std::mem::size_of::<PunktfunkRichInputEx>() {
|
||||||
|
return PunktfunkStatus::InvalidArg;
|
||||||
|
}
|
||||||
|
match unsafe { *rich }.to_rich() {
|
||||||
|
Some(r) => match c.inner.send_rich_input(r) {
|
||||||
|
Ok(()) => PunktfunkStatus::Ok,
|
||||||
|
Err(e) => e.status(),
|
||||||
|
},
|
||||||
|
None => PunktfunkStatus::InvalidArg,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// The currently active session mode — the Welcome's, until an accepted
|
/// The currently active session mode — the Welcome's, until an accepted
|
||||||
/// [`punktfunk_connection_request_mode`] switches it. Safe any time after connect.
|
/// [`punktfunk_connection_request_mode`] switches it. Safe any time after connect.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
# Rich Steam Controller & Steam Deck support
|
# Rich Steam Controller & Steam Deck support
|
||||||
|
|
||||||
> **Status:** **M0–M3 GREEN + M4 desktop-capture done (2026-06-29).** The host side is complete:
|
> **Status:** **M0–M4 GREEN — the full Steam Controller/Deck pipeline is built (2026-06-29).** Host:
|
||||||
> the virtual `hid-steam` Deck binds, is byte-exact, is a wired backend (`PUNKTFUNK_GAMEPAD=
|
> the virtual `hid-steam` Deck binds, is byte-exact, is a wired backend (`PUNKTFUNK_GAMEPAD=
|
||||||
> steamdeck`), and the protocol carries the rich Steam inputs. The **Linux + Windows SDL clients now
|
> steamdeck`), and the protocol carries the rich inputs. Clients: the Linux + Windows SDL clients
|
||||||
> capture + send** them. Remaining M4: the Decky Disable-Steam-Input UX, Apple/Android parity, and
|
> capture + send them; the Decky plugin has the Steam Deck mode + Disable-Steam-Input UX; the C-ABI
|
||||||
> the C-ABI `PunktfunkRichInputEx`/`send_rich_input2` (Apple/embedder send path).
|
> has the `TouchpadEx` send path; Apple/Android round-trip the type. Remaining is **validation, not
|
||||||
|
> construction** (see below) + the deferred extras (M5 fallback-remap polish, M6 SteamOS-host
|
||||||
|
> conflict check, M7 Windows UMDF Steam driver).
|
||||||
|
>
|
||||||
|
> **M4 (complete) result:** *(desktop capture — see the prior entry.)* Plus: **C-ABI** —
|
||||||
|
> `PunktfunkRichInputEx` (size-prefixed superset) + `punktfunk_connection_send_rich_input2` (the
|
||||||
|
> `struct_size` skew-guard precedent; the only way a C client emits `TouchpadEx`); legacy
|
||||||
|
> `PunktfunkRichInput`/`send_rich_input` byte-for-byte; `punktfunk_core.h` regenerated. **Decky** —
|
||||||
|
> a "Steam Deck" gamepad option + an unmissable **Disable-Steam-Input** instruction (shown when
|
||||||
|
> selected) + a best-effort feature-detected programmatic flip in `launchStream` (never throws; the
|
||||||
|
> manual toggle is the source of truth). **Apple/Android parity** — `GamepadType.steamController/
|
||||||
|
> steamDeck` (Swift) + `PREF_STEAMCONTROLLER/STEAMDECK` + the `0x28DE` PIDs in `prefFor` (Kotlin), so
|
||||||
|
> the type round-trips; capture stays out of scope there (iOS GameController won't surface a `28DE`
|
||||||
|
> device; Android has no rich-input plane yet). Rust workspace clippy/fmt/test green; Decky `src/`
|
||||||
|
> typechecks clean; Swift/Kotlin compile on their CI.
|
||||||
|
>
|
||||||
|
> **Pending VALIDATION (whole feature, needs hardware we don't have):** (1) recognition of the
|
||||||
|
> virtual Deck by a **running Steam** on the host; (2) a **live Deck/Steam Controller client** actually
|
||||||
|
> sending paddles/trackpads/gyro; (3) the **Moonlight paddle regression** from the M3 xpad-map change.
|
||||||
>
|
>
|
||||||
> **M4 (desktop client capture) result:** `clients/{linux,windows}/src/gamepad.rs` (the SDL services)
|
> **M4 (desktop client capture) result:** `clients/{linux,windows}/src/gamepad.rs` (the SDL services)
|
||||||
> now: set the SDL HIDAPI Steam hints (`SDL_JOYSTICK_HIDAPI_STEAMDECK`/`_STEAM`) so SDL opens Valve
|
> now: set the SDL HIDAPI Steam hints (`SDL_JOYSTICK_HIDAPI_STEAMDECK`/`_STEAM`) so SDL opens Valve
|
||||||
|
|||||||
@@ -668,6 +668,44 @@ typedef struct {
|
|||||||
} PunktfunkRichInput;
|
} PunktfunkRichInput;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||||
|
// Forward-compatible superset of [`PunktfunkRichInput`] that can also express the rich Steam
|
||||||
|
// surfaces: a *second* trackpad (`surface`), a distinct `click` vs touch, signed coordinates, and
|
||||||
|
// pressure. Sent via [`punktfunk_connection_send_rich_input2`] — the only way a C client can emit a
|
||||||
|
// `TouchpadEx`. The caller MUST set `struct_size = sizeof(PunktfunkRichInputEx)` (the ABI-skew
|
||||||
|
// guard, like [`PunktfunkConfig`]); the legacy [`PunktfunkRichInput`] +
|
||||||
|
// [`punktfunk_connection_send_rich_input`] stay byte-for-byte for existing callers.
|
||||||
|
typedef struct {
|
||||||
|
// MUST equal `sizeof(PunktfunkRichInputEx)`.
|
||||||
|
uint32_t struct_size;
|
||||||
|
// One of `PUNKTFUNK_RICH_*` (`TOUCHPAD` / `MOTION` / `TOUCHPAD_EX`).
|
||||||
|
uint8_t kind;
|
||||||
|
// Gamepad index.
|
||||||
|
uint8_t pad;
|
||||||
|
// Touchpad/TouchpadEx: contact id.
|
||||||
|
uint8_t finger;
|
||||||
|
// Touchpad/TouchpadEx: 1 = finger down / touching, 0 = lifted.
|
||||||
|
uint8_t active;
|
||||||
|
// TouchpadEx: which surface — 0 = single/DualSense, 1 = Steam left pad, 2 = Steam right pad.
|
||||||
|
uint8_t surface;
|
||||||
|
// TouchpadEx: 1 = the pad is physically clicked (depressed), distinct from a touch contact.
|
||||||
|
uint8_t click;
|
||||||
|
// Reserved for alignment; set to 0.
|
||||||
|
uint8_t _reserved[2];
|
||||||
|
// TouchpadEx: x coordinate — **signed**, centred at 0 (the real Steam report convention). For a
|
||||||
|
// legacy `TOUCHPAD` kind sent through this struct, store the unsigned `0..=65535` value's bits.
|
||||||
|
int16_t x;
|
||||||
|
// TouchpadEx: y coordinate — signed, centred at 0.
|
||||||
|
int16_t y;
|
||||||
|
// TouchpadEx: contact pressure (`0` if the surface has no force sensor).
|
||||||
|
uint16_t pressure;
|
||||||
|
// Motion: gyro (pitch, yaw, roll), raw signed-16.
|
||||||
|
int16_t gyro[3];
|
||||||
|
// Motion: accelerometer (x, y, z), raw signed-16.
|
||||||
|
int16_t accel[3];
|
||||||
|
} PunktfunkRichInputEx;
|
||||||
|
#endif
|
||||||
|
|
||||||
// A speed-test measurement, filled by [`punktfunk_connection_probe_result`]. `done` is 0 until
|
// A speed-test measurement, filled by [`punktfunk_connection_probe_result`]. `done` is 0 until
|
||||||
// the host's end-of-burst report lands, then 1 (the numbers are final). `throughput_kbps` is the
|
// the host's end-of-burst report lands, then 1 (the numbers are final). `throughput_kbps` is the
|
||||||
// delivered wire throughput to drive a bitrate choice from; `loss_pct` is the link loss and
|
// delivered wire throughput to drive a bitrate choice from; `loss_pct` is the link loss and
|
||||||
@@ -1159,6 +1197,18 @@ PunktfunkStatus punktfunk_connection_send_rich_input(PunktfunkConnection *c,
|
|||||||
const PunktfunkRichInput *rich);
|
const PunktfunkRichInput *rich);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||||
|
// Send a rich client→host input via the forward-compatible [`PunktfunkRichInputEx`] — the only way
|
||||||
|
// a C client can emit a `TouchpadEx` (a second trackpad / signed coords / pressure). Set
|
||||||
|
// `rich->struct_size = sizeof(PunktfunkRichInputEx)`; a smaller (older-layout) value is rejected.
|
||||||
|
//
|
||||||
|
// # Safety
|
||||||
|
// `c` is a valid connection handle; `rich` is null or points to at least its declared
|
||||||
|
// `struct_size` bytes.
|
||||||
|
PunktfunkStatus punktfunk_connection_send_rich_input2(PunktfunkConnection *c,
|
||||||
|
const PunktfunkRichInputEx *rich);
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||||
// The currently active session mode — the Welcome's, until an accepted
|
// The currently active session mode — the Welcome's, until an accepted
|
||||||
// [`punktfunk_connection_request_mode`] switches it. Safe any time after connect.
|
// [`punktfunk_connection_request_mode`] switches it. Safe any time after connect.
|
||||||
|
|||||||
Reference in New Issue
Block a user