Skip to content

Commit 4f42f6b

Browse files
satoshinmkripken
authored andcommitted
Implement glfw joystick API (#5175)
* Implement glfw joystick API Joystick connected/disconnected callbacks are called in refreshJoysticks(). The gamepadconnected/disconnected events are only used as a hint to refresh the joysticks, whose getGamepads() state is compared to the previous state to determine the new state.
1 parent 6f3ba39 commit 4f42f6b

File tree

5 files changed

+407
-6
lines changed

5 files changed

+407
-6
lines changed

src/library_glfw.js

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* What it does not but should probably do:
1414
* - Transmit events when glfwPollEvents, glfwWaitEvents or glfwSwapBuffers is
1515
* called. Events callbacks are called as soon as event are received.
16-
* - Joystick support.
1716
* - Input modes.
1817
* - Gamma ramps.
1918
* - Video modes.
@@ -81,6 +80,7 @@ var LibraryGLFW = {
8180
return GLFW.windows[id - 1];
8281
},
8382

83+
joystickFunc: null, // GLFWjoystickfun
8484
errorFunc: null, // GLFWerrorfun
8585
monitorFunc: null, // GLFWmonitorfun
8686
active: null, // active window
@@ -382,6 +382,14 @@ var LibraryGLFW = {
382382
#endif
383383
},
384384

385+
onGamepadConnected: function(event) {
386+
GLFW.refreshJoysticks();
387+
},
388+
389+
onGamepadDisconnected: function(event) {
390+
GLFW.refreshJoysticks();
391+
},
392+
385393
onKeydown: function(event) {
386394
GLFW.onKeyChanged(event.keyCode, 1); // GLFW_PRESS or GLFW_REPEAT
387395

@@ -641,6 +649,68 @@ var LibraryGLFW = {
641649
}
642650
},
643651

652+
setJoystickCallback: function(cbfun) {
653+
GLFW.joystickFunc = cbfun;
654+
GLFW.refreshJoysticks();
655+
},
656+
657+
joys: {}, // glfw joystick data
658+
lastGamepadState: null,
659+
lastGamepadStateFrame: null, // The integer value of Browser.mainLoop.currentFrameNumber of when the last gamepad state was produced.
660+
661+
refreshJoysticks: function() {
662+
// Produce a new Gamepad API sample if we are ticking a new game frame, or if not using emscripten_set_main_loop() at all to drive animation.
663+
if (Browser.mainLoop.currentFrameNumber !== GLFW.lastGamepadStateFrame || !Browser.mainLoop.currentFrameNumber) {
664+
GLFW.lastGamepadState = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads : null);
665+
GLFW.lastGamepadStateFrame = Browser.mainLoop.currentFrameNumber;
666+
667+
for (var joy = 0; joy < GLFW.lastGamepadState.length; ++joy) {
668+
var gamepad = GLFW.lastGamepadState[joy];
669+
670+
if (gamepad) {
671+
if (!GLFW.joys[joy]) {
672+
console.log('glfw joystick connected:',joy);
673+
GLFW.joys[joy] = {
674+
id: allocate(intArrayFromString(gamepad.id), 'i8', ALLOC_NORMAL),
675+
buttonsCount: gamepad.buttons.length,
676+
axesCount: gamepad.axes.length,
677+
buttons: allocate(new Array(gamepad.buttons.length), 'i8', ALLOC_NORMAL),
678+
axes: allocate(new Array(gamepad.axes.length*4), 'float', ALLOC_NORMAL)
679+
};
680+
681+
if (GLFW.joystickFunc) {
682+
Module['dynCall_vii'](GLFW.joystickFunc, joy, 0x00040001); // GLFW_CONNECTED
683+
}
684+
}
685+
686+
var data = GLFW.joys[joy];
687+
688+
for (var i = 0; i < gamepad.buttons.length; ++i) {
689+
setValue(data.buttons + i, gamepad.buttons[i].pressed, 'i8');
690+
}
691+
692+
for (var i = 0; i < gamepad.axes.length; ++i) {
693+
setValue(data.axes + i*4, gamepad.axes[i], 'float');
694+
}
695+
} else {
696+
if (GLFW.joys[joy]) {
697+
console.log('glfw joystick disconnected',joy);
698+
699+
if (GLFW.joystickFunc) {
700+
Module['dynCall_vii'](GLFW.joystickFunc, joy, 0x00040002); // GLFW_DISCONNECTED
701+
}
702+
703+
_free(GLFW.joys[joy].id);
704+
_free(GLFW.joys[joy].buttons);
705+
_free(GLFW.joys[joy].axes);
706+
707+
delete GLFW.joys[joy];
708+
}
709+
}
710+
}
711+
}
712+
},
713+
644714
setKeyCallback: function(winid, cbfun) {
645715
var win = GLFW.WindowFromId(winid);
646716
if (!win) return;
@@ -954,6 +1024,8 @@ var LibraryGLFW = {
9541024
GLFW.windows = new Array()
9551025
GLFW.active = null;
9561026

1027+
window.addEventListener("gamepadconnected", GLFW.onGamepadConnected, true);
1028+
window.addEventListener("gamepaddisconnected", GLFW.onGamepadDisconnected, true);
9571029
window.addEventListener("keydown", GLFW.onKeydown, true);
9581030
window.addEventListener("keypress", GLFW.onKeyPress, true);
9591031
window.addEventListener("keyup", GLFW.onKeyup, true);
@@ -973,6 +1045,8 @@ var LibraryGLFW = {
9731045
},
9741046

9751047
glfwTerminate: function() {
1048+
window.removeEventListener("gamepadconnected", GLFW.onGamepadConnected, true);
1049+
window.removeEventListener("gamepaddisconnected", GLFW.onGamepadDisconnected, true);
9761050
window.removeEventListener("keydown", GLFW.onKeydown, true);
9771051
window.removeEventListener("keypress", GLFW.onKeyPress, true);
9781052
window.removeEventListener("keyup", GLFW.onKeyup, true);
@@ -1354,15 +1428,49 @@ var LibraryGLFW = {
13541428

13551429
glfwCreateWindowSurface: function(instance, winid, allocator, surface) { throw "glfwCreateWindowSurface is not implemented."; },
13561430

1357-
glfwSetJoystickCallback: function(cbfun) { throw "glfwSetJoystickCallback is not implemented."; },
1431+
glfwJoystickPresent: function(joy) {
1432+
GLFW.refreshJoysticks();
13581433

1359-
glfwJoystickPresent: function(joy) { throw "glfwJoystickPresent is not implemented."; },
1434+
return GLFW.joys[joy] !== undefined;
1435+
},
13601436

1361-
glfwGetJoystickAxes: function(joy, count) { throw "glfwGetJoystickAxes is not implemented."; },
1437+
glfwGetJoystickAxes: function(joy, count) {
1438+
GLFW.refreshJoysticks();
1439+
1440+
var state = GLFW.joys[joy];
1441+
if (!state || !state.axes) {
1442+
setValue(count, 0, 'i32');
1443+
return;
1444+
}
1445+
1446+
setValue(count, state.axesCount, 'i32');
1447+
return state.axes;
1448+
},
13621449

1363-
glfwGetJoystickButtons: function(joy, count) { throw "glfwGetJoystickButtons is not implemented."; },
1450+
glfwGetJoystickButtons: function(joy, count) {
1451+
GLFW.refreshJoysticks();
13641452

1365-
glfwGetJoystickName: function(joy) { throw "glfwGetJoystickName is not implemented."; },
1453+
var state = GLFW.joys[joy];
1454+
if (!state || !state.buttons) {
1455+
setValue(count, 0, 'i32');
1456+
return;
1457+
}
1458+
1459+
setValue(count, state.buttonsCount, 'i32');
1460+
return state.buttons;
1461+
},
1462+
1463+
glfwGetJoystickName: function(joy) {
1464+
if (GLFW.joys[joy]) {
1465+
return GLFW.joys[joy].id;
1466+
} else {
1467+
return 0;
1468+
}
1469+
},
1470+
1471+
glfwSetJoystickCallback: function(cbfun) {
1472+
GLFW.setJoystickCallback(cbfun);
1473+
},
13661474

13671475
glfwSetClipboardString: function(win, string) {},
13681476

tests/glfw_joystick.c

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#include <stdio.h>
2+
#include <assert.h>
3+
#include <string.h>
4+
#ifdef __EMSCRIPTEN__
5+
#include <emscripten.h>
6+
#endif
7+
#define GLFW_INCLUDE_ES2
8+
#include <GLFW/glfw3.h>
9+
10+
int result = 1;
11+
12+
GLFWwindow* g_window;
13+
14+
void render();
15+
void error_callback(int error, const char* description);
16+
17+
void render() {
18+
glClearColor(0.7f, 0.7f, 0.7f, 1.0f);
19+
glClear(GL_COLOR_BUFFER_BIT);
20+
21+
for (int j = GLFW_JOYSTICK_1; j < GLFW_JOYSTICK_16; ++j) {
22+
int joy = GLFW_JOYSTICK_1 + j;
23+
if (!glfwJoystickPresent(joy)) continue;
24+
25+
static struct {
26+
int axes_count;
27+
float axes[16];
28+
int button_count;
29+
unsigned char buttons[16];
30+
} last_gamepad_state[16] = {0};
31+
32+
const char *name = glfwGetJoystickName(joy);
33+
34+
int axes_count = 0;
35+
const float *axes = glfwGetJoystickAxes(joy, &axes_count);
36+
37+
int button_count = 0;
38+
const unsigned char *buttons = glfwGetJoystickButtons(joy, &button_count);
39+
40+
last_gamepad_state[joy].axes_count = axes_count;
41+
for (int i = 0; i < axes_count; ++i) {
42+
if (last_gamepad_state[joy].axes[i] != axes[i]) {
43+
printf("(%d %s) axis %d = %f\n", joy, name, i, axes[i]);
44+
}
45+
46+
last_gamepad_state[joy].axes[i] = axes[i];
47+
}
48+
49+
last_gamepad_state[joy].button_count = button_count;
50+
for (int i = 0; i < button_count; ++i) {
51+
if (last_gamepad_state[joy].buttons[i] != buttons[i]) {
52+
printf("(%d %s) button %d = %d\n", joy, name, i, buttons[i]);
53+
}
54+
55+
last_gamepad_state[joy].buttons[i] = buttons[i];
56+
}
57+
}
58+
}
59+
60+
void joystick_callback(int joy, int event)
61+
{
62+
if (event == GLFW_CONNECTED) {
63+
printf("Joystick %d was connected: %s\n", joy, glfwGetJoystickName(joy));
64+
} else if (event == GLFW_DISCONNECTED) {
65+
printf("Joystick %d was disconnected\n", joy);
66+
}
67+
}
68+
69+
int main() {
70+
if (!glfwInit())
71+
{
72+
result = 0;
73+
printf("Could not create window. Test failed.\n");
74+
#ifdef REPORT_RESULT
75+
REPORT_RESULT();
76+
#endif
77+
return -1;
78+
}
79+
glfwWindowHint(GLFW_RESIZABLE , 1);
80+
g_window = glfwCreateWindow(600, 450, "GLFW joystick test", NULL, NULL);
81+
if (!g_window)
82+
{
83+
result = 0;
84+
printf("Could not create window. Test failed.\n");
85+
#ifdef REPORT_RESULT
86+
REPORT_RESULT();
87+
#endif
88+
glfwTerminate();
89+
return -1;
90+
}
91+
glfwMakeContextCurrent(g_window);
92+
glfwSetJoystickCallback(joystick_callback);
93+
94+
#ifdef __EMSCRIPTEN__
95+
emscripten_set_main_loop(render, 0, 1);
96+
#else
97+
while (!glfwWindowShouldClose(window)) {
98+
glfwPollEvents();
99+
}
100+
#endif
101+
102+
glfwTerminate();
103+
104+
return 0;
105+
}

tests/test_browser.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,53 @@ def test_sdl_joystick_2(self):
10971097
Popen([PYTHON, EMCC, os.path.join(self.get_dir(), 'sdl_joystick.c'), '-O2', '--minify', '0', '-o', 'page.html', '--pre-js', 'pre.js', '-lSDL', '-lGL']).communicate()
10981098
self.run_browser('page.html', '', '/report_result?2')
10991099

1100+
def test_glfw_joystick(self):
1101+
# Generates events corresponding to the Editor's Draft of the HTML5 Gamepad API.
1102+
# https://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html#idl-def-Gamepad
1103+
open(os.path.join(self.get_dir(), 'pre.js'), 'w').write('''
1104+
var gamepads = [];
1105+
// Spoof this function.
1106+
navigator['getGamepads'] = function() {
1107+
return gamepads;
1108+
};
1109+
window['addNewGamepad'] = function(id, numAxes, numButtons) {
1110+
var index = gamepads.length;
1111+
var gamepad = {
1112+
axes: new Array(numAxes),
1113+
buttons: new Array(numButtons),
1114+
id: id,
1115+
index: index
1116+
};
1117+
gamepads.push(gamepad)
1118+
var i;
1119+
for (i = 0; i < numAxes; i++) gamepads[index].axes[i] = 0;
1120+
// Buttons are objects
1121+
for (i = 0; i < numButtons; i++) gamepads[index].buttons[i] = { pressed: false, value: 0 };
1122+
1123+
// Dispatch event (required for glfw joystick; note not used in SDL test)
1124+
var event = new Event('gamepadconnected');
1125+
event.gamepad = gamepad;
1126+
window.dispatchEvent(event);
1127+
};
1128+
// FF mutates the original objects.
1129+
window['simulateGamepadButtonDown'] = function (index, button) {
1130+
gamepads[index].buttons[button].pressed = true;
1131+
gamepads[index].buttons[button].value = 1;
1132+
};
1133+
window['simulateGamepadButtonUp'] = function (index, button) {
1134+
gamepads[index].buttons[button].pressed = false;
1135+
gamepads[index].buttons[button].value = 0;
1136+
};
1137+
window['simulateAxisMotion'] = function (index, axis, value) {
1138+
gamepads[index].axes[axis] = value;
1139+
};
1140+
''')
1141+
open(os.path.join(self.get_dir(), 'test_glfw_joystick.c'), 'w').write(self.with_report_result(open(path_from_root('tests', 'test_glfw_joystick.c')).read()))
1142+
1143+
Popen([PYTHON, EMCC, os.path.join(self.get_dir(), 'test_glfw_joystick.c'), '-O2', '--minify', '0', '-o', 'page.html', '--pre-js', 'pre.js', '-lGL', '-lglfw3', '-s', 'USE_GLFW=3']).communicate()
1144+
self.run_browser('page.html', '', '/report_result?2')
1145+
1146+
11001147
def test_webgl_context_attributes(self):
11011148
# Javascript code to check the attributes support we want to test in the WebGL implementation
11021149
# (request the attribute, create a context and check its value afterwards in the context attributes).

0 commit comments

Comments
 (0)