gui: Support for DualSense haptics and trigger effects

Haptics with PulseAudio does not seem to be working properly, so using
Pipewire as a backend is recommended (and picked by default, if
available via an SDL hint).
This commit is contained in:
Johannes Baiter 2022-11-01 10:37:20 +01:00 committed by Florian Märkl
parent 7a490b5aae
commit c2f0932670
12 changed files with 343 additions and 71 deletions

View file

@ -37,7 +37,7 @@ for:
install:
- git submodule update --init --recursive
- sudo pip3 install protobuf
- brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf
- HOMEBREW_NO_AUTO_UPDATE=1 brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf
- scripts/build-ffmpeg.sh
build_script:

View file

@ -67,12 +67,12 @@ if(CHIAKI_ENABLE_CLI)
endif()
target_link_libraries(chiaki Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Concurrent Qt5::Multimedia Qt5::OpenGL Qt5::Svg)
target_link_libraries(chiaki SDL2::SDL2)
if(APPLE)
target_link_libraries(chiaki Qt5::MacExtras)
target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_QT_MACEXTRAS)
endif()
if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER)
target_link_libraries(chiaki SDL2::SDL2)
target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER)
endif()
if(CHIAKI_ENABLE_SETSU)

View file

@ -77,6 +77,7 @@ class Controller : public QObject
int id;
ChiakiOrientationTracker orientation_tracker;
ChiakiControllerState state;
bool is_dualsense;
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
QMap<QPair<Sint64, Sint64>, uint8_t> touch_ids;
@ -91,9 +92,43 @@ class Controller : public QObject
QString GetName();
ChiakiControllerState GetState();
void SetRumble(uint8_t left, uint8_t right);
void SetTriggerEffects(uint8_t type_left, const uint8_t *data_left, uint8_t type_right, const uint8_t *data_right);
bool IsDualSense();
signals:
void StateChanged();
};
/* PS5 trigger effect documentation:
https://controllers.fandom.com/wiki/Sony_DualSense#FFB_Trigger_Modes
Taken from SDL2, licensed under the zlib license,
Copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
https://github.com/libsdl-org/SDL/blob/release-2.24.1/test/testgamecontroller.c#L263-L289
*/
typedef struct
{
Uint8 ucEnableBits1; /* 0 */
Uint8 ucEnableBits2; /* 1 */
Uint8 ucRumbleRight; /* 2 */
Uint8 ucRumbleLeft; /* 3 */
Uint8 ucHeadphoneVolume; /* 4 */
Uint8 ucSpeakerVolume; /* 5 */
Uint8 ucMicrophoneVolume; /* 6 */
Uint8 ucAudioEnableBits; /* 7 */
Uint8 ucMicLightMode; /* 8 */
Uint8 ucAudioMuteBits; /* 9 */
Uint8 rgucRightTriggerEffect[11]; /* 10 */
Uint8 rgucLeftTriggerEffect[11]; /* 21 */
Uint8 rgucUnknown1[6]; /* 32 */
Uint8 ucLedFlags; /* 38 */
Uint8 rgucUnknown2[2]; /* 39 */
Uint8 ucLedAnim; /* 41 */
Uint8 ucLedBrightness; /* 42 */
Uint8 ucPadLights; /* 43 */
Uint8 ucLedRed; /* 44 */
Uint8 ucLedGreen; /* 45 */
Uint8 ucLedBlue; /* 46 */
} DS5EffectsState_t;
#endif // CHIAKI_CONTROLLERMANAGER_H

View file

@ -63,6 +63,9 @@ class Settings : public QObject
void SetLogVerbose(bool enabled) { settings.setValue("settings/log_verbose", enabled); }
uint32_t GetLogLevelMask();
bool GetDualSenseEnabled() const { return settings.value("settings/dualsense_enabled", false).toBool(); }
void SetDualSenseEnabled(bool enabled) { settings.setValue("settings/dualsense_enabled", enabled); }
ChiakiVideoResolutionPreset GetResolution() const;
void SetResolution(ChiakiVideoResolutionPreset resolution);

View file

@ -20,6 +20,7 @@ class SettingsDialog : public QDialog
QCheckBox *log_verbose_check_box;
QComboBox *disconnect_action_combo_box;
QCheckBox *dualsense_check_box;
QComboBox *resolution_combo_box;
QComboBox *fps_combo_box;
@ -37,6 +38,7 @@ class SettingsDialog : public QDialog
private slots:
void LogVerboseChanged();
void DualSenseChanged();
void DisconnectActionSelected();
void ResolutionSelected();

