Skip to content

Commit 76aa601

Browse files
committed
TilemapLayer rendering double-copy
- Memory optimization of delta-scroll shifting. - The copy canvas is shared between all TilemapLayers - The copy is done in segments to reduce the memory usage of the copy canvas. Currently this is a 1/4 ratio. - Device has the feature (by browser) check to see if bitblt works - TilemapLayer will automatically enable the double-copy as needed - Device.whenReady can be used to override the device value
1 parent 0ad21e3 commit 76aa601

3 files changed

Lines changed: 124 additions & 46 deletions

File tree

src/system/Canvas.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Phaser.Canvas = {
1818
* @method Phaser.Canvas.create
1919
* @param {number} [width=256] - The width of the canvas element.
2020
* @param {number} [height=256] - The height of the canvas element..
21-
* @param {string} [id=''] - If given this will be set as the ID of the canvas element, otherwise no ID will be set.
21+
* @param {string} [id=(none)] - If specified, and not the empty string, this will be set as the ID of the canvas element. Otherwise no ID will be set.
2222
* @return {HTMLCanvasElement} The newly created canvas element.
2323
*/
2424
create: function (width, height, id) {

src/system/Device.js

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ Phaser.Device = function () {
143143
*/
144144
this.canvas = false;
145145

146+
/**
147+
* @property {?boolean} canvasBitBltShift - True if canvas supports a 'copy' bitblt onto itself when the source and destination regions overlap.
148+
* @default
149+
*/
150+
this.canvasBitBltShift = null;
151+
152+
/**
153+
* @property {boolean} webGL - Is webGL available?
154+
* @default
155+
*/
156+
this.webGL = false;
157+
146158
/**
147159
* @property {boolean} file - Is file available?
148160
* @default
@@ -161,12 +173,6 @@ Phaser.Device = function () {
161173
*/
162174
this.localStorage = false;
163175

164-
/**
165-
* @property {boolean} webGL - Is webGL available?
166-
* @default
167-
*/
168-
this.webGL = false;
169-
170176
/**
171177
* @property {boolean} worker - Is worker available?
172178
* @default
@@ -224,7 +230,7 @@ Phaser.Device = function () {
224230
this.mspointer = false;
225231

226232
/**
227-
* @property {string|null} wheelType - The newest type of Wheel/Scroll event supported: 'wheel', 'mousewheel', 'DOMMouseScroll'
233+
* @property {?string} wheelType - The newest type of Wheel/Scroll event supported: 'wheel', 'mousewheel', 'DOMMouseScroll'
228234
* @default
229235
* @protected
230236
*/
@@ -387,6 +393,8 @@ Phaser.Device = function () {
387393
*/
388394
this.iPad = false;
389395

396+
// Device features
397+
390398
/**
391399
* @property {number} pixelRatio - PixelRatio of the host device?
392400
* @default
@@ -643,16 +651,9 @@ Phaser.Device._initialize = function () {
643651

644652
device.file = !!window['File'] && !!window['FileReader'] && !!window['FileList'] && !!window['Blob'];
645653
device.fileSystem = !!window['requestFileSystem'];
646-
device.webGL = ( function () { try { var canvas = document.createElement( 'canvas' ); /*Force screencanvas to false*/ canvas.screencanvas = false; return !! window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ); } catch( e ) { return false; } } )();
647654

648-
if (device.webGL === null || device.webGL === false)
649-
{
650-
device.webGL = false;
651-
}
652-
else
653-
{
654-
device.webGL = true;
655-
}
655+
device.webGL = ( function () { try { var canvas = document.createElement( 'canvas' ); /*Force screencanvas to false*/ canvas.screencanvas = false; return !! window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ); } catch( e ) { return false; } } )();
656+
device.webGL = !!device.webGL;
656657

657658
device.worker = !!window['Worker'];
658659

@@ -662,6 +663,17 @@ Phaser.Device._initialize = function () {
662663

663664
device.getUserMedia = !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
664665

666+
if (device.ie || device.firefox || device.chrome)
667+
{
668+
// Believed to work - need verification of Chrome for iOS, or other browsers using UIWebKit.
669+
device.canvasBitBltShift = true;
670+
}
671+
if (device.safari || device.mobileSafari)
672+
{
673+
// Known not to work
674+
device.canvasBitBltShift = false;
675+
}
676+
665677
}
666678

