Skip to content

Commit 0646bb3

Browse files
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
1 parent a8ef7a6 commit 0646bb3

File tree

5 files changed

+354
-2
lines changed

5 files changed

+354
-2
lines changed

packages/design-system/src/css/style.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,15 @@ audio.comfy-audio.empty-audio-widget {
13281328
font-size 0.1s ease;
13291329
}
13301330

1331+
/* Performance optimization during canvas interaction */
1332+
.transform-pane--interacting .lg-node * {
1333+
transition: none !important;
1334+
}
1335+
1336+
.transform-pane--interacting .lg-node {
1337+
will-change: transform;
1338+
}
1339+
13311340
/* ===================== Mask Editor Styles ===================== */
13321341
/* To be migrated to Tailwind later */
13331342
#maskEditor_brush {

src/renderer/core/layout/__tests__/TransformPane.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,73 @@ describe('TransformPane', () => {
117117
})
118118
})
119119

120+
describe('canvas event listeners', () => {
121+
it('should add event listeners to canvas on mount', async () => {
122+
const mockCanvas = createMockCanvas()
123+
mount(TransformPane, {
124+
props: {
125+
canvas: mockCanvas
126+
}
127+
})
128+
129+
await nextTick()
130+
131+
expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith(
132+
'wheel',
133+
expect.any(Function),
134+
expect.any(Object)
135+
)
136+
expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith(
137+
'pointerdown',
138+
expect.any(Function),
139+
expect.any(Object)
140+
)
141+
expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith(
142+
'pointerup',
143+
expect.any(Function),
144+
expect.any(Object)
145+
)
146+
expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith(
147+
'pointercancel',
148+
expect.any(Function),
149+
expect.any(Object)
150+
)
151+
})
152+
153+
it('should remove event listeners on unmount', async () => {
154+
const mockCanvas = createMockCanvas()
155+
const wrapper = mount(TransformPane, {
156+
props: {
157+
canvas: mockCanvas
158+
}
159+
})
160+
161+
await nextTick()
162+
wrapper.unmount()
163+
164+
expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith(
165+
'wheel',
166+
expect.any(Function),
167+
expect.any(Object)
168+
)
169+
expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith(
170+
'pointerdown',
171+
expect.any(Function),
172+
expect.any(Object)
173+
)
174+
expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith(
175+
'pointerup',
176+
expect.any(Function),
177+
expect.any(Object)
178+
)
179+
expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith(
180+
'pointercancel',
181+
expect.any(Function),
182+
expect.any(Object)
183+
)
184+
})
185+
})
186+
120187
describe('interaction state management', () => {
121188
it('should apply interacting class during interactions', async () => {
122189
const mockCanvas = createMockCanvas()
@@ -131,7 +198,9 @@ describe('TransformPane', () => {
131198
const transformPane = wrapper.find('[data-testid="transform-pane"]')
132199

133200
// Initially should not have interacting class
134-
expect(transformPane.classes()).not.toContain('will-change-transform')
201+
expect(transformPane.classes()).not.toContain(
202+
'transform-pane--interacting'
203+
)
135204
})
136205

137206
it('should handle pointer events for node delegation', async () => {

src/renderer/core/layout/transform/TransformPane.vue

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
<template>
22
<div
33
data-testid="transform-pane"
4-
class="absolute inset-0 w-full h-full pointer-events-none"
4+
:class="
5+
cn(
6+
'absolute inset-0 w-full h-full pointer-events-none',
7+
isInteracting ? 'transform-pane--interacting' : 'will-change-auto'
8+
)
9+
"
510
:style="transformStyle"
611
>
712
<!-- Vue nodes will be rendered here -->
@@ -11,9 +16,12 @@
1116

1217
<script setup lang="ts">
1318
import { useRafFn } from '@vueuse/core'
19+
import { computed } from 'vue'
1420
1521
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
22+
import { useTransformSettling } from '@/renderer/core/layout/transform/useTransformSettling'
1623
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
24+
import { cn } from '@/utils/tailwindUtil'
1725
1826
interface TransformPaneProps {
1927
canvas?: LGraphCanvas
@@ -23,6 +31,11 @@ const props = defineProps<TransformPaneProps>()
2331
2432
const { transformStyle, syncWithCanvas } = useTransformState()
2533
34+
const canvasElement = computed(() => props.canvas?.canvas)
35+
const { isTransforming: isInteracting } = useTransformSettling(canvasElement, {
36+
settleDelay: 16
37+
})
38+
2639
useRafFn(
2740
() => {
2841
if (!props.canvas) {
@@ -33,3 +46,9 @@ useRafFn(
3346
{ immediate: true }
3447
)
3548
</script>
49+
50+
<style scoped>
51+
.transform-pane--interacting {
52+
will-change: transform;
53+
}
54+
</style>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useDebounceFn, useEventListener } from '@vueuse/core'
2+
import { ref } from 'vue'
3+
import type { MaybeRefOrGetter } from 'vue'
4+
5+
interface TransformSettlingOptions {
6+
/**
7+
* Delay in ms before transform is considered "settled" after last interaction
8+
* @default 200
9+
*/
10+
settleDelay?: number
11+
/**
12+
* Whether to use passive event listeners (better performance but can't preventDefault)
13+
* @default true
14+
*/
15+
passive?: boolean
16+
}
17+
18+
/**
19+
* Tracks when canvas zoom transforms are actively changing vs settled.
20+
*
21+
* This composable helps optimize rendering quality during zoom transformations.
22+
* When the user is actively zooming, we can reduce rendering quality
23+
* for better performance. Once the transform "settles" (stops changing), we can
24+
* trigger high-quality re-rasterization.
25+
*
26+
* The settling concept prevents constant quality switching during interactions
27+
* by waiting for a period of inactivity before considering the transform complete.
28+
*
29+
* Uses VueUse's useEventListener for automatic cleanup and useDebounceFn for
30+
* efficient settle detection.
31+
*
32+
* @example
33+
* ```ts
34+
* const { isTransforming } = useTransformSettling(canvasRef, {
35+
* settleDelay: 200
36+
* })
37+
*
38+
* // Use in CSS classes or rendering logic
39+
* const cssClass = computed(() => ({
40+
* 'low-quality': isTransforming.value,
41+
* 'high-quality': !isTransforming.value
42+
* }))
43+
* ```
44+
*/
45+
export function useTransformSettling(
46+
target: MaybeRefOrGetter<HTMLElement | null | undefined>,
47+
options: TransformSettlingOptions = {}
48+
) {
49+
const { settleDelay = 256, passive = true } = options
50+
51+
const isTransforming = ref(false)
52+
53+
/**
54+
* Mark transform as active
55+
*/
56+
const markTransformActive = () => {
57+
isTransforming.value = true
58+
}
59+
60+
/**
61+
* Mark transform as settled (debounced)
62+
*/
63+
const markTransformSettled = useDebounceFn(() => {
64+
isTransforming.value = false
65+
}, settleDelay)
66+
67+
/**
68+
* Handle zoom transform event - mark active then queue settle
69+
*/
70+
const handleWheel = () => {
71+
markTransformActive()
72+
void markTransformSettled()
73+
}
74+
75+
// Register wheel event listener with auto-cleanup
76+
useEventListener(target, 'wheel', handleWheel, {
77+
capture: true,
78+
passive
79+
})
80+
81+
return {
82+
isTransforming
83+
}
84+
}

0 commit comments

Comments
 (0)