View file

@ -56,6 +56,7 @@ struct StreamSessionConnectInfo
bool fullscreen;
TransformMode transform_mode;
bool enable_keyboard;
bool enable_dualsense;
StreamSessionConnectInfo(
Settings *settings,
@ -101,17 +102,23 @@ class StreamSession : public QObject
unsigned int audio_buffer_size;
QAudioOutput *audio_output;
QIODevice *audio_io;
SDL_AudioDeviceID haptics_output;
uint8_t *haptics_resampler_buf;
QMap<Qt::Key, int> key_map;
void PushAudioFrame(int16_t *buf, size_t samples_count);
void PushHapticsFrame(uint8_t *buf, size_t buf_size);
#if CHIAKI_GUI_ENABLE_SETSU
void HandleSetsuEvent(SetsuEvent *event);
#endif
private slots:
void InitAudio(unsigned int channels, unsigned int rate);
void InitHaptics();
void Event(ChiakiEvent *event);
void DisconnectHaptics();
void ConnectHaptics();
public:
explicit StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent = nullptr);

View file

@ -68,6 +68,12 @@ static QSet<QString> chiaki_motion_controller_guids({
"030000008f0e00001431000000000000",
});
static QSet<QPair<int16_t, int16_t>> chiaki_dualsense_controller_ids({
// in format (vendor id, product id)
QPair<int16_t, int16_t>(0x054c, 0x0ce6), // DualSense controller
QPair<int16_t, int16_t>(0x054c, 0x0df2), // DualSense Edge controller
});
static ControllerManager *instance = nullptr;
#define UPDATE_INTERVAL_MS 4
@ -84,6 +90,15 @@ ControllerManager::ControllerManager(QObject *parent)
{
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
SDL_SetMainReady();
#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
#endif
#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
#endif
#ifdef SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
#endif
if(SDL_Init(SDL_INIT_GAMECONTROLLER) < 0)
{
const char *err = SDL_GetError();
@ -225,7 +240,8 @@ void ControllerManager::ControllerClosed(Controller *controller)
open_controllers.remove(controller->GetDeviceID());
}
Controller::Controller(int device_id, ControllerManager *manager) : QObject(manager)
Controller::Controller(int device_id, ControllerManager *manager)
: QObject(manager), is_dualsense(false)
{
this->id = device_id;
this->manager = manager;
@ -244,8 +260,10 @@ Controller::Controller(int device_id, ControllerManager *manager) : QObject(mana
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE);
if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO))
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE);
break;
#endif
auto controller_id = QPair<int16_t, int16_t>(SDL_GameControllerGetVendor(controller), SDL_GameControllerGetProduct(controller));
is_dualsense = chiaki_dualsense_controller_ids.contains(controller_id);
break;
}
}
#endif
@ -255,7 +273,12 @@ Controller::~Controller()
{
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
if(controller)
{
// Clear trigger effects, SDL doesn't do it automatically
const uint8_t clear_effect[10] = { 0 };
this->SetTriggerEffects(0x05, clear_effect, 0x05, clear_effect);
SDL_GameControllerClose(controller);
}
#endif
manager->ControllerClosed(this);
}
@ -487,3 +510,28 @@ void Controller::SetRumble(uint8_t left, uint8_t right)
SDL_GameControllerRumble(controller, (uint16_t)left << 8, (uint16_t)right << 8, 5000);
#endif
}
void Controller::SetTriggerEffects(uint8_t type_left, const uint8_t *data_left, uint8_t type_right, const uint8_t *data_right)
{
#if defined(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) && SDL_VERSION_ATLEAST(2, 0, 16)
if(!is_dualsense || !controller)
return;
DS5EffectsState_t state;
SDL_zero(state);
state.ucEnableBits1 |= (0x04 /* left trigger */ | 0x08 /* right trigger */);
state.rgucLeftTriggerEffect[0] = type_left;
SDL_memcpy(state.rgucLeftTriggerEffect + 1, data_left, 10);
state.rgucRightTriggerEffect[0] = type_right;
SDL_memcpy(state.rgucRightTriggerEffect + 1, data_right, 10);
SDL_GameControllerSendEffect(controller, &state, sizeof(state));
#endif
}
bool Controller::IsDualSense()
{
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
if(!controller)
return false;
return is_dualsense;
#endif
}

View file

