From 716f58acf92e400f6e454cbbfcb1217fcba8a6a7 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sat, 26 Oct 2024 09:21:58 +0800 Subject: [PATCH 01/12] separate render frames from logic frames Signed-off-by: FurryR --- src/engine/runtime.js | 42 ++++++------- src/engine/tw-frame-loop.js | 120 +++++++++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 47 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 0f0c74a86f3..98fb67c1407 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -193,12 +193,6 @@ let stepProfilerId = -1; */ let stepThreadsProfilerId = -1; -/** - * Numeric ID for RenderWebGL.draw in Profiler instances. - * @type {number} - */ -let rendererDrawProfilerId = -1; - /** * Manages targets, scripts, and the sequencer. * @constructor @@ -2548,24 +2542,24 @@ class Runtime extends EventEmitter { // Store threads that completed this iteration for testing and other // internal purposes. this._lastStepDoneThreads = doneThreads; - if (this.renderer) { - // @todo: Only render when this.redrawRequested or clones rendered. - if (this.profiler !== null) { - if (rendererDrawProfilerId === -1) { - rendererDrawProfilerId = this.profiler.idByName('RenderWebGL.draw'); - } - this.profiler.start(rendererDrawProfilerId); - } - // tw: do not draw if document is hidden or a rAF loop is running - // Checking for the animation frame loop is more reliable than using - // interpolationEnabled in some edge cases - if (!document.hidden && !this.frameLoop._interpolationAnimation) { - this.renderer.draw(); - } - if (this.profiler !== null) { - this.profiler.stop(); - } - } + // if (this.renderer) { + // // @todo: Only render when this.redrawRequested or clones rendered. + // if (this.profiler !== null) { + // if (rendererDrawProfilerId === -1) { + // rendererDrawProfilerId = this.profiler.idByName('RenderWebGL.draw'); + // } + // this.profiler.start(rendererDrawProfilerId); + // } + // // tw: do not draw if document is hidden or a rAF loop is running + // // Checking for the animation frame loop is more reliable than using + // // interpolationEnabled in some edge cases + // if (!document.hidden && !this.frameLoop._interpolationAnimation) { + // this.renderer.draw(); + // } + // if (this.profiler !== null) { + // this.profiler.stop(); + // } + // } if (this._refreshTargets) { this.emit(Runtime.TARGETS_UPDATE, false /* Don't emit project changed */); diff --git a/src/engine/tw-frame-loop.js b/src/engine/tw-frame-loop.js index 45423739623..7c5cd839b11 100644 --- a/src/engine/tw-frame-loop.js +++ b/src/engine/tw-frame-loop.js @@ -1,26 +1,50 @@ // Due to the existence of features such as interpolation and "0 FPS" being treated as "screen refresh rate", // The VM loop logic has become much more complex +/** + * Numeric ID for RenderWebGL.draw in Profiler instances. + * @type {number} + */ +let rendererDrawProfilerId = -1; + // Use setTimeout to polyfill requestAnimationFrame in Node.js environments -const _requestAnimationFrame = typeof requestAnimationFrame === 'function' ? - requestAnimationFrame : - (f => setTimeout(f, 1000 / 60)); -const _cancelAnimationFrame = typeof requestAnimationFrame === 'function' ? - cancelAnimationFrame : - clearTimeout; +const _requestAnimationFrame = + typeof requestAnimationFrame === 'function' ? + requestAnimationFrame : + f => setTimeout(f, 1000 / 60); +const _cancelAnimationFrame = + typeof requestAnimationFrame === 'function' ? + cancelAnimationFrame : + clearTimeout; -const animationFrameWrapper = callback => { +const taskWrapper = (callback, requestFn, cancelFn) => { let id; + let cancelled = false; const handle = () => { - id = _requestAnimationFrame(handle); + id = requestFn(handle); callback(); }; - const cancel = () => _cancelAnimationFrame(id); - id = _requestAnimationFrame(handle); + const cancel = () => { + if (!cancelled) cancelFn(id); + cancelled = true; + }; + id = requestFn(handle); return { cancel }; }; +// const animationFrameWrapper = callback => { +// let id; +// const handle = () => { +// id = _requestAnimationFrame(handle); +// callback(); +// }; +// const cancel = () => _cancelAnimationFrame(id); +// id = _requestAnimationFrame(handle); +// return { +// cancel +// }; +// }; class FrameLoop { constructor (runtime) { @@ -28,13 +52,19 @@ class FrameLoop { this.running = false; this.setFramerate(30); this.setInterpolation(false); - - this.stepCallback = this.stepCallback.bind(this); - this.interpolationCallback = this.interpolationCallback.bind(this); + this._lastRenderTime = 0; this._stepInterval = null; - this._interpolationAnimation = null; - this._stepAnimation = null; + // this._interpolationAnimation = null; + this._renderInterval = null; + } + + _updateRenderTime () { + this._lastRenderTime = this._getRenderTime(); + } + + _getRenderTime () { + return (global.performance || Date).now(); } setFramerate (fps) { @@ -51,8 +81,40 @@ class FrameLoop { this.runtime._step(); } - interpolationCallback () { - this.runtime._renderInterpolatedPositions(); + renderCallback () { + if (this.runtime.renderer) { + if (this.interpolation && this.framerate !== 0) { + if (!document.hidden) { + this.runtime._renderInterpolatedPositions(); + } + } else if ( + this._getRenderTime() - this._lastRenderTime >= + this.runtime.currentStepTime + ) { + // @todo: Only render when this.redrawRequested or clones rendered. + if (this.runtime.profiler !== null) { + if (rendererDrawProfilerId === -1) { + rendererDrawProfilerId = + this.profiler.idByName('RenderWebGL.draw'); + } + this.runtime.profiler.start(rendererDrawProfilerId); + } + // tw: do not draw if document is hidden or a rAF loop is running + // Checking for the animation frame loop is more reliable than using + // interpolationEnabled in some edge cases + if (!document.hidden) { + this.runtime.renderer.draw(); + } + if (this.runtime.profiler !== null) { + this.runtime.profiler.stop(); + } + } + if (this.framerate === 0) { + this.runtime.currentStepTime = + this._getRenderTime() - this._lastRenderTime; + } + this._updateRenderTime(); + } } _restart () { @@ -65,14 +127,26 @@ class FrameLoop { start () { this.running = true; if (this.framerate === 0) { - this._stepAnimation = animationFrameWrapper(this.stepCallback); - this.runtime.currentStepTime = 1000 / 60; + this._stepInterval = this.renderInterval = taskWrapper( + () => { + this.stepCallback(); + this.renderCallback(); + }, + _requestAnimationFrame, + _cancelAnimationFrame + ); } else { // Interpolation should never be enabled when framerate === 0 as that's just redundant - if (this.interpolation) { - this._interpolationAnimation = animationFrameWrapper(this.interpolationCallback); - } - this._stepInterval = setInterval(this.stepCallback, 1000 / this.framerate); + this._renderInterval = taskWrapper( + () => this.renderCallback(), + _requestAnimationFrame, + _cancelAnimationFrame + ); + this._stepInterval = taskWrapper( + this.stepCallback, + fn => setInterval(fn, 1000 / this.framerate), + clearInterval + ); this.runtime.currentStepTime = 1000 / this.framerate; } } From 7b9f4d4847275b743b2597fe260620af01773f74 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sat, 26 Oct 2024 12:59:16 +0800 Subject: [PATCH 02/12] support framerate over 250 Signed-off-by: FurryR --- src/blocks/scratch3_procedures.js | 8 ++- src/compiler/irgen.js | 5 ++ src/compiler/jsgen.js | 3 + src/engine/runtime.js | 21 ++++--- src/engine/tw-frame-loop.js | 92 ++++++++++++++++--------------- 5 files changed, 75 insertions(+), 54 deletions(-) diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js index 679fdf128e8..bcf3b3dcc8d 100644 --- a/src/blocks/scratch3_procedures.js +++ b/src/blocks/scratch3_procedures.js @@ -103,10 +103,14 @@ class Scratch3ProcedureBlocks { argumentReporterStringNumber (args, util) { const value = util.getParam(args.VALUE); if (value === null) { - // tw: support legacy block - if (String(args.VALUE).toLowerCase() === 'last key pressed') { + // tw: support legacy block and screen refresh time + const lowercaseValue = String(args.VALUE).toLowerCase(); + if (lowercaseValue === 'last key pressed') { return util.ioQuery('keyboard', 'getLastKeyPressed'); } + if (lowercaseValue === 'screen refresh time') { + return this.runtime.screenRefreshTime; + } // When the parameter is not found in the most recent procedure // call, the default is always 0. return 0; diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index b1b8ded5e38..2e6e57df115 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -194,6 +194,11 @@ class ScriptTreeGenerator { kind: 'tw.lastKeyPressed' }; } + if (name.toLowerCase() === 'screen refresh time') { + return { + kind: 'tw.screenRefreshTime' + }; + } } if (index === -1) { return { diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 914e5cc72b1..810f7b4888d 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -739,6 +739,9 @@ class JSGenerator { case 'tw.lastKeyPressed': return new TypedInput('runtime.ioDevices.keyboard.getLastKeyPressed()', TYPE_STRING); + case 'tw.screenRefreshTime': + return new TypedInput('(runtime.screenRefreshTime / 1000)', TYPE_NUMBER); + case 'var.get': return this.descendVariable(node.variable); diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 98fb67c1407..899ba8dddd4 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -439,6 +439,14 @@ class Runtime extends EventEmitter { */ this.platform = Object.assign({}, platform); + /** + * Screen refresh time speculated from screen refresh rate, in milliseconds. + * Indicates time passed between two screen refreshments. + * Based on site isolation status, the resolution could be ~0.1ms or lower. + * @type {!number} + */ + this.screenRefreshTime = 0; + this._initScratchLink(); this.resetRunId(); @@ -463,7 +471,6 @@ class Runtime extends EventEmitter { this.debug = false; - this._lastStepTime = Date.now(); this.interpolationEnabled = false; this._defaultStoredSettings = this._generateAllProjectOptions(); @@ -2470,8 +2477,8 @@ class Runtime extends EventEmitter { } _renderInterpolatedPositions () { - const frameStarted = this._lastStepTime; - const now = Date.now(); + const frameStarted = this.frameLoop._lastRenderTime; + const now = this.frameLoop.now(); const timeSinceStart = now - frameStarted; const progressInFrame = Math.min(1, Math.max(0, timeSinceStart / this.currentStepTime)); @@ -2576,9 +2583,9 @@ class Runtime extends EventEmitter { this.profiler.reportFrames(); } - if (this.interpolationEnabled) { - this._lastStepTime = Date.now(); - } + // if (this.interpolationEnabled) { + // this._lastStepTime = Date.now(); + // } } /** @@ -2639,7 +2646,7 @@ class Runtime extends EventEmitter { setFramerate (framerate) { // Setting framerate to anything greater than this is unnecessary and can break the sequencer // Additionally, the JS spec says intervals can't run more than once every 4ms (250/s) anyways - if (framerate > 250) framerate = 250; + // if (framerate > 250) framerate = 250; // Convert negative framerates to 1FPS // Note that 0 is a special value which means "matching device screen refresh rate" if (framerate < 0) framerate = 1; diff --git a/src/engine/tw-frame-loop.js b/src/engine/tw-frame-loop.js index 7c5cd839b11..64d3a84202d 100644 --- a/src/engine/tw-frame-loop.js +++ b/src/engine/tw-frame-loop.js @@ -17,12 +17,12 @@ const _cancelAnimationFrame = cancelAnimationFrame : clearTimeout; -const taskWrapper = (callback, requestFn, cancelFn) => { +const taskWrapper = (callback, requestFn, cancelFn, manualInterval) => { let id; let cancelled = false; - const handle = () => { - id = requestFn(handle); - callback(); + const handle = (...args) => { + if (manualInterval) id = requestFn(handle); + callback(...args); }; const cancel = () => { if (!cancelled) cancelFn(id); @@ -33,18 +33,6 @@ const taskWrapper = (callback, requestFn, cancelFn) => { cancel }; }; -// const animationFrameWrapper = callback => { -// let id; -// const handle = () => { -// id = _requestAnimationFrame(handle); -// callback(); -// }; -// const cancel = () => _cancelAnimationFrame(id); -// id = _requestAnimationFrame(handle); -// return { -// cancel -// }; -// }; class FrameLoop { constructor (runtime) { @@ -53,18 +41,14 @@ class FrameLoop { this.setFramerate(30); this.setInterpolation(false); this._lastRenderTime = 0; + this._lastStepTime = 0; this._stepInterval = null; - // this._interpolationAnimation = null; this._renderInterval = null; } - _updateRenderTime () { - this._lastRenderTime = this._getRenderTime(); - } - - _getRenderTime () { - return (global.performance || Date).now(); + now () { + return (performance || Date).now(); } setFramerate (fps) { @@ -78,17 +62,28 @@ class FrameLoop { } stepCallback () { + const now = this.now(); this.runtime._step(); + this._lastStepTime = now; + } + + stepImmediateCallback () { + const now = this.now(); + if (now - this._lastStepTime >= this.runtime.currentStepTime) { + this.runtime._step(); + this._lastStepTime = now; + } } renderCallback () { if (this.runtime.renderer) { + const renderTime = this.now(); if (this.interpolation && this.framerate !== 0) { if (!document.hidden) { this.runtime._renderInterpolatedPositions(); } } else if ( - this._getRenderTime() - this._lastRenderTime >= + renderTime - this._lastRenderTime >= this.runtime.currentStepTime ) { // @todo: Only render when this.redrawRequested or clones rendered. @@ -109,11 +104,11 @@ class FrameLoop { this.runtime.profiler.stop(); } } + this.runtime.screenRefreshTime = renderTime - this._lastRenderTime; // Screen refresh time (from rate) if (this.framerate === 0) { - this.runtime.currentStepTime = - this._getRenderTime() - this._lastRenderTime; + this.runtime.currentStepTime = this.runtime.screenRefreshTime; } - this._updateRenderTime(); + this._lastRenderTime = renderTime; } } @@ -127,41 +122,48 @@ class FrameLoop { start () { this.running = true; if (this.framerate === 0) { - this._stepInterval = this.renderInterval = taskWrapper( + this._stepInterval = this._renderInterval = taskWrapper( () => { this.stepCallback(); this.renderCallback(); }, _requestAnimationFrame, - _cancelAnimationFrame + _cancelAnimationFrame, + true ); } else { // Interpolation should never be enabled when framerate === 0 as that's just redundant this._renderInterval = taskWrapper( - () => this.renderCallback(), + this.renderCallback.bind(this), _requestAnimationFrame, - _cancelAnimationFrame - ); - this._stepInterval = taskWrapper( - this.stepCallback, - fn => setInterval(fn, 1000 / this.framerate), - clearInterval + _cancelAnimationFrame, + true ); + if (this.framerate > 250) { + // High precision implementation via setImmediate + // bug: very unfriendly to DevTools + this._stepInterval = taskWrapper( + this.stepImmediateCallback.bind(this), + global.setImmediate, + global.clearImmediate, + true + ); + } else { + this._stepInterval = taskWrapper( + this.stepCallback.bind(this), + fn => setInterval(fn, 1000 / this.framerate), + clearInterval, + false + ); + } this.runtime.currentStepTime = 1000 / this.framerate; } } stop () { this.running = false; - clearInterval(this._stepInterval); - if (this._interpolationAnimation) { - this._interpolationAnimation.cancel(); - } - if (this._stepAnimation) { - this._stepAnimation.cancel(); - } - this._interpolationAnimation = null; - this._stepAnimation = null; + this._renderInterval.cancel(); + this._stepInterval.cancel(); } } From 8fc70154bc3c55fdcc7d6fcc353fb974e1886625 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sat, 26 Oct 2024 13:00:43 +0800 Subject: [PATCH 03/12] clean up code Signed-off-by: FurryR --- src/engine/runtime.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 899ba8dddd4..e300ba9683a 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2549,24 +2549,6 @@ class Runtime extends EventEmitter { // Store threads that completed this iteration for testing and other // internal purposes. this._lastStepDoneThreads = doneThreads; - // if (this.renderer) { - // // @todo: Only render when this.redrawRequested or clones rendered. - // if (this.profiler !== null) { - // if (rendererDrawProfilerId === -1) { - // rendererDrawProfilerId = this.profiler.idByName('RenderWebGL.draw'); - // } - // this.profiler.start(rendererDrawProfilerId); - // } - // // tw: do not draw if document is hidden or a rAF loop is running - // // Checking for the animation frame loop is more reliable than using - // // interpolationEnabled in some edge cases - // if (!document.hidden && !this.frameLoop._interpolationAnimation) { - // this.renderer.draw(); - // } - // if (this.profiler !== null) { - // this.profiler.stop(); - // } - // } if (this._refreshTargets) { this.emit(Runtime.TARGETS_UPDATE, false /* Don't emit project changed */); From dca76dc76ced5bf64eddd0eeb27ac99ad3402fd1 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sat, 26 Oct 2024 13:03:38 +0800 Subject: [PATCH 04/12] add restrictions to high precision implementation Signed-off-by: FurryR --- src/engine/tw-frame-loop.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/engine/tw-frame-loop.js b/src/engine/tw-frame-loop.js index 64d3a84202d..5daa753d69a 100644 --- a/src/engine/tw-frame-loop.js +++ b/src/engine/tw-frame-loop.js @@ -20,9 +20,9 @@ const _cancelAnimationFrame = const taskWrapper = (callback, requestFn, cancelFn, manualInterval) => { let id; let cancelled = false; - const handle = (...args) => { + const handle = () => { if (manualInterval) id = requestFn(handle); - callback(...args); + callback(); }; const cancel = () => { if (!cancelled) cancelFn(id); @@ -123,10 +123,10 @@ class FrameLoop { this.running = true; if (this.framerate === 0) { this._stepInterval = this._renderInterval = taskWrapper( - () => { + (() => { this.stepCallback(); this.renderCallback(); - }, + }), _requestAnimationFrame, _cancelAnimationFrame, true @@ -139,8 +139,8 @@ class FrameLoop { _cancelAnimationFrame, true ); - if (this.framerate > 250) { - // High precision implementation via setImmediate + if (this.framerate > 250 && global.setImmediate && global.clearImmediate) { + // High precision implementation via setImmediate (polyfilled) // bug: very unfriendly to DevTools this._stepInterval = taskWrapper( this.stepImmediateCallback.bind(this), From 1aa486b177f1f8c4c9c24ef533f55d17919611ce Mon Sep 17 00:00:00 2001 From: FurryR Date: Sat, 26 Oct 2024 13:26:49 +0800 Subject: [PATCH 05/12] fix interpolation Signed-off-by: FurryR --- src/engine/runtime.js | 6 +----- src/engine/tw-frame-loop.js | 8 +++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index e300ba9683a..a5ddfa51c4d 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2477,7 +2477,7 @@ class Runtime extends EventEmitter { } _renderInterpolatedPositions () { - const frameStarted = this.frameLoop._lastRenderTime; + const frameStarted = this.frameLoop._lastStepTime; const now = this.frameLoop.now(); const timeSinceStart = now - frameStarted; const progressInFrame = Math.min(1, Math.max(0, timeSinceStart / this.currentStepTime)); @@ -2564,10 +2564,6 @@ class Runtime extends EventEmitter { this.profiler.stop(); this.profiler.reportFrames(); } - - // if (this.interpolationEnabled) { - // this._lastStepTime = Date.now(); - // } } /** diff --git a/src/engine/tw-frame-loop.js b/src/engine/tw-frame-loop.js index 5daa753d69a..630e243d29a 100644 --- a/src/engine/tw-frame-loop.js +++ b/src/engine/tw-frame-loop.js @@ -62,16 +62,14 @@ class FrameLoop { } stepCallback () { - const now = this.now(); this.runtime._step(); - this._lastStepTime = now; + this._lastStepTime = this.now(); } stepImmediateCallback () { - const now = this.now(); - if (now - this._lastStepTime >= this.runtime.currentStepTime) { + if (this.now() - this._lastStepTime >= this.runtime.currentStepTime) { this.runtime._step(); - this._lastStepTime = now; + this._lastStepTime = this.now(); } } From ea7fd176feeb35aa5e1e7fbdfaa084a3e61bf7f8 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sat, 26 Oct 2024 14:39:52 +0800 Subject: [PATCH 06/12] fix renderTime Signed-off-by: FurryR --- src/engine/tw-frame-loop.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/engine/tw-frame-loop.js b/src/engine/tw-frame-loop.js index 630e243d29a..3b30fd0d617 100644 --- a/src/engine/tw-frame-loop.js +++ b/src/engine/tw-frame-loop.js @@ -80,6 +80,8 @@ class FrameLoop { if (!document.hidden) { this.runtime._renderInterpolatedPositions(); } + this.runtime.screenRefreshTime = renderTime - this._lastRenderTime; // Screen refresh time (from rate) + this._lastRenderTime = renderTime; } else if ( renderTime - this._lastRenderTime >= this.runtime.currentStepTime @@ -101,12 +103,12 @@ class FrameLoop { if (this.runtime.profiler !== null) { this.runtime.profiler.stop(); } + this.runtime.screenRefreshTime = renderTime - this._lastRenderTime; // Screen refresh time (from rate) + this._lastRenderTime = renderTime; + if (this.framerate === 0) { + this.runtime.currentStepTime = this.runtime.screenRefreshTime; + } } - this.runtime.screenRefreshTime = renderTime - this._lastRenderTime; // Screen refresh time (from rate) - if (this.framerate === 0) { - this.runtime.currentStepTime = this.runtime.screenRefreshTime; - } - this._lastRenderTime = renderTime; } } From 75b8ee708722cecd4ee609314f2458b6b90e0693 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sat, 26 Oct 2024 16:04:10 +0800 Subject: [PATCH 07/12] fix sequencer Signed-off-by: FurryR --- src/engine/runtime.js | 3 --- src/engine/sequencer.js | 5 ++++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index a5ddfa51c4d..ed0b8f312cc 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2622,9 +2622,6 @@ class Runtime extends EventEmitter { * @param {number} framerate Target frames per second */ setFramerate (framerate) { - // Setting framerate to anything greater than this is unnecessary and can break the sequencer - // Additionally, the JS spec says intervals can't run more than once every 4ms (250/s) anyways - // if (framerate > 250) framerate = 250; // Convert negative framerates to 1FPS // Note that 0 is a special value which means "matching device screen refresh rate" if (framerate < 0) framerate = 1; diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 1f3bbc5ed63..840a96cd8a0 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -88,7 +88,6 @@ class Sequencer { // 3. Either turbo mode, or no redraw has been requested by a primitive. while (this.runtime.threads.length > 0 && numActiveThreads > 0 && - this.timer.timeElapsed() < WORK_TIME && (this.runtime.turboMode || !this.runtime.redrawRequested)) { if (this.runtime.profiler !== null) { if (stepThreadsInnerProfilerId === -1) { @@ -164,6 +163,10 @@ class Sequencer { } this.runtime.threads.length = nextActiveThread; } + + // tw: Detect timer here so the sequencer won't break when FPS is greater than 1000 + // and performance.now() is not available. + if (this.timer.timeElapsed() < WORK_TIME) break; } this.activeThread = null; From f6445797c963e9e1814bfbe8c90746394503d84b Mon Sep 17 00:00:00 2001 From: FurryR Date: Sat, 26 Oct 2024 16:04:26 +0800 Subject: [PATCH 08/12] typo Signed-off-by: FurryR --- src/engine/sequencer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 840a96cd8a0..f9eff015e02 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -166,7 +166,7 @@ class Sequencer { // tw: Detect timer here so the sequencer won't break when FPS is greater than 1000 // and performance.now() is not available. - if (this.timer.timeElapsed() < WORK_TIME) break; + if (this.timer.timeElapsed() >= WORK_TIME) break; } this.activeThread = null; From 9b1f56e7a8e161828790f4367b2a16bd013f47b2 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sat, 26 Oct 2024 16:39:50 +0800 Subject: [PATCH 09/12] fix tests Signed-off-by: FurryR --- src/engine/sequencer.js | 3 +++ src/engine/tw-frame-loop.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index f9eff015e02..b493694d066 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -82,6 +82,9 @@ class Sequencer { // Whether `stepThreads` has run through a full single tick. let ranFirstTick = false; const doneThreads = []; + + // tw: If this happens, the runtime is in initialization, do not execute any thread. + if (this.runtime.currentStepTime === 0) return []; // Conditions for continuing to stepping threads: // 1. We must have threads in the list, and some must be active. // 2. Time elapsed must be less than WORK_TIME. diff --git a/src/engine/tw-frame-loop.js b/src/engine/tw-frame-loop.js index 3b30fd0d617..52ddfbcf8aa 100644 --- a/src/engine/tw-frame-loop.js +++ b/src/engine/tw-frame-loop.js @@ -131,6 +131,7 @@ class FrameLoop { _cancelAnimationFrame, true ); + this.runtime.currentStepTime = 0; } else { // Interpolation should never be enabled when framerate === 0 as that's just redundant this._renderInterval = taskWrapper( From e8003be54941142a4e1f9ae64074b3e0a8beb378 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sun, 27 Oct 2024 13:06:26 +0800 Subject: [PATCH 10/12] replace argument reporter with sensing Signed-off-by: FurryR --- src/blocks/scratch3_procedures.js | 8 ++------ src/blocks/scratch3_sensing.js | 1 + src/compiler/irgen.js | 9 ++++----- src/compiler/jsgen.js | 5 ++--- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js index bcf3b3dcc8d..679fdf128e8 100644 --- a/src/blocks/scratch3_procedures.js +++ b/src/blocks/scratch3_procedures.js @@ -103,14 +103,10 @@ class Scratch3ProcedureBlocks { argumentReporterStringNumber (args, util) { const value = util.getParam(args.VALUE); if (value === null) { - // tw: support legacy block and screen refresh time - const lowercaseValue = String(args.VALUE).toLowerCase(); - if (lowercaseValue === 'last key pressed') { + // tw: support legacy block + if (String(args.VALUE).toLowerCase() === 'last key pressed') { return util.ioQuery('keyboard', 'getLastKeyPressed'); } - if (lowercaseValue === 'screen refresh time') { - return this.runtime.screenRefreshTime; - } // When the parameter is not found in the most recent procedure // call, the default is always 0. return 0; diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js index 15c071cb7f5..f70ea3532d0 100644 --- a/src/blocks/scratch3_sensing.js +++ b/src/blocks/scratch3_sensing.js @@ -244,6 +244,7 @@ class Scratch3SensingBlocks { current (args) { const menuOption = Cast.toString(args.CURRENTMENU).toLowerCase(); + if (menuOption === 'refreshtime') return (this.runtime.screenRefreshTime / 1000); const date = new Date(); switch (menuOption) { case 'year': return date.getFullYear(); diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index 2e6e57df115..f999b0d4dce 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -194,11 +194,6 @@ class ScriptTreeGenerator { kind: 'tw.lastKeyPressed' }; } - if (name.toLowerCase() === 'screen refresh time') { - return { - kind: 'tw.screenRefreshTime' - }; - } } if (index === -1) { return { @@ -589,6 +584,10 @@ class ScriptTreeGenerator { return { kind: 'sensing.second' }; + case 'refreshtime': + return { + kind: 'sensing.refrehTime' + }; } return { kind: 'constant', diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 810f7b4888d..f4e542f42cf 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -724,6 +724,8 @@ class JSGenerator { } case 'sensing.second': return new TypedInput(`(new Date().getSeconds())`, TYPE_NUMBER); + case 'sensing.refreshTime': + return new TypedInput('(runtime.screenRefreshTime / 1000)', TYPE_NUMBER); case 'sensing.touching': return new TypedInput(`target.isTouchingObject(${this.descendInput(node.object).asUnknown()})`, TYPE_BOOLEAN); case 'sensing.touchingColor': @@ -739,9 +741,6 @@ class JSGenerator { case 'tw.lastKeyPressed': return new TypedInput('runtime.ioDevices.keyboard.getLastKeyPressed()', TYPE_STRING); - case 'tw.screenRefreshTime': - return new TypedInput('(runtime.screenRefreshTime / 1000)', TYPE_NUMBER); - case 'var.get': return this.descendVariable(node.variable); From 0fd961bd86f33dd33487c56bb639b24cfc788ef1 Mon Sep 17 00:00:00 2001 From: FurryR Date: Sun, 27 Oct 2024 18:22:46 +0800 Subject: [PATCH 11/12] fix vsync Signed-off-by: FurryR --- src/engine/tw-frame-loop.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/tw-frame-loop.js b/src/engine/tw-frame-loop.js index 52ddfbcf8aa..aa7054ac372 100644 --- a/src/engine/tw-frame-loop.js +++ b/src/engine/tw-frame-loop.js @@ -83,6 +83,7 @@ class FrameLoop { this.runtime.screenRefreshTime = renderTime - this._lastRenderTime; // Screen refresh time (from rate) this._lastRenderTime = renderTime; } else if ( + this.framerate === 0 || renderTime - this._lastRenderTime >= this.runtime.currentStepTime ) { From 84967f48c6a2b089281436dd7489a0b9bfde18ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E8=B0=B7=20=E5=87=8C?= Date: Wed, 25 Dec 2024 18:25:42 +0800 Subject: [PATCH 12/12] Fix profiler undefined --- src/engine/tw-frame-loop.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/tw-frame-loop.js b/src/engine/tw-frame-loop.js index aa7054ac372..70ec721858a 100644 --- a/src/engine/tw-frame-loop.js +++ b/src/engine/tw-frame-loop.js @@ -91,7 +91,7 @@ class FrameLoop { if (this.runtime.profiler !== null) { if (rendererDrawProfilerId === -1) { rendererDrawProfilerId = - this.profiler.idByName('RenderWebGL.draw'); + this.runtime.profiler.idByName('RenderWebGL.draw'); } this.runtime.profiler.start(rendererDrawProfilerId); }