Skip to content

Commit 2924d05

Browse files
kyhavlovlsteinCopilot
authored
Fix an issue with regional guidance and multiple quick-queued generations after moving bbox (#8613)
* Fix an issue with multiple quick-queued generations after moving bbox After moving the canvas bbox we still handed out the previous regional-guidance mask because only two parts of the system knew anything had changed. The adapter’s cache key doesn’t include the bbox, so the next few graph builds reused the stale mask from before the move; if the user queued several runs back‑to‑back, every background enqueue except the last skipped rerasterizing altogether because another raster job was still in flight. The fix makes the canvas manager invalidate each region adapter’s cached mask whenever the bbox (or a related setting) changes, and—if a reraster is already running—queues up and waits instead of bailing. Now the first run after a bbox edit forces a new mask, and rapid-fire enqueues just wait their turn, so every queued generation gets the correct regional prompt. * (fix) Update invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts Fixes race condition identified during copilot review. Co-authored-by: Copilot <[email protected]> * Update invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts Co-authored-by: Copilot <[email protected]> * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Lincoln Stein <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent b7e28e4 commit 2924d05

File tree

4 files changed

+89
-1
lines changed

4 files changed

+89
-1
lines changed

invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,14 @@ export abstract class CanvasEntityAdapterBase<T extends CanvasEntityState, U ext
546546
this.renderer.syncKonvaCache();
547547
};
548548

549+
/**
550+
* Invalidates the raster cache for this entity by delegating to the renderer's
551+
* `invalidateRasterCache` method.
552+
*/
553+
invalidateRasterCache = () => {
554+
this.renderer.invalidateRasterCache();
555+
};
556+
549557
/**
550558
* Synchronizes the entity's locked state with the canvas.
551559
*/

invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
6565
*/
6666
renderers = new SyncableMap<string, AnyObjectRenderer>();
6767

68+
/**
69+
* Tracks the cache keys used when rasterizing this entity so they can be invalidated on demand.
70+
*/
71+
rasterCacheKeys = new Set<string>();
72+
6873
/**
6974
* A object containing singleton Konva nodes.
7075
*/
@@ -476,7 +481,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
476481
}): Promise<ImageDTO> => {
477482
const rasterizingAdapter = this.manager.stateApi.$rasterizingAdapter.get();
478483
if (rasterizingAdapter) {
479-
assert(false, `Already rasterizing an entity: ${rasterizingAdapter.id}`);
484+
await this.manager.stateApi.waitForRasterizationToFinish();
480485
}
481486

482487
const { rect, replaceObjects, attrs, bg, ignoreCache } = {
@@ -493,6 +498,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
493498
if (cachedImageName && !ignoreCache) {
494499
imageDTO = await getImageDTOSafe(cachedImageName);
495500
if (imageDTO) {
501+
this.rasterCacheKeys.add(hash);
496502
this.log.trace({ rect, cachedImageName, imageDTO }, 'Using cached rasterized image');
497503
return imageDTO;
498504
}
@@ -524,6 +530,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
524530
replaceObjects,
525531
});
526532
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
533+
this.rasterCacheKeys.add(hash);
527534
return imageDTO;
528535
} catch (error) {
529536
this.log.error({ rasterizeArgs, error: serializeError(error as Error) }, 'Failed to rasterize entity');
@@ -533,6 +540,22 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
533540
}
534541
};
535542

543+
/**
544+
* Invalidates all cached rasterizations for this entity by removing the cached image
545+
* names from the image cache and clearing the tracked raster cache keys. This forces
546+
* future rasterizations to regenerate images instead of using potentially stale
547+
* cached versions.
548+
*/
549+
invalidateRasterCache = () => {
550+
if (this.rasterCacheKeys.size === 0) {
551+
return;
552+
}
553+
for (const key of this.rasterCacheKeys) {
554+
this.manager.cache.imageNameCache.delete(key);
555+
}
556+
this.rasterCacheKeys.clear();
557+
};
558+
536559
cloneObjectGroup = (arg: { attrs?: GroupConfig } = {}): Konva.Group => {
537560
const { attrs } = arg;
538561
const clone = this.konva.objectGroup.clone();

invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,17 @@ export class CanvasManager extends CanvasModuleBase {
199199
];
200200
};
201201

202+
/**
203+
* Invalidates the raster cache for all regional guidance adapters.
204+
* This should be called when bbox or related regional guidance settings change
205+
* to ensure that cached masks are regenerated with the new settings.
206+
*/
207+
invalidateRegionalGuidanceRasterCache = () => {
208+
for (const adapter of this.adapters.regionMasks.values()) {
209+
adapter.invalidateRasterCache();
210+
}
211+
};
212+
202213
createAdapter = (entityIdentifier: CanvasEntityIdentifier): CanvasEntityAdapter => {
203214
if (isRasterLayerEntityIdentifier(entityIdentifier)) {
204215
const adapter = new CanvasEntityAdapterRasterLayer(entityIdentifier, this);

invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,52 @@ export class CanvasStateApiModule extends CanvasModuleBase {
213213
*/
214214
setGenerationBbox = (rect: Rect) => {
215215
this.store.dispatch(bboxChangedFromCanvas(rect));
216+
this.manager.invalidateRegionalGuidanceRasterCache();
217+
};
218+
219+
/**
220+
* Waits for the current rasterization operation to complete.
221+
*
222+
* If no rasterization is in progress, this returns immediately. Use this
223+
* before starting a new rasterization to avoid multiple simultaneous
224+
* rasterization operations acting on the same canvas state.
225+
*
226+
* @returns A promise that resolves once rasterization has finished or
227+
* immediately if no rasterization is in progress.
228+
*/
229+
waitForRasterizationToFinish = async () => {
230+
if (!this.$rasterizingAdapter.get()) {
231+
return;
232+
}
233+
234+
await new Promise<void>((resolve) => {
235+
// Ensure we only resolve once, even if multiple events fire.
236+
let resolved = false;
237+
238+
// Re-check before subscribing to avoid a race where rasterization completes
239+
// between the outer check and listener registration.
240+
if (!this.$rasterizingAdapter.get()) {
241+
resolved = true;
242+
resolve();
243+
return;
244+
}
245+
246+
const unsubscribe = this.$rasterizingAdapter.listen((adapter) => {
247+
if (!adapter && !resolved) {
248+
resolved = true;
249+
unsubscribe();
250+
resolve();
251+
}
252+
});
253+
254+
// Re-check immediately after subscribing to close the race where
255+
// rasterization completes between the check above and `listen()`.
256+
if (!this.$rasterizingAdapter.get() && !resolved) {
257+
resolved = true;
258+
unsubscribe();
259+
resolve();
260+
}
261+
});
216262
};
217263

218264
/**

0 commit comments

Comments
 (0)