From e2e4627cf0619f9b5b5fbcd8ad3ad9f904375976 Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 11 Sep 2026 23:26:34 +0200 Subject: [PATCH] Upstream hajimehoshi/ebiten state of the touched files before 943205c3c1 --- internal/gamepad/export_linux_test.go | 25 + internal/gamepad/gamepad_linux.go | 732 ++++++++++++++++++++++++++ 2 files changed, 757 insertions(+) create mode 100644 internal/gamepad/export_linux_test.go create mode 100644 internal/gamepad/gamepad_linux.go diff --git a/internal/gamepad/export_linux_test.go b/internal/gamepad/export_linux_test.go new file mode 100644 index 0000000..24090a2 --- /dev/null +++ b/internal/gamepad/export_linux_test.go @@ -0,0 +1,25 @@ +// Copyright 2026 The Ebitengine 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 && !nintendosdk && !playstation5 + +package gamepad + +import "unsafe" + +const ( + FFEffectSize = unsafe.Sizeof(ff_effect{}) + FFEffectUnionOffset = unsafe.Offsetof(ff_effect{}.u) + FFEffectRumbleOffset = unsafe.Offsetof(ff_effect{}.u) + unsafe.Offsetof(ff_effect_union{}.rumble) +) diff --git a/internal/gamepad/gamepad_linux.go b/internal/gamepad/gamepad_linux.go new file mode 100644 index 0000000..375dd73 --- /dev/null +++ b/internal/gamepad/gamepad_linux.go @@ -0,0 +1,732 @@ +// 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 && !nintendosdk && !playstation5 + +package gamepad + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "runtime" + "time" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/hajimehoshi/ebiten/v2/internal/gamepaddb" + "github.com/hajimehoshi/ebiten/v2/internal/mathutil" +) + +const dirName = "/dev/input" + +var reEvent = regexp.MustCompile(`^event[0-9]+$`) + +func isBitSet(s []byte, bit int) bool { + return s[bit/8]&(1<<(bit%8)) != 0 +} + +type nativeGamepadsImpl struct { + inotifyPlus1 int + watch int +} + +func newNativeGamepadsImpl() nativeGamepads { + return &nativeGamepadsImpl{} +} + +func (g *nativeGamepadsImpl) init(gamepads *gamepads) (err error) { + // Check the existence of the directory `dirName`. + var stat unix.Stat_t + if err := unix.Stat(dirName, &stat); err != nil { + if err == unix.ENOENT { + return nil + } + // `/dev/input` might not be accessible in some environments (#3057). + if err == unix.EACCES { + return nil + } + return fmt.Errorf("gamepad: Stat failed: %w", err) + } + if stat.Mode&unix.S_IFDIR == 0 { + return nil + } + + inotify, err := unix.InotifyInit1(unix.IN_NONBLOCK | unix.IN_CLOEXEC) + if err != nil { + return fmt.Errorf("gamepad: InotifyInit1 failed: %w", err) + } + g.inotifyPlus1 = inotify + 1 + defer func() { + if err != nil { + _ = unix.Close(g.inotifyPlus1 - 1) + g.inotifyPlus1 = 0 + } + }() + + // Register for IN_ATTRIB to get notified when udev is done. + // This works well in practice but the true way is libudev. + watch, err := unix.InotifyAddWatch(g.inotifyPlus1-1, dirName, unix.IN_CREATE|unix.IN_ATTRIB|unix.IN_DELETE) + if err != nil { + return fmt.Errorf("gamepad: InotifyAddWatch failed: %w", err) + } + g.watch = watch + + ents, err := os.ReadDir(dirName) + if err != nil { + return fmt.Errorf("gamepad: ReadDir(%s) failed: %w", dirName, err) + } + for _, ent := range ents { + if ent.IsDir() { + continue + } + if !reEvent.MatchString(ent.Name()) { + continue + } + if err := g.openGamepad(gamepads, filepath.Join(dirName, ent.Name())); err != nil { + return err + } + } + + return nil +} + +func (*nativeGamepadsImpl) openGamepad(gamepads *gamepads, path string) error { + if gamepads.find(func(gamepad *Gamepad) bool { + return gamepad.native.(*nativeGamepadImpl).path == path + }) != nil { + return nil + } + + // Rumble requires write access to upload and play force feedback effects. + // Fall back to read-only when write access is not permitted: the gamepad + // still works, without rumble. + writable := true + fd, err := unix.Open(path, unix.O_RDWR|unix.O_NONBLOCK, 0) + if err == unix.EACCES || err == unix.EPERM { + writable = false + fd, err = unix.Open(path, unix.O_RDONLY|unix.O_NONBLOCK, 0) + } + if err != nil { + if err == unix.EACCES { + return nil + } + // This happens with the Snap sandbox. + if err == unix.EPERM { + return nil + } + // This happens just after a disconnection. + if err == unix.ENOENT { + return nil + } + return fmt.Errorf("gamepad: Open failed: %w", err) + } + owned := true + defer func() { + if owned { + _ = unix.Close(fd) + } + }() + + evBits := make([]byte, (unix.EV_CNT+7)/8) + keyBits := make([]byte, (_KEY_CNT+7)/8) + absBits := make([]byte, (_ABS_CNT+7)/8) + var id input_id + if err := ioctl(fd, _EVIOCGBIT(0, uint(len(evBits))), unsafe.Pointer(&evBits[0])); err != nil { + return fmt.Errorf("gamepad: ioctl for evBits failed: %w", err) + } + if err := ioctl(fd, _EVIOCGBIT(unix.EV_KEY, uint(len(keyBits))), unsafe.Pointer(&keyBits[0])); err != nil { + return fmt.Errorf("gamepad: ioctl for keyBits failed: %w", err) + } + if err := ioctl(fd, _EVIOCGBIT(unix.EV_ABS, uint(len(absBits))), unsafe.Pointer(&absBits[0])); err != nil { + return fmt.Errorf("gamepad: ioctl for absBits failed: %w", err) + } + if err := ioctl(fd, _EVIOCGID(), unsafe.Pointer(&id)); err != nil { + return fmt.Errorf("gamepad: ioctl for an ID failed: %w", err) + } + + if !isBitSet(evBits, unix.EV_ABS) { + owned = false + return unix.Close(fd) + } + + cname := make([]byte, 256) + name := "Unknown" + // TODO: Is it OK to ignore the error here? + if err := ioctl(fd, uint(_EVIOCGNAME(uint(len(cname)))), unsafe.Pointer(&cname[0])); err == nil { + name = unix.ByteSliceToString(cname) + } + + var sdlID string + if id.vendor != 0 && id.product != 0 && id.version != 0 { + sdlID = fmt.Sprintf("%02x%02x0000%02x%02x0000%02x%02x0000%02x%02x0000", + byte(id.bustype), byte(id.bustype>>8), + byte(id.vendor), byte(id.vendor>>8), + byte(id.product), byte(id.product>>8), + byte(id.version), byte(id.version>>8)) + } else { + bs := []byte(name) + if len(bs) < 12 { + bs = append(bs, make([]byte, 12-len(bs))...) + } + sdlID = fmt.Sprintf("%02x%02x0000%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + byte(id.bustype), byte(id.bustype>>8), + bs[0], bs[1], bs[2], bs[3], bs[4], bs[5], bs[6], bs[7], bs[8], bs[9], bs[10], bs[11]) + } + + supportsRumble := false + if writable && isBitSet(evBits, unix.EV_FF) { + ffBits := make([]byte, (_FF_CNT+7)/8) + if err := ioctl(fd, _EVIOCGBIT(unix.EV_FF, uint(len(ffBits))), unsafe.Pointer(&ffBits[0])); err == nil { + supportsRumble = isBitSet(ffBits, _FF_RUMBLE) + } + } + + n := &nativeGamepadImpl{ + path: path, + fdPlus1: fd + 1, + supportsRumble: supportsRumble, + effectID: -1, + } + + var axisCount int + var buttonCount int + var hatCount int + for i := range n.keyMap { + n.keyMap[i] = -1 + } + for i := range n.absMap { + n.absMap[i] = -1 + } + for code := _BTN_MISC; code < _KEY_CNT; code++ { + if !isBitSet(keyBits, code) { + continue + } + n.keyMap[code-_BTN_MISC] = buttonCount + buttonCount++ + } + for code := 0; code < _ABS_CNT; code++ { + if !isBitSet(absBits, code) { + continue + } + if code >= _ABS_HAT0X && code <= _ABS_HAT3Y { + // Write the hat index both for the X and the Y hat axis. + // That way, the hat can be referenced using either axis, which is used by the code building hatMappingInput. + n.absMap[code] = hatCount + code++ + n.absMap[code] = hatCount + hatCount++ + continue + } + if err := ioctl(n.fdPlus1-1, uint(_EVIOCGABS(uint(code))), unsafe.Pointer(&n.absInfo[code])); err != nil { + return fmt.Errorf("gamepad: ioctl for an abs at openGamepad failed: %w", err) + } + n.absMap[code] = axisCount + axisCount++ + } + + n.axisCount_ = axisCount + n.buttonCount_ = buttonCount + n.hatCount_ = hatCount + + n.computeStandardLayout(id.vendor) + + if err := n.pollAbsState(); err != nil { + return err + } + + owned = false + gp := gamepads.add(name, sdlID) + gp.native = n + runtime.AddCleanup(gp, func(n *nativeGamepadImpl) { + n.close() + }, n) + + return nil +} + +func (g *nativeGamepadsImpl) update(gamepads *gamepads) error { + if g.inotifyPlus1 == 0 { + return nil + } + + buf := make([]byte, 16384) + n, err := unix.Read(g.inotifyPlus1-1, buf[:]) + if err != nil { + if err == unix.EAGAIN { + return nil + } + return fmt.Errorf("gamepad: Read failed: %w", err) + } + buf = buf[:n] + + for len(buf) > 0 { + e := unix.InotifyEvent{ + Wd: int32(buf[0]) | int32(buf[1])<<8 | int32(buf[2])<<16 | int32(buf[3])<<24, + Mask: uint32(buf[4]) | uint32(buf[5])<<8 | uint32(buf[6])<<16 | uint32(buf[7])<<24, + Cookie: uint32(buf[8]) | uint32(buf[9])<<8 | uint32(buf[10])<<16 | uint32(buf[11])<<24, + Len: uint32(buf[12]) | uint32(buf[13])<<8 | uint32(buf[14])<<16 | uint32(buf[15])<<24, + } + if e.Len == 0 { + buf = buf[16:] + continue + } + name := unix.ByteSliceToString(buf[16 : 16+e.Len-1]) // len includes the null terminate. + buf = buf[16+e.Len:] + if !reEvent.MatchString(name) { + continue + } + + path := filepath.Join(dirName, name) + if e.Mask&(unix.IN_CREATE|unix.IN_ATTRIB) != 0 { + if err := g.openGamepad(gamepads, path); err != nil { + return err + } + continue + } + if e.Mask&unix.IN_DELETE != 0 { + if gp := gamepads.find(func(gamepad *Gamepad) bool { + return gamepad.native.(*nativeGamepadImpl).path == path + }); gp != nil { + // Lock the gamepad so the close cannot race with a + // concurrent Vibrate using the file descriptor. + withNative(gp, func(n *nativeGamepadImpl) { + n.close() + }) + gamepads.remove(func(gamepad *Gamepad) bool { + return gamepad == gp + }) + } + continue + } + } + + return nil +} + +type nativeGamepadImpl struct { + fdPlus1 int + path string + keyMap [_KEY_CNT - _BTN_MISC]int + absMap [_ABS_CNT]int + absInfo [_ABS_CNT]input_absinfo + dropped bool + + supportsRumble bool + effectID int16 + + axes [_ABS_CNT]float64 + buttons [_KEY_CNT - _BTN_MISC]bool + hats [4]int + + axisCount_ int + buttonCount_ int + hatCount_ int + + stdAxisMap map[gamepaddb.StandardAxis]mappingInput + stdButtonMap map[gamepaddb.StandardButton]mappingInput +} + +func (g *nativeGamepadImpl) close() { + if g.fdPlus1 == 0 { + return + } + _ = unix.Close(g.fdPlus1 - 1) + g.fdPlus1 = 0 +} + +func (g *nativeGamepadImpl) update(gamepad *gamepads) error { + if g.fdPlus1 == 0 { + return nil + } + + for { + buf := make([]byte, unsafe.Sizeof(input_event{})) + // TODO: Should the returned byte count be cared? + if _, err := unix.Read(g.fdPlus1-1, buf); err != nil { + if err == unix.EAGAIN { + break + } + // Disconnected + if err == unix.ENODEV { + g.close() + return nil + } + return fmt.Errorf("gamepad: Read failed: %w", err) + } + + const ( + offsetTyp = unsafe.Offsetof(input_event{}.typ) + offsetCode = unsafe.Offsetof(input_event{}.code) + offsetValue = unsafe.Offsetof(input_event{}.value) + ) + // time is not used. + e := input_event{ + typ: uint16(buf[offsetTyp]) | uint16(buf[offsetTyp+1])<<8, + code: uint16(buf[offsetCode]) | uint16(buf[offsetCode+1])<<8, + value: int32(buf[offsetValue]) | int32(buf[offsetValue+1])<<8 | int32(buf[offsetValue+2])<<16 | int32(buf[offsetValue+3])<<24, + } + + if e.typ == unix.EV_SYN && e.code == _SYN_DROPPED { + g.dropped = true + } + if g.dropped { + // Ignore events through the next SYN_REPORT, then restore the device state. + if e.typ == unix.EV_SYN && e.code == _SYN_REPORT { + if err := g.pollAbsState(); err != nil { + return fmt.Errorf("gamepad: poll absolute state: %w", err) + } + if err := g.pollKeyState(); err != nil { + return fmt.Errorf("gamepad: poll key state: %w", err) + } + g.dropped = false + } + continue + } + + switch e.typ { + case unix.EV_KEY: + if int(e.code-_BTN_MISC) < len(g.keyMap) { + idx := g.keyMap[e.code-_BTN_MISC] + if idx < 0 { + continue + } + g.buttons[idx] = e.value != 0 + } + case unix.EV_ABS: + g.handleAbsEvent(int(e.code), e.value) + } + } + return nil +} + +func (g *nativeGamepadImpl) pollKeyState() error { + var keyBits [(_KEY_CNT + 7) / 8]byte + if err := ioctl(g.fdPlus1-1, _EVIOCGKEY(uint(len(keyBits))), unsafe.Pointer(&keyBits[0])); err != nil { + return fmt.Errorf("gamepad: ioctl for keys at pollKeyState failed: %w", err) + } + for code, index := range g.keyMap { + if index >= 0 { + g.buttons[index] = isBitSet(keyBits[:], code+_BTN_MISC) + } + } + return nil +} + +func (g *nativeGamepadImpl) pollAbsState() error { + for code := range _ABS_CNT { + if g.absMap[code] < 0 { + continue + } + if err := ioctl(g.fdPlus1-1, uint(_EVIOCGABS(uint(code))), unsafe.Pointer(&g.absInfo[code])); err != nil { + return fmt.Errorf("gamepad: ioctl for an abs at pollAbsState failed: %w", err) + } + g.handleAbsEvent(code, g.absInfo[code].value) + } + return nil +} + +func (g *nativeGamepadImpl) handleAbsEvent(code int, value int32) { + index := g.absMap[code] + if index < 0 { + return + } + + if code >= _ABS_HAT0X && code <= _ABS_HAT3Y { + axis := (code - _ABS_HAT0X) % 2 + + switch axis { + case 0: + switch { + case value < 0: + g.hats[index] |= hatLeft + g.hats[index] &^= hatRight + case value > 0: + g.hats[index] &^= hatLeft + g.hats[index] |= hatRight + default: + g.hats[index] &^= hatLeft | hatRight + } + case 1: + switch { + case value < 0: + g.hats[index] |= hatUp + g.hats[index] &^= hatDown + case value > 0: + g.hats[index] &^= hatUp + g.hats[index] |= hatDown + default: + g.hats[index] &^= hatUp | hatDown + } + } + return + } + + info := g.absInfo[code] + v := float64(value) + if r := float64(info.maximum) - float64(info.minimum); r != 0 { + v = (v - float64(info.minimum)) / r + v = v*2 - 1 + } + g.axes[index] = v +} + +func (g *nativeGamepadImpl) computeStandardLayout(vendor uint16) { + g.stdAxisMap = map[gamepaddb.StandardAxis]mappingInput{} + g.stdButtonMap = map[gamepaddb.StandardButton]mappingInput{} + + // NOTE: assignments to the same value are in exact reverse order as SDL2, + // so we can just overwrite rather than checking. + + // BTN_GAMEPAD implies that the kernel module implements standard mapping. + if b := g.keyMap[_BTN_GAMEPAD-_BTN_MISC]; b < 0 { + return + } + + // A and B buttons go by name. + if b := g.keyMap[_BTN_A-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonRightBottom] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_B-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonRightRight] = buttonMappingInput{g: g, button: b} + } + if vendor == 0x054c /* USB_VENDOR_SONY */ { + // Sony uses WEST/NORTH buttons. + if b := g.keyMap[_BTN_WEST-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonRightLeft] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_NORTH-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonRightTop] = buttonMappingInput{g: g, button: b} + } + } else { + // Xbox uses X/Y buttons. + // Note that this is the opposite assignment following the WEST/NORTH mappings, + // and contradicts Linux kernel documentation which states + // that buttons are always assigned by physical location. + // However, it matches actual Xbox gamepads, and SDL2 has the same logic. + if b := g.keyMap[_BTN_X-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonRightLeft] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_Y-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonRightTop] = buttonMappingInput{g: g, button: b} + } + } + + // Center and thumb buttons. + if b := g.keyMap[_BTN_SELECT-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonCenterLeft] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_START-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonCenterRight] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_THUMBL-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonLeftStick] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_THUMBR-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonRightStick] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_MODE-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonCenterCenter] = buttonMappingInput{g: g, button: b} + } + + // Shoulder buttons can be analog or digital. Prefer digital ones. + if h := g.absMap[_ABS_HAT1Y]; h >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonFrontTopLeft] = hatMappingInput{g: g, hat: h, direction: hatDown} + } + if h := g.absMap[_ABS_HAT1X]; h >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonFrontTopRight] = hatMappingInput{g: g, hat: h, direction: hatRight} + } + if b := g.keyMap[_BTN_TL-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonFrontTopLeft] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_TR-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonFrontTopRight] = buttonMappingInput{g: g, button: b} + } + + // Triggers can be analog or digital. Prefer analog ones. + if b := g.keyMap[_BTN_TL2-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonFrontBottomLeft] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_TR2-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonFrontBottomRight] = buttonMappingInput{g: g, button: b} + } + if a := g.absMap[_ABS_Z]; a >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonFrontBottomLeft] = axisMappingInput{g: g, axis: a} + } + if a := g.absMap[_ABS_RZ]; a >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonFrontBottomRight] = axisMappingInput{g: g, axis: a} + } + if h := g.absMap[_ABS_HAT2Y]; h >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonFrontBottomLeft] = hatMappingInput{g: g, hat: h, direction: hatDown} + } + if h := g.absMap[_ABS_HAT2X]; h >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonFrontBottomRight] = hatMappingInput{g: g, hat: h, direction: hatRight} + } + + // D-pad can be analog or digital. Prefer digital one. + if h := g.absMap[_ABS_HAT0X]; h >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonLeftLeft] = hatMappingInput{g: g, hat: h, direction: hatLeft} + g.stdButtonMap[gamepaddb.StandardButtonLeftRight] = hatMappingInput{g: g, hat: h, direction: hatRight} + } + if h := g.absMap[_ABS_HAT0Y]; h >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonLeftTop] = hatMappingInput{g: g, hat: h, direction: hatUp} + g.stdButtonMap[gamepaddb.StandardButtonLeftBottom] = hatMappingInput{g: g, hat: h, direction: hatDown} + } + if b := g.keyMap[_BTN_DPAD_UP-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonLeftTop] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_DPAD_DOWN-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonLeftBottom] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_DPAD_LEFT-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonLeftLeft] = buttonMappingInput{g: g, button: b} + } + if b := g.keyMap[_BTN_DPAD_RIGHT-_BTN_MISC]; b >= 0 { + g.stdButtonMap[gamepaddb.StandardButtonLeftRight] = buttonMappingInput{g: g, button: b} + } + + // Left stick. + if a := g.absMap[_ABS_X]; a >= 0 { + g.stdAxisMap[gamepaddb.StandardAxisLeftStickHorizontal] = axisMappingInput{g: g, axis: a} + } + if a := g.absMap[_ABS_Y]; a >= 0 { + g.stdAxisMap[gamepaddb.StandardAxisLeftStickVertical] = axisMappingInput{g: g, axis: a} + } + + // Right stick. + if a := g.absMap[_ABS_RX]; a >= 0 { + g.stdAxisMap[gamepaddb.StandardAxisRightStickHorizontal] = axisMappingInput{g: g, axis: a} + } + if a := g.absMap[_ABS_RY]; a >= 0 { + g.stdAxisMap[gamepaddb.StandardAxisRightStickVertical] = axisMappingInput{g: g, axis: a} + } +} + +func (g *nativeGamepadImpl) hasOwnStandardLayoutMapping() bool { + return len(g.stdAxisMap) != 0 || len(g.stdButtonMap) != 0 +} + +func (g *nativeGamepadImpl) standardAxisInOwnMapping(axis gamepaddb.StandardAxis) mappingInput { + return g.stdAxisMap[axis] +} + +func (g *nativeGamepadImpl) standardButtonInOwnMapping(button gamepaddb.StandardButton) mappingInput { + return g.stdButtonMap[button] +} + +func (g *nativeGamepadImpl) axisCount() int { + return g.axisCount_ +} + +func (g *nativeGamepadImpl) buttonCount() int { + return g.buttonCount_ +} + +func (g *nativeGamepadImpl) hatCount() int { + return g.hatCount_ +} + +func (g *nativeGamepadImpl) isAxisReady(axis int) bool { + return axis >= 0 && axis < g.axisCount() +} + +func (g *nativeGamepadImpl) axisValue(axis int) float64 { + if axis < 0 || axis >= g.axisCount_ { + return 0 + } + return g.axes[axis] +} + +func (g *nativeGamepadImpl) isButtonPressed(button int) bool { + if button < 0 || button >= g.buttonCount_ { + return false + } + return g.buttons[button] +} + +func (g *nativeGamepadImpl) buttonValue(button int) float64 { + if g.isButtonPressed(button) { + return 1 + } + return 0 +} + +func (g *nativeGamepadImpl) hatState(hat int) int { + if hat < 0 || hat >= g.hatCount_ { + return hatCentered + } + return g.hats[hat] +} + +func (g *nativeGamepadImpl) vibrate(duration time.Duration, strongMagnitude float64, weakMagnitude float64) { + if !g.supportsRumble || g.fdPlus1 == 0 { + return + } + + strongMagnitude = mathutil.Clamp01(strongMagnitude) + weakMagnitude = mathutil.Clamp01(weakMagnitude) + + if strongMagnitude <= 0 && weakMagnitude <= 0 { + g.writeFFEvent(0) + return + } + + if duration <= 0 { + return + } + + // The kernel stops the effect once the replay length has passed, so no + // duration tracking is needed here. A replay length of 0 would play the + // effect with no time limit, so keep it at least 1. + ms := duration.Milliseconds() + if ms < 1 { + ms = 1 + } + if ms > 0xffff { + ms = 0xffff + } + + // An ID of -1 lets the kernel assign an ID to a new effect. Uploading + // with the ID of an already uploaded effect updates the effect in place. + effect := ff_effect{ + typ: _FF_RUMBLE, + id: g.effectID, + } + effect.replay.length = uint16(ms) + effect.u.rumble.strong_magnitude = motorMagnitude(strongMagnitude) + effect.u.rumble.weak_magnitude = motorMagnitude(weakMagnitude) + + if err := ioctl(g.fdPlus1-1, _EVIOCSFF(), unsafe.Pointer(&effect)); err != nil { + return + } + g.effectID = effect.id + + g.writeFFEvent(1) +} + +// writeFFEvent starts (value 1) or stops (value 0) playing the uploaded force +// feedback effect. It does nothing when no effect has been uploaded. +func (g *nativeGamepadImpl) writeFFEvent(value int32) { + if g.effectID < 0 { + return + } + e := input_event{ + typ: unix.EV_FF, + code: uint16(g.effectID), + value: value, + } + _, _ = unix.Write(g.fdPlus1-1, unsafe.Slice((*byte)(unsafe.Pointer(&e)), int(unsafe.Sizeof(e)))) +}