<commit_before>/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
 * Copyright 2013 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*global rgbaObjToStr, FirefoxCom, Timer, FrameCounter, metrics, coreOptions, OptionSet, Option, appendToFrameTerminal, frameWriter*/

var rendererOptions = coreOptions.register(new OptionSet("Renderer Options"));
var traceRenderer = rendererOptions.register(new Option("tr", "traceRenderer", "number", 0, "trace renderer execution"));
var disablePreVisitor = rendererOptions.register(new Option("dpv", "disablePreVisitor", "boolean", false, "disable pre visitor"));
var disableRenderVisitor = rendererOptions.register(new Option("drv", "disableRenderVisitor", "boolean", false, "disable render visitor"));
var disableMouseVisitor = rendererOptions.register(new Option("dmv", "disableMouseVisitor", "boolean", false, "disable mouse visitor"));
var showRedrawRegions = rendererOptions.register(new Option("rr", "showRedrawRegions", "boolean", false, "show redraw regions"));

var CanvasCache = {
  cache: [],
  getCanvas: function getCanvas(protoCanvas) {
    var tempCanvas = this.cache.shift();
    if (!tempCanvas) {
      tempCanvas = {
        canvas: document.createElement('canvas')
      };
      tempCanvas.ctx = tempCanvas.canvas.getContext('kanvas-2d');
    }
    tempCanvas.canvas.width = protoCanvas.width;
    tempCanvas.canvas.height = protoCanvas.height;
    tempCanvas.ctx.save();
    return tempCanvas;
  },
  releaseCanvas: function releaseCanvas(tempCanvas) {
    tempCanvas.ctx.restore();
    this.cache.push(tempCanvas);
  }
};

function isCanvasVisible(canvas) {
  if (canvas.ownerDocument.hidden) { // Page Visibility API
    return false;
  }
  if (canvas.mozVisible === false) { // HACK Canvas Visibility API
    return false;
  }
  return true;
}

function visitContainer(container, visitor) {
  var children = container._children;

  visitor.childrenStart(container);

  for (var i = 0, n = children.length; i < n; i++) {
    var child = children[i];
    if (!child) {
      continue;
    }

    if (visitor.ignoreVisibleAttribute || (child._visible && !child._maskedObject)) {
      var isContainer = flash.display.DisplayObjectContainer.class.isInstanceOf(child) ||
                        flash.display.SimpleButton.class.isInstanceOf(child);

      visitor.visit(child, isContainer, visitContainer);
    }
  }

  visitor.childrenEnd(container);
}

