| /* |
| Simple DirectMedia Layer |
| Copyright (C) 1997-2014 Sam Lantinga <[email protected]> |
| |
| This software is provided 'as-is', without any express or implied |
| warranty. In no event will the authors be held liable for any damages |
| arising from the use of this software. |
| |
| Permission is granted to anyone to use this software for any purpose, |
| including commercial applications, and to alter it and redistribute it |
| freely, subject to the following restrictions: |
| |
| 1. The origin of this software must not be misrepresented; you must not |
| claim that you wrote the original software. If you use this software |
| in a product, an acknowledgment in the product documentation would be |
| appreciated but is not required. |
| 2. Altered source versions must be plainly marked as such, and must not be |
| misrepresented as being the original software. |
| 3. This notice may not be removed or altered from any source distribution. |
| */ |
| #include "../../SDL_internal.h" |
| |
| #ifdef SDL_HAPTIC_IOKIT |
| |
| #include "SDL_assert.h" |
| #include "SDL_haptic.h" |
| #include "../SDL_syshaptic.h" |
| #include "SDL_joystick.h" |
| #include "../../joystick/SDL_sysjoystick.h" /* For the real SDL_Joystick */ |
| #include "../../joystick/darwin/SDL_sysjoystick_c.h" /* For joystick hwdata */ |
| #include "SDL_syshaptic_c.h" |
| |
| #include <IOKit/IOKitLib.h> |
| #include <IOKit/hid/IOHIDKeys.h> |
| #include <IOKit/hid/IOHIDUsageTables.h> |
| #include <ForceFeedback/ForceFeedback.h> |
| #include <ForceFeedback/ForceFeedbackConstants.h> |
| |
| #ifndef IO_OBJECT_NULL |
| #define IO_OBJECT_NULL ((io_service_t)0) |
| #endif |
| |
| /* |
| * List of available haptic devices. |
| */ |
| typedef struct SDL_hapticlist_item |
| { |
| char name[256]; /* Name of the device. */ |
| |
| io_service_t dev; /* Node we use to create the device. */ |
| SDL_Haptic *haptic; /* Haptic currently associated with it. */ |
| |
| /* Usage pages for determining if it's a mouse or not. */ |
| long usage; |
| long usagePage; |
| |
| struct SDL_hapticlist_item *next; |
| } SDL_hapticlist_item; |
| |
| |
| /* |
| * Haptic system hardware data. |
| */ |
| struct haptic_hwdata |
| { |
| FFDeviceObjectReference device; /* Hardware device. */ |
| UInt8 axes[3]; |
| }; |
| |
| |
| /* |
| * Haptic system effect data. |
| */ |
| struct haptic_hweffect |
| { |
| FFEffectObjectReference ref; /* Reference. */ |
| struct FFEFFECT effect; /* Hardware effect. */ |
| }; |
| |
| /* |
| * Prototypes. |
| */ |
| static void SDL_SYS_HapticFreeFFEFFECT(FFEFFECT * effect, int type); |
| static int HIDGetDeviceProduct(io_service_t dev, char *name); |
| |
| static SDL_hapticlist_item *SDL_hapticlist = NULL; |
| static SDL_hapticlist_item *SDL_hapticlist_tail = NULL; |
| static int numhaptics = -1; |
| |
| /* |
| * Like strerror but for force feedback errors. |
| */ |
| static const char * |
| FFStrError(HRESULT err) |
| { |
| switch (err) { |
| case FFERR_DEVICEFULL: |
| return "device full"; |
| /* This should be valid, but for some reason isn't defined... */ |
| /* case FFERR_DEVICENOTREG: |
| return "device not registered"; */ |
| case FFERR_DEVICEPAUSED: |
| return "device paused"; |
| case FFERR_DEVICERELEASED: |
| return "device released"; |
| case FFERR_EFFECTPLAYING: |
| return "effect playing"; |
| case FFERR_EFFECTTYPEMISMATCH: |
| return "effect type mismatch"; |
| case FFERR_EFFECTTYPENOTSUPPORTED: |
| return "effect type not supported"; |
| case FFERR_GENERIC: |
| return "undetermined error"; |
| case FFERR_HASEFFECTS: |
| return "device has effects"; |
| case FFERR_INCOMPLETEEFFECT: |
| return "incomplete effect"; |
| case FFERR_INTERNAL: |
| return "internal fault"; |
| case FFERR_INVALIDDOWNLOADID: |
| return "invalid download id"; |
| case FFERR_INVALIDPARAM: |
| return "invalid parameter"; |
| case FFERR_MOREDATA: |
| return "more data"; |
| case FFERR_NOINTERFACE: |
| return "interface not supported"; |
| case FFERR_NOTDOWNLOADED: |
| return "effect is not downloaded"; |
| case FFERR_NOTINITIALIZED: |
| return "object has not been initialized"; |
| case FFERR_OUTOFMEMORY: |
| return "out of memory"; |
| case FFERR_UNPLUGGED: |
| return "device is unplugged"; |
| case FFERR_UNSUPPORTED: |
| return "function call unsupported"; |
| case FFERR_UNSUPPORTEDAXIS: |
| return "axis unsupported"; |
| |
| default: |
| return "unknown error"; |
| } |
| } |
| |
| |
| /* |
| * Initializes the haptic subsystem. |
| */ |
| int |
| SDL_SYS_HapticInit(void) |
| { |
| IOReturn result; |
| io_iterator_t iter; |
| CFDictionaryRef match; |
| io_service_t device; |
| |
| if (numhaptics != -1) { |
| return SDL_SetError("Haptic subsystem already initialized!"); |
| } |
| numhaptics = 0; |
| |
| /* Get HID devices. */ |
| match = IOServiceMatching(kIOHIDDeviceKey); |
| if (match == NULL) { |
| return SDL_SetError("Haptic: Failed to get IOServiceMatching."); |
| } |
| |
| /* Now search I/O Registry for matching devices. */ |
| result = IOServiceGetMatchingServices(kIOMasterPortDefault, match, &iter); |
| if (result != kIOReturnSuccess) { |
| return SDL_SetError("Haptic: Couldn't create a HID object iterator."); |
| } |
| /* IOServiceGetMatchingServices consumes dictionary. */ |
| |
| if (!IOIteratorIsValid(iter)) { /* No iterator. */ |
| return 0; |
| } |
| |
| while ((device = IOIteratorNext(iter)) != IO_OBJECT_NULL) { |
| MacHaptic_MaybeAddDevice(device); |
| /* always release as the AddDevice will retain IF it's a forcefeedback device */ |
| IOObjectRelease(device); |
| } |
| IOObjectRelease(iter); |
| |
| return numhaptics; |
| } |
| |
| int |
| SDL_SYS_NumHaptics() |
| { |
| return numhaptics; |
| } |
| |
| static SDL_hapticlist_item * |
| HapticByDevIndex(int device_index) |
| { |
| SDL_hapticlist_item *item = SDL_hapticlist; |
| |
| if ((device_index < 0) || (device_index >= numhaptics)) { |
| return NULL; |
| } |
| |
| while (device_index > 0) { |
| SDL_assert(item != NULL); |
| --device_index; |
| item = item->next; |
| } |
| |
| return item; |
| } |
| |
| int |
| MacHaptic_MaybeAddDevice( io_object_t device ) |
| { |
| IOReturn result; |
| CFMutableDictionaryRef hidProperties; |
| CFTypeRef refCF; |
| SDL_hapticlist_item *item; |
| |
| if (numhaptics == -1) { |
| return -1; /* not initialized. We'll pick these up on enumeration if we init later. */ |
| } |
| |
| /* Check for force feedback. */ |
| if (FFIsForceFeedback(device) != FF_OK) { |
| return -1; |
| } |
| |
| /* Make sure we don't already have it */ |
| for (item = SDL_hapticlist; item ; item = item->next) |
| { |
| if (IOObjectIsEqualTo((io_object_t) item->dev, device)) { |
| /* Already added */ |
| return -1; |
| } |
| } |
| |
| item = (SDL_hapticlist_item *)SDL_calloc(1, sizeof(SDL_hapticlist_item)); |
| if (item == NULL) { |
| return SDL_SetError("Could not allocate haptic storage"); |
| } |
| |
| /* retain it as we are going to keep it around a while */ |
| IOObjectRetain(device); |
| |
| /* Set basic device data. */ |
| HIDGetDeviceProduct(device, item->name); |
| item->dev = device; |
| |
| /* Set usage pages. */ |
| hidProperties = 0; |
| refCF = 0; |
| result = IORegistryEntryCreateCFProperties(device, |
| &hidProperties, |
| kCFAllocatorDefault, |
| kNilOptions); |
| if ((result == KERN_SUCCESS) && hidProperties) { |
| refCF = |
| CFDictionaryGetValue(hidProperties, |
| CFSTR(kIOHIDPrimaryUsagePageKey)); |
| if (refCF) { |
| if (!CFNumberGetValue(refCF, kCFNumberLongType, |
| &item->usagePage)) |
| SDL_SetError |
| ("Haptic: Recieving device's usage page."); |
| refCF = |
| CFDictionaryGetValue(hidProperties, |
| CFSTR(kIOHIDPrimaryUsageKey)); |
| if (refCF) { |
| if (!CFNumberGetValue(refCF, kCFNumberLongType, |
| &item->usage)) |
| SDL_SetError("Haptic: Recieving device's usage."); |
| } |
| } |
| CFRelease(hidProperties); |
| } |
| |
| if (SDL_hapticlist_tail == NULL) { |
| SDL_hapticlist = SDL_hapticlist_tail = item; |
| } else { |
| SDL_hapticlist_tail->next = item; |
| SDL_hapticlist_tail = item; |
| } |
| |
| /* Device has been added. */ |
| ++numhaptics; |
| |
| return numhaptics; |
| } |
| |
| int |
| MacHaptic_MaybeRemoveDevice( io_object_t device ) |
| { |
| SDL_hapticlist_item *item; |
| SDL_hapticlist_item *prev = NULL; |
| |
| if (numhaptics == -1) { |
| return -1; /* not initialized. ignore this. */ |
| } |
| |
| for (item = SDL_hapticlist; item != NULL; item = item->next) { |
| /* found it, remove it. */ |
| if (IOObjectIsEqualTo((io_object_t) item->dev, device)) { |
| const int retval = item->haptic ? item->haptic->index : -1; |
| |
| if (prev != NULL) { |
| prev->next = item->next; |
| } else { |
| SDL_assert(SDL_hapticlist == item); |
| SDL_hapticlist = item->next; |
| } |
| if (item == SDL_hapticlist_tail) { |
| SDL_hapticlist_tail = prev; |
| } |
| |
| /* Need to decrement the haptic count */ |
| --numhaptics; |
| /* !!! TODO: Send a haptic remove event? */ |
| |
| IOObjectRelease(item->dev); |
| SDL_free(item); |
| return retval; |
| } |
| prev = item; |
| } |
| |
| return -1; |
| } |
| |
| /* |
| * Return the name of a haptic device, does not need to be opened. |
| */ |
| const char * |
| SDL_SYS_HapticName(int index) |
| { |
| SDL_hapticlist_item *item; |
| item = HapticByDevIndex(index); |
| return item->name; |
| } |
| |
| /* |
| * Gets the device's product name. |
| */ |
| static int |
| HIDGetDeviceProduct(io_service_t dev, char *name) |
| { |
| CFMutableDictionaryRef hidProperties, usbProperties; |
| io_registry_entry_t parent1, parent2; |
| kern_return_t ret; |
| |
| hidProperties = usbProperties = 0; |
| |
| ret = IORegistryEntryCreateCFProperties(dev, &hidProperties, |
| kCFAllocatorDefault, kNilOptions); |
| if ((ret != KERN_SUCCESS) || !hidProperties) { |
| return SDL_SetError("Haptic: Unable to create CFProperties."); |
| } |
| |
| /* Mac OS X currently is not mirroring all USB properties to HID page so need to look at USB device page also |
| * get dictionary for USB properties: step up two levels and get CF dictionary for USB properties |
| */ |
| if ((KERN_SUCCESS == |
| IORegistryEntryGetParentEntry(dev, kIOServicePlane, &parent1)) |
| && (KERN_SUCCESS == |
| IORegistryEntryGetParentEntry(parent1, kIOServicePlane, &parent2)) |
| && (KERN_SUCCESS == |
| IORegistryEntryCreateCFProperties(parent2, &usbProperties, |
| kCFAllocatorDefault, |
| kNilOptions))) { |
| if (usbProperties) { |
| CFTypeRef refCF = 0; |
| /* get device info |
| * try hid dictionary first, if fail then go to USB dictionary |
| */ |
| |
| |
| /* Get product name */ |
| refCF = |
| CFDictionaryGetValue(hidProperties, CFSTR(kIOHIDProductKey)); |
| if (!refCF) |
| refCF = |
| CFDictionaryGetValue(usbProperties, |
| CFSTR("USB Product Name")); |
| if (refCF) { |
| if (!CFStringGetCString(refCF, name, 256, |
| CFStringGetSystemEncoding())) { |
| return SDL_SetError |
| ("Haptic: CFStringGetCString error retrieving pDevice->product."); |
| } |
| } |
| |
| CFRelease(usbProperties); |
| } else { |
| return SDL_SetError |
| ("Haptic: IORegistryEntryCreateCFProperties failed to create usbProperties."); |
| } |
| |
| /* Release stuff. */ |
| if (kIOReturnSuccess != IOObjectRelease(parent2)) { |
| SDL_SetError("Haptic: IOObjectRelease error with parent2."); |
| } |
| if (kIOReturnSuccess != IOObjectRelease(parent1)) { |
| SDL_SetError("Haptic: IOObjectRelease error with parent1."); |
| } |
| } else { |
| return SDL_SetError("Haptic: Error getting registry entries."); |
| } |
| |
| return 0; |
| } |
| |
| |
| #define FF_TEST(ff, s) \ |
| if (features.supportedEffects & (ff)) supported |= (s) |
| /* |
| * Gets supported features. |
| */ |
| static unsigned int |
| GetSupportedFeatures(SDL_Haptic * haptic) |
| { |
| HRESULT ret; |
| FFDeviceObjectReference device; |
| FFCAPABILITIES features; |
| unsigned int supported; |
| Uint32 val; |
| |
| device = haptic->hwdata->device; |
| |
| ret = FFDeviceGetForceFeedbackCapabilities(device, &features); |
| if (ret != FF_OK) { |
| return SDL_SetError("Haptic: Unable to get device's supported features."); |
| } |
| |
| supported = 0; |
| |
| /* Get maximum effects. */ |
| haptic->neffects = features.storageCapacity; |
| haptic->nplaying = features.playbackCapacity; |
| |
| /* Test for effects. */ |
| FF_TEST(FFCAP_ET_CONSTANTFORCE, SDL_HAPTIC_CONSTANT); |
| FF_TEST(FFCAP_ET_RAMPFORCE, SDL_HAPTIC_RAMP); |
| /* !!! FIXME: put this back when we have more bits in 2.1 */ |
| /* FF_TEST(FFCAP_ET_SQUARE, SDL_HAPTIC_SQUARE); */ |
| FF_TEST(FFCAP_ET_SINE, SDL_HAPTIC_SINE); |
| FF_TEST(FFCAP_ET_TRIANGLE, SDL_HAPTIC_TRIANGLE); |
| FF_TEST(FFCAP_ET_SAWTOOTHUP, SDL_HAPTIC_SAWTOOTHUP); |
| FF_TEST(FFCAP_ET_SAWTOOTHDOWN, SDL_HAPTIC_SAWTOOTHDOWN); |
| FF_TEST(FFCAP_ET_SPRING, SDL_HAPTIC_SPRING); |
| FF_TEST(FFCAP_ET_DAMPER, SDL_HAPTIC_DAMPER); |
| FF_TEST(FFCAP_ET_INERTIA, SDL_HAPTIC_INERTIA); |
| FF_TEST(FFCAP_ET_FRICTION, SDL_HAPTIC_FRICTION); |
| FF_TEST(FFCAP_ET_CUSTOMFORCE, SDL_HAPTIC_CUSTOM); |
| |
| /* Check if supports gain. */ |
| ret = FFDeviceGetForceFeedbackProperty(device, FFPROP_FFGAIN, |
| &val, sizeof(val)); |
| if (ret == FF_OK) |
| supported |= SDL_HAPTIC_GAIN; |
| else if (ret != FFERR_UNSUPPORTED) { |
| return SDL_SetError("Haptic: Unable to get if device supports gain: %s.", |
| FFStrError(ret)); |
| } |
| |
| /* Checks if supports autocenter. */ |
| ret = FFDeviceGetForceFeedbackProperty(device, FFPROP_AUTOCENTER, |
| &val, sizeof(val)); |
| if (ret == FF_OK) |
| supported |= SDL_HAPTIC_AUTOCENTER; |
| else if (ret != FFERR_UNSUPPORTED) { |
| return SDL_SetError |
| ("Haptic: Unable to get if device supports autocenter: %s.", |
| FFStrError(ret)); |
| } |
| |
| /* Check for axes, we have an artificial limit on axes */ |
| haptic->naxes = ((features.numFfAxes) > 3) ? 3 : features.numFfAxes; |
| /* Actually store the axes we want to use */ |
| SDL_memcpy(haptic->hwdata->axes, features.ffAxes, |
| haptic->naxes * sizeof(Uint8)); |
| |
| /* Always supported features. */ |
| supported |= SDL_HAPTIC_STATUS | SDL_HAPTIC_PAUSE; |
| |
| haptic->supported = supported; |
| return 0;; |
| } |
| |
| |
| /* |
| * Opens the haptic device from the file descriptor. |
| */ |
| static int |
| SDL_SYS_HapticOpenFromService(SDL_Haptic * haptic, io_service_t service) |
| { |
| HRESULT ret; |
| int ret2; |
| |
| /* Allocate the hwdata */ |
| haptic->hwdata = (struct haptic_hwdata *) |
| SDL_malloc(sizeof(*haptic->hwdata)); |
| if (haptic->hwdata == NULL) { |
| SDL_OutOfMemory(); |
| goto creat_err; |
| } |
| SDL_memset(haptic->hwdata, 0, sizeof(*haptic->hwdata)); |
| |
| /* Open the device */ |
| ret = FFCreateDevice(service, &haptic->hwdata->device); |
| if (ret != FF_OK) { |
| SDL_SetError("Haptic: Unable to create device from service: %s.", |
| FFStrError(ret)); |
| goto creat_err; |
| } |
| |
| /* Get supported features. */ |
| ret2 = GetSupportedFeatures(haptic); |
| if (ret2 < 0) { |
| goto open_err; |
| } |
| |
| |
| /* Reset and then enable actuators. */ |
| ret = FFDeviceSendForceFeedbackCommand(haptic->hwdata->device, |
| FFSFFC_RESET); |
| if (ret != FF_OK) { |
| SDL_SetError("Haptic: Unable to reset device: %s.", FFStrError(ret)); |
| goto open_err; |
| } |
| ret = FFDeviceSendForceFeedbackCommand(haptic->hwdata->device, |
| FFSFFC_SETACTUATORSON); |
| if (ret != FF_OK) { |
| SDL_SetError("Haptic: Unable to enable actuators: %s.", |
| FFStrError(ret)); |
| goto open_err; |
| } |
| |
| |
| /* Allocate effects memory. */ |
| haptic->effects = (struct haptic_effect *) |
| SDL_malloc(sizeof(struct haptic_effect) * haptic->neffects); |
| if (haptic->effects == NULL) { |
| SDL_OutOfMemory(); |
| goto open_err; |
| } |
| /* Clear the memory */ |
| SDL_memset(haptic->effects, 0, |
| sizeof(struct haptic_effect) * haptic->neffects); |
| |
| return 0; |
| |
| /* Error handling */ |
| open_err: |
| FFReleaseDevice(haptic->hwdata->device); |
| creat_err: |
| if (haptic->hwdata != NULL) { |
| free(haptic->hwdata); |
| haptic->hwdata = NULL; |
| } |
| return -1; |
| |
| } |
| |
| |
| /* |
| * Opens a haptic device for usage. |
| */ |
| int |
| SDL_SYS_HapticOpen(SDL_Haptic * haptic) |
| { |
| SDL_hapticlist_item *item; |
| item = HapticByDevIndex(haptic->index); |
| |
| return SDL_SYS_HapticOpenFromService(haptic, item->dev); |
| } |
| |
| |
| /* |
| * Opens a haptic device from first mouse it finds for usage. |
| */ |
| int |
| SDL_SYS_HapticMouse(void) |
| { |
| int device_index = 0; |
| SDL_hapticlist_item *item; |
| |
| for (item = SDL_hapticlist; item; item = item->next) { |
| if ((item->usagePage == kHIDPage_GenericDesktop) && |
| (item->usage == kHIDUsage_GD_Mouse)) { |
| return device_index; |
| } |
| ++device_index; |
| } |
| |
| return -1; |
| } |
| |
| |
| /* |
| * Checks to see if a joystick has haptic features. |
| */ |
| int |
| SDL_SYS_JoystickIsHaptic(SDL_Joystick * joystick) |
| { |
| if (joystick->hwdata->ffservice != 0) |
| return SDL_TRUE; |
| return SDL_FALSE; |
| } |
| |
| |
| /* |
| * Checks to see if the haptic device and joystick are in reality the same. |
| */ |
| int |
| SDL_SYS_JoystickSameHaptic(SDL_Haptic * haptic, SDL_Joystick * joystick) |
| { |
| if (IOObjectIsEqualTo((io_object_t) ((size_t)haptic->hwdata->device), |
| joystick->hwdata->ffservice)) |
| return 1; |
| return 0; |
| } |
| |
| |
| /* |
| * Opens a SDL_Haptic from a SDL_Joystick. |
| */ |
| int |
| SDL_SYS_HapticOpenFromJoystick(SDL_Haptic * haptic, SDL_Joystick * joystick) |
| { |
| int device_index = 0; |
| SDL_hapticlist_item *item; |
| |
| for (item = SDL_hapticlist; item; item = item->next) { |
| if (IOObjectIsEqualTo((io_object_t) item->dev, |
| joystick->hwdata->ffservice)) { |
| haptic->index = device_index; |
| break; |
| } |
| ++device_index; |
| } |
| |
| return SDL_SYS_HapticOpenFromService(haptic, joystick->hwdata->ffservice); |
| } |
| |
| |
| /* |
| * Closes the haptic device. |
| */ |
| void |
| SDL_SYS_HapticClose(SDL_Haptic * haptic) |
| { |
| if (haptic->hwdata) { |
| |
| /* Free Effects. */ |
| SDL_free(haptic->effects); |
| haptic->effects = NULL; |
| haptic->neffects = 0; |
| |
| /* Clean up */ |
| FFReleaseDevice(haptic->hwdata->device); |
| |
| /* Free */ |
| SDL_free(haptic->hwdata); |
| haptic->hwdata = NULL; |
| } |
| } |
| |
| |
| /* |
| * Clean up after system specific haptic stuff |
| */ |
| void |
| SDL_SYS_HapticQuit(void) |
| { |
| SDL_hapticlist_item *item; |
| SDL_hapticlist_item *next = NULL; |
| |
| for (item = SDL_hapticlist; item; item = next) { |
| next = item->next; |
| /* Opened and not closed haptics are leaked, this is on purpose. |
| * Close your haptic devices after usage. */ |
| |
| /* Free the io_service_t */ |
| IOObjectRelease(item->dev); |
| SDL_free(item); |
| } |
| numhaptics = -1; |
| } |
| |
| |
| /* |
| * Converts an SDL trigger button to an FFEFFECT trigger button. |
| */ |
| static DWORD |
| FFGetTriggerButton(Uint16 button) |
| { |
| DWORD dwTriggerButton; |
| |
| dwTriggerButton = FFEB_NOTRIGGER; |
| |
| if (button != 0) { |
| dwTriggerButton = FFJOFS_BUTTON(button - 1); |
| } |
| |
| return dwTriggerButton; |
| } |
| |
| |
| /* |
| * Sets the direction. |
| */ |
| static int |
| SDL_SYS_SetDirection(FFEFFECT * effect, SDL_HapticDirection * dir, int naxes) |
| { |
| LONG *rglDir; |
| |
| /* Handle no axes a part. */ |
| if (naxes == 0) { |
| effect->dwFlags |= FFEFF_SPHERICAL; /* Set as default. */ |
| effect->rglDirection = NULL; |
| return 0; |
| } |
| |
| /* Has axes. */ |
| rglDir = SDL_malloc(sizeof(LONG) * naxes); |
| if (rglDir == NULL) { |
| return SDL_OutOfMemory(); |
| } |
| SDL_memset(rglDir, 0, sizeof(LONG) * naxes); |
| effect->rglDirection = rglDir; |
| |
| switch (dir->type) { |
| case SDL_HAPTIC_POLAR: |
| effect->dwFlags |= FFEFF_POLAR; |
| rglDir[0] = dir->dir[0]; |
| return 0; |
| case SDL_HAPTIC_CARTESIAN: |
| effect->dwFlags |= FFEFF_CARTESIAN; |
| rglDir[0] = dir->dir[0]; |
| if (naxes > 1) |
| rglDir[1] = dir->dir[1]; |
| if (naxes > 2) |
| rglDir[2] = dir->dir[2]; |
| return 0; |
| case SDL_HAPTIC_SPHERICAL: |
| effect->dwFlags |= FFEFF_SPHERICAL; |
| rglDir[0] = dir->dir[0]; |
| if (naxes > 1) |
| rglDir[1] = dir->dir[1]; |
| if (naxes > 2) |
| rglDir[2] = dir->dir[2]; |
| return 0; |
| |
| default: |
| return SDL_SetError("Haptic: Unknown direction type."); |
| } |
| } |
| |
| |
| /* Clamps and converts. */ |
| #define CCONVERT(x) (((x) > 0x7FFF) ? 10000 : ((x)*10000) / 0x7FFF) |
| /* Just converts. */ |
| #define CONVERT(x) (((x)*10000) / 0x7FFF) |
| /* |
| * Creates the FFEFFECT from a SDL_HapticEffect. |
| */ |
| static int |
| SDL_SYS_ToFFEFFECT(SDL_Haptic * haptic, FFEFFECT * dest, |
| SDL_HapticEffect * src) |
| { |
| int i; |
| FFCONSTANTFORCE *constant; |
| FFPERIODIC *periodic; |
| FFCONDITION *condition; /* Actually an array of conditions - one per axis. */ |
| FFRAMPFORCE *ramp; |
| FFCUSTOMFORCE *custom; |
| FFENVELOPE *envelope; |
| SDL_HapticConstant *hap_constant; |
| SDL_HapticPeriodic *hap_periodic; |
| SDL_HapticCondition *hap_condition; |
| SDL_HapticRamp *hap_ramp; |
| SDL_HapticCustom *hap_custom; |
| DWORD *axes; |
| |
| /* Set global stuff. */ |
| SDL_memset(dest, 0, sizeof(FFEFFECT)); |
| dest->dwSize = sizeof(FFEFFECT); /* Set the structure size. */ |
| dest->dwSamplePeriod = 0; /* Not used by us. */ |
| dest->dwGain = 10000; /* Gain is set globally, not locally. */ |
| dest->dwFlags = FFEFF_OBJECTOFFSETS; /* Seems obligatory. */ |
| |
| /* Envelope. */ |
| envelope = SDL_malloc(sizeof(FFENVELOPE)); |
| if (envelope == NULL) { |
| return SDL_OutOfMemory(); |
| } |
| SDL_memset(envelope, 0, sizeof(FFENVELOPE)); |
| dest->lpEnvelope = envelope; |
| envelope->dwSize = sizeof(FFENVELOPE); /* Always should be this. */ |
| |
| /* Axes. */ |
| dest->cAxes = haptic->naxes; |
| if (dest->cAxes > 0) { |
| axes = SDL_malloc(sizeof(DWORD) * dest->cAxes); |
| if (axes == NULL) { |
| return SDL_OutOfMemory(); |
| } |
| axes[0] = haptic->hwdata->axes[0]; /* Always at least one axis. */ |
| if (dest->cAxes > 1) { |
| axes[1] = haptic->hwdata->axes[1]; |
| } |
| if (dest->cAxes > 2) { |
| axes[2] = haptic->hwdata->axes[2]; |
| } |
| dest->rgdwAxes = axes; |
| } |
| |
| |
| /* The big type handling switch, even bigger then Linux's version. */ |
| switch (src->type) { |
| case SDL_HAPTIC_CONSTANT: |
| hap_constant = &src->constant; |
| constant = SDL_malloc(sizeof(FFCONSTANTFORCE)); |
| if (constant == NULL) { |
| return SDL_OutOfMemory(); |
| } |
| SDL_memset(constant, 0, sizeof(FFCONSTANTFORCE)); |
| |
| /* Specifics */ |
| constant->lMagnitude = CONVERT(hap_constant->level); |
| dest->cbTypeSpecificParams = sizeof(FFCONSTANTFORCE); |
| dest->lpvTypeSpecificParams = constant; |
| |
| /* Generics */ |
| dest->dwDuration = hap_constant->length * 1000; /* In microseconds. */ |
| dest->dwTriggerButton = FFGetTriggerButton(hap_constant->button); |
| dest->dwTriggerRepeatInterval = hap_constant->interval; |
| dest->dwStartDelay = hap_constant->delay * 1000; /* In microseconds. */ |
| |
| /* Direction. */ |
| if (SDL_SYS_SetDirection(dest, &hap_constant->direction, dest->cAxes) |
| < 0) { |
| return -1; |
| } |
| |
| /* Envelope */ |
| if ((hap_constant->attack_length == 0) |
| && (hap_constant->fade_length == 0)) { |
| SDL_free(envelope); |
| dest->lpEnvelope = NULL; |
| } else { |
| envelope->dwAttackLevel = CCONVERT(hap_constant->attack_level); |
| envelope->dwAttackTime = hap_constant->attack_length * 1000; |
| envelope->dwFadeLevel = CCONVERT(hap_constant->fade_level); |
| envelope->dwFadeTime = hap_constant->fade_length * 1000; |
| } |
| |
| break; |
| |
| case SDL_HAPTIC_SINE: |
| /* !!! FIXME: put this back when we have more bits in 2.1 */ |
| /* case SDL_HAPTIC_SQUARE: */ |
| case SDL_HAPTIC_TRIANGLE: |
| case SDL_HAPTIC_SAWTOOTHUP: |
| case SDL_HAPTIC_SAWTOOTHDOWN: |
| hap_periodic = &src->periodic; |
| periodic = SDL_malloc(sizeof(FFPERIODIC)); |
| if (periodic == NULL) { |
| return SDL_OutOfMemory(); |
| } |
| SDL_memset(periodic, 0, sizeof(FFPERIODIC)); |
| |
| /* Specifics */ |
| periodic->dwMagnitude = CONVERT(hap_periodic->magnitude); |
| periodic->lOffset = CONVERT(hap_periodic->offset); |
| periodic->dwPhase = hap_periodic->phase; |
| periodic->dwPeriod = hap_periodic->period * 1000; |
| dest->cbTypeSpecificParams = sizeof(FFPERIODIC); |
| dest->lpvTypeSpecificParams = periodic; |
| |
| /* Generics */ |
| dest->dwDuration = hap_periodic->length * 1000; /* In microseconds. */ |
| dest->dwTriggerButton = FFGetTriggerButton(hap_periodic->button); |
| dest->dwTriggerRepeatInterval = hap_periodic->interval; |
| dest->dwStartDelay = hap_periodic->delay * 1000; /* In microseconds. */ |
| |
| /* Direction. */ |
| if (SDL_SYS_SetDirection(dest, &hap_periodic->direction, dest->cAxes) |
| < 0) { |
| return -1; |
| } |
| |
| /* Envelope */ |
| if ((hap_periodic->attack_length == 0) |
| && (hap_periodic->fade_length == 0)) { |
| SDL_free(envelope); |
| dest->lpEnvelope = NULL; |
| } else { |
| envelope->dwAttackLevel = CCONVERT(hap_periodic->attack_level); |
| envelope->dwAttackTime = hap_periodic->attack_length * 1000; |
| envelope->dwFadeLevel = CCONVERT(hap_periodic->fade_level); |
| envelope->dwFadeTime = hap_periodic->fade_length * 1000; |
| } |
| |
| break; |
| |
| case SDL_HAPTIC_SPRING: |
| case SDL_HAPTIC_DAMPER: |
| case SDL_HAPTIC_INERTIA: |
| case SDL_HAPTIC_FRICTION: |
| hap_condition = &src->condition; |
| condition = SDL_malloc(sizeof(FFCONDITION) * dest->cAxes); |
| if (condition == NULL) { |
| return SDL_OutOfMemory(); |
| } |
| SDL_memset(condition, 0, sizeof(FFCONDITION)); |
| |
| /* Specifics */ |
| for (i = 0; i < dest->cAxes; i++) { |
| condition[i].lOffset = CONVERT(hap_condition->center[i]); |
| condition[i].lPositiveCoefficient = |
| CONVERT(hap_condition->right_coeff[i]); |
| condition[i].lNegativeCoefficient = |
| CONVERT(hap_condition->left_coeff[i]); |
| condition[i].dwPositiveSaturation = |
| CCONVERT(hap_condition->right_sat[i]); |
| condition[i].dwNegativeSaturation = |
| CCONVERT(hap_condition->left_sat[i]); |
| condition[i].lDeadBand = CCONVERT(hap_condition->deadband[i]); |
| } |
| dest->cbTypeSpecificParams = sizeof(FFCONDITION) * dest->cAxes; |
| dest->lpvTypeSpecificParams = condition; |
| |
| /* Generics */ |
| dest->dwDuration = hap_condition->length * 1000; /* In microseconds. */ |
| dest->dwTriggerButton = FFGetTriggerButton(hap_condition->button); |
| dest->dwTriggerRepeatInterval = hap_condition->interval; |
| dest->dwStartDelay = hap_condition->delay * 1000; /* In microseconds. */ |
| |
| /* Direction. */ |
| if (SDL_SYS_SetDirection(dest, &hap_condition->direction, dest->cAxes) |
| < 0) { |
| return -1; |
| } |
| |
| /* Envelope - Not actually supported by most CONDITION implementations. */ |
| SDL_free(dest->lpEnvelope); |
| dest->lpEnvelope = NULL; |
| |
| break; |
| |
| case SDL_HAPTIC_RAMP: |
| hap_ramp = &src->ramp; |
| ramp = SDL_malloc(sizeof(FFRAMPFORCE)); |
| if (ramp == NULL) { |
| return SDL_OutOfMemory(); |
| } |
| SDL_memset(ramp, 0, sizeof(FFRAMPFORCE)); |
| |
| /* Specifics */ |
| ramp->lStart = CONVERT(hap_ramp->start); |
| ramp->lEnd = CONVERT(hap_ramp->end); |
| dest->cbTypeSpecificParams = sizeof(FFRAMPFORCE); |
| dest->lpvTypeSpecificParams = ramp; |
| |
| /* Generics */ |
| dest->dwDuration = hap_ramp->length * 1000; /* In microseconds. */ |
| dest->dwTriggerButton = FFGetTriggerButton(hap_ramp->button); |
| dest->dwTriggerRepeatInterval = hap_ramp->interval; |
| dest->dwStartDelay = hap_ramp->delay * 1000; /* In microseconds. */ |
| |
| /* Direction. */ |
| if (SDL_SYS_SetDirection(dest, &hap_ramp->direction, dest->cAxes) < 0) { |
| return -1; |
| } |
| |
| /* Envelope */ |
| if ((hap_ramp->attack_length == 0) && (hap_ramp->fade_length == 0)) { |
| SDL_free(envelope); |
| dest->lpEnvelope = NULL; |
| } else { |
| envelope->dwAttackLevel = CCONVERT(hap_ramp->attack_level); |
| envelope->dwAttackTime = hap_ramp->attack_length * 1000; |
| envelope->dwFadeLevel = CCONVERT(hap_ramp->fade_level); |
| envelope->dwFadeTime = hap_ramp->fade_length * 1000; |
| } |
| |
| break; |
| |
| case SDL_HAPTIC_CUSTOM: |
| hap_custom = &src->custom; |
| custom = SDL_malloc(sizeof(FFCUSTOMFORCE)); |
| if (custom == NULL) { |
| return SDL_OutOfMemory(); |
| } |
| SDL_memset(custom, 0, sizeof(FFCUSTOMFORCE)); |
| |
| /* Specifics */ |
| custom->cChannels = hap_custom->channels; |
| custom->dwSamplePeriod = hap_custom->period * 1000; |
| custom->cSamples = hap_custom->samples; |
| custom->rglForceData = |
| SDL_malloc(sizeof(LONG) * custom->cSamples * custom->cChannels); |
| for (i = 0; i < hap_custom->samples * hap_custom->channels; i++) { /* Copy data. */ |
| custom->rglForceData[i] = CCONVERT(hap_custom->data[i]); |
| } |
| dest->cbTypeSpecificParams = sizeof(FFCUSTOMFORCE); |
| dest->lpvTypeSpecificParams = custom; |
| |
| /* Generics */ |
| dest->dwDuration = hap_custom->length * 1000; /* In microseconds. */ |
| dest->dwTriggerButton = FFGetTriggerButton(hap_custom->button); |
| dest->dwTriggerRepeatInterval = hap_custom->interval; |
| dest->dwStartDelay = hap_custom->delay * 1000; /* In microseconds. */ |
| |
| /* Direction. */ |
| if (SDL_SYS_SetDirection(dest, &hap_custom->direction, dest->cAxes) < |
| 0) { |
| return -1; |
| } |
| |
| /* Envelope */ |
| if ((hap_custom->attack_length == 0) |
| && (hap_custom->fade_length == 0)) { |
| SDL_free(envelope); |
| dest->lpEnvelope = NULL; |
| } else { |
| envelope->dwAttackLevel = CCONVERT(hap_custom->attack_level); |
| envelope->dwAttackTime = hap_custom->attack_length * 1000; |
| envelope->dwFadeLevel = CCONVERT(hap_custom->fade_level); |
| envelope->dwFadeTime = hap_custom->fade_length * 1000; |
| } |
| |
| break; |
| |
| |
| default: |
| return SDL_SetError("Haptic: Unknown effect type."); |
| } |
| |
| return 0; |
| } |
| |
| |
| /* |
| * Frees an FFEFFECT allocated by SDL_SYS_ToFFEFFECT. |
| */ |
| static void |
| SDL_SYS_HapticFreeFFEFFECT(FFEFFECT * effect, int type) |
| { |
| FFCUSTOMFORCE *custom; |
| |
| SDL_free(effect->lpEnvelope); |
| effect->lpEnvelope = NULL; |
| SDL_free(effect->rgdwAxes); |
| effect->rgdwAxes = NULL; |
| if (effect->lpvTypeSpecificParams != NULL) { |
| if (type == SDL_HAPTIC_CUSTOM) { /* Must free the custom data. */ |
| custom = (FFCUSTOMFORCE *) effect->lpvTypeSpecificParams; |
| SDL_free(custom->rglForceData); |
| custom->rglForceData = NULL; |
| } |
| SDL_free(effect->lpvTypeSpecificParams); |
| effect->lpvTypeSpecificParams = NULL; |
| } |
| SDL_free(effect->rglDirection); |
| effect->rglDirection = NULL; |
| } |
| |
| |
| /* |
| * Gets the effect type from the generic SDL haptic effect wrapper. |
| */ |
| CFUUIDRef |
| SDL_SYS_HapticEffectType(Uint16 type) |
| { |
| switch (type) { |
| case SDL_HAPTIC_CONSTANT: |
| return kFFEffectType_ConstantForce_ID; |
| |
| case SDL_HAPTIC_RAMP: |
| return kFFEffectType_RampForce_ID; |
| |
| /* !!! FIXME: put this back when we have more bits in 2.1 */ |
| /* case SDL_HAPTIC_SQUARE: |
| return kFFEffectType_Square_ID; */ |
| |
| case SDL_HAPTIC_SINE: |
| return kFFEffectType_Sine_ID; |
| |
| case SDL_HAPTIC_TRIANGLE: |
| return kFFEffectType_Triangle_ID; |
| |
| case SDL_HAPTIC_SAWTOOTHUP: |
| return kFFEffectType_SawtoothUp_ID; |
| |
| case SDL_HAPTIC_SAWTOOTHDOWN: |
| return kFFEffectType_SawtoothDown_ID; |
| |
| case SDL_HAPTIC_SPRING: |
| return kFFEffectType_Spring_ID; |
| |
| case SDL_HAPTIC_DAMPER: |
| return kFFEffectType_Damper_ID; |
| |
| case SDL_HAPTIC_INERTIA: |
| return kFFEffectType_Inertia_ID; |
| |
| case SDL_HAPTIC_FRICTION: |
| return kFFEffectType_Friction_ID; |
| |
| case SDL_HAPTIC_CUSTOM: |
| return kFFEffectType_CustomForce_ID; |
| |
| default: |
| SDL_SetError("Haptic: Unknown effect type."); |
| return NULL; |
| } |
| } |
| |
| |
| /* |
| * Creates a new haptic effect. |
| */ |
| int |
| SDL_SYS_HapticNewEffect(SDL_Haptic * haptic, struct haptic_effect *effect, |
| SDL_HapticEffect * base) |
| { |
| HRESULT ret; |
| CFUUIDRef type; |
| |
| /* Alloc the effect. */ |
| effect->hweffect = (struct haptic_hweffect *) |
| SDL_malloc(sizeof(struct haptic_hweffect)); |
| if (effect->hweffect == NULL) { |
| SDL_OutOfMemory(); |
| goto err_hweffect; |
| } |
| |
| /* Get the type. */ |
| type = SDL_SYS_HapticEffectType(base->type); |
| if (type == NULL) { |
| goto err_hweffect; |
| } |
| |
| /* Get the effect. */ |
| if (SDL_SYS_ToFFEFFECT(haptic, &effect->hweffect->effect, base) < 0) { |
| goto err_effectdone; |
| } |
| |
| /* Create the actual effect. */ |
| ret = FFDeviceCreateEffect(haptic->hwdata->device, type, |
| &effect->hweffect->effect, |
| &effect->hweffect->ref); |
| if (ret != FF_OK) { |
| SDL_SetError("Haptic: Unable to create effect: %s.", FFStrError(ret)); |
| goto err_effectdone; |
| } |
| |
| return 0; |
| |
| err_effectdone: |
| SDL_SYS_HapticFreeFFEFFECT(&effect->hweffect->effect, base->type); |
| err_hweffect: |
| SDL_free(effect->hweffect); |
| effect->hweffect = NULL; |
| return -1; |
| } |
| |
| |
| /* |
| * Updates an effect. |
| */ |
| int |
| SDL_SYS_HapticUpdateEffect(SDL_Haptic * haptic, |
| struct haptic_effect *effect, |
| SDL_HapticEffect * data) |
| { |
| HRESULT ret; |
| FFEffectParameterFlag flags; |
| FFEFFECT temp; |
| |
| /* Get the effect. */ |
| SDL_memset(&temp, 0, sizeof(FFEFFECT)); |
| if (SDL_SYS_ToFFEFFECT(haptic, &temp, data) < 0) { |
| goto err_update; |
| } |
| |
| /* Set the flags. Might be worthwhile to diff temp with loaded effect and |
| * only change those parameters. */ |
| flags = FFEP_DIRECTION | |
| FFEP_DURATION | |
| FFEP_ENVELOPE | |
| FFEP_STARTDELAY | |
| FFEP_TRIGGERBUTTON | |
| FFEP_TRIGGERREPEATINTERVAL | FFEP_TYPESPECIFICPARAMS; |
| |
| /* Create the actual effect. */ |
| ret = FFEffectSetParameters(effect->hweffect->ref, &temp, flags); |
| if (ret != FF_OK) { |
| SDL_SetError("Haptic: Unable to update effect: %s.", FFStrError(ret)); |
| goto err_update; |
| } |
| |
| /* Copy it over. */ |
| SDL_SYS_HapticFreeFFEFFECT(&effect->hweffect->effect, data->type); |
| SDL_memcpy(&effect->hweffect->effect, &temp, sizeof(FFEFFECT)); |
| |
| return 0; |
| |
| err_update: |
| SDL_SYS_HapticFreeFFEFFECT(&temp, data->type); |
| return -1; |
| } |
| |
| |
| /* |
| * Runs an effect. |
| */ |
| int |
| SDL_SYS_HapticRunEffect(SDL_Haptic * haptic, struct haptic_effect *effect, |
| Uint32 iterations) |
| { |
| HRESULT ret; |
| Uint32 iter; |
| |
| /* Check if it's infinite. */ |
| if (iterations == SDL_HAPTIC_INFINITY) { |
| iter = FF_INFINITE; |
| } else |
| iter = iterations; |
| |
| /* Run the effect. */ |
| ret = FFEffectStart(effect->hweffect->ref, iter, 0); |
| if (ret != FF_OK) { |
| return SDL_SetError("Haptic: Unable to run the effect: %s.", |
| FFStrError(ret)); |
| } |
| |
| return 0; |
| } |
| |
| |
| /* |
| * Stops an effect. |
| */ |
| int |
| SDL_SYS_HapticStopEffect(SDL_Haptic * haptic, struct haptic_effect *effect) |
| { |
| HRESULT ret; |
| |
| ret = FFEffectStop(effect->hweffect->ref); |
| if (ret != FF_OK) { |
| return SDL_SetError("Haptic: Unable to stop the effect: %s.", |
| FFStrError(ret)); |
| } |
| |
| return 0; |
| } |
| |
| |
| /* |
| * Frees the effect. |
| */ |
| void |
| SDL_SYS_HapticDestroyEffect(SDL_Haptic * haptic, struct haptic_effect *effect) |
| { |
| HRESULT ret; |
| |
| ret = |
| FFDeviceReleaseEffect(haptic->hwdata->device, effect->hweffect->ref); |
| if (ret != FF_OK) { |
| SDL_SetError("Haptic: Error removing the effect from the device: %s.", |
| FFStrError(ret)); |
| } |
| SDL_SYS_HapticFreeFFEFFECT(&effect->hweffect->effect, |
| effect->effect.type); |
| SDL_free(effect->hweffect); |
| effect->hweffect = NULL; |
| } |
| |
| |
| /* |
| * Gets the status of a haptic effect. |
| */ |
| int |
| SDL_SYS_HapticGetEffectStatus(SDL_Haptic * haptic, |
| struct haptic_effect *effect) |
| { |
| HRESULT ret; |
| FFEffectStatusFlag status; |
| |
| ret = FFEffectGetEffectStatus(effect->hweffect->ref, &status); |
| if (ret != FF_OK) { |
| SDL_SetError("Haptic: Unable to get effect status: %s.", |
| FFStrError(ret)); |
| return -1; |
| } |
| |
| if (status == 0) |
| return SDL_FALSE; |
| return SDL_TRUE; /* Assume it's playing or emulated. */ |
| } |
| |
| |
| /* |
| * Sets the gain. |
| */ |
| int |
| SDL_SYS_HapticSetGain(SDL_Haptic * haptic, int gain) |
| { |
| HRESULT ret; |
| Uint32 val; |
| |
| val = gain * 100; /* Mac OS X uses 0 to 10,000 */ |
| ret = |
| FFDeviceSetForceFeedbackProperty(haptic->hwdata->device, |
| FFPROP_FFGAIN, &val); |
| if (ret != FF_OK) { |
| return SDL_SetError("Haptic: Error setting gain: %s.", FFStrError(ret)); |
| } |
| |
| return 0; |
| } |
| |
| |
| /* |
| * Sets the autocentering. |
| */ |
| int |
| SDL_SYS_HapticSetAutocenter(SDL_Haptic * haptic, int autocenter) |
| { |
| HRESULT ret; |
| Uint32 val; |
| |
| /* Mac OS X only has 0 (off) and 1 (on) */ |
| if (autocenter == 0) |
| val = 0; |
| else |
| val = 1; |
| |
| ret = FFDeviceSetForceFeedbackProperty(haptic->hwdata->device, |
| FFPROP_AUTOCENTER, &val); |
| if (ret != FF_OK) { |
| return SDL_SetError("Haptic: Error setting autocenter: %s.", |
| FFStrError(ret)); |
| } |
| |
| return 0; |
| } |
| |
| |
| /* |
| * Pauses the device. |
| */ |
| int |
| SDL_SYS_HapticPause(SDL_Haptic * haptic) |
| { |
| HRESULT ret; |
| |
| ret = FFDeviceSendForceFeedbackCommand(haptic->hwdata->device, |
| FFSFFC_PAUSE); |
| if (ret != FF_OK) { |
| return SDL_SetError("Haptic: Error pausing device: %s.", FFStrError(ret)); |
| } |
| |
| return 0; |
| } |
| |
| |
| /* |
| * Unpauses the device. |
| */ |
| int |
| SDL_SYS_HapticUnpause(SDL_Haptic * haptic) |
| { |
| HRESULT ret; |
| |
| ret = FFDeviceSendForceFeedbackCommand(haptic->hwdata->device, |
| FFSFFC_CONTINUE); |
| if (ret != FF_OK) { |
| return SDL_SetError("Haptic: Error pausing device: %s.", FFStrError(ret)); |
| } |
| |
| return 0; |
| } |
| |
| |
| /* |
| * Stops all currently playing effects. |
| */ |
| int |
| SDL_SYS_HapticStopAll(SDL_Haptic * haptic) |
| { |
| HRESULT ret; |
| |
| ret = FFDeviceSendForceFeedbackCommand(haptic->hwdata->device, |
| FFSFFC_STOPALL); |
| if (ret != FF_OK) { |
| return SDL_SetError("Haptic: Error stopping device: %s.", FFStrError(ret)); |
| } |
| |
| return 0; |
| } |
| |
| |
| #endif /* SDL_HAPTIC_IOKIT */ |