667679
/**

src/tilemap/TilemapLayer.js

Lines changed: 95 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* A TilemapLayer is a Phaser.Image/Sprite that renders a specific TileLayer of a Tilemap.
99
*
1010
* Since a TilemapLayer is a Sprite it can be moved around the display, added to other groups or display objects, etc.
11+
*
1112
* By default TilemapLayers have fixedToCamera set to `true`. Changing this will break Camera follow and scrolling behaviour.
1213
*
1314
* @class Phaser.TilemapLayer
@@ -61,7 +62,7 @@ Phaser.TilemapLayer = function (game, tilemap, index, width, height) {
6162
* @property {HTMLCanvasElement} canvas
6263
* @protected
6364
*/
64-
this.canvas = Phaser.Canvas.create(width, height, '', true);
65+
this.canvas = Phaser.Canvas.create(width, height);
6566

6667
/**
6768
* The 2d context of the canvas.
@@ -70,12 +71,6 @@ Phaser.TilemapLayer = function (game, tilemap, index, width, height) {
7071
*/
7172
this.context = this.canvas.getContext('2d');
7273

73-
/**
74-
* @property {?HTMLCanvasElement} _copyCanvas
75-
* @private
76-
*/
77-
this._copyCanvas = Phaser.Canvas.create(width, height, '', true);
78-
7974
/**
8075
* Required Pixi var.
8176
* @property {PIXI.BaseTexture} baseTexture
@@ -130,16 +125,24 @@ Phaser.TilemapLayer = function (game, tilemap, index, width, height) {
130125
/**
131126
* Settings that control standard (non-diagnostic) rendering.
132127
*
133-
* @public
134-
* @property {boolean} enableScrollDelta - When enabled, only new newly exposed areas of the layer are redraw after scrolling. This can greatly improve scrolling rendering performance, especially when there are many small tiles.
128+
* @property {boolean} [enableScrollDelta=true] - Delta scroll rendering only draws tiles/edges as them come into view.
129+
* This can greatly improve scrolling rendering performance, especially when there are many small tiles.
130+
* It should only be disabled in rare cases.
131+
*
132+
* @property {?DOMCanvasElement} [copyCanvas=(auto)] - [Internal] If set, force using a separate (shared) copy canvas.
133+
* Using a canvas bitblt/copy when the source and destinations region overlap produces unexpected behavior
134+
* in some browsers, notably Safari.
135+
*
136+
* @property {integer} copySliceCount - [Internal] The number of vertical slices to copy when using a `copyCanvas`.
137+
* This is ratio of the pixel count of the primary canvas to the copy canvas.
138+
*
135139
* @default
136140
*/
137141
this.renderSettings = {
138-
139142
enableScrollDelta: true,
140-
141-
overdrawRatio: 0.20
142-
143+
overdrawRatio: 0.20,
144+
copyCanvas: null,
145+
copySliceCount: 4
143146
};
144147

145148
/**
@@ -264,6 +267,35 @@ Phaser.TilemapLayer = function (game, tilemap, index, width, height) {
264267
*/
265268
this._results = [];
266269

270+
if (!game.device.canvasBitBltShift)
271+
{
272+
this.renderSettings.copyCanvas = Phaser.TilemapLayer.ensureSharedCopyCanvas();
273+
}
274+
275+
};
276+
277+
/**
278+
* The shared double-copy canvas, created as needed.
279+
*
280+
* @private
281+
* @static
282+
*/
283+
Phaser.TilemapLayer.sharedCopyCanvas = null;
284+
285+
/**
286+
* Create if needed (and return) a shared copy canvas that is shared across all TilemapLayers.
287+
*
288+
* Code that uses the canvas is responsible to ensure the dimensions and save/restore state as appropriate.
289+
*
290+
* @protected
291+
* @static
292+
*/
293+
Phaser.TilemapLayer.ensureSharedCopyCanvas = function () {
294+
if (!this.sharedCopyCanvas)
295+
{
296+
this.sharedCopyCanvas = Phaser.Canvas.create(2, 2);
297+
}
298+
return this.sharedCopyCanvas;
267299
};
268300

269301
Phaser.TilemapLayer.prototype = Object.create(Phaser.Image.prototype);
@@ -597,7 +629,8 @@ Object.defineProperty(Phaser.TilemapLayer.prototype, "wrap", {
597629
});
598630