function RenderVisitor(root, ctx, refreshStage) {
  this.root = root;
  this.ctx = ctx;
  this.depth = 0;
  this.refreshStage = refreshStage;

  this.clipDepth = null;
  this.clipStack = null;
}
RenderVisitor.prototype = {
  ignoreVisibleAttribute: false,
  start: function () {
    visitContainer(this.root, this);
  },
  startFragment: function() {
    var isContainer = flash.display.DisplayObjectContainer.class.isInstanceOf(this.root) ||
                      flash.display.SimpleButton.class.isInstanceOf(this.root);

    this.visit(this.root, isContainer, visitContainer);
  },
  childrenStart: function(parent) {
    if (this.depth === 0) {
      var ctx = this.ctx;

      ctx.save();

      if (!this.refreshStage) {
        ctx.clip();
      }

      var bgcolor = this.root._color;
      if (bgcolor) {
        if (bgcolor.alpha < 255) {
          ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        }
        if (bgcolor.alpha > 0) {
          ctx.fillStyle = rgbaObjToStr(bgcolor);
          if (this.refreshStage) {
            ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
          } else {
            ctx.fill();
          }
        }
      }

      ctx.mozFillRule = 'evenodd';
    }
    this.depth++;

    if (this.clipDepth && this.clipDepth.length > 0) {
      // saving the parent clipping state
      this.clipStack = {
        depth: this.depth,
        clip: this.clipDepth,
        next: this.clipStack
      };
      this.clipDepth = null;
    }
  },
  childrenEnd: function(parent) {
    if (this.clipDepth) {
      // removing existing clippings
      while (this.clipDepth.length > 0) {
        this.clipDepth.pop();
        this.ctx.restore();
      }
      this.clipDepth = null;
    }
    // checking if we saved the parent clipping state
    if (this.clipStack && this.clipStack.depth === this.depth) {
      this.clipDepth = this.clipStack.clip;
      this.clipStack = this.clipStack.next;
    }

    this.depth--;
    if (this.depth === 0) {
      this.ctx.restore();
    }
  },
  visit: function (child, isContainer, visitContainer, isClippingMask) {
    var ctx = this.ctx;

    var clippingMask = isClippingMask === true;

    if (!clippingMask) {
      // removing clipping if the required character depth is achived
      while (this.clipDepth && this.clipDepth.length > 0 &&
          child._depth > this.clipDepth[0]) {
        this.clipDepth.shift();
        ctx.restore();
      }
      if (child._clipDepth) {
        if (!this.clipDepth) {
          this.clipDepth = [];
        }
        clippingMask = true;
        // saving clipping until certain character depth
        this.clipDepth.unshift(child._clipDepth);
        ctx.save();
      }
    }

    if (clippingMask && isContainer) {
      ctx.save();
      renderDisplayObject(child, ctx, child._currentTransform, child._cxform, clippingMask, this.refreshStage);
      for (var i = 0, n = child._children.length; i < n; i++) {
        var child1 = child._children[i];
        if (!child1) {
          continue;
        }
        if (this.ignoreVisibleAttribute || (child1._visible && !child1._maskedObject)) {
          var isContainer = flash.display.DisplayObjectContainer.class.isInstanceOf(child1) ||
                            flash.display.SimpleButton.class.isInstanceOf(child1);
          this.visit(child1, isContainer, this, true);
        }
      }
      ctx.restore();
      ctx.clip();
      return;
    }

    ctx.save();

    var blendModeCanvas;
    var blendModeClass = flash.display.BlendMode.class;
    if (child._blendMode !== blendModeClass.NORMAL) {

      // TODO:

      // These Flash blend modes have no canvas equivalent:
      // - blendModeClass.SUBTRACT
      // - blendModeClass.INVERT
      // - blendModeClass.SHADER
      // - blendModeClass.ADD

      // These blend modes are actually Porter-Duff compositing operators.
      // The backdrop is the nearest parent with blendMode set to LAYER.
      // When there is no LAYER parent, they are ignored (treated as NORMAL).
      // - blendModeClass.ALPHA (destination-in)
      // - blendModeClass.ERASE (destination-out)
      // - blendModeClass.LAYER [defines backdrop]

      var blendModeCanvas;
      switch (child._blendMode) {
        case blendModeClass.MULTIPLY:   blendModeCanvas = "multiply";        break;
        case blendModeClass.SCREEN:     blendModeCanvas = "screen";          break;
        case blendModeClass.LIGHTEN:    blendModeCanvas = "lighten";         break;
        case blendModeClass.DARKEN:     blendModeCanvas = "darken";          break;
        case blendModeClass.DIFFERENCE: blendModeCanvas = "difference";      break;
        case blendModeClass.OVERLAY:    blendModeCanvas = "overlay";         break;
        case blendModeClass.HARDLIGHT:  blendModeCanvas = "hard-light";      break;
      }
    }
    ctx.globalCompositeOperation = (blendModeCanvas !== undefined) ? blendModeCanvas : "normal";

    if (child._mask) {
      // TODO create canvas small enough to fit the object and
      // TODO cache the results when cacheAsBitmap is set
      var tempCanvas, tempCtx, maskCanvas, maskCtx;
      maskCanvas = CanvasCache.getCanvas(ctx.canvas);
      maskCtx = maskCanvas.ctx;
      maskCtx.currentTransform = ctx.currentTransform;
      var isMaskContainer = flash.display.DisplayObjectContainer.class.isInstanceOf(child._mask) ||
                            flash.display.SimpleButton.class.isInstanceOf(child._mask);
      this.ctx = maskCtx;
      this.visit(child._mask, isMaskContainer, visitContainer);
      this.ctx = ctx;

      tempCanvas = CanvasCache.getCanvas(ctx.canvas);
      tempCtx = tempCanvas.ctx;
      tempCtx.currentTransform = ctx.currentTransform;
      renderDisplayObject(child, tempCtx, child._currentTransform, child._cxform, clippingMask, this.refreshStage);

      if (isContainer) {
        this.ctx = tempCtx;
        visitContainer(child, this);
        this.ctx = ctx;
      }

      tempCtx.globalCompositeOperation = 'destination-in';
      tempCtx.setTransform(1, 0, 0, 1, 0, 0);
      tempCtx.drawImage(maskCanvas.canvas, 0, 0);

      ctx.save();
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.drawImage(tempCanvas.canvas, 0, 0);
      ctx.restore();

      CanvasCache.releaseCanvas(tempCanvas);
      CanvasCache.releaseCanvas(maskCanvas);
    } else {
      renderDisplayObject(child, ctx, child._currentTransform, child._cxform, clippingMask, this.refreshStage);

      if (isContainer) {
        visitContainer(child, this);
      }
    }

    ctx.restore();

    if (clippingMask) {
      ctx.clip();
    }
  }
};

