diff options
author | John Vilk <jvilk@cs.umass.edu> | 2013-11-06 22:08:33 -0500 |
---|---|---|
committer | John Vilk <jvilk@cs.umass.edu> | 2013-11-10 02:25:17 -0500 |
commit | 39b138d4ddda29b535285bc1c5223641dcacce28 (patch) | |
tree | 84684a42f652d043e03918034f7d3b972f45d84b | |
parent | d446e2b3c3b96474106b9bd47f35d901abb14f6d (diff) |
[SDL] Joystick API implementation using HTML5 Gamepad API
Works in browsers that implement the working draft of the standard (current Chrome / Firefox stable):
http://www.w3.org/TR/2012/WD-gamepad-20120529/#gamepad-interface
...and browsers that implement the editor's draft (current Firefox Nightly):
https://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html#idl-def-Gamepad
Contains unit tests for both event types.
-rw-r--r-- | AUTHORS | 1 | ||||
-rw-r--r-- | src/library_sdl.js | 234 | ||||
-rw-r--r-- | src/struct_info.json | 15 | ||||
-rw-r--r-- | tests/sdl_joystick.c | 123 | ||||
-rw-r--r-- | tests/test_browser.py | 76 |
5 files changed, 435 insertions, 14 deletions
@@ -107,3 +107,4 @@ a license to everyone to use it as detailed in LICENSE.) * Michael Tirado <icetooth333@gmail.com> * Ben Noordhuis <info@bnoordhuis.nl> * Bob Roberts <bobroberts177@gmail.com> +* John Vilk <jvilk@cs.umass.edu> diff --git a/src/library_sdl.js b/src/library_sdl.js index 5b43b7ab..c46364ff 100644 --- a/src/library_sdl.js +++ b/src/library_sdl.js @@ -75,6 +75,7 @@ var LibrarySDL = { textInput: false, startTime: null, + initFlags: 0, // The flags passed to SDL_Init buttonState: 0, modState: 0, DOMButtons: [0, 0, 0], @@ -639,6 +640,21 @@ var LibrarySDL = { {{{ makeSetValue('ptr', C_STRUCTS.SDL_ResizeEvent.h, 'event.h', 'i32') }}}; break; } + case 'joystick_button_up': case 'joystick_button_down': { + var state = event.type === 'joystick_button_up' ? 0 : 1; + {{{ makeSetValue('ptr', C_STRUCTS.SDL_JoyButtonEvent.type, 'SDL.DOMEventToSDLEvent[event.type]', 'i32') }}}; + {{{ makeSetValue('ptr', C_STRUCTS.SDL_JoyButtonEvent.which, 'event.index', 'i8') }}}; + {{{ makeSetValue('ptr', C_STRUCTS.SDL_JoyButtonEvent.button, 'event.button', 'i8') }}}; + {{{ makeSetValue('ptr', C_STRUCTS.SDL_JoyButtonEvent.state, 'state', 'i8') }}}; + break; + } + case 'joystick_axis_motion': { + {{{ makeSetValue('ptr', C_STRUCTS.SDL_JoyAxisEvent.type, 'SDL.DOMEventToSDLEvent[event.type]', 'i32') }}}; + {{{ makeSetValue('ptr', C_STRUCTS.SDL_JoyAxisEvent.which, 'event.index', 'i8') }}}; + {{{ makeSetValue('ptr', C_STRUCTS.SDL_JoyAxisEvent.axis, 'event.axis', 'i8') }}}; + {{{ makeSetValue('ptr', C_STRUCTS.SDL_JoyAxisEvent.value, 'SDL.joystickAxisValueConversion(event.value)', 'i32') }}}; + break; + } default: throw 'Unhandled SDL event: ' + event.type; } }, @@ -695,7 +711,109 @@ var LibrarySDL = { for (var i = 0; i < num; i++) { console.log(' diagonal ' + i + ':' + [data[i*surfData.width*4 + i*4 + 0], data[i*surfData.width*4 + i*4 + 1], data[i*surfData.width*4 + i*4 + 2], data[i*surfData.width*4 + i*4 + 3]]); } - } + }, + + // Joystick helper methods and state + + joystickEventState: 0, + lastJoystickState: {}, // Map from SDL_Joystick* to their last known state. Required to determine if a change has occurred. + // Maps Joystick names to pointers. Allows us to avoid reallocating memory for + // joystick names each time this function is called. + joystickNamePool: {}, + recordJoystickState: function(joystick, state) { + // Standardize button state. + var buttons = new Array(state.buttons.length); + for (var i = 0; i < state.buttons.length; i++) { + buttons[i] = SDL.getJoystickButtonState(state.buttons[i]); + } + + SDL.lastJoystickState[joystick] = { + buttons: buttons, + axes: state.axes.slice(0), + timestamp: state.timestamp, + index: state.index, + id: state.id + }; + }, + // Retrieves the button state of the given gamepad button. + // Abstracts away implementation differences. + // Returns 'true' if pressed, 'false' otherwise. + getJoystickButtonState: function(button) { + if (typeof button === 'object') { + // Current gamepad API editor's draft (Firefox Nightly) + // https://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html#idl-def-GamepadButton + return button.pressed; + } else { + // Current gamepad API working draft (Firefox / Chrome Stable) + // http://www.w3.org/TR/2012/WD-gamepad-20120529/#gamepad-interface + return button > 0; + } + }, + // Queries for and inserts controller events into the SDL queue. + queryJoysticks: function() { + for (var joystick in SDL.lastJoystickState) { + var state = SDL.getGamepad(joystick - 1); + var prevState = SDL.lastJoystickState[joystick]; + // Check only if the timestamp has differed. + // NOTE: Timestamp is not available in Firefox. + if (typeof state.timestamp !== 'number' || state.timestamp !== prevState.timestamp) { + var i; + for (i = 0; i < state.buttons.length; i++) { + var buttonState = SDL.getJoystickButtonState(state.buttons[i]); + // NOTE: The previous state already has a boolean representation of + // its button, so no need to standardize its button state here. + if (buttonState !== prevState.buttons[i]) { + // Insert button-press event. + SDL.events.push({ + type: buttonState ? 'joystick_button_down' : 'joystick_button_up', + joystick: joystick, + index: joystick - 1, + button: i + }); + } + } + for (i = 0; i < state.axes.length; i++) { + if (state.axes[i] !== prevState.axes[i]) { + // Insert axes-change event. + SDL.events.push({ + type: 'joystick_axis_motion', + joystick: joystick, + index: joystick - 1, + axis: i, + value: state.axes[i] + }); + } + } + + SDL.recordJoystickState(joystick, state); + } + } + }, + // Converts the double-based browser axis value [-1, 1] into SDL's 16-bit + // value [-32768, 32767] + joystickAxisValueConversion: function(value) { + // Ensures that 0 is 0, 1 is 32767, and -1 is 32768. + return Math.ceil(((value+1) * 32767.5) - 32768); + }, + + getGamepads: function() { + var fcn = navigator.getGamepads || navigator.webkitGamepads || navigator.mozGamepads || navigator.gamepads || navigator.webkitGetGamepads; + if (fcn !== undefined) { + // The function must be applied on the navigator object. + return fcn.apply(navigator); + } else { + return []; + } + }, + + // Helper function: Returns the gamepad if available, or null if not. + getGamepad: function(deviceIndex) { + var gamepads = SDL.getGamepads(); + if (gamepads.length > deviceIndex && deviceIndex >= 0) { + return gamepads[deviceIndex]; + } + return null; + }, }, SDL_Linked_Version: function() { @@ -708,8 +826,10 @@ var LibrarySDL = { return SDL.version; }, - SDL_Init: function(what) { + SDL_Init: function(initFlags) { SDL.startTime = Date.now(); + SDL.initFlags = initFlags; + // capture all key events. we just keep down and up, but also capture press to prevent default actions if (!Module['doNotCaptureKeyboard']) { document.addEventListener("keydown", SDL.receiveEvent); @@ -718,6 +838,15 @@ var LibrarySDL = { window.addEventListener("blur", SDL.receiveEvent); document.addEventListener("visibilitychange", SDL.receiveEvent); } + + if (initFlags & 0x200) { + // SDL_INIT_JOYSTICK + // Firefox will not give us Joystick data unless we register this NOP + // callback. + // https://bugzilla.mozilla.org/show_bug.cgi?id=936104 + addEventListener("gamepadconnected", function() {}); + } + window.addEventListener("unload", SDL.receiveEvent); SDL.keyboardState = _malloc(0x10000); // Our SDL needs 512, but 64K is safe for older SDLs _memset(SDL.keyboardState, 0, 0x10000); @@ -730,6 +859,12 @@ var LibrarySDL = { SDL.DOMEventToSDLEvent['mousemove'] = 0x400 /* SDL_MOUSEMOTION */; SDL.DOMEventToSDLEvent['unload'] = 0x100 /* SDL_QUIT */; SDL.DOMEventToSDLEvent['resize'] = 0x7001 /* SDL_VIDEORESIZE/SDL_EVENT_COMPAT2 */; + // These are not technically DOM events; the HTML gamepad API is poll-based. + // However, we define them here, as the rest of the SDL code assumes that + // all SDL events originate as DOM events. + SDL.DOMEventToSDLEvent['joystick_axis_motion'] = 0x600 /* SDL_JOYAXISMOTION */; + SDL.DOMEventToSDLEvent['joystick_button_down'] = 0x603 /* SDL_JOYBUTTONDOWN */; + SDL.DOMEventToSDLEvent['joystick_button_up'] = 0x604 /* SDL_JOYBUTTONUP */; return 0; // success }, @@ -1189,6 +1324,11 @@ var LibrarySDL = { }, SDL_PollEvent: function(ptr) { + if (SDL.initFlags & 0x200 && SDL.joystickEventState) { + // If SDL_INIT_JOYSTICK was supplied AND the joystick system is configured + // to automatically query for events, query for joystick events. + SDL.queryJoysticks(); + } if (SDL.events.length === 0) return 0; if (ptr) { SDL.makeCEvent(SDL.events.shift(), ptr); @@ -2375,37 +2515,103 @@ var LibrarySDL = { // Joysticks - SDL_NumJoysticks: function() { return 0; }, + SDL_NumJoysticks: function() { + var count = 0; + var gamepads = SDL.getGamepads(); + // The length is not the number of gamepads; check which ones are defined. + for (var i = 0; i < gamepads.length; i++) { + if (gamepads[i] !== undefined) count++; + } + return count; + }, - SDL_JoystickName: function(deviceIndex) { return 0; }, + SDL_JoystickName: function(deviceIndex) { + var gamepad = SDL.getGamepad(deviceIndex); + if (gamepad) { + var name = gamepad.id; + if (SDL.joystickNamePool.hasOwnProperty(name)) { + return SDL.joystickNamePool[name]; + } + return SDL.joystickNamePool[name] = allocate(intArrayFromString(name), 'i8', ALLOC_NORMAL); + } + return 0; + }, - SDL_JoystickOpen: function(deviceIndex) { return 0; }, + SDL_JoystickOpen: function(deviceIndex) { + var gamepad = SDL.getGamepad(deviceIndex); + if (gamepad) { + // Use this as a unique 'pointer' for this joystick. + var joystick = deviceIndex+1; + SDL.recordJoystickState(joystick, gamepad); + return joystick; + } + return 0; + }, - SDL_JoystickOpened: function(deviceIndex) { return 0; }, + SDL_JoystickOpened: function(deviceIndex) { + return SDL.lastJoystickState.hasOwnProperty(deviceIndex+1) ? 1 : 0; + }, - SDL_JoystickIndex: function(joystick) { return 0; }, + SDL_JoystickIndex: function(joystick) { + // joystick pointers are simply the deviceIndex+1. + return joystick - 1; + }, - SDL_JoystickNumAxes: function(joystick) { return 0; }, + SDL_JoystickNumAxes: function(joystick) { + var gamepad = SDL.getGamepad(joystick - 1); + if (gamepad) { + return gamepad.axes.length; + } + return 0; + }, SDL_JoystickNumBalls: function(joystick) { return 0; }, SDL_JoystickNumHats: function(joystick) { return 0; }, - SDL_JoystickNumButtons: function(joystick) { return 0; }, + SDL_JoystickNumButtons: function(joystick) { + var gamepad = SDL.getGamepad(joystick - 1); + if (gamepad) { + return gamepad.buttons.length; + } + return 0; + }, - SDL_JoystickUpdate: function() {}, + SDL_JoystickUpdate: function() { + SDL.queryJoysticks(); + }, - SDL_JoystickEventState: function(state) { return 0; }, + SDL_JoystickEventState: function(state) { + if (state < 0) { + // SDL_QUERY: Return current state. + return SDL.joystickEventState; + } + return SDL.joystickEventState = state; + }, - SDL_JoystickGetAxis: function(joystick, axis) { return 0; }, + SDL_JoystickGetAxis: function(joystick, axis) { + var gamepad = SDL.getGamepad(joystick - 1); + if (gamepad && gamepad.axes.length > axis) { + return SDL.joystickAxisValueConversion(gamepad.axes[axis]); + } + return 0; + }, SDL_JoystickGetHat: function(joystick, hat) { return 0; }, SDL_JoystickGetBall: function(joystick, ball, dxptr, dyptr) { return -1; }, - SDL_JoystickGetButton: function(joystick, button) { return 0; }, + SDL_JoystickGetButton: function(joystick, button) { + var gamepad = SDL.getGamepad(joystick - 1); + if (gamepad && gamepad.buttons.length > button) { + return SDL.getJoystickButtonState(gamepad.buttons[button]) ? 1 : 0; + } + return 0; + }, - SDL_JoystickClose: function(joystick) {}, + SDL_JoystickClose: function(joystick) { + delete SDL.lastJoystickState[joystick]; + }, // Misc diff --git a/src/struct_info.json b/src/struct_info.json index 5b4726e8..f6499295 100644 --- a/src/struct_info.json +++ b/src/struct_info.json @@ -961,6 +961,21 @@ "x", "y" ], + "SDL_JoyAxisEvent": [ + "type", + "which", + "axis", + "padding1", + "padding2", + "value" + ], + "SDL_JoyButtonEvent": [ + "type", + "which", + "button", + "state", + "padding1" + ], "SDL_ResizeEvent": [ "type", "w", diff --git a/tests/sdl_joystick.c b/tests/sdl_joystick.c new file mode 100644 index 00000000..7035050f --- /dev/null +++ b/tests/sdl_joystick.c @@ -0,0 +1,123 @@ +#include <stdio.h> +#include <SDL/SDL.h> +#include <SDL/SDL_ttf.h> +#include <assert.h> +#include <string.h> +#include <emscripten.h> + +int result = 1; + +void assertJoystickEvent(int expectedGamepad, int expectedType, int expectedIndex, int expectedValue) { + SDL_Event event; + while(1) { + // Loop ends either when assertion fails (we run out of events), or we find + // the event we're looking for. + assert(SDL_PollEvent(&event) == 1); + if (event.type != expectedType) { + continue; + } + switch(event.type) { + case SDL_JOYAXISMOTION: { + assert(event.jaxis.which == expectedGamepad); + assert(event.jaxis.axis == expectedIndex); + assert(event.jaxis.value == expectedValue); + break; + } + case SDL_JOYBUTTONUP: case SDL_JOYBUTTONDOWN: { + assert(event.jbutton.which == expectedGamepad); + assert(event.jbutton.button == expectedIndex); + assert(event.jbutton.state == expectedValue); + break; + } + } + // Break out of while loop. + break; + } +} + +void assertNoJoystickEvent() { + SDL_Event event; + while(SDL_PollEvent(&event)) { + switch(event.type) { + case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP: case SDL_JOYAXISMOTION: { + // Fail. + assert(0); + } + } + } +} + +void main_2(void* arg); + +int main() { + SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK); + SDL_Surface *screen = SDL_SetVideoMode(600, 450, 32, SDL_HWSURFACE); + emscripten_async_call(main_2, NULL, 3000); // avoid startup delays and intermittent errors + return 0; +} + +void main_2(void* arg) { + // TODO: At the moment, we only support joystick support through polling. + emscripten_run_script("window.addNewGamepad('Pad Thai', 4, 16)"); + emscripten_run_script("window.addNewGamepad('Pad Kee Mao', 0, 4)"); + // Check that the joysticks exist properly. + assert(SDL_NumJoysticks() == 2); + assert(!SDL_JoystickOpened(0)); + assert(!SDL_JoystickOpened(1)); + SDL_Joystick* pad1 = SDL_JoystickOpen(0); + assert(SDL_JoystickOpened(0)); + assert(SDL_JoystickIndex(pad1) == 0); + assert(strncmp(SDL_JoystickName(0), "Pad Thai", 9) == 0); + assert(strncmp(SDL_JoystickName(1), "Pad Kee Mao", 12) == 0); + assert(SDL_JoystickNumAxes(pad1) == 4); + assert(SDL_JoystickNumButtons(pad1) == 16); + + // Button events. + emscripten_run_script("window.simulateGamepadButtonDown(0, 1)"); + // We didn't tell SDL to automatically update this joystick's state. + assertNoJoystickEvent(); + SDL_JoystickUpdate(); + assertJoystickEvent(0, SDL_JOYBUTTONDOWN, 1, SDL_PRESSED); + assert(SDL_JoystickGetButton(pad1, 1) == 1); + // Enable automatic updates. + SDL_JoystickEventState(SDL_ENABLE); + assert(SDL_JoystickEventState(SDL_QUERY) == SDL_ENABLE); + emscripten_run_script("window.simulateGamepadButtonUp(0, 1)"); + assertJoystickEvent(0, SDL_JOYBUTTONUP, 1, SDL_RELEASED); + assert(SDL_JoystickGetButton(pad1, 1) == 0); + // No button change: Should not result in a new event. + emscripten_run_script("window.simulateGamepadButtonUp(0, 1)"); + assertNoJoystickEvent(); + // Joystick 1 is not opened; should not result in a new event. + emscripten_run_script("window.simulateGamepadButtonDown(1, 1)"); + assertNoJoystickEvent(); + + // Joystick wiggling + emscripten_run_script("window.simulateAxisMotion(0, 0, 1)"); + assertJoystickEvent(0, SDL_JOYAXISMOTION, 0, 32767); + assert(SDL_JoystickGetAxis(pad1, 0) == 32767); + emscripten_run_script("window.simulateAxisMotion(0, 0, 0)"); + assertJoystickEvent(0, SDL_JOYAXISMOTION, 0, 0); + assert(SDL_JoystickGetAxis(pad1, 0) == 0); + emscripten_run_script("window.simulateAxisMotion(0, 1, -1)"); + assertJoystickEvent(0, SDL_JOYAXISMOTION, 1, -32768); + assert(SDL_JoystickGetAxis(pad1, 1) == -32768); + emscripten_run_script("window.simulateAxisMotion(0, 1, -1)"); + // No joystick change: Should not result in a new event. + assertNoJoystickEvent(); + // Joystick 1 is not opened; should not result in a new event. + emscripten_run_script("window.simulateAxisMotion(1, 1, -1)"); + assertNoJoystickEvent(); + + SDL_JoystickClose(pad1); + assert(!SDL_JoystickOpened(0)); + + // Joystick 0 is closed; we should not process any new gamepad events from it. + emscripten_run_script("window.simulateGamepadButtonDown(0, 1)"); + assertNoJoystickEvent(); + + // End test. + result = 2; + printf("Test passed!\n"); +} + diff --git a/tests/test_browser.py b/tests/test_browser.py index d52f109f..1900e2cf 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -874,6 +874,82 @@ keydown(100);keyup(100); // trigger the end def test_glut_wheelevents(self): self.btest('glut_wheelevents.c', '1') + def test_sdl_joystick_1(self): + # Generates events corresponding to the Working Draft of the HTML5 Gamepad API. + # http://www.w3.org/TR/2012/WD-gamepad-20120529/#gamepad-interface + open(os.path.join(self.get_dir(), 'pre.js'), 'w').write(''' + var gamepads = []; + // Spoof this function. + navigator['getGamepads'] = function() { + return gamepads; + }; + window['addNewGamepad'] = function(id, numAxes, numButtons) { + var index = gamepads.length; + gamepads.push({ + axes: new Array(numAxes), + buttons: new Array(numButtons), + id: id, + index: index + }); + var i; + for (i = 0; i < numAxes; i++) gamepads[index].axes[i] = 0; + for (i = 0; i < numButtons; i++) gamepads[index].buttons[i] = 0; + }; + window['simulateGamepadButtonDown'] = function (index, button) { + gamepads[index].buttons[button] = 1; + }; + window['simulateGamepadButtonUp'] = function (index, button) { + gamepads[index].buttons[button] = 0; + }; + window['simulateAxisMotion'] = function (index, axis, value) { + gamepads[index].axes[axis] = value; + }; + ''') + open(os.path.join(self.get_dir(), 'sdl_joystick.c'), 'w').write(self.with_report_result(open(path_from_root('tests', 'sdl_joystick.c')).read())) + + Popen([PYTHON, EMCC, os.path.join(self.get_dir(), 'sdl_joystick.c'), '-O2', '--minify', '0', '-o', 'page.html', '--pre-js', 'pre.js']).communicate() + self.run_browser('page.html', '', '/report_result?2') + + def test_sdl_joystick_2(self): + # Generates events corresponding to the Editor's Draft of the HTML5 Gamepad API. + # https://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html#idl-def-Gamepad + open(os.path.join(self.get_dir(), 'pre.js'), 'w').write(''' + var gamepads = []; + // Spoof this function. + navigator['getGamepads'] = function() { + return gamepads; + }; + window['addNewGamepad'] = function(id, numAxes, numButtons) { + var index = gamepads.length; + gamepads.push({ + axes: new Array(numAxes), + buttons: new Array(numButtons), + id: id, + index: index + }); + var i; + for (i = 0; i < numAxes; i++) gamepads[index].axes[i] = 0; + // Buttons are objects + for (i = 0; i < numButtons; i++) gamepads[index].buttons[i] = { pressed: false, value: 0 }; + }; + // FF mutates the original objects. + window['simulateGamepadButtonDown'] = function (index, button) { + gamepads[index].buttons[button].pressed = true; + gamepads[index].buttons[button].value = 1; + }; + window['simulateGamepadButtonUp'] = function (index, button) { + gamepads[index].buttons[button].pressed = false; + gamepads[index].buttons[button].value = 0; + }; + window['simulateAxisMotion'] = function (index, axis, value) { + gamepads[index].axes[axis] = value; + }; + ''') + open(os.path.join(self.get_dir(), 'sdl_joystick.c'), 'w').write(self.with_report_result(open(path_from_root('tests', 'sdl_joystick.c')).read())) + + Popen([PYTHON, EMCC, os.path.join(self.get_dir(), 'sdl_joystick.c'), '-O2', '--minify', '0', '-o', 'page.html', '--pre-js', 'pre.js']).communicate() + self.run_browser('page.html', '', '/report_result?2') + def test_webgl_context_attributes(self): # Javascript code to check the attributes support we want to test in the WebGL implementation # (request the attribute, create a context and check its value afterwards in the context attributes). |