Using a very simple WebGL scene with a 4x4 grid of Containers, each Container has enableFilters() and exactly one filters.internal.addMask(...) call.
Even though the visual setup is simple (basic rectangles/circles, no heavy effects), FPS drops significantly when the masked containers are active.
import * as Phaser from 'phaser'
const COLS = 4
const ROWS = 4
const root = document.getElementById('app')!
Object.assign(document.body.style, { margin: '0', overflow: 'hidden' })
Object.assign(root.style, { width: '100vw', height: '100vh' })
type Cell = { container: Phaser.GameObjects.Container; extras: Phaser.GameObjects.GameObject[] }
class ReproScene extends Phaser.Scene {
private info?: Phaser.GameObjects.Text
private startBtn?: Phaser.GameObjects.Text
private stopBtn?: Phaser.GameObjects.Text
private cells: Cell[] = []
private running = false
create(): void {
this.info = this.add.text(12, 12, '', {
fontFamily: 'monospace',
fontSize: '14px',
color: '#e2e8f0',
backgroundColor: '#111827cc',
padding: { x: 8, y: 6 },
}).setDepth(1000)
this.startBtn = this.makeButton('START', '#14532d', () => this.start()).setPosition(20, 120)
this.stopBtn = this.makeButton('STOP', '#7f1d1d', () => this.stop()).setPosition(110, 120)
this.start()
}
override update(): void {
this.info?.setText([
'Phaser 4 Mask Repro',
`renderer: ${this.game.renderer.type === Phaser.WEBGL ? 'WebGL' : 'Canvas'}`,
`running: ${this.running ? 'yes' : 'no'}`,
`containers: ${this.cells.length}`,
`fps: ${Math.round(this.game.loop.actualFps)}`,
])
}
private makeButton(label: string, bg: string, onClick: () => void): Phaser.GameObjects.Text {
return this.add.text(0, 0, label, {
fontFamily: 'monospace',
fontSize: '13px',
color: '#fff',
backgroundColor: bg,
padding: { x: 9, y: 6 },
}).setInteractive({ useHandCursor: true }).on('pointerdown', onClick) as Phaser.GameObjects.Text
}
private start(): void {
if (this.running) return
const w = this.scale.width
const h = this.scale.height
const padX = 20
const top = 170
const gap = 14
const cellW = (w - padX * 2 - gap * (COLS - 1)) / COLS
const cellH = (h - top - 20 - gap * (ROWS - 1)) / ROWS
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const cx = padX + c * (cellW + gap) + cellW * 0.5
const cy = top + r * (cellH + gap) + cellH * 0.5
const box = this.add.rectangle(0, 0, cellW * 0.86, cellH * 0.86, 0x3a86ff, 0.9).setStrokeStyle(2, 0xffffff, 0.7)
const container = this.add.container(cx, cy, [box])
// visible helper shapes (not used as mask source)
const maskCircle = this.add.circle(cx, cy, Math.min(cellW, cellH) * 0.26, 0xffffff, 0.24)
const maskBar = this.add.rectangle(cx, cy, cellW * 0.36, cellH * 0.9, 0xffffff, 0.24).setRotation(0.4)
// actual mask source
const maskHole = this.add.circle(cx, cy, Math.min(cellW, cellH) * 0.1, 0xffffff, 0.24)
container.enableFilters()
container.filters?.internal.addMask(maskHole, true, this.cameras.main, 'world')
this.cells.push({ container, extras: [maskCircle, maskBar, maskHole] })
}
}
this.running = true
}
private stop(): void {
for (const cell of this.cells) {
cell.container.destroy(true)
for (const obj of cell.extras) obj.destroy()
}
this.cells = []
this.running = false
}
}
new Phaser.Game({
type: Phaser.WEBGL,
parent: root,
width: '100%',
height: '100%',
scene: [ReproScene],
scale: { mode: Phaser.Scale.RESIZE, autoCenter: Phaser.Scale.CENTER_BOTH },
})
Version
Description
Using a very simple WebGL scene with a 4x4 grid of Containers, each Container has
enableFilters()and exactly onefilters.internal.addMask(...)call.Even though the visual setup is simple (basic rectangles/circles, no heavy effects), FPS drops significantly when the masked containers are active.
Expected behavior:
Actual behavior:
Repro flow:
Example Test Code