function renderDisplayObject(child, ctx, transform, cxform, clip, refreshStage) {
  if (transform) {
    var m = transform;
    if (m.a * m.d == m.b * m.c) {
      // Workaround for bug 844184 -- the object is invisible
      ctx.closePath();
      ctx.rect(0, 0, 0, 0);
      ctx.clip();
    } else {
      ctx.transform(m.a, m.b, m.c, m.d, m.tx, m.ty);
    }
  }

  if (cxform) {
    // We only support alpha channel transformation for now
    ctx.globalAlpha = (ctx.globalAlpha * cxform.alphaMultiplier + cxform.alphaOffset) / 256;
  }
  if (child._alpha !== 1) {
    ctx.globalAlpha *= child._alpha;
  }

  if (!refreshStage && !child._invalid) {
    return;
  }

  // TODO: move into Graphics class
  if (child._graphics) {
    var graphics = child._graphics;

    if (graphics._bitmap) {
      ctx.translate(child._bbox.xMin, child._bbox.yMin);
      ctx.drawImage(graphics._bitmap, 0, 0);
    } else {
      graphics.draw(ctx, clip, child.ratio);
    }
  }

  if (child.draw)
    child.draw(ctx, child.ratio);

  child._invalid = false;
}

var renderingTerminated = false;

var samplesLeftPlusOne = 0;

function triggerSampling(count) {
  assert (count > 0);
  samplesLeftPlusOne = -count - 1;
}

function sampleStart() {
  if (!samplesLeftPlusOne) {
    return;
  }
  if (samplesLeftPlusOne < 0) {
    console.profile("Sample");
    samplesLeftPlusOne *= -1;
  }
  if (samplesLeftPlusOne > 0) {
    console.info("Sampling Frame: " + (samplesLeftPlusOne - 1));
  }
}

function sampleEnd() {
  if (!samplesLeftPlusOne) {
    return;
  }
  samplesLeftPlusOne --;
  if (samplesLeftPlusOne === 1) {
    console.profileEnd("Sample");
  }
}

