Skip to content

Commit c80c4fc

Browse files
committed
fix: compositor cache survives workflow tab switches
1 parent ce2b236 commit c80c4fc

6 files changed

Lines changed: 130 additions & 2 deletions

File tree

src/extensions/core/imageCompositor.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import type { CompositorBBox } from '@/renderer/extensions/compositor/composables/compositorLayerState'
22
import { resetCompositorStateWidgets } from '@/renderer/extensions/compositor/composables/compositorWidgets'
3+
import type { CompositorLayersSnapshot } from '@/renderer/extensions/compositor/composables/useCompositorLayers'
34
import {
45
clearCompositorLayers,
56
clearCompositorPreviewOverride,
6-
setCompositorLayers
7+
restoreCompositorLayers,
8+
setCompositorLayers,
9+
snapshotCompositorLayers
710
} from '@/renderer/extensions/compositor/composables/useCompositorLayers'
811
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
12+
import { registerWorkflowTransientState } from '@/platform/workflow/management/workflowTransientState'
913
import type { NodeOutputWith } from '@/schemas/apiSchema'
1014
import { useExtensionService } from '@/services/extensionService'
1115

@@ -16,6 +20,12 @@ type ImageCompositorOutput = NodeOutputWith<{
1620
compositor_state_stale?: boolean[]
1721
}>
1822

