From ff24f7718c2486ec0eb70f94f4b91057754abf5a Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 10 Jan 2022 22:10:29 +0900 Subject: [PATCH] internal/uidriver/glfw: use an original implementation for macOS Updates #1452 --- internal/gamepad/gamepad.go | 230 +++++++++++++ internal/gamepad/gamepad_darwin.go | 396 +++++++++++++++++++++++ internal/uidriver/glfw/gamepad_glfw.go | 255 +++++++++++++++ internal/uidriver/glfw/gamepad_native.go | 133 ++++++++ internal/uidriver/glfw/input.go | 228 +------------ 5 files changed, 1017 insertions(+), 225 deletions(-) create mode 100644 internal/gamepad/gamepad.go create mode 100644 internal/gamepad/gamepad_darwin.go create mode 100644 internal/uidriver/glfw/gamepad_glfw.go create mode 100644 internal/uidriver/glfw/gamepad_native.go diff --git a/internal/gamepad/gamepad.go b/internal/gamepad/gamepad.go new file mode 100644 index 000000000..1c53ebc37 --- /dev/null +++ b/internal/gamepad/gamepad.go @@ -0,0 +1,230 @@ +// Copyright 2022 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gamepad + +import ( + "sync" + + "github.com/hajimehoshi/ebiten/v2/internal/driver" + "github.com/hajimehoshi/ebiten/v2/internal/gamepaddb" +) + +const ( + hatCentered = 0 + hatUp = 1 + hatRight = 2 + hatDown = 4 + hatLeft = 8 + hatRightUp = hatRight | hatUp + hatRightDown = hatRight | hatDown + hatLeftUp = hatLeft | hatUp + hatLeftDown = hatLeft | hatDown +) + +type gamepads struct { + gamepads []*Gamepad + m sync.Mutex + + nativeGamepads +} + +var theGamepads gamepads + +func init() { + theGamepads.nativeGamepads.init() +} + +func AppendGamepadIDs(ids []driver.GamepadID) []driver.GamepadID { + return theGamepads.appendGamepadIDs(ids) +} + +func Update() { + theGamepads.update() +} + +func Get(id driver.GamepadID) *Gamepad { + return theGamepads.get(id) +} + +func (g *gamepads) appendGamepadIDs(ids []driver.GamepadID) []driver.GamepadID { + g.m.Lock() + defer g.m.Unlock() + + for i, gp := range g.gamepads { + if gp != nil && gp.present() { + ids = append(ids, driver.GamepadID(i)) + } + } + return ids +} + +func (g *gamepads) update() { + g.m.Lock() + defer g.m.Unlock() + + for _, gp := range g.gamepads { + if gp != nil { + gp.update() + } + } +} + +func (g *gamepads) get(id driver.GamepadID) *Gamepad { + g.m.Lock() + defer g.m.Unlock() + + if id < 0 || int(id) >= len(g.gamepads) { + return nil + } + return g.gamepads[id] +} + +func (g *gamepads) find(cond func(*Gamepad) bool) *Gamepad { + g.m.Lock() + defer g.m.Unlock() + + for _, gp := range g.gamepads { + if gp == nil { + continue + } + if cond(gp) { + return gp + } + } + return nil +} + +func (g *gamepads) add(name, sdlID string) *Gamepad { + g.m.Lock() + defer g.m.Unlock() + + for i, gp := range g.gamepads { + if gp == nil { + gp := &Gamepad{ + name: name, + sdlID: sdlID, + } + g.gamepads[i] = gp + return gp + } + } + + gp := &Gamepad{ + name: name, + sdlID: sdlID, + } + g.gamepads = append(g.gamepads, gp) + return gp +} + +func (g *gamepads) remove(cond func(*Gamepad) bool) { + g.m.Lock() + defer g.m.Unlock() + + for i, gp := range g.gamepads { + if gp == nil { + continue + } + if cond(gp) { + g.gamepads[i] = nil + } + } +} + +type Gamepad struct { + name string + sdlID string + m sync.Mutex + + nativeGamepad +} + +func (g *Gamepad) update() { + g.m.Lock() + defer g.m.Unlock() + + g.nativeGamepad.update() +} + +func (g *Gamepad) Name() string { + // This is immutable and doesn't have to be protected by a mutex. + return g.name +} + +func (g *Gamepad) SDLID() string { + // This is immutable and doesn't have to be protected by a mutex. + return g.sdlID +} + +func (g *Gamepad) AxisNum() int { + g.m.Lock() + defer g.m.Unlock() + + return g.nativeGamepad.axisNum() +} + +func (g *Gamepad) ButtonNum() int { + g.m.Lock() + defer g.m.Unlock() + + return g.nativeGamepad.buttonNum() +} + +func (g *Gamepad) HatNum() int { + g.m.Lock() + defer g.m.Unlock() + + return g.nativeGamepad.hatNum() +} + +func (g *Gamepad) Axis(axis int) float64 { + g.m.Lock() + defer g.m.Unlock() + + return g.nativeGamepad.axisValue(axis) +} + +func (g *Gamepad) Button(button int) bool { + g.m.Lock() + defer g.m.Unlock() + + return g.nativeGamepad.isButtonPressed(button) +} + +func (g *Gamepad) Hat(hat int) int { + g.m.Lock() + defer g.m.Unlock() + + return g.nativeGamepad.hatState(hat) +} + +func (g *Gamepad) IsStandardLayoutAvailable() bool { + g.m.Lock() + defer g.m.Unlock() + + return gamepaddb.HasStandardLayoutMapping(g.sdlID) +} + +func (g *Gamepad) StandardAxisValue(axis driver.StandardGamepadAxis) float64 { + return gamepaddb.AxisValue(g.sdlID, axis, g) +} + +func (g *Gamepad) StandardButtonValue(button driver.StandardGamepadButton) float64 { + return gamepaddb.ButtonValue(g.sdlID, button, g) +} + +func (g *Gamepad) IsStandardButtonPressed(button driver.StandardGamepadButton) bool { + return gamepaddb.IsButtonPressed(g.sdlID, button, g) +} diff --git a/internal/gamepad/gamepad_darwin.go b/internal/gamepad/gamepad_darwin.go new file mode 100644 index 000000000..d4d82c103 --- /dev/null +++ b/internal/gamepad/gamepad_darwin.go @@ -0,0 +1,396 @@ +// Copyright 2022 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !ios +// +build !ios + +package gamepad + +import ( + "fmt" + "sort" + "unsafe" +) + +// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit +// +// #include +// #include +// +// static CFStringRef cfStringRefIOHIDVendorIDKey() { +// return CFSTR(kIOHIDVendorIDKey); +// } +// +// static CFStringRef cfStringRefIOHIDProductIDKey() { +// return CFSTR(kIOHIDProductIDKey); +// } +// +// static CFStringRef cfStringRefIOHIDVersionNumberKey() { +// return CFSTR(kIOHIDVersionNumberKey); +// } +// +// static CFStringRef cfStringRefIOHIDProductKey() { +// return CFSTR(kIOHIDProductKey); +// } +// +// static CFStringRef cfStringRefIOHIDDeviceUsagePageKey() { +// return CFSTR(kIOHIDDeviceUsagePageKey); +// } +// +// static CFStringRef cfStringRefIOHIDDeviceUsageKey() { +// return CFSTR(kIOHIDDeviceUsageKey); +// } +// +// void ebitenGamepadMatchingCallback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef device); +// void ebitenGamepadRemovalCallback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef device); +import "C" + +type nativeGamepads struct { + hidManager C.IOHIDManagerRef +} + +type nativeGamepad struct { + device C.IOHIDDeviceRef + axes elements + buttons elements + hats elements + + axisValues []float64 + buttonValues []bool + hatValues []int +} + +type element struct { + native C.IOHIDElementRef + usage int + index int + minimum int + maximum int +} + +type elements []element + +func (e elements) Len() int { + return len(e) +} + +func (e elements) Less(i, j int) bool { + if e[i].usage != e[j].usage { + return e[i].usage < e[j].usage + } + if e[i].index != e[j].index { + return e[i].index < e[j].index + } + return false +} + +func (e elements) Swap(i, j int) { + e[i], e[j] = e[j], e[i] +} + +func (g *nativeGamepad) present() bool { + return g.device != 0 +} + +func (g *nativeGamepad) elementValue(e *element) int { + if g.device == 0 { + return 0 + } + + var valueRef C.IOHIDValueRef + if C.IOHIDDeviceGetValue(g.device, e.native, &valueRef) == C.kIOReturnSuccess { + return int(C.IOHIDValueGetIntegerValue(valueRef)) + } + + return 0 +} + +func (g *nativeGamepad) update() { + if cap(g.axisValues) < len(g.axes) { + g.axisValues = make([]float64, len(g.axes)) + } + g.axisValues = g.axisValues[:len(g.axes)] + + if cap(g.buttonValues) < len(g.buttons) { + g.buttonValues = make([]bool, len(g.buttons)) + } + g.buttonValues = g.buttonValues[:len(g.buttons)] + + if cap(g.hatValues) < len(g.hats) { + g.hatValues = make([]int, len(g.hats)) + } + g.hatValues = g.hatValues[:len(g.hats)] + + for i, a := range g.axes { + raw := g.elementValue(&a) + if raw < a.minimum { + a.minimum = raw + } + if raw > a.maximum { + a.maximum = raw + } + var value float64 + if size := a.maximum - a.minimum; size != 0 { + value = 2*float64(raw-a.minimum)/float64(size) - 1 + } + g.axisValues[i] = value + } + + for i, b := range g.buttons { + g.buttonValues[i] = g.elementValue(&b) > 0 + } + + hatStates := []int{ + hatUp, + hatRightUp, + hatRight, + hatRightDown, + hatDown, + hatLeftDown, + hatLeft, + hatLeftUp, + } + for i, h := range g.hats { + if state := g.elementValue(&h); state < 0 || state >= len(hatStates) { + g.hatValues[i] = hatCentered + } else { + g.hatValues[i] = hatStates[state] + } + } +} + +func (g *nativeGamepad) axisNum() int { + return len(g.axisValues) +} + +func (g *nativeGamepad) buttonNum() int { + return len(g.buttonValues) +} + +func (g *nativeGamepad) hatNum() int { + return len(g.hatValues) +} + +func (g *nativeGamepad) axisValue(axis int) float64 { + if axis < 0 || axis >= len(g.axisValues) { + return 0 + } + return g.axisValues[axis] +} + +func (g *nativeGamepad) isButtonPressed(button int) bool { + if button < 0 || button >= len(g.buttonValues) { + return false + } + return g.buttonValues[button] +} + +func (g *nativeGamepad) hatState(hat int) int { + if hat < 0 || hat >= len(g.hatValues) { + return hatCentered + } + return g.hatValues[hat] +} + +func (g *nativeGamepads) init() { + var dicts []unsafe.Pointer + + page := C.kHIDPage_GenericDesktop + for _, usage := range []uint{ + C.kHIDUsage_GD_Joystick, + C.kHIDUsage_GD_GamePad, + C.kHIDUsage_GD_MultiAxisController, + } { + pageRef := C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberIntType, unsafe.Pointer(&page)) + if pageRef == 0 { + panic("gamepad: CFNumberCreate returned nil") + } + defer C.CFRelease(C.CFTypeRef(pageRef)) + + usageRef := C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberIntType, unsafe.Pointer(&usage)) + if usageRef == 0 { + panic("gamepad: CFNumberCreate returned nil") + } + defer C.CFRelease(C.CFTypeRef(usageRef)) + + keys := []unsafe.Pointer{ + unsafe.Pointer(C.cfStringRefIOHIDDeviceUsagePageKey()), + unsafe.Pointer(C.cfStringRefIOHIDDeviceUsageKey()), + } + values := []unsafe.Pointer{ + unsafe.Pointer(pageRef), + unsafe.Pointer(usageRef), + } + + dict := C.CFDictionaryCreate(C.kCFAllocatorDefault, &keys[0], &values[0], C.CFIndex(len(keys)), &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) + if dict == 0 { + panic("gamepad: CFDictionaryCreate returned nil") + } + defer C.CFRelease(C.CFTypeRef(unsafe.Pointer(dict))) + + dicts = append(dicts, unsafe.Pointer(dict)) + } + + matching := C.CFArrayCreate(C.kCFAllocatorDefault, &dicts[0], C.CFIndex(len(dicts)), &C.kCFTypeArrayCallBacks) + if matching == 0 { + panic("gamepad: CFArrayCreateMutable returned nil") + } + defer C.CFRelease(C.CFTypeRef(matching)) + + g.hidManager = C.IOHIDManagerCreate(C.kCFAllocatorDefault, C.kIOHIDOptionsTypeNone) + if C.IOHIDManagerOpen(g.hidManager, C.kIOHIDOptionsTypeNone) != C.kIOReturnSuccess { + panic("gamepad: IOHIDManagerOpen failed") + } + + C.IOHIDManagerSetDeviceMatchingMultiple(g.hidManager, matching) + C.IOHIDManagerRegisterDeviceMatchingCallback(g.hidManager, C.IOHIDDeviceCallback(C.ebitenGamepadMatchingCallback), nil) + C.IOHIDManagerRegisterDeviceRemovalCallback(g.hidManager, C.IOHIDDeviceCallback(C.ebitenGamepadRemovalCallback), nil) + + C.IOHIDManagerScheduleWithRunLoop(g.hidManager, C.CFRunLoopGetMain(), C.kCFRunLoopDefaultMode) + + // Execute the run loop once in order to register any initially-attached gamepads. + C.CFRunLoopRunInMode(C.kCFRunLoopDefaultMode, 0, 0 /* false */) +} + +//export ebitenGamepadMatchingCallback +func ebitenGamepadMatchingCallback(ctx unsafe.Pointer, res C.IOReturn, sender unsafe.Pointer, device C.IOHIDDeviceRef) { + if theGamepads.find(func(g *Gamepad) bool { + return g.device == device + }) != nil { + return + } + + name := "Unknown" + if prop := C.IOHIDDeviceGetProperty(device, C.cfStringRefIOHIDProductKey()); prop != 0 { + var cstr [256]C.char + C.CFStringGetCString(C.CFStringRef(prop), &cstr[0], C.CFIndex(len(cstr)), C.kCFStringEncodingUTF8) + name = C.GoString(&cstr[0]) + } + + var vendor uint32 + if prop := C.IOHIDDeviceGetProperty(device, C.cfStringRefIOHIDVendorIDKey()); prop != 0 { + C.CFNumberGetValue(C.CFNumberRef(prop), C.kCFNumberSInt32Type, unsafe.Pointer(&vendor)) + } + + var product uint32 + if prop := C.IOHIDDeviceGetProperty(device, C.cfStringRefIOHIDProductIDKey()); prop != 0 { + C.CFNumberGetValue(C.CFNumberRef(prop), C.kCFNumberSInt32Type, unsafe.Pointer(&product)) + } + + var version uint32 + if prop := C.IOHIDDeviceGetProperty(device, C.cfStringRefIOHIDVersionNumberKey()); prop != 0 { + C.CFNumberGetValue(C.CFNumberRef(prop), C.kCFNumberSInt32Type, unsafe.Pointer(&version)) + } + + var sdlID string + if vendor != 0 && product != 0 { + sdlID = fmt.Sprintf("03000000%02x%02x0000%02x%02x0000%02x%02x0000", + byte(vendor), byte(vendor>>8), + byte(product), byte(product>>8), + byte(version), byte(version>>8)) + } else { + bs := []byte(name) + if len(bs) < 12 { + bs = append(bs, make([]byte, 12-len(bs))...) + } + sdlID = fmt.Sprintf("05000000%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + bs[0], bs[1], bs[2], bs[3], bs[4], bs[5], bs[6], bs[7], bs[8], bs[9], bs[10], bs[11]) + } + + elements := C.IOHIDDeviceCopyMatchingElements(device, 0, C.kIOHIDOptionsTypeNone) + defer C.CFRelease(C.CFTypeRef(elements)) + + g := theGamepads.add(name, sdlID) + g.device = device + + for i := C.CFIndex(0); i < C.CFArrayGetCount(elements); i++ { + native := (C.IOHIDElementRef)(C.CFArrayGetValueAtIndex(elements, i)) + if C.CFGetTypeID(C.CFTypeRef(native)) != C.IOHIDElementGetTypeID() { + continue + } + + typ := C.IOHIDElementGetType(native) + if typ != C.kIOHIDElementTypeInput_Axis && + typ != C.kIOHIDElementTypeInput_Button && + typ != C.kIOHIDElementTypeInput_Misc { + continue + } + + usage := C.IOHIDElementGetUsage(native) + page := C.IOHIDElementGetUsagePage(native) + + switch page { + case C.kHIDPage_GenericDesktop: + switch usage { + case C.kHIDUsage_GD_X, C.kHIDUsage_GD_Y, C.kHIDUsage_GD_Z, + C.kHIDUsage_GD_Rx, C.kHIDUsage_GD_Ry, C.kHIDUsage_GD_Rz, + C.kHIDUsage_GD_Slider, C.kHIDUsage_GD_Dial, C.kHIDUsage_GD_Wheel: + g.axes = append(g.axes, element{ + native: native, + usage: int(usage), + index: len(g.axes), + minimum: int(C.IOHIDElementGetLogicalMin(native)), + maximum: int(C.IOHIDElementGetLogicalMax(native)), + }) + case C.kHIDUsage_GD_Hatswitch: + g.hats = append(g.hats, element{ + native: native, + usage: int(usage), + index: len(g.hats), + minimum: int(C.IOHIDElementGetLogicalMin(native)), + maximum: int(C.IOHIDElementGetLogicalMax(native)), + }) + case C.kHIDUsage_GD_DPadUp, C.kHIDUsage_GD_DPadRight, C.kHIDUsage_GD_DPadDown, C.kHIDUsage_GD_DPadLeft, + C.kHIDUsage_GD_SystemMainMenu, C.kHIDUsage_GD_Select, C.kHIDUsage_GD_Start: + g.buttons = append(g.buttons, element{ + native: native, + usage: int(usage), + index: len(g.buttons), + minimum: int(C.IOHIDElementGetLogicalMin(native)), + maximum: int(C.IOHIDElementGetLogicalMax(native)), + }) + } + case C.kHIDPage_Simulation: + switch usage { + case C.kHIDUsage_Sim_Accelerator, C.kHIDUsage_Sim_Brake, C.kHIDUsage_Sim_Throttle, C.kHIDUsage_Sim_Rudder, C.kHIDUsage_Sim_Steering: + g.axes = append(g.axes, element{ + native: native, + usage: int(usage), + index: len(g.axes), + minimum: int(C.IOHIDElementGetLogicalMin(native)), + maximum: int(C.IOHIDElementGetLogicalMax(native)), + }) + } + case C.kHIDPage_Button, C.kHIDPage_Consumer: + g.buttons = append(g.buttons, element{ + native: native, + usage: int(usage), + index: len(g.buttons), + minimum: int(C.IOHIDElementGetLogicalMin(native)), + maximum: int(C.IOHIDElementGetLogicalMax(native)), + }) + } + } + + sort.Stable(g.axes) + sort.Stable(g.buttons) + sort.Stable(g.hats) +} + +//export ebitenGamepadRemovalCallback +func ebitenGamepadRemovalCallback(ctx unsafe.Pointer, res C.IOReturn, sender unsafe.Pointer, device C.IOHIDDeviceRef) { + theGamepads.remove(func(g *Gamepad) bool { + return g.device == device + }) +} diff --git a/internal/uidriver/glfw/gamepad_glfw.go b/internal/uidriver/glfw/gamepad_glfw.go new file mode 100644 index 000000000..96b59f1ae --- /dev/null +++ b/internal/uidriver/glfw/gamepad_glfw.go @@ -0,0 +1,255 @@ +// Copyright 2022 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !android && !js && !darwin +// +build !android,!js,!darwin + +package glfw + +import ( + "time" + + "github.com/hajimehoshi/ebiten/v2/internal/driver" + "github.com/hajimehoshi/ebiten/v2/internal/gamepaddb" + "github.com/hajimehoshi/ebiten/v2/internal/glfw" +) + +type nativeGamepads struct { +} + +func (i *Input) updateGamepads() { + for id := glfw.Joystick(0); id < glfw.Joystick(len(i.gamepads)); id++ { + i.gamepads[id].valid = false + if !id.Present() { + continue + } + + buttons := id.GetButtons() + + // A gamepad can be detected even though there are not. Apparently, some special devices are + // recognized as gamepads by GLFW. In this case, the number of the 'buttons' can exceeds the + // maximum. Skip such devices as a tentative solution (#1173). + if len(buttons) > driver.GamepadButtonNum { + continue + } + + i.gamepads[id].valid = true + + i.gamepads[id].buttonNum = len(buttons) + for b := 0; b < len(i.gamepads[id].buttonPressed); b++ { + if len(buttons) <= b { + i.gamepads[id].buttonPressed[b] = false + continue + } + i.gamepads[id].buttonPressed[b] = glfw.Action(buttons[b]) == glfw.Press + } + + axes32 := id.GetAxes() + i.gamepads[id].axisNum = len(axes32) + for a := 0; a < len(i.gamepads[id].axes); a++ { + if len(axes32) <= a { + i.gamepads[id].axes[a] = 0 + continue + } + i.gamepads[id].axes[a] = float64(axes32[a]) + } + + hats := id.GetHats() + i.gamepads[id].hatsNum = len(hats) + for h := 0; h < len(i.gamepads[id].hats); h++ { + if len(hats) <= h { + i.gamepads[id].hats[h] = 0 + continue + } + i.gamepads[id].hats[h] = int(hats[h]) + } + + // Note that GLFW's gamepad GUID follows SDL's GUID. + i.gamepads[id].guid = id.GetGUID() + i.gamepads[id].name = id.GetName() + } +} + +func (i *Input) AppendGamepadIDs(gamepadIDs []driver.GamepadID) []driver.GamepadID { + if !i.ui.isRunning() { + return nil + } + + i.ui.m.RLock() + defer i.ui.m.RUnlock() + for id, g := range i.gamepads { + if g.valid { + gamepadIDs = append(gamepadIDs, driver.GamepadID(id)) + } + } + return gamepadIDs +} + +func (i *Input) GamepadSDLID(id driver.GamepadID) string { + if !i.ui.isRunning() { + return "" + } + + i.ui.m.RLock() + defer i.ui.m.RUnlock() + if len(i.gamepads) <= int(id) { + return "" + } + return i.gamepads[id].guid +} + +func (i *Input) GamepadName(id driver.GamepadID) string { + if !i.ui.isRunning() { + return "" + } + + i.ui.m.RLock() + defer i.ui.m.RUnlock() + if len(i.gamepads) <= int(id) { + return "" + } + return i.gamepads[id].name +} + +func (i *Input) GamepadAxisNum(id driver.GamepadID) int { + if !i.ui.isRunning() { + return 0 + } + + i.ui.m.RLock() + defer i.ui.m.RUnlock() + if len(i.gamepads) <= int(id) { + return 0 + } + return i.gamepads[id].axisNum +} + +func (i *Input) GamepadAxisValue(id driver.GamepadID, axis int) float64 { + if !i.ui.isRunning() { + return 0 + } + + i.ui.m.RLock() + defer i.ui.m.RUnlock() + if len(i.gamepads) <= int(id) { + return 0 + } + return i.gamepads[id].axes[axis] +} + +func (i *Input) GamepadButtonNum(id driver.GamepadID) int { + if !i.ui.isRunning() { + return 0 + } + + i.ui.m.RLock() + defer i.ui.m.RUnlock() + if len(i.gamepads) <= int(id) { + return 0 + } + return i.gamepads[id].buttonNum +} + +func (i *Input) IsGamepadButtonPressed(id driver.GamepadID, button driver.GamepadButton) bool { + if !i.ui.isRunning() { + return false + } + + i.ui.m.RLock() + defer i.ui.m.RUnlock() + if len(i.gamepads) <= int(id) { + return false + } + return i.gamepads[id].buttonPressed[button] +} + +func (i *Input) IsStandardGamepadLayoutAvailable(id driver.GamepadID) bool { + i.ui.m.Lock() + defer i.ui.m.Unlock() + + if len(i.gamepads) <= int(id) { + return false + } + g := i.gamepads[int(id)] + return gamepaddb.HasStandardLayoutMapping(g.guid) +} + +func (i *Input) StandardGamepadAxisValue(id driver.GamepadID, axis driver.StandardGamepadAxis) float64 { + i.ui.m.Lock() + defer i.ui.m.Unlock() + + if len(i.gamepads) <= int(id) { + return 0 + } + g := i.gamepads[int(id)] + return gamepaddb.AxisValue(g.guid, axis, gamepadState{&g}) +} + +func (i *Input) StandardGamepadButtonValue(id driver.GamepadID, button driver.StandardGamepadButton) float64 { + i.ui.m.Lock() + defer i.ui.m.Unlock() + + if len(i.gamepads) <= int(id) { + return 0 + } + g := i.gamepads[int(id)] + return gamepaddb.ButtonValue(g.guid, button, gamepadState{&g}) +} + +func (i *Input) IsStandardGamepadButtonPressed(id driver.GamepadID, button driver.StandardGamepadButton) bool { + i.ui.m.Lock() + defer i.ui.m.Unlock() + + if len(i.gamepads) <= int(id) { + return false + } + g := i.gamepads[int(id)] + return gamepaddb.IsButtonPressed(g.guid, button, gamepadState{&g}) +} + +func (i *Input) VibrateGamepad(id driver.GamepadID, duration time.Duration, strongMagnitude float64, weakMagnitude float64) { + // TODO: Implement this (#1452) +} + +func init() { + // Confirm that all the hat state values are the same. + if gamepaddb.HatUp != glfw.HatUp { + panic("glfw: gamepaddb.HatUp must equal to glfw.HatUp but not") + } + if gamepaddb.HatRight != glfw.HatRight { + panic("glfw: gamepaddb.HatRight must equal to glfw.HatRight but not") + } + if gamepaddb.HatDown != glfw.HatDown { + panic("glfw: gamepaddb.HatDown must equal to glfw.HatDown but not") + } + if gamepaddb.HatLeft != glfw.HatLeft { + panic("glfw: gamepaddb.HatLeft must equal to glfw.HatLeft but not") + } +} + +type gamepadState struct { + g *gamepad +} + +func (s gamepadState) Axis(index int) float64 { + return s.g.axes[index] +} + +func (s gamepadState) Button(index int) bool { + return s.g.buttonPressed[index] +} + +func (s gamepadState) Hat(index int) int { + return s.g.hats[index] +} diff --git a/internal/uidriver/glfw/gamepad_native.go b/internal/uidriver/glfw/gamepad_native.go new file mode 100644 index 000000000..f340f870f --- /dev/null +++ b/internal/uidriver/glfw/gamepad_native.go @@ -0,0 +1,133 @@ +// Copyright 2022 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build darwin && !ios +// +build darwin,!ios + +package glfw + +import ( + "time" + + "github.com/hajimehoshi/ebiten/v2/internal/driver" + gamepadpkg "github.com/hajimehoshi/ebiten/v2/internal/gamepad" +) + +type nativeGamepads struct{} + +func (i *Input) updateGamepads() { + gamepadpkg.Update() +} + +func (i *Input) AppendGamepadIDs(gamepadIDs []driver.GamepadID) []driver.GamepadID { + return gamepadpkg.AppendGamepadIDs(gamepadIDs) +} + +func (i *Input) GamepadSDLID(id driver.GamepadID) string { + g := gamepadpkg.Get(id) + if g == nil { + return "" + } + return g.SDLID() +} + +func (i *Input) GamepadName(id driver.GamepadID) string { + g := gamepadpkg.Get(id) + if g == nil { + return "" + } + return g.Name() +} + +func (i *Input) GamepadAxisNum(id driver.GamepadID) int { + g := gamepadpkg.Get(id) + if g == nil { + return 0 + } + return g.AxisNum() +} + +func (i *Input) GamepadAxisValue(id driver.GamepadID, axis int) float64 { + g := gamepadpkg.Get(id) + if g == nil { + return 0 + } + return g.Axis(axis) +} + +func (i *Input) GamepadButtonNum(id driver.GamepadID) int { + g := gamepadpkg.Get(id) + if g == nil { + return 0 + } + + // For backward compatibility, hats are treated as buttons in GLFW. + return g.ButtonNum() + g.HatNum()*4 +} + +func (i *Input) IsGamepadButtonPressed(id driver.GamepadID, button driver.GamepadButton) bool { + g := gamepadpkg.Get(id) + if g == nil { + return false + } + + nbuttons := g.ButtonNum() + if int(button) < nbuttons { + return g.Button(int(button)) + } + + // For backward compatibility, hats are treated as buttons in GLFW. + if hat := (int(button) - nbuttons) / 4; hat < g.HatNum() { + dir := (int(button) - nbuttons) % 4 + return g.Hat(hat)&(1< driver.GamepadButtonNum { - continue - } - - i.gamepads[id].valid = true - - i.gamepads[id].buttonNum = len(buttons) - for b := 0; b < len(i.gamepads[id].buttonPressed); b++ { - if len(buttons) <= b { - i.gamepads[id].buttonPressed[b] = false - continue - } - i.gamepads[id].buttonPressed[b] = glfw.Action(buttons[b]) == glfw.Press - } - - axes32 := id.GetAxes() - i.gamepads[id].axisNum = len(axes32) - for a := 0; a < len(i.gamepads[id].axes); a++ { - if len(axes32) <= a { - i.gamepads[id].axes[a] = 0 - continue - } - i.gamepads[id].axes[a] = float64(axes32[a]) - } - - hats := id.GetHats() - i.gamepads[id].hatsNum = len(hats) - for h := 0; h < len(i.gamepads[id].hats); h++ { - if len(hats) <= h { - i.gamepads[id].hats[h] = 0 - continue - } - i.gamepads[id].hats[h] = int(hats[h]) - } - - // Note that GLFW's gamepad GUID follows SDL's GUID. - i.gamepads[id].guid = id.GetGUID() - i.gamepads[id].name = id.GetName() - } -} - -func (i *Input) IsStandardGamepadLayoutAvailable(id driver.GamepadID) bool { - i.ui.m.Lock() - defer i.ui.m.Unlock() - - if len(i.gamepads) <= int(id) { - return false - } - g := i.gamepads[int(id)] - return gamepaddb.HasStandardLayoutMapping(g.guid) -} - -func (i *Input) StandardGamepadAxisValue(id driver.GamepadID, axis driver.StandardGamepadAxis) float64 { - i.ui.m.Lock() - defer i.ui.m.Unlock() - - if len(i.gamepads) <= int(id) { - return 0 - } - g := i.gamepads[int(id)] - return gamepaddb.AxisValue(g.guid, axis, gamepadState{&g}) -} - -func (i *Input) StandardGamepadButtonValue(id driver.GamepadID, button driver.StandardGamepadButton) float64 { - i.ui.m.Lock() - defer i.ui.m.Unlock() - - if len(i.gamepads) <= int(id) { - return 0 - } - g := i.gamepads[int(id)] - return gamepaddb.ButtonValue(g.guid, button, gamepadState{&g}) -} - -func (i *Input) IsStandardGamepadButtonPressed(id driver.GamepadID, button driver.StandardGamepadButton) bool { - i.ui.m.Lock() - defer i.ui.m.Unlock() - - if len(i.gamepads) <= int(id) { - return false - } - g := i.gamepads[int(id)] - return gamepaddb.IsButtonPressed(g.guid, button, gamepadState{&g}) -} - -func (i *Input) VibrateGamepad(id driver.GamepadID, duration time.Duration, strongMagnitude float64, weakMagnitude float64) { - // TODO: Implement this (#1452) -} - -func init() { - // Confirm that all the hat state values are the same. - if gamepaddb.HatUp != glfw.HatUp { - panic("glfw: gamepaddb.HatUp must equal to glfw.HatUp but not") - } - if gamepaddb.HatRight != glfw.HatRight { - panic("glfw: gamepaddb.HatRight must equal to glfw.HatRight but not") - } - if gamepaddb.HatDown != glfw.HatDown { - panic("glfw: gamepaddb.HatDown must equal to glfw.HatDown but not") - } - if gamepaddb.HatLeft != glfw.HatLeft { - panic("glfw: gamepaddb.HatLeft must equal to glfw.HatLeft but not") - } -} - -type gamepadState struct { - g *gamepad -} - -func (s gamepadState) Axis(index int) float64 { - return s.g.axes[index] -} - -func (s gamepadState) Button(index int) bool { - return s.g.buttonPressed[index] -} - -func (s gamepadState) Hat(index int) int { - return s.g.hats[index] + i.updateGamepads() }