function renderStage(stage, ctx, events) {
  var frameWidth, frameHeight;

  function updateRenderTransform() {
    frameWidth = ctx.canvas.width;
    frameHeight = ctx.canvas.height;

    var scaleX = frameWidth / stage._stageWidth;
    var scaleY = frameHeight / stage._stageHeight;

    switch (stage._scaleMode) {
    case 'exactFit':
      break;
    case 'noBorder':
      if (scaleX > scaleY) {
        scaleY = scaleX;
      } else {
        scaleX = scaleY;
      }
      break;
    case 'noScale':
      var pixelRatio = ctx.canvas._pixelRatio || 1;
      scaleX = pixelRatio;
      scaleY = pixelRatio;
      break;
    case 'showAll':
      if (scaleX < scaleY) {
        scaleY = scaleX;
      } else {
        scaleX = scaleY;
      }
      break;
    }

    var align = stage._align;
    var offsetX, offsetY;
    if (align.indexOf('L') >= 0) {
      offsetX = 0;
    } else if (align.indexOf('R') >= 0) {
      offsetX = frameWidth - scaleX * stage._stageWidth;
    } else {
      offsetX = (frameWidth - scaleX * stage._stageWidth) / 2;
    }
    if (align.indexOf('T') >= 0) {
      offsetY = 0;
    } else if (align.indexOf('B') >= 0) {
      offsetY = frameHeight - scaleY * stage._stageHeight;
    } else {
      offsetY = (frameHeight - scaleY * stage._stageHeight) / 2;
    }

    ctx.setTransform(scaleX, 0, 0, scaleY, offsetX, offsetY);

    stage._canvasState = {
      canvas: ctx.canvas,
      scaleX: scaleX,
      scaleY: scaleY,
      offsetX: offsetX,
      offsetY: offsetY
    };
  }

  updateRenderTransform();

  var frameTime = 0;
  var maxDelay = 1000 / stage._frameRate;
  var nextRenderAt = Date.now();

  var requestAnimationFrame = window.requestAnimationFrame ||
                              window.mozRequestAnimationFrame ||
                              window.webkitRequestAnimationFrame ||
                              window.oRequestAnimationFrame ||
                              window.msRequestAnimationFrame ||
                              window.setTimeout;

  var renderDummyBalls;

  var dummyBalls;
  if (typeof FirefoxCom !== 'undefined' &&
    FirefoxCom.requestSync('getBoolPref', {pref: 'shumway.dummyMode', def: false})) {
    var radius = 10;
    var speed = 1;
    var canvasState = stage._canvasState;
    var scaleX = canvasState.scaleX, scaleY = canvasState.scaleY;
    dummyBalls = [];
    for (var i = 0; i < 10; i++) {
      dummyBalls.push({
        position: {
          x: radius + Math.random() * ((ctx.canvas.width - 2 * radius) / scaleX),
          y: radius + Math.random() * ((ctx.canvas.height - 2 * radius) / scaleY)
        },
        velocity: {x: speed * (Math.random() - 0.5), y: speed * (Math.random() - 0.5)}
      });
    }
    ctx.fillStyle = "black";
    ctx.lineWidth = 2;
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    renderDummyBalls = function () {
      ctx.fillStyle = "black";
      ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
      ctx.strokeStyle = "green";
      dummyBalls.forEach(function (ball) {
        var position = ball.position;
        var velocity = ball.velocity;
        ctx.beginPath();
        ctx.arc(position.x, position.y, radius, 0, Math.PI * 2, true);
        ctx.stroke();
        var x = (position.x + velocity.x);
        var y = (position.y + velocity.y);
        if (x < radius || x > ctx.canvas.width / scaleX - radius) {
          velocity.x *= -1;
        }
        if (y < radius || y > ctx.canvas.height / scaleY - radius) {
          velocity.y *= -1;
        }
        position.x += velocity.x;
        position.y += velocity.y;
      });
    };
  }

  console.timeEnd("Initialize Renderer");
  console.timeEnd("Total");

  var firstRun = true;
  var frameCount = 0;
  var frameFPSAverage = new metrics.Average(120);
  (function draw() {
    var now = Date.now();
    var renderFrame;
    var renderFrame = now >= nextRenderAt;
    if (renderFrame && events.onBeforeFrame) {
      var e = { cancel: false };
      events.onBeforeFrame(e);
      renderFrame = !e.cancel;
    }

    if (renderFrame && renderDummyBalls) {
      frameTime = now;
      nextRenderAt = frameTime + maxDelay;

      renderDummyBalls();

      requestAnimationFrame(draw);
      return;
    }

    sampleStart();

    var refreshStage = false;
    if (stage._invalid) {
      updateRenderTransform();
      stage._invalid = false;
      refreshStage = true;
    }

    var mouseMoved = false;
    if (stage._mouseMoved) {
      stage._mouseMoved = false;
      mouseMoved = stage._mouseOver;
    }

    if (renderFrame || refreshStage || mouseMoved) {
      FrameCounter.clear();
      var frameStartTime = performance.now();
      traceRenderer.value && appendToFrameTerminal("Begin Frame #" + (frameCount++), "purple");

      var domain = avm2.systemDomain;

      if (renderFrame) {
        frameTime = now;
        nextRenderAt = frameTime + (1000 / stage._frameRate);

        if (firstRun) {
          // Initial display list is already constructed, skip frame construction phase.
          firstRun = false;
        } else {
          domain.broadcastMessage("declareFrame");
          domain.broadcastMessage("enterFrame");
          domain.broadcastMessage("constructChildren");
        }

        domain.broadcastMessage("frameConstructed");
        domain.broadcastMessage("executeFrame");
        domain.broadcastMessage("exitFrame");
      }

      if (stage._deferRenderEvent) {
        stage._deferRenderEvent = false;
        domain.broadcastMessage("render", "render");
      }

      if (refreshStage || renderFrame) {
        var canvasVisible = isCanvasVisible(ctx.canvas);
        ctx.beginPath();
        if (canvasVisible && !disablePreVisitor.value) {
          stage._showRedrawRegions(showRedrawRegions.value);
          traceRenderer.value && frameWriter.enter("> Pre Visitor");
          stage._processInvalidRegions(ctx);
          traceRenderer.value && frameWriter.leave("< Pre Visitor");
        }
        if (canvasVisible && !disableRenderVisitor.value) {
          traceRenderer.value && frameWriter.enter("> Render Visitor");
          (new RenderVisitor(stage, ctx, refreshStage)).start();
          traceRenderer.value && frameWriter.leave("< Render Visitor");
        }
      }

      if (mouseMoved && !disableMouseVisitor.value) {
        traceRenderer.value && frameWriter.enter("> Mouse Visitor");
        stage._handleMouse();
        traceRenderer.value && frameWriter.leave("< Mouse Visitor");

        stage._syncCursor();
      }

      if (renderFrame) {
        domain.broadcastMessage("destructFrame");
      }

      if (renderFrame && events.onAfterFrame) {
        events.onAfterFrame();
      }

      if (traceRenderer.value) {
        for (var name in FrameCounter.counts) {
          appendToFrameTerminal(name + ": " + FrameCounter.counts[name], "gray");
        }
        var frameElapsedTime = performance.now() - frameStartTime;
        var frameFPS = 1000 / frameElapsedTime;
        frameFPSAverage.push(frameFPS);
        traceRenderer.value && appendToFrameTerminal("End Frame Time: " + frameElapsedTime.toFixed(2) + " (" + frameFPS.toFixed(2) + " fps, " + frameFPSAverage.average().toFixed(2) + " average fps)", "purple");

      }
    } else {
      traceRenderer.value && appendToFrameTerminal("Skip Frame", "black");
    }

    sampleEnd();

    if (renderingTerminated) {
      if (events.onTerminated) {
        events.onTerminated();
      }
      return;
    }

    requestAnimationFrame(draw);
  })();
}
<commit_msg>Compensate for renderDisplayObject transform during bitmap draw<commit_after>/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
 * Copyright 2013 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*global rgbaObjToStr, FirefoxCom, Timer, FrameCounter, metrics, coreOptions, OptionSet, Option, appendToFrameTerminal, frameWriter*/

