Interpreter fixes, enhancements, features ()

* Thread stepping rework; interp.redraw equivalent

* Add turbo mode and pause mode

* Yielding behavior to match Scratch 2.0

* Implement warp-mode procedure threads

* Add check for recursive call

* Inline wait block timer

* Revert to setInterval and always drawing

* Restore yielding in glide

* 30TPS compatibility mode

* 5-call count recursion limit

* Removing dead primitive code

* To simplify, access runtime.threads inline in `stepThreads`.

* Warp mode/timer fixes; recursive check fixes; clean-up

* Add basic single-stepping

* Add single-stepping speed slider

* Allow yielding threads to run in single-stepping

* Restore inactive threads tracking for block glows

* Add clock pausing during pause mode

* Documentation and clean-up throughout

* Don't look for block glows in `thread.topBlock`.

* Add null check for block glows; rename `_updateScriptGlows` to reflect block glowing

* Use the current executed block for glow, instead of stack

* Add more comments to `stepToProcedure`, and re-arrange to match 2.0

* Tweak to Blocks.prototype.getTopLevelScript

* Revert previous

* Fix threads array to be resilient to changes during `stepThreads`

* Restore inactive threads filtering

* Fix typo in "procedure"

* !! instead of == true
This commit is contained in:
Tim Mickel 2016-10-17 23:23:16 -04:00 committed by GitHub
parent 060d1ab2a5
commit e49f076fa1
14 changed files with 705 additions and 281 deletions
src/engine

View file

