Skip to content

Commit

Permalink
Common Win32 gamepad API + test (#1355)
Browse files Browse the repository at this point in the history
* Common Win32 gamepad API + test

* Fix KStructure memory handling in Node.JS and Android

* Minor

* Fix KStructure.fixedBytes alignment

* Improve toString in some scenarios
  • Loading branch information
soywiz authored Feb 21, 2023
1 parent 3066a5a commit f2dc2c9
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 324 deletions.
26 changes: 14 additions & 12 deletions kmem/src/androidMain/kotlin/com/soywiz/kmem/dyn/KStructureImpl.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.soywiz.kmem.dyn

import com.soywiz.kmem.*

actual class KArena actual constructor() {
actual fun allocBytes(size: Int): KPointer = KPointer(ByteArray(size))
actual fun clear(): Unit = Unit
Expand All @@ -19,15 +21,15 @@ actual abstract class KStructureBase {
actual fun KPointer(address: Long): KPointer = TODO()
actual val KPointer.address: Long get() = TODO()

actual fun KPointer.getByte(offset: Int): Byte = TODO()
actual fun KPointer.setByte(offset: Int, value: Byte): Unit = TODO()
actual fun KPointer.getShort(offset: Int): Short = TODO()
actual fun KPointer.setShort(offset: Int, value: Short): Unit = TODO()
actual fun KPointer.getInt(offset: Int): Int = TODO()
actual fun KPointer.setInt(offset: Int, value: Int): Unit = TODO()
actual fun KPointer.getFloat(offset: Int): Float = TODO()
actual fun KPointer.setFloat(offset: Int, value: Float): Unit = TODO()
actual fun KPointer.getDouble(offset: Int): Double = TODO()
actual fun KPointer.setDouble(offset: Int, value: Double): Unit = TODO()
actual fun KPointer.getLong(offset: Int): Long = TODO()
actual fun KPointer.setLong(offset: Int, value: Long): Unit = TODO()
actual fun KPointer.getByte(offset: Int): Byte = this.ptr.readS8(offset).toByte()
actual fun KPointer.setByte(offset: Int, value: Byte): Unit = this.ptr.write8(offset, value.toInt())
actual fun KPointer.getShort(offset: Int): Short = this.ptr.readS16LE(offset).toShort()
actual fun KPointer.setShort(offset: Int, value: Short): Unit = this.ptr.write16LE(offset, value.toInt())
actual fun KPointer.getInt(offset: Int): Int = this.ptr.readS32LE(offset)
actual fun KPointer.setInt(offset: Int, value: Int): Unit = this.ptr.write32LE(offset, value)
actual fun KPointer.getFloat(offset: Int): Float = this.ptr.readF32LE(offset)
actual fun KPointer.setFloat(offset: Int, value: Float): Unit = this.ptr.writeF32LE(offset, value)
actual fun KPointer.getDouble(offset: Int): Double = this.ptr.readF64LE(offset)
actual fun KPointer.setDouble(offset: Int, value: Double): Unit = this.ptr.writeF64LE(offset, value)
actual fun KPointer.getLong(offset: Int): Long = this.ptr.readS64LE(offset)
actual fun KPointer.setLong(offset: Int, value: Long): Unit = this.ptr.write64LE(offset, value)
16 changes: 15 additions & 1 deletion kmem/src/commonMain/kotlin/com/soywiz/kmem/dyn/KStructure.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ open class KStructure(pointer: KPointer?) : KStructureBase() {
fun nativeLong(): KMemDelegateNativeLongProperty = layout.nativeLong()
fun kpointer(): KMemDelegateKPointerProperty = layout.kpointer()
fun <T> pointer(): KMemDelegatePointerProperty<T> = layout.pointer<T>()
fun fixedBytes(size: Int): KMemDelegateFixedBytesProperty = layout.fixedBytes(size)

//private var _pointer: KPointer? = pointer; private set
//val pointer: KPointer by lazy {
Expand All @@ -118,7 +119,7 @@ open class KMemLayoutBuilder {
offset
}

private fun alloc(size: Int) = align(size).offset.also { this.offset += size }
private fun alloc(size: Int, align: Int = size) = align(align).offset.also { this.offset += size }

//fun int() = alloc(Int.SIZE_BYTES)
//fun nativeLong() = alloc(NativeLong.SIZE)
Expand All @@ -134,6 +135,7 @@ open class KMemLayoutBuilder {
fun nativeLong() = KMemDelegateNativeLongProperty(alloc(LONG_SIZE))
fun kpointer() = KMemDelegateKPointerProperty(alloc(POINTER_SIZE))
fun <T> pointer() = KMemDelegatePointerProperty<T>(alloc(POINTER_SIZE))
fun fixedBytes(size: Int, align: Int = 1): KMemDelegateFixedBytesProperty = KMemDelegateFixedBytesProperty(alloc(size * Byte.SIZE_BYTES, align), size)
}


Expand All @@ -142,6 +144,18 @@ inline class KMemDelegateByteProperty(val offset: Int) {
operator fun setValue(obj: KStructure, property: KProperty<*>, i: Byte) { obj.pointerSure.setByte(offset, i) }
}

class KMemDelegateFixedBytesProperty(val offset: Int, val size: Int) {
val bytes = ByteArray(size)
operator fun getValue(obj: KStructure, property: KProperty<*>): ByteArray {
for (n in 0 until size) bytes[n] = obj.pointerSure.getByte(offset + n)
return bytes
}
operator fun setValue(obj: KStructure, property: KProperty<*>, i: ByteArray) {
for (n in 0 until size) obj.pointerSure.setByte(offset + n, i[n])
}
}


inline class KMemDelegateBoolProperty(val offset: Int) {
operator fun getValue(obj: KStructure, property: KProperty<*>): Boolean = obj.pointerSure.getInt(offset) != 0
operator fun setValue(obj: KStructure, property: KProperty<*>, i: Boolean) { obj.pointerSure.setInt(offset, if (i) 1 else 0) }
Expand Down
26 changes: 14 additions & 12 deletions kmem/src/jsMain/kotlin/com/soywiz/kmem/dyn/KStructureImpl.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.soywiz.kmem.dyn

import com.soywiz.kmem.*

actual class KArena actual constructor() {
actual fun allocBytes(size: Int): KPointer = KPointer(ByteArray(size))
actual fun clear(): Unit = Unit
Expand All @@ -19,15 +21,15 @@ actual abstract class KStructureBase {
actual fun KPointer(address: Long): KPointer = TODO()
actual val KPointer.address: Long get() = TODO()

actual fun KPointer.getByte(offset: Int): Byte = TODO()
actual fun KPointer.setByte(offset: Int, value: Byte): Unit = TODO()
actual fun KPointer.getShort(offset: Int): Short = TODO()
actual fun KPointer.setShort(offset: Int, value: Short): Unit = TODO()
actual fun KPointer.getInt(offset: Int): Int = TODO()
actual fun KPointer.setInt(offset: Int, value: Int): Unit = TODO()
actual fun KPointer.getFloat(offset: Int): Float = TODO()
actual fun KPointer.setFloat(offset: Int, value: Float): Unit = TODO()
actual fun KPointer.getDouble(offset: Int): Double = TODO()
actual fun KPointer.setDouble(offset: Int, value: Double): Unit = TODO()
actual fun KPointer.getLong(offset: Int): Long = TODO()
actual fun KPointer.setLong(offset: Int, value: Long): Unit = TODO()
actual fun KPointer.getByte(offset: Int): Byte = this.ptr.readS8(offset).toByte()
actual fun KPointer.setByte(offset: Int, value: Byte): Unit = this.ptr.write8(offset, value.toInt())
actual fun KPointer.getShort(offset: Int): Short = this.ptr.readS16LE(offset).toShort()
actual fun KPointer.setShort(offset: Int, value: Short): Unit = this.ptr.write16LE(offset, value.toInt())
actual fun KPointer.getInt(offset: Int): Int = this.ptr.readS32LE(offset)
actual fun KPointer.setInt(offset: Int, value: Int): Unit = this.ptr.write32LE(offset, value)
actual fun KPointer.getFloat(offset: Int): Float = this.ptr.readF32LE(offset)
actual fun KPointer.setFloat(offset: Int, value: Float): Unit = this.ptr.writeF32LE(offset, value)
actual fun KPointer.getDouble(offset: Int): Double = this.ptr.readF64LE(offset)
actual fun KPointer.setDouble(offset: Int, value: Double): Unit = this.ptr.writeF64LE(offset, value)
actual fun KPointer.getLong(offset: Int): Long = this.ptr.readS64LE(offset)
actual fun KPointer.setLong(offset: Int, value: Long): Unit = this.ptr.write64LE(offset, value)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import com.sun.jna.Pointer

actual class KArena actual constructor() {
private val pointers = arrayListOf<Memory>()
actual fun allocBytes(size: Int): KPointer = KPointer(Memory(size.toLong()).also { pointers += it })
actual fun allocBytes(size: Int): KPointer = KPointer(Memory(size.toLong()).also {
it.clear()
pointers += it
})
actual fun clear() {
for (n in 0 until pointers.size) pointers[n].clear()
pointers.clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import platform.posix.*

actual class KArena actual constructor() {
private val arena = Arena()
actual fun allocBytes(size: Int): KPointer = arena.allocArray<ByteVar>(size)
actual fun allocBytes(size: Int): KPointer {
return arena.allocArray<ByteVar>(size).also {
memset(it, 0, size.convert())
}
}
actual fun clear(): Unit = arena.clear()
}

Expand Down
25 changes: 14 additions & 11 deletions korgw/src/commonMain/kotlin/com/soywiz/korev/InputGamepad.kt
Original file line number Diff line number Diff line change
Expand Up @@ -223,20 +223,23 @@ class GamepadInfo(
GameStick.LEFT -> get(GameButton.LY)
GameStick.RIGHT -> get(GameButton.RY)
}
override fun toString(): String = buildString {
fun toStringEx(includeButtons: Boolean = true): String = buildString {
append("Gamepad[$index][$fullName]")
append("[")
var count = 0
for (button in GameButton.values()) {
val value = this@GamepadInfo[button]
if (value != 0.0) {
if (count > 0) append(",")
append("$button=${value.niceStr}")
count++
if (includeButtons) {
append("[")
var count = 0
for (button in GameButton.values()) {
val value = this@GamepadInfo[button]
if (value != 0.0) {
if (count > 0) append(",")
append("$button=${value.niceStr}")
count++
}
}
append("]")
}
append("]")
}
override fun toString(): String = toStringEx(includeButtons = false)
}

data class GamePadConnectionEvent(
Expand Down Expand Up @@ -272,5 +275,5 @@ data class GamePadUpdateEvent @JvmOverloads constructor(
}
}

override fun toString(): String = "GamePadUpdateEvent(${gamepads.filter { it.connected }})"
override fun toString(): String = "GamePadUpdateEvent(${gamepads.filter { it.connected }.map { it.toStringEx() }})"
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.soywiz.korgw.win32

import com.soywiz.kmem.*
import com.soywiz.kmem.dyn.*
import com.soywiz.korev.*
import com.soywiz.korio.lang.*

internal class Win32XInputEventAdapterCommon(val xinput: XInput?, val joy: Joy32?) {
private val controllers = Array(GamepadInfo.MAX_CONTROLLERS) { GamepadInfo(it) }

fun updateGamepads(emitter: GamepadInfoEmitter) {
if (xinput == null) return

emitter.dispatchGamepadUpdateStart()
kmemScoped {
val state = XInputState(allocBytes(XInputState().size))
for (n in 0 until GamepadInfo.MAX_CONTROLLERS) {
val connected = xinput.XInputGetState(n, state) == XInput.SUCCESS
val gamepad = controllers[n]
if (connected) {
val buttons = state.wButtons.toInt() and 0xFFFF

gamepad.setDigital(GameButton.UP, buttons, XInput.GAMEPAD_DPAD_UP)
gamepad.setDigital(GameButton.DOWN, buttons, XInput.GAMEPAD_DPAD_DOWN)
gamepad.setDigital(GameButton.LEFT, buttons, XInput.GAMEPAD_DPAD_LEFT)
gamepad.setDigital(GameButton.RIGHT, buttons, XInput.GAMEPAD_DPAD_RIGHT)
gamepad.setDigital(GameButton.BACK, buttons, XInput.GAMEPAD_BACK)
gamepad.setDigital(GameButton.START, buttons, XInput.GAMEPAD_START)
gamepad.setDigital(GameButton.LEFT_THUMB, buttons, XInput.GAMEPAD_LEFT_THUMB)
gamepad.setDigital(GameButton.RIGHT_THUMB, buttons, XInput.GAMEPAD_RIGHT_THUMB)
gamepad.setDigital(GameButton.LEFT_SHOULDER, buttons, XInput.GAMEPAD_LEFT_SHOULDER)
gamepad.setDigital(GameButton.RIGHT_SHOULDER, buttons, XInput.GAMEPAD_RIGHT_SHOULDER)
gamepad.setDigital(GameButton.XBOX_A, buttons, XInput.GAMEPAD_A)
gamepad.setDigital(GameButton.XBOX_B, buttons, XInput.GAMEPAD_B)
gamepad.setDigital(GameButton.XBOX_X, buttons, XInput.GAMEPAD_X)
gamepad.setDigital(GameButton.XBOX_Y, buttons, XInput.GAMEPAD_Y)
gamepad.rawButtons[GameButton.LEFT_TRIGGER.index] = convertUByteRangeToDouble(state.bLeftTrigger)
gamepad.rawButtons[GameButton.RIGHT_TRIGGER.index] = convertUByteRangeToDouble(state.bRightTrigger)
gamepad.rawButtons[GameButton.LX.index] = GamepadInfo.withoutDeadRange(convertShortRangeToDouble(state.sThumbLX))
gamepad.rawButtons[GameButton.LY.index] = GamepadInfo.withoutDeadRange(convertShortRangeToDouble(state.sThumbLY))
gamepad.rawButtons[GameButton.RX.index] = GamepadInfo.withoutDeadRange(convertShortRangeToDouble(state.sThumbRX))
gamepad.rawButtons[GameButton.RY.index] = GamepadInfo.withoutDeadRange(convertShortRangeToDouble(state.sThumbRY))

if (gamepad.name == null) {
kmemScoped {
val joyCapsW = JoyCapsW(allocBytes(JoyCapsW.SIZE))
if (joy?.joyGetDevCapsW(n, joyCapsW, JoyCapsW.SIZE) == 0) {
gamepad.name = joyCapsW.name
}
}
}
emitter.dispatchGamepadUpdateAdd(gamepad)
}
}
emitter.dispatchGamepadUpdateEnd()
}
}

private fun GamepadInfo.setDigital(button: GameButton, buttons: Int, bit: Int) {
this.rawButtons[button.index] = if (buttons.hasBitSet(bit)) 1f else 0f
}

private fun convertShortRangeToDouble(value: Short): Float = value.toFloat().convertRangeClamped(Short.MIN_VALUE.toFloat(), Short.MAX_VALUE.toFloat(), -1f, +1f)
private fun convertUByteRangeToDouble(value: Byte): Float = (value.toInt() and 0xFF).toFloat().convertRangeClamped(0f, 255f, 0f, +1f)
}

// Used this as reference:
// https://github.com/fantarama/JXInput/blob/86356e7a4037bbb1f3478c7333555e00b3601bde/XInputJNA/src/main/java/com/microsoft/xinput/XInput.java
internal class XInputState(pointer: KPointer? = null) : KStructure(pointer) {
var dwPacketNumber by int() // offset: 0
var wButtons by short() // offset: 4
var bLeftTrigger by byte() // offset: 6
var bRightTrigger by byte() // offset: 7
var sThumbLX by short() // offset: 8
var sThumbLY by short() // offset: 10
var sThumbRX by short() // offset: 12
var sThumbRY by short() // offset: 14
override fun toString(): String =
"XInputState(dwPacketNumber=$dwPacketNumber, wButtons=$wButtons, bLeftTrigger=$bLeftTrigger, bRightTrigger=$bRightTrigger, sThumbLX=$sThumbLX, sThumbLY=$sThumbLY, sThumbRX=$sThumbRX, sThumbRY=$sThumbRY)"
}

internal class JoyCapsW(pointer: KPointer? = null) : KStructure(pointer) {
companion object {
val SIZE = 728
}

var wMid: Short by short()
var wPid: Short by short()
var szPname by fixedBytes(32 * 2)
var name: String
get() = szPname.toString(Charsets.UTF16_LE).trimEnd('\u0000').also {
//println("JoyCapsW.name='$it'")
}
set(value) {
szPname = run {
ByteArray(szPname.size).also {
val new = value.toByteArray(Charsets.UTF16_LE)
arraycopy(new, 0, it, 0, new.size)
}
}

}
override fun toString(): String =
"JoyCapsW(name=$name)"
}

internal fun interface XInput {
fun XInputGetState(dwUserIndex: Int, pState: XInputState): Int // pState: XInputState

companion object {
const val SUCCESS = 0
const val ERROR_DEVICE_NOT_CONNECTED = 0x0000048F

const val GAMEPAD_DPAD_UP = 0
const val GAMEPAD_DPAD_DOWN = 1
const val GAMEPAD_DPAD_LEFT = 2
const val GAMEPAD_DPAD_RIGHT = 3
const val GAMEPAD_START = 4
const val GAMEPAD_BACK = 5
const val GAMEPAD_LEFT_THUMB = 6
const val GAMEPAD_RIGHT_THUMB = 7
const val GAMEPAD_LEFT_SHOULDER = 8
const val GAMEPAD_RIGHT_SHOULDER = 9
const val GAMEPAD_UNKNOWN_10 = 10
const val GAMEPAD_UNKNOWN_11 = 11
const val GAMEPAD_A = 12
const val GAMEPAD_B = 13
const val GAMEPAD_X = 14
const val GAMEPAD_Y = 15

const val SIZE = 16
}
}

internal fun interface Joy32 {
fun joyGetDevCapsW(uJoyID: Int, pjc: JoyCapsW, cbjc: Int): Int // pjc: JoyCapsW
}
Loading

0 comments on commit f2dc2c9

Please sign in to comment.