var rendererOptions = coreOptions.register(new OptionSet("Renderer Options"));
var traceRenderer = rendererOptions.register(new Option("tr", "traceRenderer", "number", 0, "trace renderer execution"));
var disablePreVisitor = rendererOptions.register(new Option("dpv", "disablePreVisitor", "boolean", false, "disable pre visitor"));
var disableRenderVisitor = rendererOptions.register(new Option("drv", "disableRenderVisitor", "boolean", false, "disable render visitor"));
var disableMouseVisitor = rendererOptions.register(new Option("dmv", "disableMouseVisitor", "boolean", false, "disable mouse visitor"));
var showRedrawRegions = rendererOptions.register(new Option("rr", "showRedrawRegions", "boolean", false, "show redraw regions"));

var CanvasCache = {
  cache: [],
  getCanvas: function getCanvas(protoCanvas) {
    var tempCanvas = this.cache.shift();
    if (!tempCanvas) {
      tempCanvas = {
        canvas: document.createElement('canvas')
      };
      tempCanvas.ctx = tempCanvas.canvas.getContext('kanvas-2d');
    }
    tempCanvas.canvas.width = protoCanvas.width;
    tempCanvas.canvas.height = protoCanvas.height;
    tempCanvas.ctx.save();
    return tempCanvas;
  },
  releaseCanvas: function releaseCanvas(tempCanvas) {
    tempCanvas.ctx.restore();
    this.cache.push(tempCanvas);
  }
};

function isCanvasVisible(canvas) {
  if (canvas.ownerDocument.hidden) { // Page Visibility API
    return false;
  }
  if (canvas.mozVisible === false) { // HACK Canvas Visibility API
    return false;
  }
  return true;
}

function visitContainer(container, visitor) {
  var children = container._children;

  visitor.childrenStart(container);

  for (var i = 0, n = children.length; i < n; i++) {
    var child = children[i];
    if (!child) {
      continue;
    }

    if (visitor.ignoreVisibleAttribute || (child._visible && !child._maskedObject)) {
      var isContainer = flash.display.DisplayObjectContainer.class.isInstanceOf(child) ||
                        flash.display.SimpleButton.class.isInstanceOf(child);

      visitor.visit(child, isContainer, visitContainer);
    }
  }

  visitor.childrenEnd(container);
}