@ -27,8 +27,6 @@ function Runtime () {
// Bind event emitter
EventEmitter.call(this);
// State for the runtime
/**
* Target management and storage.
* @type {Array.<!Target>}
@ -45,33 +43,131 @@ function Runtime () {
/** @type {!Sequencer} */
this.sequencer = new Sequencer(this);
/**
* Storage container for flyout blocks.
* These will execute on `_editingTarget.`
* @type {!Blocks}
*/
this.flyoutBlocks = new Blocks();
/**
* Currently known editing target for the VM.
* @type {?Target}
*/
this._editingTarget = null;
/**
* Map to look up a block primitive's implementation function by its opcode.
* This is a two-step lookup: package name first, then primitive name.
* @type {Object.<string, Function>}
*/
this._primitives = {};
/**
* Map to look up hat blocks' metadata.
* Keys are opcode for hat, values are metadata objects.
* @type {Object.<string, Object>}
*/
this._hats = {};
/**
* Currently known values for edge-activated hats.
* Keys are block ID for the hat; values are the currently known values.
* @type {Object.<string, *>}
*/
this._edgeActivatedHatValues = {};
/**
* A list of script block IDs that were glowing during the previous frame.
* @type {!Array.<!string>}
*/
this._scriptGlowsPreviousFrame = [];
/**
* A list of block IDs that were glowing during the previous frame.
* @type {!Array.<!string>}
*/
this._blockGlowsPreviousFrame = [];
/**
* Currently known number of clones, used to enforce clone limit.
* @type {number}
*/
this._cloneCounter = 0;
/**
* Whether the project is in "turbo mode."
* @type {Boolean}
*/
this.turboMode = false;
/**
* Whether the project is in "pause mode."
* @type {Boolean}
*/
this.pauseMode = false;
/**
* Whether the project is in "compatibility mode" (30 TPS).
* @type {Boolean}
*/
this.compatibilityMode = false;
/**
* Whether the project is in "single stepping mode."
* @type {Boolean}
*/
this.singleStepping = false;
/**
* How fast in ms "single stepping mode" should run, in ms.
* Can be updated dynamically.
* @type {!number}
*/
this.singleStepInterval = 1000 / 10;
/**
* A reference to the current runtime stepping interval, set
* by a `setInterval`.
* @type {!number}
*/
this._steppingInterval = null;
/**
* Current length of a step.
* Changes as mode switches, and used by the sequencer to calculate
* WORK_TIME.
* @type {!number}
*/
this.currentStepTime = null;
/**
* Whether any primitive has requested a redraw.
* Affects whether `Sequencer.stepThreads` will yield
* after stepping each thread.
* Reset on every frame.
* @type {boolean}
*/
this.redrawRequested = false;
// Register all given block packages.
this._registerBlockPackages();
// Register and initialize "IO devices", containers for processing
// I/O related data.
/** @type {Object.<string, Object>} */
this.ioDevices = {
'clock': new Clock(),
'keyboard': new Keyboard(this),
'mouse': new Mouse(this)
};
this._scriptGlowsPreviousFrame = [];
this._editingTarget = null;
/**
* Currently known number of clones.
* @type {number}
*/
this._cloneCounter = 0;
}
/**
* Inherit from EventEmitter
*/
util.inherits(Runtime, EventEmitter);
/**
* Width of the stage, in pixels.
* @const {number}
@ -115,15 +211,15 @@ Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF';
Runtime.VISUAL_REPORT = 'VISUAL_REPORT';
/**
* Inherit from EventEmitter
*/
util.inherits(Runtime, EventEmitter);
/**
* How rapidly we try to step threads, in ms.
* How rapidly we try to step threads by default, in ms.
*/
Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
/**
* In compatibility mode, how rapidly we try to step threads, in ms.
*/
Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY = 1000 / 30;
/**
* How many clones can be created at a time.
* @const {number}
@ -175,9 +271,6 @@ Runtime.prototype.getOpcodeFunction = function (opcode) {
return this._primitives[opcode];
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* Return whether an opcode represents a hat block.
* @param {!string} opcode The opcode to look up.
@ -235,7 +328,7 @@ Runtime.prototype.attachRenderer = function (renderer) {
*/
Runtime.prototype._pushThread = function (id, target) {
var thread = new Thread(id);
thread.setTarget(target);
thread.target = target;
thread.pushStack(id);
this.threads.push(thread);
return thread;
@ -449,6 +542,10 @@ Runtime.prototype.stopAll = function () {
* inactive threads after each iteration.
*/
Runtime.prototype._step = function () {
if (this.pauseMode) {
// Don't do any execution while in pause mode.
return;
}
// Find all edge-activated hats, and add them to threads to be evaluated.
for (var hatType in this._hats) {
var hat = this._hats[hatType];
@ -456,37 +553,113 @@ Runtime.prototype._step = function () {
this.startHats(hatType);
}
}
var inactiveThreads = this.sequencer.stepThreads(this.threads);
this._updateScriptGlows();
for (var i = 0; i < inactiveThreads.length; i++) {
this._removeThread(inactiveThreads[i]);
this.redrawRequested = false;
var inactiveThreads = this.sequencer.stepThreads();
this._updateGlows(inactiveThreads);
};
/**
* Set the current editing target known by the runtime.
* @param {!Target} editingTarget New editing target.
*/
Runtime.prototype.setEditingTarget = function (editingTarget) {
this._editingTarget = editingTarget;
// Script glows must be cleared.
this._scriptGlowsPreviousFrame = [];
this._updateGlows();
};
/**
* Set whether we are in pause mode.
* @param {boolean} pauseModeOn True iff in pause mode.
*/
Runtime.prototype.setPauseMode = function (pauseModeOn) {
// Inform the project clock/timer to pause/resume its time.
if (this.ioDevices.clock) {
if (pauseModeOn && !this.pauseMode) {
this.ioDevices.clock.pause();
}
if (!pauseModeOn && this.pauseMode) {
this.ioDevices.clock.resume();
}
}
this.pauseMode = pauseModeOn;
};
/**
* Set whether we are in 30 TPS compatibility mode.
* @param {boolean} compatibilityModeOn True iff in compatibility mode.
*/
Runtime.prototype.setCompatibilityMode = function (compatibilityModeOn) {
this.compatibilityMode = compatibilityModeOn;
if (this._steppingInterval) {
self.clearInterval(this._steppingInterval);
this.start();
}
};
Runtime.prototype.setEditingTarget = function (editingTarget) {
this._scriptGlowsPreviousFrame = [];
this._editingTarget = editingTarget;
this._updateScriptGlows();
/**
* Set whether we are in single-stepping mode.
* @param {boolean} singleSteppingOn True iff in single-stepping mode.
*/
Runtime.prototype.setSingleSteppingMode = function (singleSteppingOn) {
this.singleStepping = singleSteppingOn;
if (this._steppingInterval) {
self.clearInterval(this._steppingInterval);
this.start();
}
};
Runtime.prototype._updateScriptGlows = function () {
/**
* Set the speed during single-stepping mode.
* @param {number} speed Interval length to step threads, in ms.
*/
Runtime.prototype.setSingleSteppingSpeed = function (speed) {
this.singleStepInterval = 1000 / speed;
if (this._steppingInterval) {
self.clearInterval(this._steppingInterval);
this.start();
}
};
/**
* Emit glows/glow clears for blocks and scripts after a single tick.
* Looks at `this.threads` and notices which have turned on/off new glows.
* @param {Array.<Thread>=} opt_extraThreads Optional list of inactive threads.
*/
Runtime.prototype._updateGlows = function (opt_extraThreads) {
var searchThreads = [];
searchThreads.push.apply(searchThreads, this.threads);
if (opt_extraThreads) {
searchThreads.push.apply(searchThreads, opt_extraThreads);
}
// Set of scripts that request a glow this frame.
var requestedGlowsThisFrame = [];
var requestedBlockGlowsThisFrame = [];
// Final set of scripts glowing during this frame.
var finalScriptGlows = [];
var finalBlockGlows = [];
// Find all scripts that should be glowing.
for (var i = 0; i < this.threads.length; i++) {
var thread = this.threads[i];
for (var i = 0; i < searchThreads.length; i++) {
var thread = searchThreads[i];
var target = thread.target;
if (thread.requestScriptGlowInFrame && target == this._editingTarget) {
var blockForThread = thread.peekStack() || thread.topBlock;
var script = target.blocks.getTopLevelScript(blockForThread);
if (!script) {
// Attempt to find in flyout blocks.
script = this.flyoutBlocks.getTopLevelScript(blockForThread);
if (target == this._editingTarget) {
var blockForThread = thread.blockGlowInFrame;
if (thread.requestScriptGlowInFrame) {
var script = target.blocks.getTopLevelScript(blockForThread);
if (!script) {
// Attempt to find in flyout blocks.
script = this.flyoutBlocks.getTopLevelScript(
blockForThread
);
}
if (script) {
requestedGlowsThisFrame.push(script);
}
}
if (script) {
requestedGlowsThisFrame.push(script);
// Only show block glows in single-stepping mode.
if (this.singleStepping && blockForThread) {
requestedBlockGlowsThisFrame.push(blockForThread);
}
}
}
@ -509,7 +682,30 @@ Runtime.prototype._updateScriptGlows = function () {
finalScriptGlows.push(currentFrameGlow);
}
}
for (var m = 0; m < this._blockGlowsPreviousFrame.length; m++) {
var previousBlockGlow = this._blockGlowsPreviousFrame[m];
if (requestedBlockGlowsThisFrame.indexOf(previousBlockGlow) < 0) {
// Glow turned off.
try {
this.glowBlock(previousBlockGlow, false);
} catch (e) {
// Block has been removed.
}
} else {
// Still glowing.
finalBlockGlows.push(previousBlockGlow);
}
}
for (var p = 0; p < requestedBlockGlowsThisFrame.length; p++) {
var currentBlockFrameGlow = requestedBlockGlowsThisFrame[p];
if (this._blockGlowsPreviousFrame.indexOf(currentBlockFrameGlow) < 0) {
// Glow turned on.
this.glowBlock(currentBlockFrameGlow, true);
finalBlockGlows.push(currentBlockFrameGlow);
}
}
this._scriptGlowsPreviousFrame = finalScriptGlows;
this._blockGlowsPreviousFrame = finalBlockGlows;
};
/**
@ -617,22 +813,38 @@ Runtime.prototype.getTargetForStage = function () {
}
};
/**
* Tell the runtime to request a redraw.
* Use after a clone/sprite has completed some visible operation on the stage.
*/
Runtime.prototype.requestRedraw = function () {
this.redrawRequested = true;
};
/**
* Handle an animation frame from the main thread.
*/
Runtime.prototype.animationFrame = function () {
if (this.renderer) {
// @todo: Only render when this.redrawRequested or clones rendered.
this.renderer.draw();
}
};
/**
* Set up timers to repeatedly step in a browser
* Set up timers to repeatedly step in a browser.
*/
Runtime.prototype.start = function () {
self.setInterval(function() {
var interval = Runtime.THREAD_STEP_INTERVAL;
if (this.singleStepping) {
interval = this.singleStepInterval;
} else if (this.compatibilityMode) {
interval = Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY;
}
this.currentStepTime = interval;
this._steppingInterval = self.setInterval(function() {
this._step();
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
}.bind(this), interval);
};
module.exports = Runtime;