Skip to content
Open

Sdl3 #949

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ jobs:
run: cargo build
- name: Run tests
run: cargo test
# SDL gamepad manager: opt-in library feature. Compile + run the
# feature-gated registration test, and assert sdl3 stays out of the
# default dependency graph.
- name: Test SDL gamepad opt-in feature
shell: bash
run: |
cargo test -p buttplug_client_in_process --features sdl-gamepad-manager
if cargo tree -e features -p buttplug_client_in_process | grep -Eq '(^|[[:space:]])sdl3 v[0-9]'; then
echo "::error::sdl3 leaked into buttplug_client_in_process default features"
exit 1
fi
cargo tree -e features -p buttplug_client_in_process --features sdl-gamepad-manager | grep -Eq '(^|[[:space:]])sdl3 v[0-9]' || {
echo "::error::sdl3 missing from buttplug_client_in_process with sdl-gamepad-manager enabled"
exit 1
}
# SDL3 threading spike (automated half): headless init + no-pump
# enumeration on a dedicated spawned thread, on every CI OS. Empty gamepad
# set is acceptable (CI runners have no controllers).
- name: SDL3 threading spike
shell: bash
run: cargo run -p buttplug_server_hwmgr_sdl_gamepad --example sdl3_thread_spike
# Only run doc gen on windows. It has the most code to build anyways, all other projects are a subset of it.
- name: Run doc gen
if: startsWith(matrix.os, 'windows')
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Buttplug is a framework for interfacing with intimate hardware devices. It uses
- `serial`, `hid` - USB serial and HID devices
- `lovense_dongle`, `lovense_connect` - Lovense-specific (deprecated)
- `xinput` - Windows gamepad vibration
- `sdl_gamepad` - Cross-platform gamepad rumble via SDL3 (opt-in)
- `websocket` - WebSocket device forwarders
- `simulated` - In-process simulated devices (no real hardware; lives in `buttplug_server`)

Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ members = [
"crates/buttplug_server_hwmgr_websocket",
"crates/buttplug_server_hwmgr_webbluetooth",
"crates/buttplug_server_hwmgr_xinput",
"crates/buttplug_server_hwmgr_sdl_gamepad",
"crates/buttplug_tests",
"crates/buttplug_transport_websocket_tungstenite",
"crates/buttplug_wasm",
Expand All @@ -37,6 +38,7 @@ default-members = [
"crates/buttplug_server_hwmgr_serial",
"crates/buttplug_server_hwmgr_websocket",
"crates/buttplug_server_hwmgr_xinput",
"crates/buttplug_server_hwmgr_sdl_gamepad",
"crates/buttplug_tests",
"crates/buttplug_transport_websocket_tungstenite",
"crates/intiface_engine",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ This project consists of the following crates:
| [buttplug_server_hwmgr_serial](crates/buttplug_server_hwmgr_serial/) | Serial device communication support |
| [buttplug_server_hwmgr_websocket](crates/buttplug_server_hwmgr_websocket/) | Websocket device communication suppor, used for devices that may connect in ways not directly supported by other formats |
| [buttplug_server_hwmgr_xinput](crates/buttplug_server_hwmgr_xinput/) | XInput gamepad support (windows only) |
| [buttplug_server_hwmgr_sdl_gamepad](crates/buttplug_server_hwmgr_sdl_gamepad/) | Cross-platform gamepad rumble via SDL3 (opt-in) |
| [buttplug_tests](crates/buttplug_tests/) | For tests that need the whole framework |
| [buttplug_transport_websocket_tungstenite](crates/buttplug_transport_websocket_tungstenite/) | Communications transport for clients/servers using tokio-tungstenite |
| [intiface_engine](crates/intiface_engine/) | Command line interface for running a Buttplug server |
Expand Down
4 changes: 4 additions & 0 deletions crates/buttplug_client_in_process/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ lovense-connect-service-manager=["buttplug_server_hwmgr_lovense_connect"]
serial-manager=["buttplug_server_hwmgr_serial"]
websocket-manager=["buttplug_server_hwmgr_websocket"]
xinput-manager=["buttplug_server_hwmgr_xinput"]
# Opt-in cross-platform gamepad manager via SDL3. Deliberately NOT in default:
# building SDL3 from source is too heavy for default library consumers.
sdl-gamepad-manager=["buttplug_server_hwmgr_sdl_gamepad"]
tokio-runtime = ["buttplug_core/tokio-runtime", "buttplug_client/tokio-runtime", "buttplug_server/tokio-runtime"]
wasm = ["buttplug_core/wasm", "buttplug_client/wasm", "buttplug_server/wasm"]

Expand All @@ -43,6 +46,7 @@ buttplug_server_hwmgr_lovense_dongle = { version = "11.0.0", path = "../buttplug
buttplug_server_hwmgr_serial = { version = "11.0.0", path = "../buttplug_server_hwmgr_serial", optional = true}
buttplug_server_hwmgr_websocket = { version = "11.0.0", path = "../buttplug_server_hwmgr_websocket", optional = true}
buttplug_server_hwmgr_xinput = { version = "11.0.0", path = "../buttplug_server_hwmgr_xinput", optional = true}
buttplug_server_hwmgr_sdl_gamepad = { version = "11.0.0", path = "../buttplug_server_hwmgr_sdl_gamepad", optional = true}
futures = "0.3.33"
futures-util = "0.3.33"
thiserror = "2.0.19"
Expand Down
63 changes: 55 additions & 8 deletions crates/buttplug_client_in_process/src/in_process_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,41 @@ pub async fn in_process_client(client_name: &str) -> ButtplugClient {
.unwrap();

let mut device_manager_builder = ServerDeviceManagerBuilder::new(dcm);
register_comm_managers(&mut device_manager_builder);
let server_builder = ButtplugServerBuilder::new(device_manager_builder.finish().unwrap());
let server = server_builder.finish().unwrap();
let connector = ButtplugInProcessClientConnectorBuilder::default()
.server(server)
.finish();
let client = ButtplugClient::new(client_name);
client.connect(connector).await.unwrap();
client
}

/// Registers every comm manager selected by this crate's cargo features, and
/// returns the names of the managers that were registered so tests can assert
/// feature wiring (single source of truth: `in_process_client` uses this and
/// ignores the result).
// With no manager features enabled (how e.g. buttplug_tests consumes this
// crate), nothing is registered and the builder parameter goes unused.
#[allow(unused_mut, unused_variables)]
fn register_comm_managers(
device_manager_builder: &mut ServerDeviceManagerBuilder,
) -> Vec<&'static str> {
let mut registered = vec![];
#[cfg(feature = "btleplug-manager")]
{
use buttplug_server_hwmgr_btleplug::BtlePlugCommunicationManagerBuilder;
device_manager_builder.comm_manager(BtlePlugCommunicationManagerBuilder::default());
registered.push("btleplug");
}
#[cfg(feature = "websocket-manager")]
{
use buttplug_server_hwmgr_websocket::WebsocketServerDeviceCommunicationManagerBuilder;
device_manager_builder.comm_manager(
WebsocketServerDeviceCommunicationManagerBuilder::default().listen_on_all_interfaces(true),
);
registered.push("websocket-server");
}
#[cfg(all(
feature = "serial-manager",
Expand All @@ -69,12 +93,14 @@ pub async fn in_process_client(client_name: &str) -> ButtplugClient {
{
use buttplug_server_hwmgr_serial::SerialPortCommunicationManagerBuilder;
device_manager_builder.comm_manager(SerialPortCommunicationManagerBuilder::default());
registered.push("serial");
}
#[cfg(feature = "lovense-connect-service-manager")]
{
use buttplug_server_hwmgr_lovense_connect::LovenseConnectServiceCommunicationManagerBuilder;
device_manager_builder
.comm_manager(LovenseConnectServiceCommunicationManagerBuilder::default());
registered.push("lovense-connect-service");
}
#[cfg(all(
feature = "lovense-dongle-manager",
Expand All @@ -83,18 +109,39 @@ pub async fn in_process_client(client_name: &str) -> ButtplugClient {
{
use buttplug_server_hwmgr_lovense_dongle::LovenseHIDDongleCommunicationManagerBuilder;
device_manager_builder.comm_manager(LovenseHIDDongleCommunicationManagerBuilder::default());
registered.push("lovense-dongle");
}
#[cfg(all(feature = "xinput-manager", target_os = "windows"))]
{
use buttplug_server_hwmgr_xinput::XInputDeviceCommunicationManagerBuilder;
device_manager_builder.comm_manager(XInputDeviceCommunicationManagerBuilder::default());
registered.push("xinput");
}
// SDL gamepad manager is opt-in (not in the default feature set) and, unlike
// XInput, is cross-platform: no OS gate.
#[cfg(feature = "sdl-gamepad-manager")]
{
use buttplug_server_hwmgr_sdl_gamepad::SdlGamepadCommunicationManagerBuilder;
device_manager_builder.comm_manager(SdlGamepadCommunicationManagerBuilder::default());
registered.push("sdl-gamepad");
}
registered
}

#[cfg(all(test, feature = "sdl-gamepad-manager"))]
mod tests {
use super::*;

#[test]
fn feature_registers_sdl_manager() {
let dcm = DeviceConfigurationManagerBuilder::default()
.finish()
.unwrap();
let mut builder = ServerDeviceManagerBuilder::new(dcm);
let registered = register_comm_managers(&mut builder);
assert!(
registered.contains(&"sdl-gamepad"),
"SDL gamepad manager must be registered when the feature is enabled, got {registered:?}"
);
}
let server_builder = ButtplugServerBuilder::new(device_manager_builder.finish().unwrap());
let server = server_builder.finish().unwrap();
let connector = ButtplugInProcessClientConnectorBuilder::default()
.server(server)
.finish();
let client = ButtplugClient::new(client_name);
client.connect(connector).await.unwrap();
client
}
33 changes: 26 additions & 7 deletions crates/buttplug_server/src/device/device_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,25 @@ use buttplug_core::{
ButtplugResultFuture,
errors::{ButtplugDeviceError, ButtplugError},
message::{
self, ButtplugMessage, ButtplugServerMessageV4, DeviceFeature, DeviceMessageInfoV4,
InputCommandType, InputType, OutputValue, StopCmdV4,
self,
ButtplugMessage,
ButtplugServerMessageV4,
DeviceFeature,
DeviceMessageInfoV4,
InputCommandType,
InputType,
OutputValue,
StopCmdV4,
},
task_span,
util::async_manager,
util::stream::convert_broadcast_receiver_to_stream,
util::task::TaskGroup,
};
use buttplug_server_device_config::{
DeviceConfigurationManager, ServerDeviceDefinition, ServerDeviceFeatureOutput,
DeviceConfigurationManager,
ServerDeviceDefinition,
ServerDeviceFeatureOutput,
UserDeviceIdentifier,
};
use dashmap::DashMap;
Expand All @@ -51,14 +60,17 @@ use uuid::Uuid;
use crate::{
ButtplugServerResultFuture,
message::{
ButtplugServerDeviceMessage, checked_input_cmd::CheckedInputCmdV4,
checked_output_cmd::CheckedOutputCmdV4, server_device_attributes::ServerDeviceAttributes,
ButtplugServerDeviceMessage,
checked_input_cmd::CheckedInputCmdV4,
checked_output_cmd::CheckedOutputCmdV4,
server_device_attributes::ServerDeviceAttributes,
spec_enums::ButtplugDeviceCommandMessageUnionV4,
},
};

use super::{
InternalDeviceEvent, OutputObservation,
InternalDeviceEvent,
OutputObservation,
device_task::{DeviceTaskConfig, DeviceTaskMessage, WRITE_ACK_TIMEOUT, run_owned_device_task},
hardware::{Hardware, HardwareCommand, HardwareConnector, HardwareEvent},
protocol::{ProtocolHandler, ProtocolKeepaliveStrategy, ProtocolSpecializer},
Expand Down Expand Up @@ -581,7 +593,14 @@ pub(super) async fn build_device_handle(
// put it in an unknown state if anything fails.

// Check in the DeviceConfigurationManager to make sure we have attributes for this device.
let definition = if let Some(attrs) = device_config_manager.device_definition(&identifier) {
// Connectors may carry explicit selection metadata naming the base definition they chose
// (e.g. SDL gamepad rumble layout); when present, resolve and reconcile against that base.
// An invalid selection is a connection failure, never a silent fallback to defaults.
let definition = if let Some(selection) = hardware.definition_selection() {
device_config_manager
.device_definition_with_selection(&identifier, selection)
.map_err(|e| ButtplugDeviceError::DeviceConfigurationError(e.to_string()))?
} else if let Some(attrs) = device_config_manager.device_definition(&identifier) {
attrs
} else {
return Err(ButtplugDeviceError::DeviceConfigurationError(format!(
Expand Down
21 changes: 20 additions & 1 deletion crates/buttplug_server/src/device/hardware/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ use std::{collections::HashSet, fmt::Debug, sync::Arc, time::Duration};

use async_trait::async_trait;
use buttplug_core::errors::ButtplugDeviceError;
use buttplug_server_device_config::{Endpoint, ProtocolCommunicationSpecifier};
use buttplug_server_device_config::{
DeviceDefinitionSelection,
Endpoint,
ProtocolCommunicationSpecifier,
};
use futures::future::BoxFuture;
use futures_util::FutureExt;
use getset::{CopyGetters, Getters};
Expand Down Expand Up @@ -258,6 +262,13 @@ pub struct Hardware {
/// Device name
#[getset(get = "pub")]
name: String,
/// Optional connected-definition selection metadata, set by connectors that
/// pick a device definition themselves (e.g. SDL gamepad rumble layout
/// selection). When present, device configuration resolves against the
/// selected base definition instead of the ordinary identifier lookup. Not
/// persisted and never part of device identity.
#[getset(get = "pub")]
definition_selection: Option<DeviceDefinitionSelection>,
/// Device address
#[getset(get = "pub")]
address: String,
Expand Down Expand Up @@ -293,10 +304,18 @@ impl Hardware {
message_gap: *message_gap,
internal_impl,
requires_keepalive,
definition_selection: None,
last_write_time: Arc::new(RwLock::new(Instant::now())),
}
}

/// Attach connected-definition selection metadata (builder style), to be
/// called by the connector before the `Hardware` is shared.
pub fn with_definition_selection(mut self, selection: DeviceDefinitionSelection) -> Self {
self.definition_selection = Some(selection);
self
}

pub async fn time_since_last_write(&self) -> Duration {
Instant::now().duration_since(*self.last_write_time.read().await)
}
Expand Down
5 changes: 5 additions & 0 deletions crates/buttplug_server/src/device/protocol_impl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ pub mod raw_protocol;
pub mod realov;
pub mod sakuraneko;
pub mod satisfyer;
pub mod sdl_gamepad;
pub mod sensee;
pub mod sensee_capsule;
pub mod sensee_v2;
Expand Down Expand Up @@ -598,6 +599,10 @@ pub fn get_default_protocol_map() -> HashMap<String, Arc<dyn ProtocolIdentifierF
);
add_to_protocol_map(&mut map, xibao::setup::XibaoIdentifierFactory::default());
add_to_protocol_map(&mut map, xinput::setup::XInputIdentifierFactory::default());
add_to_protocol_map(
&mut map,
sdl_gamepad::setup::SdlGamepadIdentifierFactory::default(),
);
add_to_protocol_map(
&mut map,
xiuxiuda::setup::XiuxiudaIdentifierFactory::default(),
Expand Down
Loading