function RenderVisitor(root, ctx, refreshStage) {
  this.root = root;
  this.ctx = ctx;
  this.depth = 0;
  this.refreshStage = refreshStage;

  this.clipDepth = null;
  this.clipStack = null;
}
RenderVisitor.prototype = {
  ignoreVisibleAttribute: false,
  start: function () {
    visitContainer(this.root, this);
  },
  startFragment: function() {
    var isContainer = flash.display.DisplayObjectContainer.class.isInstanceOf(this.root) ||
                      flash.display.SimpleButton.class.isInstanceOf(this.root);

    // HACK compensate for visit()/renderDisplayObject() transform
    var t = this.root.transform.matrix;
    t.invert();
    this.ctx.save();
    this.ctx.transform(t.a, t.b, t.c, t.d, t.tx, t.ty);

    this.visit(this.root, isContainer, visitContainer);

    this.ctx.restore();
  },
  childrenStart: function(parent) {
    if (this.depth === 0) {
      var ctx = this.ctx;

      ctx.save();

      if (!this.refreshStage) {
        ctx.clip();
      }

      var bgcolor = this.root._color;
      if (bgcolor) {
        if (bgcolor.alpha < 255) {
          ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        }
        if (bgcolor.alpha > 0) {
          ctx.fillStyle = rgbaObjToStr(bgcolor);
          if (this.refreshStage) {
            ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
          } else {
            ctx.fill();
          }
        }
      }

      ctx.mozFillRule = 'evenodd';
    }
    this.depth++;

    if (this.clipDepth && this.clipDepth.length > 0) {
      // saving the parent clipping state
      this.clipStack = {
        depth: this.depth,
        clip: this.clipDepth,
        next: this.clipStack
      };
      this.clipDepth = null;
    }
  },
  childrenEnd: function(parent) {
    if (this.clipDepth) {
      // removing existing clippings
      while (this.clipDepth.length > 0) {
        this.clipDepth.pop();
        this.ctx.restore();
      }
      this.clipDepth = null;
    }
    // checking if we saved the parent clipping state
    if (this.clipStack && this.clipStack.depth === this.depth) {
      this.clipDepth = this.clipStack.clip;
      this.clipStack = this.clipStack.next;
    }

    this.depth--;
    if (this.depth === 0) {
      this.ctx.restore();
    }
  },
  visit: function (child, isContainer, visitContainer, isClippingMask) {
    var ctx = this.ctx;

    var clippingMask = isClippingMask === true;

    if (!clippingMask) {
      // removing clipping if the required character depth is achived
      while (this.clipDepth && this.clipDepth.length > 0 &&
          child._depth > this.clipDepth[0]) {
        this.clipDepth.shift();
        ctx.restore();
      }
      if (child._clipDepth) {
        if (!this.clipDepth) {
          this.clipDepth = [];
        }
        clippingMask = true;
        // saving clipping until certain character depth
        this.clipDepth.unshift(child._clipDepth);
        ctx.save();
      }
    }

    if (clippingMask && isContainer) {
      ctx.save();
      renderDisplayObject(child, ctx, child._currentTransform, child._cxform, clippingMask, this.refreshStage);
      for (var i = 0, n = child._children.length; i < n; i++) {
        var child1 = child._children[i];
        if (!child1) {
          continue;
        }
        if (this.ignoreVisibleAttribute || (child1._visible && !child1._maskedObject)) {
          var isContainer = flash.display.DisplayObjectContainer.class.isInstanceOf(child1) ||
                            flash.display.SimpleButton.class.isInstanceOf(child1);
          this.visit(child1, isContainer, this, true);
        }
      }
      ctx.restore();
      ctx.clip();
      return;
    }

    ctx.save();

    var blendModeCanvas;
    var blendModeClass = flash.display.BlendMode.class;
    if (child._blendMode !== blendModeClass.NORMAL) {

      // TODO:

      // These Flash blend modes have no canvas equivalent:
      // - blendModeClass.SUBTRACT
      // - blendModeClass.INVERT
      // - blendModeClass.SHADER
      // - blendModeClass.ADD

      // These blend modes are actually Porter-Duff compositing operators.
      // The backdrop is the nearest parent with blendMode set to LAYER.
      // When there is no LAYER parent, they are ignored (treated as NORMAL).
      // - blendModeClass.ALPHA (destination-in)
      // - blendModeClass.ERASE (destination-out)
      // - blendModeClass.LAYER [defines backdrop]

      var blendModeCanvas;
      switch (child._blendMode) {
        case blendModeClass.MULTIPLY:   blendModeCanvas = "multiply";        break;
        case blendModeClass.SCREEN:     blendModeCanvas = "screen";          break;
        case blendModeClass.LIGHTEN:    blendModeCanvas = "lighten";         break;
        case blendModeClass.DARKEN:     blendModeCanvas = "darken";          break;
        case blendModeClass.DIFFERENCE: blendModeCanvas = "difference";      break;
        case blendModeClass.OVERLAY:    blendModeCanvas = "overlay";         break;
        case blendModeClass.HARDLIGHT:  blendModeCanvas = "hard-light";      break;
      }
    }
    ctx.globalCompositeOperation = (blendModeCanvas !== undefined) ? blendModeCanvas : "normal";

    if (child._mask) {
      // TODO create canvas small enough to fit the object and
      // TODO cache the results when cacheAsBitmap is set
      var tempCanvas, tempCtx, maskCanvas, maskCtx;
      maskCanvas = CanvasCache.getCanvas(ctx.canvas);
      maskCtx = maskCanvas.ctx;
      maskCtx.currentTransform = ctx.currentTransform;
      var isMaskContainer = flash.display.DisplayObjectContainer.class.isInstanceOf(child._mask) ||
                            flash.display.SimpleButton.class.isInstanceOf(child._mask);
      this.ctx = maskCtx;
      this.visit(child._mask, isMaskContainer, visitContainer);
      this.ctx = ctx;

      tempCanvas = CanvasCache.getCanvas(ctx.canvas);
      tempCtx = tempCanvas.ctx;
      tempCtx.currentTransform = ctx.currentTransform;
      renderDisplayObject(child, tempCtx, child._currentTransform, child._cxform, clippingMask, this.refreshStage);

      if (isContainer) {
        this.ctx = tempCtx;
        visitContainer(child, this);
        this.ctx = ctx;
      }

      tempCtx.globalCompositeOperation = 'destination-in';
      tempCtx.setTransform(1, 0, 0, 1, 0, 0);
      tempCtx.drawImage(maskCanvas.canvas, 0, 0);

      ctx.save();
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.drawImage(tempCanvas.canvas, 0, 0);
      ctx.restore();

      CanvasCache.releaseCanvas(tempCanvas);
      CanvasCache.releaseCanvas(maskCanvas);
    } else {
      renderDisplayObject(child, ctx, child._currentTransform, child._cxform, clippingMask, this.refreshStage);

      if (isContainer) {
        visitContainer(child, this);
      }
    }

    ctx.restore();

    if (clippingMask) {
      ctx.clip();
    }
  }
};

