From fa45294d0ce064dca0211dda6512063a781bfce6 Mon Sep 17 00:00:00 2001 From: chrisd1100 Date: Fri, 8 Nov 2024 20:55:17 -0500 Subject: [PATCH 1/5] Add MTY_WindowGetViewport --- src/app.c | 14 ++++++++++++++ src/matoya.h | 11 +++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/app.c b/src/app.c index 6890e2fb..6e90d682 100644 --- a/src/app.c +++ b/src/app.c @@ -9,6 +9,7 @@ #include "gfx/mod.h" #include "gfx/mod-ui.h" +#include "gfx/viewport.h" // GFX @@ -102,6 +103,19 @@ static bool gfx_begin_ui(struct window_common *cmn, MTY_Device *device) return cmn->gfx_ui != NULL; } +void MTY_WindowGetViewport(MTY_App *app, MTY_Window window, const MTY_RenderDesc *desc, + float *x, float *y, float *w, float *h) +{ + struct window_common *cmn = mty_window_get_common(app, window); + if (!cmn) + return; + + MTY_RenderDesc mutated = *desc; + gfx_ctx_get_size(cmn, &mutated.viewWidth, &mutated.viewHeight); + + mty_viewport(&mutated, x, y, w, h); +} + void MTY_WindowDrawQuad(MTY_App *app, MTY_Window window, const void *image, const MTY_RenderDesc *desc) { struct window_common *cmn = mty_window_get_common(app, window); diff --git a/src/matoya.h b/src/matoya.h index c8e10848..0ac9b355 100644 --- a/src/matoya.h +++ b/src/matoya.h @@ -1162,6 +1162,17 @@ MTY_WindowSetFullscreen(MTY_App *app, MTY_Window window, bool fullscreen); MTY_EXPORT void MTY_WindowWarpCursor(MTY_App *app, MTY_Window window, uint32_t x, uint32_t y); +/// @brief Calculate the viewport for a window given an MTY_RenderDesc. +/// @param app The MTY_App. +/// @param window An MTY_Window. +/// @param desc An MTY_RenderDesc that could be passed to MTY_WindowDrawQuad. +/// @param x Set to the location of the left edge of the viewport. +/// @param y Set to the location of the top edge of the viewport. +/// @param w Set to the width of the viewport. +/// @param h Set to the height of the viewport. +void MTY_WindowGetViewport(MTY_App *app, MTY_Window window, const MTY_RenderDesc *desc, + float *x, float *y, float *w, float *h); + /// @brief Draw a quad with a raw image and MTY_RenderDesc. /// @param app The MTY_App. /// @param window An MTY_Window. From 7f69254f3f69d38a37da573a9ae860ffa832808a Mon Sep 17 00:00:00 2001 From: Shiv Kokroo Date: Sun, 10 Nov 2024 01:01:47 +0530 Subject: [PATCH 2/5] Xbox rumble support attempt #1 --- src/hid/hid.c | 10 +++++++++- src/hid/xboxw.h | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/hid/hid.c b/src/hid/hid.c index 199f77ba..452f3ca7 100644 --- a/src/hid/hid.c +++ b/src/hid/hid.c @@ -4,7 +4,7 @@ #include "hid.h" #include "utils.h" - +#include "stdio.h" #include #include @@ -88,6 +88,10 @@ void mty_hid_driver_init(struct hid_dev *device) case MTY_CTYPE_XBOX: xbox_init(device); break; + case MTY_CTYPE_XBOXW: + printf("Got to initing MTY_CTYPE_XBOXW"); + xboxw_init(device); + break; } } @@ -134,6 +138,10 @@ void mty_hid_driver_rumble(struct hid *hid, uint32_t id, uint16_t low, uint16_t case MTY_CTYPE_XBOX: xbox_rumble(device, low, high); break; + case MTY_CTYPE_XBOXW: + printf("IT IS AN XBOXW!"); + xboxw_rumble(device, low, high); + break; case MTY_CTYPE_DEFAULT: mty_hid_default_rumble(hid, id, low, high); break; diff --git a/src/hid/xboxw.h b/src/hid/xboxw.h index fdf2fbcf..1d33d9d9 100644 --- a/src/hid/xboxw.h +++ b/src/hid/xboxw.h @@ -4,6 +4,52 @@ #pragma once +struct xboxw_state { + bool rumble; + uint16_t low; + uint16_t high; +}; + +// Rumble + +static void xboxw_rumble(struct hid_dev *device, uint16_t low, uint16_t high) +{ + struct xbox_state *ctx = mty_hid_device_get_state(device); + + printf("Got here 1"); + uint8_t rumble_packet[12] = {0x00, 0x01, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + rumble_packet[5] = ctx->low >> 8; + rumble_packet[6] = ctx->high >> 8; + printf("Got here 2"); + + mty_hid_device_write(device, rumble_packet, sizeof(rumble_packet)); + printf("Got here 3"); + +} + +static void xboxw_do_rumble(struct hid_dev *device) +{ + struct xbox_state *ctx = mty_hid_device_get_state(device); + + if (ctx->rumble) { + uint8_t rumble_packet[12] = {0x00, 0x01, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + rumble_packet[5] = ctx->low >> 8; + rumble_packet[6] = ctx->high >> 8; + printf("Got here 2"); + + mty_hid_device_write(device, rumble_packet, sizeof(rumble_packet)); + printf("Got here 3"); + ctx->rumble = false; + } +} + +static void xboxw_init(struct hid_dev *device) +{ + printf("Got to xboxw_init"); + struct xboxw_state *ctx = mty_hid_device_get_state(device); + ctx->rumble = true; +} + static bool xboxw_state(struct hid_dev *device, const void *data, size_t size, MTY_ControllerEvent *c) { const uint8_t *d = data; @@ -65,6 +111,7 @@ static bool xboxw_state(struct hid_dev *device, const void *data, size_t size, M c->buttons[MTY_CBUTTON_LEFT_TRIGGER] = c->axes[MTY_CAXIS_TRIGGER_L].value > 0; c->buttons[MTY_CBUTTON_RIGHT_TRIGGER] = c->axes[MTY_CAXIS_TRIGGER_R].value > 0; + xboxw_do_rumble(device); return true; } From 15c27818ce2ae0be76af7e7668a7ae2ce51f807e Mon Sep 17 00:00:00 2001 From: Shiv Kokroo Date: Sun, 10 Nov 2024 01:02:01 +0530 Subject: [PATCH 3/5] Test script and program to check rumble --- bang.sh | 3 +++ test/src/0-minimal.c | 19 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 bang.sh diff --git a/bang.sh b/bang.sh new file mode 100644 index 00000000..b7118eb5 --- /dev/null +++ b/bang.sh @@ -0,0 +1,3 @@ +make +cd test +make 0-minimal \ No newline at end of file diff --git a/test/src/0-minimal.c b/test/src/0-minimal.c index 8f49a65e..83de0b69 100644 --- a/test/src/0-minimal.c +++ b/test/src/0-minimal.c @@ -1,22 +1,29 @@ #include "matoya.h" - +#include "stdio.h" // Your top level application context struct context { MTY_App *app; bool quit; }; +// This function will fire for each event // This function will fire for each event static void event_func(const MTY_Event *evt, void *opaque) { - struct context *ctx = opaque; + struct context *ctx = opaque; - MTY_PrintEvent(evt); + MTY_PrintEvent(evt); - if (evt->type == MTY_EVENT_CLOSE) - ctx->quit = true; -} + if (evt->type == MTY_EVENT_CLOSE) + ctx->quit = true; + + if (evt->type == MTY_EVENT_CONTROLLER) { + printf("Controller ID: %u\n", evt->controller.id); // Print the controller ID + printf("Triggering Rumble\n"); + MTY_AppRumbleController(ctx->app, evt->controller.id, UINT16_MAX, UINT16_MAX); + } +} // This function fires once per "cycle", either blocked by a // call to MTY_WindowPresent or limited by MTY_AppSetTimeout static bool app_func(void *opaque) From 0de0791a4b2f33e5bd9b680e946e6ed7c9f9c330 Mon Sep 17 00:00:00 2001 From: Shiv Kokroo Date: Sun, 10 Nov 2024 01:02:47 +0530 Subject: [PATCH 4/5] Revert "Add MTY_WindowGetViewport" This reverts commit fa45294d0ce064dca0211dda6512063a781bfce6. --- src/app.c | 14 -------------- src/matoya.h | 11 ----------- 2 files changed, 25 deletions(-) diff --git a/src/app.c b/src/app.c index 6e90d682..6890e2fb 100644 --- a/src/app.c +++ b/src/app.c @@ -9,7 +9,6 @@ #include "gfx/mod.h" #include "gfx/mod-ui.h" -#include "gfx/viewport.h" // GFX @@ -103,19 +102,6 @@ static bool gfx_begin_ui(struct window_common *cmn, MTY_Device *device) return cmn->gfx_ui != NULL; } -void MTY_WindowGetViewport(MTY_App *app, MTY_Window window, const MTY_RenderDesc *desc, - float *x, float *y, float *w, float *h) -{ - struct window_common *cmn = mty_window_get_common(app, window); - if (!cmn) - return; - - MTY_RenderDesc mutated = *desc; - gfx_ctx_get_size(cmn, &mutated.viewWidth, &mutated.viewHeight); - - mty_viewport(&mutated, x, y, w, h); -} - void MTY_WindowDrawQuad(MTY_App *app, MTY_Window window, const void *image, const MTY_RenderDesc *desc) { struct window_common *cmn = mty_window_get_common(app, window); diff --git a/src/matoya.h b/src/matoya.h index 0ac9b355..c8e10848 100644 --- a/src/matoya.h +++ b/src/matoya.h @@ -1162,17 +1162,6 @@ MTY_WindowSetFullscreen(MTY_App *app, MTY_Window window, bool fullscreen); MTY_EXPORT void MTY_WindowWarpCursor(MTY_App *app, MTY_Window window, uint32_t x, uint32_t y); -/// @brief Calculate the viewport for a window given an MTY_RenderDesc. -/// @param app The MTY_App. -/// @param window An MTY_Window. -/// @param desc An MTY_RenderDesc that could be passed to MTY_WindowDrawQuad. -/// @param x Set to the location of the left edge of the viewport. -/// @param y Set to the location of the top edge of the viewport. -/// @param w Set to the width of the viewport. -/// @param h Set to the height of the viewport. -void MTY_WindowGetViewport(MTY_App *app, MTY_Window window, const MTY_RenderDesc *desc, - float *x, float *y, float *w, float *h); - /// @brief Draw a quad with a raw image and MTY_RenderDesc. /// @param app The MTY_App. /// @param window An MTY_Window. From 209c5ce517d6aeb037af483ab8f75d1be85a8461 Mon Sep 17 00:00:00 2001 From: Shiv Kokroo Date: Sat, 23 Aug 2025 22:03:08 +0200 Subject: [PATCH 5/5] v1 --- GNUmakefile | 1 + bang.sh | 1 + src/hid/hid.c | 4 +- src/hid/xboxw.h | 39 ++-- src/unix/apple/macosx/hid.c | 40 +++- src/unix/apple/macosx/hid_gc.h | 29 +++ src/unix/apple/macosx/hid_gc.m | 350 +++++++++++++++++++++++++++++++++ test/GNUmakefile | 4 +- test/src/0-minimal.c | 2 +- test/test_gc | Bin 0 -> 53080 bytes test/test_gc.m | 119 +++++++++++ 11 files changed, 566 insertions(+), 23 deletions(-) create mode 100644 src/unix/apple/macosx/hid_gc.h create mode 100644 src/unix/apple/macosx/hid_gc.m create mode 100755 test/test_gc create mode 100644 test/test_gc.m diff --git a/GNUmakefile b/GNUmakefile index 08c4a3ed..ed8a1633 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -192,6 +192,7 @@ OBJS := $(OBJS) \ src/hid/hid.o \ src/unix/apple/image.o \ src/unix/apple/macosx/hid.o \ + src/unix/apple/macosx/hid_gc.o \ src/unix/apple/macosx/gfx/metal-ctx.o else diff --git a/bang.sh b/bang.sh index b7118eb5..eacff07c 100644 --- a/bang.sh +++ b/bang.sh @@ -1,3 +1,4 @@ make cd test +clear make 0-minimal \ No newline at end of file diff --git a/src/hid/hid.c b/src/hid/hid.c index 452f3ca7..f113d024 100644 --- a/src/hid/hid.c +++ b/src/hid/hid.c @@ -89,7 +89,7 @@ void mty_hid_driver_init(struct hid_dev *device) xbox_init(device); break; case MTY_CTYPE_XBOXW: - printf("Got to initing MTY_CTYPE_XBOXW"); + printf("Got to initing MTY_CTYPE_XBOXW\n"); xboxw_init(device); break; } @@ -139,7 +139,7 @@ void mty_hid_driver_rumble(struct hid *hid, uint32_t id, uint16_t low, uint16_t xbox_rumble(device, low, high); break; case MTY_CTYPE_XBOXW: - printf("IT IS AN XBOXW!"); + printf("IT IS AN XBOXW!\n"); xboxw_rumble(device, low, high); break; case MTY_CTYPE_DEFAULT: diff --git a/src/hid/xboxw.h b/src/hid/xboxw.h index 1d33d9d9..1556d78b 100644 --- a/src/hid/xboxw.h +++ b/src/hid/xboxw.h @@ -14,40 +14,47 @@ struct xboxw_state { static void xboxw_rumble(struct hid_dev *device, uint16_t low, uint16_t high) { - struct xbox_state *ctx = mty_hid_device_get_state(device); + struct xboxw_state *ctx = mty_hid_device_get_state(device); - printf("Got here 1"); - uint8_t rumble_packet[12] = {0x00, 0x01, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - rumble_packet[5] = ctx->low >> 8; - rumble_packet[6] = ctx->high >> 8; - printf("Got here 2"); + printf("Got here 1\n"); + // Xbox 360 wired controller rumble packet format + uint8_t rumble_packet[8] = {0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + rumble_packet[3] = low >> 8; // Low frequency motor intensity + rumble_packet[4] = high >> 8; // High frequency motor intensity + printf("Got here 2 - low: %u, high: %u\n", low >> 8, high >> 8); - mty_hid_device_write(device, rumble_packet, sizeof(rumble_packet)); - printf("Got here 3"); + mty_hid_device_write(device, rumble_packet, sizeof(rumble_packet)); + printf("Got here 3\n"); + // Store values for potential retransmission + ctx->low = low; + ctx->high = high; + ctx->rumble = true; } static void xboxw_do_rumble(struct hid_dev *device) { - struct xbox_state *ctx = mty_hid_device_get_state(device); + struct xboxw_state *ctx = mty_hid_device_get_state(device); if (ctx->rumble) { - uint8_t rumble_packet[12] = {0x00, 0x01, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - rumble_packet[5] = ctx->low >> 8; - rumble_packet[6] = ctx->high >> 8; - printf("Got here 2"); + uint8_t rumble_packet[8] = {0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + rumble_packet[3] = ctx->low >> 8; + rumble_packet[4] = ctx->high >> 8; + printf("Got here 2\n"); mty_hid_device_write(device, rumble_packet, sizeof(rumble_packet)); - printf("Got here 3"); + printf("Got here 4\n"); ctx->rumble = false; } } static void xboxw_init(struct hid_dev *device) { - printf("Got to xboxw_init"); + printf("Got to xboxw_init\n"); struct xboxw_state *ctx = mty_hid_device_get_state(device); - ctx->rumble = true; + ctx->rumble = false; + ctx->low = 0; + ctx->high = 0; } static bool xboxw_state(struct hid_dev *device, const void *data, size_t size, MTY_ControllerEvent *c) diff --git a/src/unix/apple/macosx/hid.c b/src/unix/apple/macosx/hid.c index cc7af297..3f9ed675 100644 --- a/src/unix/apple/macosx/hid.c +++ b/src/unix/apple/macosx/hid.c @@ -3,9 +3,11 @@ // You can obtain one at https://spdx.org/licenses/MIT.html. #include "hid/hid.h" +#include #include #include +#include "hid_gc.h" #define HID_DEV_GET_USAGE(dev) \ hid_device_get_prop_int(dev, CFSTR(kIOHIDPrimaryUsageKey)) @@ -63,6 +65,11 @@ static void hid_device_destroy(void *hdevice) struct hid_dev *ctx = hdevice; + // Clean up GCController rumble context if it exists + if (ctx->device) { + mty_hid_gc_cleanup(ctx->device); + } + MTY_Free(ctx->state); MTY_Free(ctx); } @@ -223,17 +230,44 @@ void mty_hid_destroy(struct hid **hid) void mty_hid_device_write(struct hid_dev *ctx, const void *buf, size_t size) { const uint8_t *buf8 = buf; - + + // Check if this is an Xbox controller rumble packet + if (ctx->vid == 0x045E && size >= 8 && buf8[0] == 0x00 && buf8[1] == 0x08) { + // This is an Xbox rumble packet, use GCController instead + uint8_t low_intensity = buf8[3]; + uint8_t high_intensity = buf8[4]; + // Convert 8-bit (0-255) to 16-bit (0-65535) range + uint16_t low = ((uint16_t)low_intensity << 8) | low_intensity; + uint16_t high = ((uint16_t)high_intensity << 8) | high_intensity; + + printf("Xbox rumble: low=%u, high=%u\n", low, high); + + if (mty_hid_gc_rumble(ctx->device, low, high)) { + return; // Successfully sent rumble via GCController + } + // Fall through to try IOHIDDeviceSetReport if GCController fails + } + + printf("buf8[0]: %d\n", buf8[0]); + printf("size: %zu\n", size); + printf("ctx->device: %p\n", ctx->device); + if (buf8[0] == 0) { buf += 1; size -= 1; } + printf("buf after adjustment: %p\n", buf); + printf("size after adjustment: %zu\n", size); + IOReturn e = IOHIDDeviceSetReport(ctx->device, kIOHIDReportTypeOutput, buf8[0], buf, size); - if (e != kIOReturnSuccess) + printf("Return from IOHIDDeviceSetReport (e): 0x%X\n", e); + + if (e != kIOReturnSuccess) { + printf("'IOHIDDeviceSetReport' failed with error 0x%X\n", e); MTY_Log("'IOHIDDeviceSetReport' failed with error 0x%X", e); + } } - bool mty_hid_device_feature(struct hid_dev *ctx, void *buf, size_t size, size_t *size_out) { const uint8_t *buf8 = buf; diff --git a/src/unix/apple/macosx/hid_gc.h b/src/unix/apple/macosx/hid_gc.h new file mode 100644 index 00000000..1e328bac --- /dev/null +++ b/src/unix/apple/macosx/hid_gc.h @@ -0,0 +1,29 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT License was not distributed with this file, +// You can obtain one at https://spdx.org/licenses/MIT.html. + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Send rumble to an Xbox controller using GCController framework +// device: IOHIDDeviceRef pointer +// low: Low frequency rumble intensity (0-65535) +// high: High frequency rumble intensity (0-65535) +// Returns true if rumble was sent successfully +bool mty_hid_gc_rumble(void *device, uint16_t low, uint16_t high); + +// Clean up rumble context for a device +void mty_hid_gc_cleanup(void *device); + +// Check if GCController rumble is available for a device +bool mty_hid_gc_rumble_available(void *device); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/src/unix/apple/macosx/hid_gc.m b/src/unix/apple/macosx/hid_gc.m new file mode 100644 index 00000000..a9e20ad9 --- /dev/null +++ b/src/unix/apple/macosx/hid_gc.m @@ -0,0 +1,350 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT License was not distributed with this file, +// You can obtain one at https://spdx.org/licenses/MIT.html. + +#import +#import +#import +#import + +// Structure to hold rumble context for a controller +@interface MTYRumbleMotor : NSObject +@property(nonatomic, strong) CHHapticEngine *engine API_AVAILABLE(macos(11.0)); +@property(nonatomic, strong) id player API_AVAILABLE(macos(11.0)); +@property(nonatomic) BOOL active; +@end + +@implementation MTYRumbleMotor + +- (void)cleanup { + @autoreleasepool { + if (@available(macOS 11.0, *)) { + if (self.player != nil) { + [self.player cancelAndReturnError:nil]; + self.player = nil; + } + if (self.engine != nil) { + [self.engine stopWithCompletionHandler:nil]; + self.engine = nil; + } + } + } +} + +- (BOOL)setIntensity:(float)intensity { + @autoreleasepool { + if (@available(macOS 11.0, *)) { + NSError *error = nil; + + if (self.engine == nil) { + NSLog(@"Haptics engine was stopped"); + return NO; + } + + // Stop rumble if intensity is 0 + if (intensity == 0.0f) { + if (self.player && self.active) { + [self.player stopAtTime:0 error:&error]; + } + self.active = NO; + return YES; + } + + // Create player if needed + if (self.player == nil) { + CHHapticEventParameter *eventParam = [[CHHapticEventParameter alloc] + initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f]; + CHHapticEvent *event = [[CHHapticEvent alloc] + initWithEventType:CHHapticEventTypeHapticContinuous + parameters:@[eventParam] + relativeTime:0 + duration:GCHapticDurationInfinite]; + CHHapticPattern *pattern = [[CHHapticPattern alloc] + initWithEvents:@[event] + parameters:@[] + error:&error]; + + if (error != nil) { + NSLog(@"Couldn't create haptic pattern: %@", error.localizedDescription); + return NO; + } + + self.player = [self.engine createPlayerWithPattern:pattern error:&error]; + if (error != nil) { + NSLog(@"Couldn't create haptic player: %@", error.localizedDescription); + return NO; + } + self.active = NO; + } + + // Update intensity + CHHapticDynamicParameter *param = [[CHHapticDynamicParameter alloc] + initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl + value:intensity + relativeTime:0]; + [self.player sendParameters:@[param] atTime:0 error:&error]; + if (error != nil) { + NSLog(@"Couldn't update haptic player: %@", error.localizedDescription); + return NO; + } + + // Start playback if not active + if (!self.active) { + [self.player startAtTime:0 error:&error]; + self.active = YES; + } + + return YES; + } + return NO; + } +} + +- (id)initWithController:(GCController *)controller locality:(GCHapticsLocality)locality API_AVAILABLE(macos(11.0)) { + @autoreleasepool { + self = [super init]; + if (self) { + NSError *error = nil; + __weak __typeof(self) weakSelf = self; + + self.engine = [controller.haptics createEngineWithLocality:locality]; + if (self.engine == nil) { + NSLog(@"Couldn't create haptics engine for locality: %@", locality); + return nil; + } + + [self.engine startAndReturnError:&error]; + if (error != nil) { + NSLog(@"Couldn't start haptics engine: %@", error.localizedDescription); + return nil; + } + + // Set up handlers for engine stopping/resetting + self.engine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) { + MTYRumbleMotor *strongSelf = weakSelf; + if (strongSelf) { + strongSelf.player = nil; + strongSelf.engine = nil; + } + }; + + self.engine.resetHandler = ^{ + MTYRumbleMotor *strongSelf = weakSelf; + if (strongSelf) { + strongSelf.player = nil; + [strongSelf.engine startAndReturnError:nil]; + } + }; + } + return self; + } +} + +@end + +// Rumble context for a controller with low and high frequency motors +@interface MTYRumbleContext : NSObject +@property(nonatomic, strong) MTYRumbleMotor *lowFrequencyMotor; +@property(nonatomic, strong) MTYRumbleMotor *highFrequencyMotor; +@property(nonatomic, strong) GCController *controller; +@end + +@implementation MTYRumbleContext + +- (id)initWithController:(GCController *)controller { + self = [super init]; + if (self) { + self.controller = controller; + + if (@available(macOS 11.0, *)) { + // Initialize rumble motors + self.lowFrequencyMotor = [[MTYRumbleMotor alloc] + initWithController:controller + locality:GCHapticsLocalityLeftHandle]; + self.highFrequencyMotor = [[MTYRumbleMotor alloc] + initWithController:controller + locality:GCHapticsLocalityRightHandle]; + + if (!self.lowFrequencyMotor || !self.highFrequencyMotor) { + NSLog(@"Failed to initialize rumble motors"); + return nil; + } + } + } + return self; +} + +- (BOOL)rumbleWithLowFrequency:(uint16_t)lowFrequency highFrequency:(uint16_t)highFrequency { + if (@available(macOS 11.0, *)) { + BOOL result = YES; + result &= [self.lowFrequencyMotor setIntensity:(float)lowFrequency / 65535.0f]; + result &= [self.highFrequencyMotor setIntensity:(float)highFrequency / 65535.0f]; + return result; + } + return NO; +} + +- (void)cleanup { + if (@available(macOS 11.0, *)) { + [self.lowFrequencyMotor cleanup]; + [self.highFrequencyMotor cleanup]; + } +} + +@end + +// Global storage for controller rumble contexts +static NSMutableDictionary *g_rumbleContexts = nil; +static dispatch_once_t g_rumbleContextsOnce; + +// Initialize the global rumble contexts dictionary +static void mty_hid_gc_init(void) { + dispatch_once(&g_rumbleContextsOnce, ^{ + g_rumbleContexts = [[NSMutableDictionary alloc] init]; + }); +} + +// Find GCController for a given IOHIDDevice +static GCController *mty_hid_gc_find_controller(IOHIDDeviceRef device) { + @autoreleasepool { + // Get vendor and product IDs from the IOHIDDevice + NSNumber *vendorID = (__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey)); + NSNumber *productID = (__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey)); + + if (!vendorID || !productID) { + NSLog(@"mty_hid_gc: No vendor/product ID"); + return nil; + } + + uint16_t vid = [vendorID unsignedShortValue]; + uint16_t pid = [productID unsignedShortValue]; + + NSLog(@"mty_hid_gc: Looking for controller VID: 0x%04X, PID: 0x%04X", vid, pid); + + // Check if this is an Xbox controller + if (vid != 0x045E) { // Microsoft vendor ID + NSLog(@"mty_hid_gc: Not a Microsoft controller"); + return nil; + } + + // Check for Xbox 360 wired controller PIDs - add more PIDs + BOOL isXbox360 = (pid == 0x028E || pid == 0x028F || pid == 0x02A1 || + pid == 0x0291 || pid == 0x0719 || pid == 0x02A0 || + pid == 0x02DD || pid == 0x02E3 || pid == 0x02FF || pid == 0x02EA); + if (!isXbox360) { + NSLog(@"mty_hid_gc: Not an Xbox 360/One controller PID"); + return nil; + } + + NSLog(@"mty_hid_gc: Found %lu GCControllers", (unsigned long)[[GCController controllers] count]); + + // Find matching GCController + for (GCController *controller in [GCController controllers]) { + // For Xbox controllers, GCController doesn't expose vendor/product IDs directly + // We need to match based on the controller type + if (@available(macOS 10.15, *)) { + NSString *productCategory = controller.productCategory; + NSString *vendorName = controller.vendorName; + NSLog(@"mty_hid_gc: Checking controller - Category: %@, Vendor: %@", productCategory, vendorName); + + // Check for Xbox controller - be more lenient with matching + if ([productCategory isEqualToString:@"Xbox One"] || + [vendorName containsString:@"Xbox"] || + [vendorName containsString:@"Microsoft"] || + [productCategory containsString:@"Xbox"]) { + NSLog(@"mty_hid_gc: Found matching Xbox controller!"); + // Found a potential match + // Store the rumble context for this controller + NSValue *deviceKey = [NSValue valueWithPointer:device]; + MTYRumbleContext *context = g_rumbleContexts[deviceKey]; + if (!context) { + context = [[MTYRumbleContext alloc] initWithController:controller]; + if (context) { + g_rumbleContexts[deviceKey] = context; + NSLog(@"mty_hid_gc: Created rumble context successfully"); + } else { + NSLog(@"mty_hid_gc: Failed to create rumble context"); + } + } + return controller; + } + } + } + + NSLog(@"mty_hid_gc: No matching GCController found"); + return nil; + } +} + +// C interface for rumble functionality +bool mty_hid_gc_rumble(void *device, uint16_t low, uint16_t high) { + @autoreleasepool { + // Only log non-zero rumble for less noise + if (low > 0 || high > 0) { + NSLog(@"mty_hid_gc_rumble: RUMBLE ON - low=%u, high=%u", low, high); + } + mty_hid_gc_init(); + + IOHIDDeviceRef hidDevice = (IOHIDDeviceRef)device; + NSValue *deviceKey = [NSValue valueWithPointer:hidDevice]; + + // Try to get existing rumble context + MTYRumbleContext *context = g_rumbleContexts[deviceKey]; + + // If no context exists, try to find and create one + if (!context) { + NSLog(@"mty_hid_gc_rumble: No existing context, trying to find controller"); + GCController *controller = mty_hid_gc_find_controller(hidDevice); + if (controller) { + if (@available(macOS 11.0, *)) { + if (controller.haptics) { + context = [[MTYRumbleContext alloc] initWithController:controller]; + if (context) { + g_rumbleContexts[deviceKey] = context; + NSLog(@"mty_hid_gc_rumble: Created new context"); + } + } else { + NSLog(@"mty_hid_gc_rumble: Controller has no haptics support"); + } + } + } + } + + // Apply rumble if we have a context + if (context) { + NSLog(@"mty_hid_gc_rumble: Applying rumble"); + return [context rumbleWithLowFrequency:low highFrequency:high]; + } + + NSLog(@"mty_hid_gc_rumble: Failed - no context available"); + return false; + } +} + +// Clean up rumble context for a device +void mty_hid_gc_cleanup(void *device) { + @autoreleasepool { + if (g_rumbleContexts) { + IOHIDDeviceRef hidDevice = (IOHIDDeviceRef)device; + NSValue *deviceKey = [NSValue valueWithPointer:hidDevice]; + + MTYRumbleContext *context = g_rumbleContexts[deviceKey]; + if (context) { + [context cleanup]; + [g_rumbleContexts removeObjectForKey:deviceKey]; + } + } + } +} + +// Check if GCController rumble is available for a device +bool mty_hid_gc_rumble_available(void *device) { + @autoreleasepool { + if (@available(macOS 11.0, *)) { + IOHIDDeviceRef hidDevice = (IOHIDDeviceRef)device; + GCController *controller = mty_hid_gc_find_controller(hidDevice); + return (controller && controller.haptics != nil); + } + return false; + } +} \ No newline at end of file diff --git a/test/GNUmakefile b/test/GNUmakefile index c11a97fc..613a9426 100644 --- a/test/GNUmakefile +++ b/test/GNUmakefile @@ -20,7 +20,9 @@ LIBS = \ -framework IOKit \ -framework Metal \ -framework QuartzCore \ - -framework WebKit + -framework WebKit \ + -framework GameController \ + -framework CoreHaptics else diff --git a/test/src/0-minimal.c b/test/src/0-minimal.c index 83de0b69..7a5675ee 100644 --- a/test/src/0-minimal.c +++ b/test/src/0-minimal.c @@ -54,4 +54,4 @@ int main(int argc, char **argv) MTY_AppDestroy(&ctx.app); return 0; -} +} \ No newline at end of file diff --git a/test/test_gc b/test/test_gc new file mode 100755 index 0000000000000000000000000000000000000000..191700b50ac01adf738ffd5b258d4a836f2ee28f GIT binary patch literal 53080 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 VCXTFV{JQz$7w-IqwwvzP{{VBLA^rdW literal 0 HcmV?d00001 diff --git a/test/test_gc.m b/test/test_gc.m new file mode 100644 index 00000000..4e33b001 --- /dev/null +++ b/test/test_gc.m @@ -0,0 +1,119 @@ +#import +#import +#import + +int main(int argc, char **argv) { + @autoreleasepool { + // Monitor for controller connections + [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + GCController *controller = note.object; + NSLog(@"Controller connected: %@ - %@", controller.vendorName, controller.productCategory); + }]; + + // List current controllers + NSArray *controllers = [GCController controllers]; + NSLog(@"Currently connected controllers: %lu", (unsigned long)controllers.count); + + for (GCController *controller in controllers) { + NSLog(@"Controller: %@ - Category: %@", controller.vendorName, controller.productCategory); + + if (@available(macOS 11.0, *)) { + if (controller.haptics) { + NSLog(@" Has haptics support!"); + + // Try to create and test rumble + NSError *error = nil; + CHHapticEngine *engine = [controller.haptics createEngineWithLocality:GCHapticsLocalityDefault]; + if (engine) { + [engine startAndReturnError:&error]; + if (!error) { + NSLog(@" Successfully started haptic engine!"); + + // Create a simple rumble pattern + CHHapticEventParameter *intensity = [[CHHapticEventParameter alloc] + initWithParameterID:CHHapticEventParameterIDHapticIntensity value:0.5]; + CHHapticEvent *event = [[CHHapticEvent alloc] + initWithEventType:CHHapticEventTypeHapticContinuous + parameters:@[intensity] + relativeTime:0 + duration:0.5]; + CHHapticPattern *pattern = [[CHHapticPattern alloc] + initWithEvents:@[event] + parameters:@[] + error:&error]; + + if (!error) { + id player = [engine createPlayerWithPattern:pattern error:&error]; + if (!error) { + NSLog(@" Testing rumble for 0.5 seconds..."); + [player startAtTime:0 error:&error]; + [NSThread sleepForTimeInterval:1.0]; + [player stopAtTime:0 error:&error]; + NSLog(@" Rumble test complete!"); + } + } + } + } + } else { + NSLog(@" No haptics support"); + } + } + } + + // Keep running for a bit to detect controllers + NSLog(@"Waiting 5 seconds for controller detection..."); + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]]; + + // Check again + controllers = [GCController controllers]; + NSLog(@"Final controller count: %lu", (unsigned long)controllers.count); + + // Test rumble on all controllers + for (GCController *controller in controllers) { + NSLog(@"Testing rumble on: %@ - %@", controller.vendorName, controller.productCategory); + + if (@available(macOS 11.0, *)) { + if (controller.haptics) { + NSError *error = nil; + CHHapticEngine *engine = [controller.haptics createEngineWithLocality:GCHapticsLocalityDefault]; + if (engine) { + [engine startAndReturnError:&error]; + if (!error) { + // Create a simple rumble pattern + CHHapticEventParameter *intensity = [[CHHapticEventParameter alloc] + initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0]; + CHHapticEvent *event = [[CHHapticEvent alloc] + initWithEventType:CHHapticEventTypeHapticContinuous + parameters:@[intensity] + relativeTime:0 + duration:1.0]; + CHHapticPattern *pattern = [[CHHapticPattern alloc] + initWithEvents:@[event] + parameters:@[] + error:&error]; + + if (!error) { + id player = [engine createPlayerWithPattern:pattern error:&error]; + if (!error) { + NSLog(@" RUMBLING NOW for 1 second..."); + [player startAtTime:0 error:&error]; + [NSThread sleepForTimeInterval:1.5]; + NSLog(@" Rumble complete!"); + } + } + } + } + } else { + NSLog(@" No haptics support"); + } + } else { + NSLog(@" macOS 11.0+ required for haptics"); + } + } + } + + return 0; +} \ No newline at end of file