From ec9a3a270be4996cc6bd7db32f5317223be9f616 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sat, 13 Jun 2026 10:05:14 -0500 Subject: [PATCH 1/4] fix: double screen resize --- CHANGELOG.md | 1 + src/engine/screen.ts | 2 -- src/spec/vitest/screen-spec.ts | 21 ++++++++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 192e87830c..74efb3ce05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fixed issue where window resize events were handled twice when using window-based display modes (FitScreen, FitScreenAndFill, FitScreenAndZoom, FillScreen), causing double resolution/viewport computation and double canvas size writes - Fixed issue where the first action in a sequence would not execute after calling `clearActions()` mid-execution. All action types now properly reset their initialization state when stopped, resolving issue #3468 - Performance: Font/Text now use smaller texture sizes, improving performance on Safari especially when rendering text diff --git a/src/engine/screen.ts b/src/engine/screen.ts index e9a587bfab..cec0b9dcb4 100644 --- a/src/engine/screen.ts +++ b/src/engine/screen.ts @@ -328,7 +328,6 @@ export class Screen { if (this._resizeObserver) { this._resizeObserver.disconnect(); } - this.parent.removeEventListener('resize', this._resizeHandler); // Safari <=13.1 workaround if (this._mediaQueryList.removeEventListener) { this._mediaQueryList.removeEventListener('change', this._pixelRatioChangeHandler); @@ -1172,7 +1171,6 @@ export class Screen { }); this._resizeObserver.observe(this.parent); } - this.parent.addEventListener('resize', this._resizeHandler); } /** diff --git a/src/spec/vitest/screen-spec.ts b/src/spec/vitest/screen-spec.ts index bd74cd1ec2..6e4839c422 100644 --- a/src/spec/vitest/screen-spec.ts +++ b/src/spec/vitest/screen-spec.ts @@ -991,9 +991,28 @@ describe('A Screen', () => { ]); expect(warnOnce.mock.calls[1]).toEqual([ 'Scaled resolution too big attempted recovery!' + - ` Pixel ratio was automatically reduced to (2) to avoid 4k texture limit.` + + ` Pixel ratio is automatically reduced to (2) to avoid 4k texture limit.` + ' Setting `ex.Engine({pixelRatio: ...}) will override any automatic recalculation, do so at your own risk.` ' + ' (read more here https://excaliburjs.com/docs/screens#understanding-viewport--resolution).' ]); }); + + it('should only register window resize listener once for Window parent (not twice)', () => { + const addEventListenerSpy = vi.spyOn(window, 'addEventListener'); + + const sut = new ex.Screen({ + canvas, + context, + browser, + displayMode: ex.DisplayMode.FitScreen, + viewport: { width: 800, height: 600 } + }); + + const resizeListenerCalls = addEventListenerSpy.mock.calls.filter( + call => call[0] === 'resize' + ); + expect(resizeListenerCalls.length).toBe(1); + + addEventListenerSpy.mockRestore(); + }); }); From af20d796baefb4bb83db83aadb2230a40b02a64e Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sun, 14 Jun 2026 18:58:09 -0500 Subject: [PATCH 2/4] fix lint --- src/spec/vitest/screen-spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/spec/vitest/screen-spec.ts b/src/spec/vitest/screen-spec.ts index 6e4839c422..2364e47ab8 100644 --- a/src/spec/vitest/screen-spec.ts +++ b/src/spec/vitest/screen-spec.ts @@ -1008,9 +1008,7 @@ describe('A Screen', () => { viewport: { width: 800, height: 600 } }); - const resizeListenerCalls = addEventListenerSpy.mock.calls.filter( - call => call[0] === 'resize' - ); + const resizeListenerCalls = addEventListenerSpy.mock.calls.filter((call) => call[0] === 'resize'); expect(resizeListenerCalls.length).toBe(1); addEventListenerSpy.mockRestore(); From 7577c0002d01b5e3a808c560c5f8dca6ff032532 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sun, 14 Jun 2026 19:55:56 -0500 Subject: [PATCH 3/4] fix tests --- src/engine/screen.ts | 4 ++++ src/spec/vitest/screen-spec.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/engine/screen.ts b/src/engine/screen.ts index cec0b9dcb4..f8f42b24dd 100644 --- a/src/engine/screen.ts +++ b/src/engine/screen.ts @@ -328,6 +328,9 @@ export class Screen { if (this._resizeObserver) { this._resizeObserver.disconnect(); } + if (!(this.parent instanceof Window)) { + this.parent.removeEventListener('resize', this._resizeHandler); + } // Safari <=13.1 workaround if (this._mediaQueryList.removeEventListener) { this._mediaQueryList.removeEventListener('change', this._pixelRatioChangeHandler); @@ -1170,6 +1173,7 @@ export class Screen { this._resizeHandler(); }); this._resizeObserver.observe(this.parent); + this.parent.addEventListener('resize', this._resizeHandler); } } diff --git a/src/spec/vitest/screen-spec.ts b/src/spec/vitest/screen-spec.ts index 2364e47ab8..fb66e9a55e 100644 --- a/src/spec/vitest/screen-spec.ts +++ b/src/spec/vitest/screen-spec.ts @@ -991,7 +991,7 @@ describe('A Screen', () => { ]); expect(warnOnce.mock.calls[1]).toEqual([ 'Scaled resolution too big attempted recovery!' + - ` Pixel ratio is automatically reduced to (2) to avoid 4k texture limit.` + + ` Pixel ratio was automatically reduced to (2) to avoid 4k texture limit.` + ' Setting `ex.Engine({pixelRatio: ...}) will override any automatic recalculation, do so at your own risk.` ' + ' (read more here https://excaliburjs.com/docs/screens#understanding-viewport--resolution).' ]); From e45641a13d281f2d3bfebea093e390465a66e463 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sun, 14 Jun 2026 21:23:35 -0500 Subject: [PATCH 4/4] update mock --- src/spec/vitest/loader-spec.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/spec/vitest/loader-spec.ts b/src/spec/vitest/loader-spec.ts index 9bcd0f5efb..a8d0e048e9 100644 --- a/src/spec/vitest/loader-spec.ts +++ b/src/spec/vitest/loader-spec.ts @@ -286,12 +286,25 @@ describe('A loader', () => { const oldPos = [loader.playButtonRootElement.style.left, loader.playButtonRootElement.style.top]; - engine.screen.viewport = { width: 100, height: 100 }; + const originalGetBoundingClientRect = engine.canvas.getBoundingClientRect; + engine.canvas.getBoundingClientRect = () => ({ + x: 0, + y: 0, + width: 500, + height: 500, + top: 0, + left: 0, + right: 500, + bottom: 500, + toJSON: () => {} + }); engine.browser.window.nativeComponent.dispatchEvent(new Event('resize')); const newPos = [loader.playButtonRootElement.style.left, loader.playButtonRootElement.style.top]; + engine.canvas.getBoundingClientRect = originalGetBoundingClientRect; + expect(oldPos).not.toEqual(newPos); });