function renderDisplayObject(child, ctx, transform, cxform, clip, refreshStage) {
  if (transform) {
    var m = transform;
    if (m.a * m.d == m.b * m.c) {
      // Workaround for bug 844184 -- the object is invisible
      ctx.closePath();
      ctx.rect(0, 0, 0, 0);
      ctx.clip();
    } else {
      ctx.transform(m.a, m.b, m.c, m.d, m.tx, m.ty);
    }
  }

  if (cxform) {
    // We only support alpha channel transformation for now
    ctx.globalAlpha = (ctx.globalAlpha * cxform.alphaMultiplier + cxform.alphaOffset) / 256;
  }
  if (child._alpha !== 1) {
    ctx.globalAlpha *= child._alpha;
  }

  if (!refreshStage && !child._invalid) {
    return;
  }

  // TODO: move into Graphics class
  if (child._graphics) {
    var graphics = child._graphics;

    if (graphics._bitmap) {
      ctx.translate(child._bbox.xMin, child._bbox.yMin);
      ctx.drawImage(graphics._bitmap, 0, 0);
    } else {
      graphics.draw(ctx, clip, child.ratio);
    }
  }

  if (child.draw)
    child.draw(ctx, child.ratio);

  child._invalid = false;
}

var renderingTerminated = false;

var samplesLeftPlusOne = 0;

function triggerSampling(count) {
  assert (count > 0);
  samplesLeftPlusOne = -count - 1;
}

function sampleStart() {
  if (!samplesLeftPlusOne) {
    return;
  }
  if (samplesLeftPlusOne < 0) {
    console.profile("Sample");
    samplesLeftPlusOne *= -1;
  }
  if (samplesLeftPlusOne > 0) {
    console.info("Sampling Frame: " + (samplesLeftPlusOne - 1));
  }
}

function sampleEnd() {
  if (!samplesLeftPlusOne) {
    return;
  }
  samplesLeftPlusOne --;
  if (samplesLeftPlusOne === 1) {
    console.profileEnd("Sample");
  }
}