23+
registerWorkflowTransientState('Comfy.ImageCompositor.layers', {
24+
snapshot: snapshotCompositorLayers,
25+
restore: (state) =>
26+
restoreCompositorLayers(state as CompositorLayersSnapshot | undefined)
27+
})
28+
1929
useExtensionService().registerExtension({
2030
name: 'Comfy.ImageCompositor',
2131

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
3+
import {
4+
registerWorkflowTransientState,
5+
restoreWorkflowTransientState,
6+
snapshotWorkflowTransientState
7+
} from './workflowTransientState'
8+
9+
describe('workflowTransientState', () => {
10+
it('round-trips each provider by its key', () => {
11+
let stateA = 'a1'
12+
registerWorkflowTransientState('test.a', {
13+
snapshot: () => stateA,
14+
restore: (state) => {
15+
stateA = typeof state === 'string' ? state : 'cleared'
16+
}
17+
})
18+
19+
const snapshot = snapshotWorkflowTransientState()
20+
stateA = 'a2'
21+
restoreWorkflowTransientState(snapshot)
22+
expect(stateA).toBe('a1')
23+
})
24+
25+
it('restores undefined into providers missing from the snapshot', () => {
26+
const restore = vi.fn()
27+
registerWorkflowTransientState('test.b', { snapshot: () => 'b', restore })
28+
29+
restoreWorkflowTransientState(new Map())
30+
expect(restore).toHaveBeenCalledWith(undefined)
31+
32+
restoreWorkflowTransientState(undefined)
33+
expect(restore).toHaveBeenLastCalledWith(undefined)
34+
})
35+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export interface WorkflowTransientStateProvider {
2+
snapshot(): unknown
3+
restore(state: unknown): void
4+
}
5+
6+
export type WorkflowTransientSnapshot = ReadonlyMap<string, unknown>
7+
8+
const providers = new Map<string, WorkflowTransientStateProvider>()
9+
10+
export function registerWorkflowTransientState(
11+
key: string,
12+
provider: WorkflowTransientStateProvider
13+
): void {
14+
providers.set(key, provider)
15+
}
16+
17+
export function snapshotWorkflowTransientState(): WorkflowTransientSnapshot {
18+
return new Map(
19+
[...providers].map(([key, provider]) => [key, provider.snapshot()])
20+
)
21+
}
22+
23+
export function restoreWorkflowTransientState(
24+
snapshot: WorkflowTransientSnapshot | undefined
25+
): void {
26+
for (const [key, provider] of providers) {
27+
provider.restore(snapshot?.get(key))
28+
}
29+
}

src/renderer/extensions/compositor/composables/useCompositorLayers.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {
1111
getCompositorLayers,
1212
getCompositorPreviewOverride,
1313
hasCompositorLayers,
14+
restoreCompositorLayers,
1415
setCompositorLayers,
15-
setCompositorPreviewOverride
16+
setCompositorPreviewOverride,
17+
snapshotCompositorLayers
1618
} from './useCompositorLayers'
1719

1820
const layerRef = { filename: 'a.png', subfolder: '', type: 'temp' }
@@ -97,6 +99,32 @@ describe('useCompositorLayers', () => {
9799
expect(getCompositorBBoxes(node)).toBeUndefined()
98100
})
99101

102+
it('survives a workflow switch through snapshot and restore', () => {
103+
setCompositorLayers(node, [layerRef], ['hash-a'], [bbox])
104+
const snapshot = snapshotCompositorLayers()
105+
106+
clearCompositorLayers(node)
107+
expect(hasCompositorLayers(node)).toBe(false)
108+
109+
restoreCompositorLayers(snapshot)
110+
expect(getCompositorLayers(node)).toEqual([layerRef])
111+
expect(getCompositorInputsFingerprint(node)).toEqual(['hash-a'])
112+
expect(getCompositorBBoxes(node)).toEqual([bbox])
113+
})
114+
115+
it('restoring another workflow snapshot replaces the whole cache', () => {
116+
setCompositorLayers(node, [layerRef])
117+
const otherWorkflow = snapshotCompositorLayers()
118+
setCompositorLayers(subgraphNode, [layerRef])
119+
120+
restoreCompositorLayers(otherWorkflow)
121+
expect(hasCompositorLayers(node)).toBe(true)
122+
expect(hasCompositorLayers(subgraphNode)).toBe(false)
123+
124+
restoreCompositorLayers(undefined)
125+
expect(hasCompositorLayers(node)).toBe(false)
126+
})
127+
100128
it('stores, replaces and clears the preview override per node', () => {
101129
expect(getCompositorPreviewOverride(node)).toBeUndefined()
102130
setCompositorPreviewOverride(node, 'blob:first')

src/renderer/extensions/compositor/composables/useCompositorLayers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ interface CompositorNodeCache {
1515
bboxes?: (CompositorBBox | null)[]
1616
}
1717

18+
export type CompositorLayersSnapshot = ReadonlyMap<
19+
NodeLocatorId,
20+
CompositorNodeCache
21+
>
22+
1823
const cacheByNode = reactive(new Map<NodeLocatorId, CompositorNodeCache>())
1924
const previewOverrideByNode = reactive(new Map<NodeLocatorId, string>())
2025

@@ -89,3 +94,16 @@ export function clearCompositorPreviewOverride(node: CompositorNodeRef): void {
8994
export function hasCompositorLayers(node: CompositorNodeRef): boolean {
9095
return (cacheByNode.get(cacheKey(node))?.layers.length ?? 0) > 0
9196
}
97+
98+
export function snapshotCompositorLayers(): CompositorLayersSnapshot {
99+
return new Map(cacheByNode)
100+
}
101+
102+
export function restoreCompositorLayers(
103+
snapshot: CompositorLayersSnapshot | undefined
104+
): void {
105+
cacheByNode.clear()
106+
for (const [key, value] of snapshot ?? []) {
107+
cacheByNode.set(key, value)
108+
}
109+
}

src/scripts/changeTracker.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import _ from 'es-toolkit/compat'
33

44
import { assert } from '@/base/assert'
55
import { LAYER_EDITOR_DIALOG_KEY } from '@/renderer/extensions/layerEditor/composables/layerEditorDialog'
6+
import type { WorkflowTransientSnapshot } from '@/platform/workflow/management/workflowTransientState'
7+
import {
8+
restoreWorkflowTransientState,
9+
snapshotWorkflowTransientState
10+
} from '@/platform/workflow/management/workflowTransientState'
611
import type { CanvasPointerEvent } from '@/lib/litegraph/src/litegraph'
712
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
813
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
@@ -269,6 +274,7 @@ export class ChangeTracker {
269274

270275
ds?: { scale: number; offset: [number, number] }
271276
nodeOutputs?: Record<string, ExecutedWsMessage['output']>
277+
private transientState?: WorkflowTransientSnapshot
272278

273279
private subgraphState?: {
274280
navigation: string[]
@@ -304,6 +310,7 @@ export class ChangeTracker {
304310
offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]]
305311
}
306312
this.nodeOutputs = useNodeOutputStore().snapshotOutputs()
313+
this.transientState = snapshotWorkflowTransientState()
307314
const navigation = useSubgraphNavigationStore().exportState()
308315
// Always store the navigation state, even if empty (root level)
309316
this.subgraphState = { navigation }
@@ -349,6 +356,7 @@ export class ChangeTracker {
349356
if (this.nodeOutputs) {
350357
useNodeOutputStore().restoreOutputs(this.nodeOutputs)
351358
}
359+
restoreWorkflowTransientState(this.transientState)
352360
if (this.subgraphState) {
353361
const { navigation } = this.subgraphState
354362
const firstInvalidIndex = navigation.findIndex(

0 commit comments

Comments
 (0)