diff options
author | Alon Zakai <alonzakai@gmail.com> | 2012-09-20 14:13:43 -0700 |
---|---|---|
committer | Alon Zakai <alonzakai@gmail.com> | 2012-09-20 14:13:43 -0700 |
commit | d9e13a5a3ceeaa500e9c75e52547c8b67950c441 (patch) | |
tree | 986b6af442e8c503bedab9912e8913dbd129d2e1 | |
parent | afa818a5cbc78bb862f0cae8b305c8033a5b0fc3 (diff) | |
parent | 83c6675368ce14ee34b52147792b1073a074ed24 (diff) |
Merge branch 'incoming'
-rw-r--r-- | AUTHORS | 2 | ||||
-rwxr-xr-x | emcc | 7 | ||||
-rw-r--r-- | src/library_browser.js | 21 | ||||
-rw-r--r-- | src/preamble.js | 15 | ||||
-rw-r--r-- | system/include/EGL/eglplatform.h | 6 | ||||
-rw-r--r-- | system/include/emscripten/emscripten.h | 26 | ||||
-rw-r--r-- | tests/browser_gc.cpp | 6 | ||||
-rw-r--r-- | tests/emscripten_api_browser.cpp | 24 | ||||
-rwxr-xr-x | tests/runner.py | 23 | ||||
-rw-r--r-- | tests/sdl_mouse.c | 6 | ||||
-rw-r--r-- | tools/reproduceriter.js | 216 | ||||
-rwxr-xr-x | tools/reproduceriter.py | 327 | ||||
-rw-r--r-- | tools/reproduceriter_shell.js | 871 | ||||
-rw-r--r-- | tools/shared.py | 38 |
14 files changed, 1238 insertions, 350 deletions
@@ -35,4 +35,4 @@ a license to everyone to use it as detailed in LICENSE.) * James Pike <totoro.friend@chilon.net> * Mokhtar Naamani <mokhtar.naamani@gmail.com> * Benjamin Stover <benjamin.stover@gmail.com> - +* Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> @@ -110,7 +110,7 @@ if DEBUG and LEAVE_INPUTS_RAW: print >> sys.stderr, 'emcc: leaving inputs raw' stdout = PIPE if not DEBUG else None # suppress output of child processes stderr = PIPE if not DEBUG else None # unless we are in DEBUG mode -shared.check_sanity() +shared.check_sanity(force=DEBUG) # Handle some global flags @@ -414,7 +414,10 @@ if TEMP_DIR: shutil.rmtree(temp_dir) # clear it os.makedirs(temp_dir) else: - temp_dir = tempfile.mkdtemp() + temp_root = os.path.join(shared.TEMP_DIR, 'emcc') + if not os.path.exists(temp_root): + os.makedirs(temp_root) + temp_dir = tempfile.mkdtemp(dir=temp_root) def in_temp(name): return os.path.join(temp_dir, os.path.basename(name)) diff --git a/src/library_browser.js b/src/library_browser.js index 43732e5b..5291dcee 100644 --- a/src/library_browser.js +++ b/src/library_browser.js @@ -384,7 +384,7 @@ mergeInto(LibraryManager.library, { if (Browser.mainLoop.queue.length > 0) { var start = Date.now(); var blocker = Browser.mainLoop.queue.shift(); - blocker.func(); + blocker.func(blocker.arg); if (Browser.mainLoop.remainingBlockers) { var remaining = Browser.mainLoop.remainingBlockers; var next = remaining%1 == 0 ? remaining-1 : Math.floor(remaining); @@ -451,13 +451,13 @@ mergeInto(LibraryManager.library, { Browser.mainLoop.resume(); }, - _emscripten_push_main_loop_blocker: function(func, name) { - Browser.mainLoop.queue.push({ func: FUNCTION_TABLE[func], name: Pointer_stringify(name), counted: true }); + _emscripten_push_main_loop_blocker: function(func, arg, name) { + Browser.mainLoop.queue.push({ func: FUNCTION_TABLE[func], arg: arg, name: Pointer_stringify(name), counted: true }); Browser.mainLoop.updateStatus(); }, - _emscripten_push_uncounted_main_loop_blocker: function(func, name) { - Browser.mainLoop.queue.push({ func: FUNCTION_TABLE[func], name: Pointer_stringify(name), counted: false }); + _emscripten_push_uncounted_main_loop_blocker: function(func, arg, name) { + Browser.mainLoop.queue.push({ func: FUNCTION_TABLE[func], arg: arg, name: Pointer_stringify(name), counted: false }); Browser.mainLoop.updateStatus(); }, @@ -467,14 +467,17 @@ mergeInto(LibraryManager.library, { Browser.mainLoop.updateStatus(); }, - emscripten_async_call: function(func, millis) { + emscripten_async_call: function(func, arg, millis) { Module['noExitRuntime'] = true; - var asyncCall = Runtime.getFuncWrapper(func); + function wrapper() { + Runtime.getFuncWrapper(func)(arg); + } + if (millis >= 0) { - setTimeout(asyncCall, millis); + setTimeout(wrapper, millis); } else { - Browser.requestAnimationFrame(asyncCall); + Browser.requestAnimationFrame(wrapper); } }, diff --git a/src/preamble.js b/src/preamble.js index b3cfca85..f1b95958 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -457,8 +457,6 @@ function getValue(ptr, type, noSafe) { } Module['getValue'] = getValue; -// Allocates memory for some data and initializes it properly. - var ALLOC_NORMAL = 0; // Tries to use _malloc() var ALLOC_STACK = 1; // Lives for the duration of the current function call var ALLOC_STATIC = 2; // Cannot be freed @@ -466,6 +464,19 @@ Module['ALLOC_NORMAL'] = ALLOC_NORMAL; Module['ALLOC_STACK'] = ALLOC_STACK; Module['ALLOC_STATIC'] = ALLOC_STATIC; +// allocate(): This is for internal use. You can use it yourself as well, but the interface +// is a little tricky (see docs right below). The reason is that it is optimized +// for multiple syntaxes to save space in generated code. So you should +// normally not use allocate(), and instead allocate memory using _malloc(), +// initialize it with setValue(), and so forth. +// @slab: An array of data, or a number. If a number, then the size of the block to allocate, +// in *bytes* (note that this is sometimes confusing: the next parameter does not +// affect this!) +// @types: Either an array of types, one for each byte (or 0 if no type at that position), +// or a single type which is used for the entire block. This only matters if there +// is initial data - if @slab is a number, then this does not matter at all and is +// ignored. +// @allocator: How to allocate memory, see ALLOC_* function allocate(slab, types, allocator) { var zeroinit, size; if (typeof slab === 'number') { diff --git a/system/include/EGL/eglplatform.h b/system/include/EGL/eglplatform.h index 4512fd84..2db2cc47 100644 --- a/system/include/EGL/eglplatform.h +++ b/system/include/EGL/eglplatform.h @@ -75,6 +75,12 @@ typedef HDC EGLNativeDisplayType; typedef HBITMAP EGLNativePixmapType; typedef HWND EGLNativeWindowType; +#elif defined(EMSCRIPTEN) + +typedef int EGLNativeDisplayType; +typedef int EGLNativeWindowType; +typedef int EGLNativePixmapType; + #elif defined(__WINSCW__) || defined(__SYMBIAN32__) /* Symbian */ typedef int EGLNativeDisplayType; diff --git a/system/include/emscripten/emscripten.h b/system/include/emscripten/emscripten.h index 4d489142..8c42a18a 100644 --- a/system/include/emscripten/emscripten.h +++ b/system/include/emscripten/emscripten.h @@ -62,20 +62,20 @@ extern void emscripten_cancel_main_loop(); * at specific time in the future. */ #if EMSCRIPTEN -extern void _emscripten_push_main_loop_blocker(void (*func)(), const char *name); -extern void _emscripten_push_uncounted_main_loop_blocker(void (*func)(), const char *name); +extern void _emscripten_push_main_loop_blocker(void (*func)(void *), void *arg, const char *name); +extern void _emscripten_push_uncounted_main_loop_blocker(void (*func)(void *), void *arg, const char *name); #else -inline void _emscripten_push_main_loop_blocker(void (*func)(), const char *name) { - func(); +inline void _emscripten_push_main_loop_blocker(void (*func)(void *), void *arg, const char *name) { + func(arg); } -inline void _emscripten_push_uncounted_main_loop_blocker(void (*func)(), const char *name) { - func(); +inline void _emscripten_push_uncounted_main_loop_blocker(void (*func)(void *), void *arg, const char *name) { + func(arg); } #endif -#define emscripten_push_main_loop_blocker(func) \ - _emscripten_push_main_loop_blocker(func, #func) -#define emscripten_push_uncounted_main_loop_blocker(func) \ - _emscripten_push_uncounted_main_loop_blocker(func, #func) +#define emscripten_push_main_loop_blocker(func, arg) \ + _emscripten_push_main_loop_blocker(func, arg, #func) +#define emscripten_push_uncounted_main_loop_blocker(func, arg) \ + _emscripten_push_uncounted_main_loop_blocker(func, arg, #func) /* * Sets the number of blockers remaining until some user-relevant @@ -99,11 +99,11 @@ inline void emscripten_set_main_loop_expected_blockers(int num) {} * mechanism is used. */ #if EMSCRIPTEN -extern void emscripten_async_call(void (*func)(), int millis); +extern void emscripten_async_call(void (*func)(void *), void *arg, int millis); #else -inline void emscripten_async_call(void (*func)(), int millis) { +inline void emscripten_async_call(void (*func)(void *), void *arg, int millis) { if (millis) SDL_Delay(millis); - func(); + func(arg); } #endif diff --git a/tests/browser_gc.cpp b/tests/browser_gc.cpp index 75dea10a..4d6cc712 100644 --- a/tests/browser_gc.cpp +++ b/tests/browser_gc.cpp @@ -16,7 +16,7 @@ void finalizer(void *ptr, void *arg) { int stage = 0; float start = 0; -void waiter() { +void waiter(void*) { if (stage == 0) { // wait for a while, see no GCing assert(global); if (emscripten_get_now() - start > 2100) { @@ -58,7 +58,7 @@ void waiter() { } } - emscripten_async_call(waiter, 100); + emscripten_async_call(waiter, NULL, 100); } int main() { @@ -89,7 +89,7 @@ int main() { void **local2Data = (void**)local2; local2Data[0] = local4; // actually ignored, because local2 is atomic, so 4 is freeable - emscripten_async_call(waiter, 100); + emscripten_async_call(waiter, NULL, 100); return 0; } diff --git a/tests/emscripten_api_browser.cpp b/tests/emscripten_api_browser.cpp index 305265e5..c209550b 100644 --- a/tests/emscripten_api_browser.cpp +++ b/tests/emscripten_api_browser.cpp @@ -11,19 +11,22 @@ extern "C" { bool pre1ed = false; bool pre2ed = false; -void pre1() { +void pre1(void *arg) { assert(!pre1ed); assert(!pre2ed); + assert((int)arg == 123); pre1ed = true; } -void pre2() { +void pre2(void *arg) { assert(pre1ed); assert(!pre2ed); + assert((int)arg == 98); pre2ed = true; } bool fived = false; -void five() { +void five(void *arg) { + assert((int)arg == 55); fived = true; emscripten_resume_main_loop(); } @@ -33,11 +36,11 @@ void mainey() { printf("mainey: %d\n", counter++); if (counter == 20) { emscripten_pause_main_loop(); - emscripten_async_call(five, 1000); + emscripten_async_call(five, (void*)55, 1000); } else if (counter == 22) { // very soon after 20, so without pausing we fail assert(fived); - emscripten_push_main_loop_blocker(pre1); - emscripten_push_main_loop_blocker(pre2); + emscripten_push_main_loop_blocker(pre1, (void*)123); + emscripten_push_main_loop_blocker(pre2, (void*)98); } else if (counter == 23) { assert(pre1ed); assert(pre2ed); @@ -47,7 +50,8 @@ void mainey() { } } -void four() { +void four(void *arg) { + assert((int)arg == 43); printf("four!\n"); emscripten_set_main_loop(mainey, 0); } @@ -56,10 +60,10 @@ void __attribute__((used)) third() { int now = SDL_GetTicks(); printf("thard! %d\n", now); assert(fabs(now - last - 1000) < 500); - emscripten_async_call(four, -1); // triggers requestAnimationFrame + emscripten_async_call(four, (void*)43, -1); // triggers requestAnimationFrame } -void second() { +void second(void *arg) { int now = SDL_GetTicks(); printf("sacond! %d\n", now); assert(fabs(now - last - 500) < 250); @@ -81,7 +85,7 @@ int main() { atexit(never); // should never be called - it is wrong to exit the runtime orderly if we have async calls! - emscripten_async_call(second, 500); + emscripten_async_call(second, (void*)0, 500); return 1; } diff --git a/tests/runner.py b/tests/runner.py index e4aba0c9..792b8939 100755 --- a/tests/runner.py +++ b/tests/runner.py @@ -7829,11 +7829,13 @@ elif 'browser' in str(sys.argv): var actual = actualCtx.getImageData(0, 0, actualImage.width, actualImage.height).data; var total = 0; - for (var x = 0; x < img.width; x++) { - for (var y = 0; y < img.height; y++) { - total += Math.abs(expected[y*img.width*4 + x*4 + 0] - actual[y*img.width*4 + x*4 + 0]); - total += Math.abs(expected[y*img.width*4 + x*4 + 1] - actual[y*img.width*4 + x*4 + 1]); - total += Math.abs(expected[y*img.width*4 + x*4 + 2] - actual[y*img.width*4 + x*4 + 2]); + var width = img.width; + var height = img.height; + for (var x = 0; x < width; x++) { + for (var y = 0; y < height; y++) { + total += Math.abs(expected[y*width*4 + x*4 + 0] - actual[y*width*4 + x*4 + 0]); + total += Math.abs(expected[y*width*4 + x*4 + 1] - actual[y*width*4 + x*4 + 1]); + total += Math.abs(expected[y*width*4 + x*4 + 2] - actual[y*width*4 + x*4 + 2]); } } var wrong = Math.floor(total / (img.width*img.height*3)); // floor, to allow some margin of error for antialiasing @@ -8805,7 +8807,7 @@ elif 'sanity' in str(sys.argv): assert (open(CONFIG_FILE).read() == open(path_from_root('settings.py')).read()), 'Settings should be copied from settings.py' # Second run, with bad EM_CONFIG - for settings in ['blah', 'LLVM_ROOT="blah"; JS_ENGINES=[]; COMPILER_ENGINE=NODE_JS=SPIDERMONKEY_ENGINE=[]']: + for settings in ['blah', 'LLVM_ROOT="blarg"; JS_ENGINES=[]; COMPILER_ENGINE=NODE_JS=SPIDERMONKEY_ENGINE=[]']: f = open(CONFIG_FILE, 'w') f.write(settings) f.close() @@ -8901,6 +8903,15 @@ elif 'sanity' in str(sys.argv): self.assertNotContained(SANITY_MESSAGE, output) self.assertNotContained(SANITY_FAIL_MESSAGE, output) + # but with EMCC_DEBUG=1 we should check + assert not os.environ.get('EMCC_DEBUG'), 'do not run sanity checks in debug mode!' + os.environ['EMCC_DEBUG'] = '1' + output = self.check_working(EMCC) + self.assertContained(SANITY_MESSAGE, output) + del os.environ['EMCC_DEBUG'] + output = self.check_working(EMCC) + self.assertNotContained(SANITY_MESSAGE, output) + # But the test runner should output = self.check_working(commands[1]) self.assertContained(SANITY_MESSAGE, output) diff --git a/tests/sdl_mouse.c b/tests/sdl_mouse.c index 0c10198a..f0282ab1 100644 --- a/tests/sdl_mouse.c +++ b/tests/sdl_mouse.c @@ -43,7 +43,7 @@ void one() { } } -void main_2(); +void main_2(void* arg); int main() { SDL_Init(SDL_INIT_VIDEO); @@ -52,12 +52,12 @@ int main() { SDL_Rect rect = { 0, 0, 600, 450 }; SDL_FillRect(screen, &rect, 0x2244ffff); - emscripten_async_call(main_2, 3000); // avoid startup delays and intermittent errors + emscripten_async_call(main_2, NULL, 3000); // avoid startup delays and intermittent errors return 0; } -void main_2() { +void main_2(void* arg) { emscripten_run_script("window.simulateMouseEvent(10, 20, -1)"); // move from 0,0 to 10,20 emscripten_run_script("window.simulateMouseEvent(10, 20, 0)"); // click emscripten_run_script("window.simulateMouseEvent(10, 20, 0)"); // click some more, but this one should be ignored through PeepEvent diff --git a/tools/reproduceriter.js b/tools/reproduceriter.js new file mode 100644 index 00000000..9dce8199 --- /dev/null +++ b/tools/reproduceriter.js @@ -0,0 +1,216 @@ + +var Recorder = (function() { + var recorder; + var init = 'reproduce='; + var initLocation = window.location.search.indexOf(init); + var replaying = initLocation >= 0; + var raf = window['requestAnimationFrame'] || + window['mozRequestAnimationFrame'] || + window['webkitRequestAnimationFrame'] || + window['msRequestAnimationFrame'] || + window['oRequestAnimationFrame']; + if (!replaying) { + // Prepare to record + recorder = {}; + // Start + recorder.frameCounter = 0; // the frame counter is used to know when to replay events + recorder.start = function() { + alert("Starting recording! Don't forget to Recorder.finish() afterwards!"); + function count() { + recorder.frameCounter++; + raf(count); + } + count(); + recorder.started = true; + }; + // Math.random + recorder.randoms = []; + recorder.random = Math.random; + Math.random = function() { + var ret = recorder.random(); + recorder.randoms.push(ret); + return ret; + }; + // Date.now, performance.now + recorder.dnows = []; + recorder.dnow = Date.now; + Date.now = function() { + var ret = recorder.dnow(); + recorder.dnows.push(ret); + return ret; + }; + recorder.pnows = []; + var pnow = performance.now || performance.webkitNow || performance.mozNow || performance.oNow || performance.msNow; + recorder.pnow = function() { return pnow.call(performance) }; + performance.now = function() { + var ret = recorder.pnow(); + recorder.pnows.push(ret); + return ret; + }; + // Events + recorder.devents = []; // document events + recorder.onEvent = function(which, callback) { + document['on' + which] = function(event) { + if (!recorder.started) return true; + event.frameCounter = recorder.frameCounter; + recorder.devents.push(event); + return callback(event); // XXX do we need to record the return value? + }; + }; + recorder.tevents = []; // custom-target events. Currently we assume a single such custom target (aside from document), e.g., a canvas for the game. + recorder.addListener = function(target, which, callback, arg) { + target.addEventListener(which, function(event) { + if (!recorder.started) return true; + event.frameCounter = recorder.frameCounter; + recorder.tevents.push(event); + return callback(event); // XXX do we need to record the return value? + }, arg); + }; + // Finish + recorder.finish = function() { + // Reorder data because pop() is faster than shift() + recorder.randoms.reverse(); + recorder.dnows.reverse(); + recorder.pnows.reverse(); + recorder.devents.reverse(); + recorder.tevents.reverse(); + // Make JSON.stringify work on data from native event objects (and only store relevant ones) + var importantProperties = { + type: 1, + movementX: 1, mozMovementX: 1, webkitMovementX: 1, + movementY: 1, mozMovementY: 1, webkitMovementY: 1, + detail: 1, + wheelDelta: 1, + pageX: 1, + pageY: 1, + button: 1, + keyCode: 1, + frameCounter: 1 + }; + function importantize(event) { + var ret = {}; + for (var prop in importantProperties) { + if (prop in event) { + ret[prop] = event[prop]; + } + } + return ret; + } + recorder.devents = recorder.devents.map(importantize); + recorder.tevents = recorder.tevents.map(importantize); + // Write out + alert('Writing out data, remember to save!'); + setTimeout(function() { + document.open(); + document.write(JSON.stringify(recorder)); + document.close(); + }, 0); + return '.'; + }; + } else { + // Load recording + var dataPath = window.location.search.substring(initLocation + init.length); + var baseURL = window.location.toString().replace('://', 'cheez999').split('?')[0].split('/').slice(0, -1).join('/').replace('cheez999', '://'); + if (baseURL[baseURL.length-1] != '/') baseURL += '/'; + var path = baseURL + dataPath; + alert('Loading replay from ' + path); + var request = new XMLHttpRequest(); + request.open('GET', path, false); + request.send(); + var raw = request.responseText; + raw = raw.substring(raw.indexOf('{'), raw.lastIndexOf('}')+1); // remove <html> etc + recorder = JSON.parse(raw); + // prepare to replay + // Start + recorder.frameCounter = 0; // the frame counter is used to know when to replay events + recorder.start = function() { + function count() { + recorder.frameCounter++; + raf(count); + // replay relevant events for this frame + while (recorder.devents.length && recorder.devents[recorder.devents.length-1].frameCounter <= recorder.frameCounter) { + var event = recorder.devents.pop(); + recorder['on' + event.type](event); + } + while (recorder.tevents.length && recorder.tevents[recorder.tevents.length-1].frameCounter <= recorder.frameCounter) { + var event = recorder.tevents.pop(); + recorder['event' + event.type](event); + } + } + count(); + }; + // Math.random + recorder.random = Math.random; + Math.random = function() { + if (recorder.randoms.length > 0) { + return recorder.randoms.pop(); + } else { + recorder.finish(); + throw 'consuming too many random values!'; + } + }; + // Date.now, performance.now + recorder.dnow = Date.now; + Date.now = function() { + if (recorder.dnows.length > 0) { + return recorder.dnows.pop(); + } else { + recorder.finish(); + throw 'consuming too many Date.now values!'; + } + }; + var pnow = performance.now || performance.webkitNow || performance.mozNow || performance.oNow || performance.msNow; + recorder.pnow = function() { return pnow.call(performance) }; + performance.now = function() { + if (recorder.pnows.length > 0) { + return recorder.pnows.pop(); + } else { + recorder.finish(); + throw 'consuming too many performance.now values!'; + } + }; + // Events + recorder.onEvent = function(which, callback) { + recorder['on' + which] = callback; + }; + recorder.eventCallbacks = {}; + recorder.addListener = function(target, which, callback, arg) { + recorder['event' + which] = callback; + }; + recorder.onFinish = []; + // Benchmarking hooks - emscripten specific + setTimeout(function() { + var totalTime = 0; + var totalSquared = 0; + var iterations = 0; + var maxTime = 0; + var curr = 0; + Module.preMainLoop = function() { + curr = recorder.pnow(); + } + Module.postMainLoop = function() { + var time = recorder.pnow() - curr; + totalTime += time; + totalSquared += time*time; + maxTime = Math.max(maxTime, time); + iterations++; + }; + recorder.onFinish.push(function() { + var mean = totalTime / iterations; + var meanSquared = totalSquared / iterations; + console.log('mean frame : ' + mean + ' ms'); + console.log('frame std dev: ' + Math.sqrt(meanSquared - (mean*mean)) + ' ms'); + console.log('max frame : ' + maxTime + ' ms'); + }); + }); + // Finish + recorder.finish = function() { + recorder.onFinish.forEach(function(finish) { + finish(); + }); + }; + } + recorder.replaying = replaying; + return recorder; +})(); + diff --git a/tools/reproduceriter.py b/tools/reproduceriter.py index 6696915d..a1912396 100755 --- a/tools/reproduceriter.py +++ b/tools/reproduceriter.py @@ -1,9 +1,6 @@ #!/usr/bin/env python ''' - -* This is a work in progress * - Reproducer Rewriter =================== @@ -19,13 +16,19 @@ Usage: 1. Run this script as - reproduceriter.py IN_DIR OUT_DIR FIRST_JS + reproduceriter.py IN_DIR OUT_DIR FIRST_JS [WINDOW_LOCATION] [ON_IDLE] IN_DIR should be the project directory, and OUT_DIR will be created with the instrumented code (OUT_DIR will be overwritten if it exists). FIRST_JS should be a path (relative to IN_DIR) to the first JavaScript file loaded by the project (this tool - will add code to that). + will add code to that). The last two parameters, WINDOW_LOCATION + and ON_IDLE, are relevant for shell builds. If WINDOW_LOCATION is + specified, we will make a build that runs in the shell and not in + a browser. WINDOW_LOCATION is the fake window.location we set in the + fake DOM, and ON_IDLE is code that runs when the fake main browser + event loop runs out of actions. (Note that only a browser build can + do recording, shell builds just replay.) You will need to call @@ -79,7 +82,26 @@ Examples * BananaBread: Unpack into a directory called bb, then one directory up, run - emscripten/tools/reproduceriter.py bb bench js/game-setup.js game.html?low,low,reproduce=repro.data + emscripten/tools/reproduceriter.py bb bench js/game-setup.js game.html?low,low,reproduce=repro.data "function(){ print('triggering click'); document.querySelector('.fullscreen-button.low-res').callEventListeners('click'); window.onIdle = null; }" + + for a shell build, or + + emscripten/tools/reproduceriter.py bb bench js/game-setup.js + + for a browser build. Since only a browser build can do recording, you would normally + make a browser build, record a trace, then make a shell build and copy the trace + there so you can run it. + + The last parameter specifies what to do when the event loop is idle: We fire an event and then set onIdle (which was this function) to null, so this is a one-time occurence. + +Notes + + * Replay can depend on browser state. One case is if you are replaying a fullscreen + game with pointer lock, then you will need to manually allow pointer lock if it + isn't already on for the machine. If you do it too early or too late, the replay + can be different, since mousemove events mean different things depending on + whether the pointer is locked or not. + ''' import os, sys, shutil, re @@ -92,6 +114,11 @@ in_dir = sys.argv[1] out_dir = sys.argv[2] first_js = sys.argv[3] window_location = sys.argv[4] if len(sys.argv) >= 5 else '' +on_idle = sys.argv[5] if len(sys.argv) >= 6 else '' + +shell = not not window_location + +dirs_to_drop = 0 if not os.path.dirname(first_js) else len(os.path.dirname(first_js).split('/')) if os.path.exists(out_dir): shutil.rmtree(out_dir) @@ -121,287 +148,13 @@ for parent, dirs, files in os.walk(out_dir): print 'add boilerplate...' -open(os.path.join(out_dir, first_js), 'w').write(''' - -// environment for shell -if (typeof nagivator == 'undefined') { - var window = { - location: { - toString: function() { - return '%s'; - }, - search: '%s', - }, - rafs: [], - requestAnimationFrame: function(func) { - window.rafs.push(func); - }, - runEventLoop: function() { - while (1) { // run forever until an exception stops this replay - var currRafs = window.rafs; - window.rafs = []; - for (var i = 0; i < currRafs.length; i++) { - currRafs[i](); - } - } - }, - }; - var document = { - getElementById: function(id) { - switch(id) { - case 'canvas': { - return { - getContext: function(which) { - switch(which) { - case 'experimental-webgl': { - return { - getExtension: function() { return 1 }, - requestPointerLock: function() { - throw 'pointerLock'; - }, - }; - } - default: throw 'canvas.getContext: ' + which; - } - }, - }; - } - default: throw 'getElementById: ' + id; - } - }, - querySelector: function() { - return { - classList: { - add: function(){}, - remove: function(){}, - }, - }; - }, - }; - var performance = { - now: function() { - return Date.now(); - }, - }; -} - -var Recorder = (function() { - var recorder; - var init = 'reproduce='; - var initLocation = window.location.search.indexOf(init); - var replaying = initLocation >= 0; - var raf = window['requestAnimationFrame'] || - window['mozRequestAnimationFrame'] || - window['webkitRequestAnimationFrame'] || - window['msRequestAnimationFrame'] || - window['oRequestAnimationFrame']; - if (!replaying) { - // Prepare to record - recorder = {}; - // Start - recorder.frameCounter = 0; // the frame counter is used to know when to replay events - recorder.start = function() { - alert("Starting recording! Don't forget to Recorder.finish() afterwards!"); - function count() { - recorder.frameCounter++; - raf(count); - } - count(); - recorder.started = true; - }; - // Math.random - recorder.randoms = []; - var random = Math.random; - Math.random = function() { - var ret = random(); - recorder.randoms.push(ret); - return ret; - }; - // Date.now, performance.now - recorder.dnows = []; - recorder.dnow = Date.now; - Date.now = function() { - var ret = recorder.dnow(); - recorder.dnows.push(ret); - return ret; - }; - recorder.pnows = []; - recorder.pnow = performance.now; - performance.now = function() { - var ret = recorder.pnow(); - recorder.pnows.push(ret); - return ret; - }; - // Events - recorder.devents = []; // document events - recorder.onEvent = function(which, callback) { - document['on' + which] = function(event) { - if (!recorder.started) return true; - event.frameCounter = recorder.frameCounter; - recorder.devents.push(event); - return callback(event); // XXX do we need to record the return value? - }; - }; - recorder.tevents = []; // custom-target events. Currently we assume a single such custom target (aside from document), e.g., a canvas for the game. - recorder.addListener = function(target, which, callback, arg) { - target.addEventListener(which, function(event) { - if (!recorder.started) return true; - event.frameCounter = recorder.frameCounter; - recorder.tevents.push(event); - return callback(event); // XXX do we need to record the return value? - }, arg); - }; - // Finish - recorder.finish = function() { - // Reorder data because pop() is faster than shift() - recorder.randoms.reverse(); - recorder.dnows.reverse(); - recorder.pnows.reverse(); - recorder.devents.reverse(); - recorder.tevents.reverse(); - // Make JSON.stringify work on data from native event objects (and only store relevant ones) - var importantProperties = { - type: 1, - movementX: 1, mozMovementX: 1, webkitMovementX: 1, - movementY: 1, mozMovementY: 1, webkitMovementY: 1, - detail: 1, - wheelDelta: 1, - pageX: 1, - pageY: 1, - button: 1, - keyCode: 1, - frameCounter: 1 - }; - function importantize(event) { - var ret = {}; - for (var prop in importantProperties) { - if (prop in event) { - ret[prop] = event[prop]; - } - } - return ret; - } - recorder.devents = recorder.devents.map(importantize); - recorder.tevents = recorder.tevents.map(importantize); - // Write out - alert('Writing out data, remember to save!'); - setTimeout(function() { - document.open(); - document.write(JSON.stringify(recorder)); - document.close(); - }, 0); - return '.'; - }; - } else { - // Load recording - var dataPath = window.location.search.substring(initLocation + init.length); - var baseURL = window.location.toString().replace('://', 'cheez999').split('?')[0].split('/').slice(0, -1).join('/').replace('cheez999', '://'); - if (baseURL[baseURL.length-1] != '/') baseURL += '/'; - var path = baseURL + dataPath; - alert('Loading replay from ' + path); - var request = new XMLHttpRequest(); - request.open('GET', path, false); - request.send(); - var raw = request.responseText; - raw = raw.substring(raw.indexOf('{'), raw.lastIndexOf('}')+1); // remove <html> etc - recorder = JSON.parse(raw); - // prepare to replay - // Start - recorder.frameCounter = 0; // the frame counter is used to know when to replay events - recorder.start = function() { - function count() { - recorder.frameCounter++; - raf(count); - // replay relevant events for this frame - while (recorder.devents.length && recorder.devents[recorder.devents.length-1].frameCounter <= recorder.frameCounter) { - var event = recorder.devents.pop(); - recorder['on' + event.type](event); - } - while (recorder.tevents.length && recorder.tevents[recorder.tevents.length-1].frameCounter <= recorder.frameCounter) { - var event = recorder.tevents.pop(); - recorder['event' + event.type](event); - } - } - count(); - }; - // Math.random - Math.random = function() { - i |