function renderStage(stage, ctx, events) {
  var frameWidth, frameHeight;

  function updateRenderTransform() {
    frameWidth = ctx.canvas.width;
    frameHeight = ctx.canvas.height;

    var scaleX = frameWidth / stage._stageWidth;
    var scaleY = frameHeight / stage._stageHeight;

    switch (stage._scaleMode) {
    case 'exactFit':
      break;
    case 'noBorder':
      if (scaleX > scaleY) {
        scaleY = scaleX;
      } else {
        scaleX = scaleY;
      }
      break;
    case 'noScale':
      var pixelRatio = ctx.canvas._pixelRatio || 1;
      scaleX = pixelRatio;
      scaleY = pixelRatio;
      break;
    case 'showAll':
      if (scaleX < scaleY) {
        scaleY = scaleX;
      } else {
        scaleX = scaleY;
      }
      break;
    }

    var align = stage._align;
    var offsetX, offsetY;
    if (align.indexOf('L') >= 0) {
      offsetX = 0;
    } else if (align.indexOf('R') >= 0) {
      offsetX = frameWidth - scaleX * stage._stageWidth;
    } else {
      offsetX = (frameWidth - scaleX * stage._stageWidth) / 2;
    }
    if (align.indexOf('T') >= 0) {
      offsetY = 0;
    } else if (align.indexOf('B') >= 0) {
      offsetY = frameHeight - scaleY * stage._stageHeight;
    } else {
      offsetY = (frameHeight - scaleY * stage._stageHeight) / 2;
    }

    ctx.setTransform(scaleX, 0, 0, scaleY, offsetX, offsetY);

    stage._canvasState = {
      canvas: ctx.canvas,
      scaleX: scaleX,
      scaleY: scaleY,
      offsetX: offsetX,
      offsetY: offsetY
    };
  }

  updateRenderTransform();

  var frameTime = 0;
  var maxDelay = 1000 / stage._frameRate;
  var nextRenderAt = Date.now();

  var requestAnimationFrame = window.requestAnimationFrame ||
                              window.mozRequestAnimationFrame ||
                              window.webkitRequestAnimationFrame ||
                              window.oRequestAnimationFrame ||
                              window.msRequestAnimationFrame ||
                              window.setTimeout;

  var renderDummyBalls;

  var dummyBalls;
  if (typeof FirefoxCom !== 'undefined' &&
    FirefoxCom.requestSync('getBoolPref', {pref: 'shumway.dummyMode', def: false})) {
    var radius = 10;
    var speed = 1;
    var canvasState = stage._canvasState;
    var scaleX = canvasState.scaleX, scaleY = canvasState.scaleY;
    dummyBalls = [];
    for (var i = 0; i < 10; i++) {
      dummyBalls.push({
        position: {
          x: radius + Math.random() * ((ctx.canvas.width - 2 * radius) / scaleX),
          y: radius + Math.random() * ((ctx.canvas.height - 2 * radius) / scaleY)
        },
        velocity: {x: speed * (Math.random() - 0.5), y: speed * (Math.random() - 0.5)}
      });
    }
    ctx.fillStyle = "black";
    ctx.lineWidth = 2;
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    renderDummyBalls = function () {
      ctx.fillStyle = "black";
      ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
      ctx.strokeStyle = "green";
      dummyBalls.forEach(function (ball) {
        var position = ball.position;
        var velocity = ball.velocity;
        ctx.beginPath();
        ctx.arc(position.x, position.y, radius, 0, Math.PI * 2, true);
        ctx.stroke();
        var x = (position.x + velocity.x);
        var y = (position.y + velocity.y);
        if (x < radius || x > ctx.canvas.width / scaleX - radius) {
          velocity.x *= -1;
        }
        if (y < radius || y > ctx.canvas.height / scaleY - radius) {
          velocity.y *= -1;
        }
        position.x += velocity.x;
        position.y += velocity.y;
      });
    };
  }

  console.timeEnd("Initialize Renderer");
  console.timeEnd("Total");

  var firstRun = true;
  var frameCount = 0;
  var frameFPSAverage = new metrics.Average(120);
  (function draw() {
    var now = Date.now();
    var renderFrame;
    var renderFrame = now >= nextRenderAt;
    if (renderFrame && events.onBeforeFrame) {
      var e = { cancel: false };
      events.onBeforeFrame(e);
      renderFrame = !e.cancel;
    }

    if (renderFrame && renderDummyBalls) {
      frameTime = now;
      nextRenderAt = frameTime + maxDelay;

      renderDummyBalls();

      requestAnimationFrame(draw);
      return;
    }

    sampleStart();

    var refreshStage = false;
    if (stage._invalid) {
      updateRenderTransform();
      stage._invalid = false;
      refreshStage = true;
    }

    var mouseMoved = false;
    if (stage._mouseMoved) {
      stage._mouseMoved = false;
      mouseMoved = stage._mouseOver;
    }

    if (renderFrame || refreshStage || mouseMoved) {
      FrameCounter.clear();
      var frameStartTime = performance.now();
      traceRenderer.value && appendToFrameTerminal("Begin Frame #" + (frameCount++), "purple");

      var domain = avm2.systemDomain;

      if (renderFrame) {
        frameTime = now;
        nextRenderAt = frameTime + (1000 / stage._frameRate);

        if (firstRun) {
          // Initial display list is already constructed, skip frame construction phase.
          firstRun = false;
        } else {
          domain.broadcastMessage("declareFrame");
          domain.broadcastMessage("enterFrame");
          domain.broadcastMessage("constructChildren");
        }

        domain.broadcastMessage("frameConstructed");
        domain.broadcastMessage("executeFrame");
        domain.broadcastMessage("exitFrame");
      }

      if (stage._deferRenderEvent) {
        stage._deferRenderEvent = false;
        domain.broadcastMessage("render", "render");
      }

      if (refreshStage || renderFrame) {
        var canvasVisible = isCanvasVisible(ctx.canvas);
        ctx.beginPath();
        if (canvasVisible && !disablePreVisitor.value) {
          stage._showRedrawRegions(showRedrawRegions.value);
          traceRenderer.value && frameWriter.enter("> Pre Visitor");
          stage._processInvalidRegions(ctx);
          traceRenderer.value && frameWriter.leave("< Pre Visitor");
        }
        if (canvasVisible && !disableRenderVisitor.value) {
          traceRenderer.value && frameWriter.enter("> Render Visitor");
          (new RenderVisitor(stage, ctx, refreshStage)).start();
          traceRenderer.value && frameWriter.leave("< Render Visitor");
        }
      }

      if (mouseMoved && !disableMouseVisitor.value) {
        traceRenderer.value && frameWriter.enter("> Mouse Visitor");
        stage._handleMouse();
        traceRenderer.value && frameWriter.leave("< Mouse Visitor");

        stage._syncCursor();
      }

      if (renderFrame) {
        domain.broadcastMessage("destructFrame");
      }

      if (renderFrame && events.onAfterFrame) {
        events.onAfterFrame();
      }

      if (traceRenderer.value) {
        for (var name in FrameCounter.counts) {
          appendToFrameTerminal(name + ": " + FrameCounter.counts[name], "gray");
        }
        var frameElapsedTime = performance.now() - frameStartTime;
        var frameFPS = 1000 / frameElapsedTime;
        frameFPSAverage.push(frameFPS);
        traceRenderer.value && appendToFrameTerminal("End Frame Time: " + frameElapsedTime.toFixed(2) + " (" + frameFPS.toFixed(2) + " fps, " + frameFPSAverage.average().toFixed(2) + " average fps)", "purple");

      }
    } else {
      traceRenderer.value && appendToFrameTerminal("Skip Frame", "black");
    }

    sampleEnd();

    if (renderingTerminated) {
      if (events.onTerminated) {
        events.onTerminated();
      }
      return;
    }

    requestAnimationFrame(draw);
  })();
}

{'fraction_non_alphanumeric': 0.12389380530973451, 'fraction_numerical': 0.00690054782975137, 'mean_word_length': 2.526423330547042, 'pattern_counts': {'":': 2, '<': 25, '<?xml version=': 0, '>': 39, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3906378', 'n_tokens_mistral': 11543, 'n_tokens_neox': 11199, 'n_words': 2567}