599631
/**
600-
* Returns the appropriate tileset for the index, updating the internal cache as required. This should only be called if `tilesets[index]` evaluates to undefined.
632+
* Returns the appropriate tileset for the index, updating the internal cache as required.
633+
* This should only be called if `tilesets[index]` evaluates to undefined.
601634
*
602635
* @method Phaser.TilemapLayer#resolveTileset
603636
* @private
@@ -631,7 +664,9 @@ Phaser.TilemapLayer.prototype.resolveTileset = function (tileIndex)
631664
};
632665

633666
/**
634-
* The TilemapLayer caches tileset look-ups. Call this method of clear the cache if tilesets have been added or updated after the layer has been rendered.
667+
* The TilemapLayer caches tileset look-ups.
668+
*
669+
* Call this method of clear the cache if tilesets have been added or updated after the layer has been rendered.
635670
*
636671
* @method Phaser.TilemapLayer#resetTilesetCache
637672
* @public
@@ -648,15 +683,19 @@ Phaser.TilemapLayer.prototype.resetTilesetCache = function ()
648683

649684
/**
650685
* Shifts the contents of the canvas - does extra math so that different browsers agree on the result.
686+
*
651687
* The specified (x/y) will be shifted to (0,0) after the copy and the newly exposed canvas area will need to be filled in.
652688
*
689+
* If `copyCanvas` is specified it will be used as an intermediate copy buffer and may be resized.
690+
*
653691
* @method Phaser.TilemapLayer#shiftCanvas
654692
* @private
655693
* @param {CanvasRenderingContext2D} context - The context to shift
656694
* @param {integer} x
657695
* @param {integer} y
696+
* @param {DOMCanvasElement} [copyCanvas=(none)] - If specified this canvas will be used as an intermediate copy buffer.
658697
*/
659-
Phaser.TilemapLayer.prototype.shiftCanvas = function (context, x, y)
698+
Phaser.TilemapLayer.prototype.shiftCanvas = function (context, x, y, copyCanvas)
660699
{
661700

662701
var canvas = context.canvas;
@@ -681,25 +720,52 @@ Phaser.TilemapLayer.prototype.shiftCanvas = function (context, x, y)
681720
sy = 0;
682721
}
683722

684-
if (!this._copyCanvas)
723+
if (copyCanvas)
685724
{
686-
// Flickers in Safari / Safari Mobile
725+
// Copying happens in slices to minimize copy canvas size overhead
726+
var sliceCount = this.renderSettings.copySliceCount;
727+
var sH = Math.ceil(copyH / sliceCount);
728+
// Ensure copy canvas is large enough
729+
if (copyCanvas.width < copyW) { copyCanvas.width = copyW; }
730+
if (copyCanvas.height < sH) { copyCanvas.height = sH; }
731+
732+
var vShift;
733+
if (dy >= sy)
734+
{
735+
// move old region up, or don't change vertically - copy top to bottom
736+
vShift = sH;
737+
}
738+
else
739+
{
740+
// move old region down - copy segments from bottom to top
741+
vShift = -sH;
742+
dy += (sH * (sliceCount - 1));
743+
sy += (sH * (sliceCount - 1));
744+
}
745+
746+
var copyContext = copyCanvas.getContext('2d');
747+
while (sliceCount--)
748+
{
749+
copyContext.clearRect(0, 0, copyW, sH);
750+
copyContext.drawImage(canvas, dx, dy, copyW, sH, 0, 0, copyW, sH);
751+
// clear allows default 'source-over' semantics
752+
context.clearRect(sx, sy, copyW, sH);
753+
context.drawImage(copyCanvas, 0, 0, copyW, sH, sx, sy, copyW, sH);
754+
755+
dy += vShift;
756+
sy += vShift;
757+
}
758+
759+
}
760+
else
761+
{
762+
// Avoids a second copy but flickers in Safari / Safari Mobile
687763
// Ref. https://github.com/photonstorm/phaser/issues/1439
688764
context.save();
689765
context.globalCompositionOperation = 'copy';
690766
context.drawImage(canvas, dx, dy, copyW, copyH, sx, sy, copyW, copyH);
691767
context.restore();
692768
}
693-
else
694-
{
695-
var cpCanvas = this._copyCanvas;
696-
var cpContext = cpCanvas.getContext('2d');
697-
cpContext.clearRect(0, 0, copyW, copyH);
698-
cpContext.drawImage(canvas, dx, dy, copyW, copyH, 0, 0, copyW, copyH);
699-
// clear allows default 'source-over' semantics
700-
context.clearRect(sx, sy, copyW, copyH);
701-
context.drawImage(cpCanvas, 0, 0, copyW, copyH, sx, sy, copyW, copyH);
702-
}
703769
};
704770

705771
/**
@@ -862,7 +928,7 @@ Phaser.TilemapLayer.prototype.renderDeltaScroll = function (shiftX, shiftY) {
862928
bottom = shiftY;
863929
}
864930

865-
this.shiftCanvas(this.context, shiftX, shiftY);
931+
this.shiftCanvas(this.context, shiftX, shiftY, this.renderSettings.copyCanvas);
866932

867933
// Transform into tile-space
868934
left = Math.floor((left + scrollX) / tw);

0 commit comments

Comments
 (0)