From 0646bb368a799394b44a186b86a516fdfc5380e8 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Fri, 12 Dec 2025 14:41:13 -0800 Subject: [PATCH] perf: add gpu hint and transform settle to prevent rasterizing while zooming (scale transform) (#7417) ## Summary Ensures the nodes get their own compositing layers during scale transform (tracked via mouse wheel events), which prevents rasterization during transform. Adds forced reflow at end of transform to ensure layers are always at correct resolution (fixes blurriness and some readability issues). Videos show testing this branch first then testing main - doing layer visualization, paint (include paint operations calculations and actual raster) visualizations, and cpu usage monitoring. https://github.com/user-attachments/assets/c5fab219-0b32-4822-9238-c4572f0d6a44 https://github.com/user-attachments/assets/7e172e8d-cc5b-4dcd-aa07-1dfc3eb65bac --- packages/design-system/src/css/style.css | 9 + .../layout/__tests__/TransformPane.test.ts | 71 +++++++- .../core/layout/transform/TransformPane.vue | 21 ++- .../layout/transform/useTransformSettling.ts | 84 +++++++++ .../graph/useTransformSettling.test.ts | 171 ++++++++++++++++++ 5 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 src/renderer/core/layout/transform/useTransformSettling.ts create mode 100644 tests-ui/tests/composables/graph/useTransformSettling.test.ts diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index 1efc86194..1153d4543 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -1328,6 +1328,15 @@ audio.comfy-audio.empty-audio-widget { font-size 0.1s ease; } +/* Performance optimization during canvas interaction */ +.transform-pane--interacting .lg-node * { + transition: none !important; +} + +.transform-pane--interacting .lg-node { + will-change: transform; +} + /* ===================== Mask Editor Styles ===================== */ /* To be migrated to Tailwind later */ #maskEditor_brush { diff --git a/src/renderer/core/layout/__tests__/TransformPane.test.ts b/src/renderer/core/layout/__tests__/TransformPane.test.ts index 4876115b9..64952a629 100644 --- a/src/renderer/core/layout/__tests__/TransformPane.test.ts +++ b/src/renderer/core/layout/__tests__/TransformPane.test.ts @@ -117,6 +117,73 @@ describe('TransformPane', () => { }) }) + describe('canvas event listeners', () => { + it('should add event listeners to canvas on mount', async () => { + const mockCanvas = createMockCanvas() + mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + await nextTick() + + expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith( + 'wheel', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith( + 'pointerdown', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith( + 'pointerup', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith( + 'pointercancel', + expect.any(Function), + expect.any(Object) + ) + }) + + it('should remove event listeners on unmount', async () => { + const mockCanvas = createMockCanvas() + const wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + await nextTick() + wrapper.unmount() + + expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith( + 'wheel', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith( + 'pointerdown', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith( + 'pointerup', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith( + 'pointercancel', + expect.any(Function), + expect.any(Object) + ) + }) + }) + describe('interaction state management', () => { it('should apply interacting class during interactions', async () => { const mockCanvas = createMockCanvas() @@ -131,7 +198,9 @@ describe('TransformPane', () => { const transformPane = wrapper.find('[data-testid="transform-pane"]') // Initially should not have interacting class - expect(transformPane.classes()).not.toContain('will-change-transform') + expect(transformPane.classes()).not.toContain( + 'transform-pane--interacting' + ) }) it('should handle pointer events for node delegation', async () => { diff --git a/src/renderer/core/layout/transform/TransformPane.vue b/src/renderer/core/layout/transform/TransformPane.vue index b39f67140..cf542bb89 100644 --- a/src/renderer/core/layout/transform/TransformPane.vue +++ b/src/renderer/core/layout/transform/TransformPane.vue @@ -1,7 +1,12 @@