@ -69,6 +69,11 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa
log_verbose_check_box->setChecked(settings->GetLogVerbose());
connect(log_verbose_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::LogVerboseChanged);
dualsense_check_box = new QCheckBox(this);
general_layout->addRow(tr("Extended DualSense Support:\nEnable haptics and adaptive triggers\nfor attached DualSense controllers.\nThis is currently experimental."), dualsense_check_box);
dualsense_check_box->setChecked(settings->GetDualSenseEnabled());
connect(dualsense_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::DualSenseChanged);
auto log_directory_label = new QLineEdit(GetLogBaseDir(), this);
log_directory_label->setReadOnly(true);
general_layout->addRow(tr("Log Directory:"), log_directory_label);
@ -322,6 +327,11 @@ void SettingsDialog::LogVerboseChanged()
settings->SetLogVerbose(log_verbose_check_box->isChecked());
}
void SettingsDialog::DualSenseChanged()
{
settings->SetDualSenseEnabled(dualsense_check_box->isChecked());
}
void SettingsDialog::FPSSelected()
{
settings->SetFPS((ChiakiVideoFPSPreset)fps_combo_box->currentData().toInt());

View file

@ -14,6 +14,12 @@
#define SETSU_UPDATE_INTERVAL_MS 4
#ifdef Q_OS_LINUX
#define DUALSENSE_AUDIO_DEVICE_NEEDLE "DualSense"
#else
#define DUALSENSE_AUDIO_DEVICE_NEEDLE "Wireless Controller"
#endif
StreamSessionConnectInfo::StreamSessionConnectInfo(
Settings *settings,
ChiakiTarget target,
@ -39,10 +45,12 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(
this->fullscreen = fullscreen;
this->transform_mode = transform_mode;
this->enable_keyboard = false; // TODO: from settings
this->enable_dualsense = settings->GetDualSenseEnabled();
}
static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user);
static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user);
static void HapticsFrameCb(uint8_t *buf, size_t buf_size, void *user);
static void EventCb(ChiakiEvent *event, void *user);
#if CHIAKI_GUI_ENABLE_SETSU
static void SessionSetsuCb(SetsuEvent *event, void *user);
@ -57,7 +65,9 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje
pi_decoder(nullptr),
#endif
audio_output(nullptr),
audio_io(nullptr)
audio_io(nullptr),
haptics_output(0),
haptics_resampler_buf(nullptr)
{
connected = false;
ChiakiErrorCode err;
@ -116,6 +126,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje
chiaki_connect_info.video_profile = connect_info.video_profile;
chiaki_connect_info.video_profile_auto_downgrade = true;
chiaki_connect_info.enable_keyboard = false;
chiaki_connect_info.enable_dualsense = connect_info.enable_dualsense;
#if CHIAKI_LIB_ENABLE_PI_DECODER
if(connect_info.decoder == Decoder::Pi && chiaki_connect_info.video_profile.codec != CHIAKI_CODEC_H264)
@ -144,6 +155,14 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje
chiaki_opus_decoder_get_sink(&opus_decoder, &audio_sink);
chiaki_session_set_audio_sink(&session, &audio_sink);
if(connect_info.enable_dualsense)
{
ChiakiAudioSink haptics_sink;
haptics_sink.user = this;
haptics_sink.frame_cb = HapticsFrameCb;
chiaki_session_set_haptics_sink(&session, &haptics_sink);
}
#if CHIAKI_LIB_ENABLE_PI_DECODER
if(pi_decoder)
chiaki_session_set_video_sample_cb(&session, chiaki_pi_decoder_video_sample_cb, pi_decoder);
@ -181,6 +200,10 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje
#endif
key_map = connect_info.key_map;
if(connect_info.enable_dualsense)
{
InitHaptics();
}
UpdateGamepads();
}
@ -208,6 +231,16 @@ StreamSession::~StreamSession()
chiaki_ffmpeg_decoder_fini(ffmpeg_decoder);
delete ffmpeg_decoder;
}
if(haptics_output > 0)
{
SDL_CloseAudioDevice(haptics_output);
haptics_output = 0;
}
if(haptics_resampler_buf)
{
free(haptics_resampler_buf);
haptics_resampler_buf = nullptr;
}
}
void StreamSession::Start()
@ -312,6 +345,8 @@ void StreamSession::UpdateGamepads()
{
CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d disconnected", controller->GetDeviceID());
controllers.remove(controller_id);
if(controller->IsDualSense())
DisconnectHaptics();
delete controller;
}
}
@ -330,6 +365,11 @@ void StreamSession::UpdateGamepads()
CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d opened: \"%s\"", controller_id, controller->GetName().toLocal8Bit().constData());
connect(controller, &Controller::StateChanged, this, &StreamSession::SendFeedbackState);
controllers[controller_id] = controller;
if(controller->IsDualSense())
{
// Connect haptics audio device with a delay to give the sound system time to set up
QTimer::singleShot(1000, this, &StreamSession::ConnectHaptics);
}
}
}
@ -388,6 +428,82 @@ void StreamSession::InitAudio(unsigned int channels, unsigned int rate)
channels, rate, audio_output->bufferSize());
}
void StreamSession::InitHaptics()
{
haptics_output = 0;
haptics_resampler_buf = nullptr;
#ifdef Q_OS_LINUX
// Haptics work most reliably with Pipewire, so try to use that if available
SDL_SetHint("SDL_AUDIODRIVER", "pipewire");
#endif
if(SDL_Init(SDL_INIT_AUDIO) < 0)
{
CHIAKI_LOGE(log.GetChiakiLog(), "Could not initialize SDL Audio for haptics output: %s", SDL_GetError());
return;
}
#ifdef Q_OS_LINUX
if(!strstr(SDL_GetCurrentAudioDriver(), "pipewire"))
{
CHIAKI_LOGW(
log.GetChiakiLog(),
"Haptics output is not using Pipewire, this may not work reliably. (was: '%s')",
SDL_GetCurrentAudioDriver());
}
#endif
SDL_AudioCVT cvt;
SDL_BuildAudioCVT(&cvt, AUDIO_S16LSB, 4, 3000, AUDIO_S16LSB, 4, 48000);
cvt.len = 240; // 10 16bit stereo samples
haptics_resampler_buf = (uint8_t*) calloc(cvt.len * cvt.len_mult, sizeof(uint8_t));
}
void StreamSession::DisconnectHaptics()
{
if(this->haptics_output > 0)
{
SDL_CloseAudioDevice(haptics_output);
this->haptics_output = 0;
}
}
void StreamSession::ConnectHaptics()
{
if(this->haptics_output > 0)
{
CHIAKI_LOGW(this->log.GetChiakiLog(), "Haptics already connected to an attached DualSense controller, ignoring additional controllers.");
return;
}
SDL_AudioSpec want, have;
SDL_zero(want);
want.freq = 48000;
want.format = AUDIO_S16LSB;
want.channels = 4;
want.samples = 480; // 10ms buffer
want.callback = NULL;
const char *device_name = nullptr;
for(int i=0; i < SDL_GetNumAudioDevices(0); i++)
{
device_name = SDL_GetAudioDeviceName(i, 0);
if(!device_name || !strstr(device_name, DUALSENSE_AUDIO_DEVICE_NEEDLE))
continue;
haptics_output = SDL_OpenAudioDevice(device_name, 0, &want, &have, 0);
if(haptics_output == 0)
{
CHIAKI_LOGE(log.GetChiakiLog(), "Could not open SDL Audio Device %s for haptics output: %s", device_name, SDL_GetError());
continue;
}
SDL_PauseAudioDevice(haptics_output, 0);
CHIAKI_LOGI(log.GetChiakiLog(), "Haptics Audio Device '%s' opened with %d channels @ %d Hz, buffer size %u (driver=%s)", device_name, have.channels, have.freq, have.size, SDL_GetCurrentAudioDriver());
return;
}
CHIAKI_LOGW(log.GetChiakiLog(), "DualSense features were enabled and a DualSense is connected, but could not find the DualSense audio device!");
return;
}
void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count)
{
if(!audio_io)
@ -395,6 +511,35 @@ void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count)
audio_io->write((const char *)buf, static_cast<qint64>(samples_count * 2 * 2));
}
void StreamSession::PushHapticsFrame(uint8_t *buf, size_t buf_size)
{
if(haptics_output == 0)
return;
SDL_AudioCVT cvt;
// Haptics samples are coming in at 3KHZ, but the DualSense expects 48KHZ
SDL_BuildAudioCVT(&cvt, AUDIO_S16LSB, 4, 3000, AUDIO_S16LSB, 4, 48000);
cvt.len = buf_size * 2;
cvt.buf = haptics_resampler_buf;
// Remix to 4 channels
for (int i=0; i < buf_size; i+=4)
{
SDL_memset(haptics_resampler_buf + i * 2, 0, 4);
SDL_memcpy(haptics_resampler_buf + (i * 2) + 4, buf + i, 4);
}
// Resample to 48kHZ
if(SDL_ConvertAudio(&cvt) != 0)
{
CHIAKI_LOGE(log.GetChiakiLog(), "Failed to resample haptics audio: %s", SDL_GetError());
return;
}
if(SDL_QueueAudio(haptics_output, cvt.buf, cvt.len_cvt) < 0)
{
CHIAKI_LOGE(log.GetChiakiLog(), "Failed to submit haptics audio to device: %s", SDL_GetError());
return;
}
}
void StreamSession::Event(ChiakiEvent *event)
{
switch(event->type)
@ -418,6 +563,19 @@ void StreamSession::Event(ChiakiEvent *event)
});
break;
}
case CHIAKI_EVENT_TRIGGER_EFFECTS: {
uint8_t type_left = event->trigger_effects.type_left;
uint8_t data_left[10];
memcpy(data_left, event->trigger_effects.left, 10);
uint8_t data_right[10];
memcpy(data_right, event->trigger_effects.right, 10);
uint8_t type_right = event->trigger_effects.type_right;
QMetaObject::invokeMethod(this, [this, type_left, data_left, type_right, data_right]() {
for(auto controller : controllers)
controller->SetTriggerEffects(type_left, data_left, type_right, data_right);
});
break;
}
default:
break;
}
@ -544,6 +702,7 @@ class StreamSessionPrivate
}
static void PushAudioFrame(StreamSession *session, int16_t *buf, size_t samples_count) { session->PushAudioFrame(buf, samples_count); }
static void PushHapticsFrame(StreamSession *session, uint8_t *buf, size_t buf_size) { session->PushHapticsFrame(buf, buf_size); }
static void Event(StreamSession *session, ChiakiEvent *event) { session->Event(event); }
#if CHIAKI_GUI_ENABLE_SETSU
static void HandleSetsuEvent(StreamSession *session, SetsuEvent *event) { session->HandleSetsuEvent(event); }
@ -563,6 +722,12 @@ static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user)
StreamSessionPrivate::PushAudioFrame(session, buf, samples_count);
}
static void HapticsFrameCb(uint8_t *buf, size_t buf_size, void *user)
{
auto session = reinterpret_cast<StreamSession *>(user);
StreamSessionPrivate::PushHapticsFrame(session, buf, buf_size);
}
static void EventCb(ChiakiEvent *event, void *user)
{
auto session = reinterpret_cast<StreamSession *>(user);

View file

@ -30,14 +30,15 @@ ninja
ninja install
cd ../..
wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1q.zip && 7z x openssl-1.1.1q.zip
wget https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-1.1.1s.zip && 7z x openssl-1.1.*.zip
wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip
export SDL_ROOT="$BUILD_ROOT/SDL2-2.0.10"
wget https://www.libsdl.org/release/SDL2-devel-2.26.2-VC.zip && 7z x SDL2-devel-2.26.2-VC.zip
export SDL_ROOT="$BUILD_ROOT/SDL2-2.26.2"
export SDL_ROOT=${SDL_ROOT//[\\]//}
echo "set(SDL2_INCLUDE_DIRS \"$SDL_ROOT/include\")
set(SDL2_LIBRARIES \"$SDL_ROOT/lib/x64/SDL2.lib\")
set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")" > "$SDL_ROOT/SDL2Config.cmake"
set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")
include($SDL_ROOT/cmake/sdl2-config-version.cmake)" > "$SDL_ROOT/SDL2Config.cmake"
mkdir protoc && cd protoc
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip
@ -55,6 +56,7 @@ echo "-- Configure"
mkdir build && cd build
cmake \
-G Ninja \
-DCMAKE_C_COMPILER=cl \

View file

@ -22,14 +22,14 @@ mkdir -p build && cd build || exit 1
cmake \
-DCMAKE_INSTALL_PREFIX="$ROOT/sdl2-prefix" \
-DSDL_ATOMIC=OFF \
-DSDL_AUDIO=OFF \
-DSDL_AUDIO=ON \
-DSDL_CPUINFO=OFF \
-DSDL_EVENTS=ON \
-DSDL_FILE=OFF \
-DSDL_FILESYSTEM=OFF \
-DSDL_HAPTIC=ON \
-DSDL_JOYSTICK=ON \
-DSDL_LOADSO=OFF \
-DSDL_LOADSO=ON \
-DSDL_RENDER=OFF \
-DSDL_SHARED=ON \
-DSDL_STATIC=OFF \

View file

@ -1,64 +1,64 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=cUKW
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
=8BHh
-----END PGP PUBLIC KEY BLOCK-----