Adapter node: IMAGE (batch) + BOUNDING_BOX (+ MASK, + LAYERS) -> LAYERS.
One document item per frame, each placed by its own box.
Why this is needed. A node that separates an image into elements emits
them as an image batch plus a list of boxes. That batch cannot be fed to
Create Layered Image with the placement intact, because
`expand_item_frames` applies the item's single x/y/w/h/name/z_index to
every frame of a batch. Sixteen layers get one placement between them.
The current workaround is to have the producer pre-place each layer on a
full-size canvas so x=0,y=0 is correct for all of them. That works but is
expensive: at 2K with 16 layers it carries ~768MB of layer tensors and
~268MB of masks to encode what cropped layers hold in a fraction of it.
This node emits one item per layer instead, so each carries its own
placement. It needs no change to the existing compositor path:
`document_items` already sorts by z_index, and `expand_item_frames`
already handles single-frame items correctly - the shared-placement
limitation only bites on batches.
Reads `metadata.name`, `metadata.z_index` and `metadata.content_rect`
where present. `crop_to_content` trims each frame out of a padded batch
and places it by its box, which is what recovers the memory win.
Also restores a `_bbox_list` parser. The equivalent (`_bbox_entries`,
`layout_bboxes`, `state_from_bboxes`) was removed in 1c4953ff along with
the rest of the bbox handling when the node moved to the LAYERS document,
so there is currently no path from a bounding box into the compositor.
Verified: two padded layers with different content sizes, cropped and
placed independently, composite to within 0.50/255 of expectation - the
8-bit quantisation floor of the PIL round trip inside the compositor.
The 76 existing compositor tests still pass.
Blending exists in two implementations - the numpy compositor and the
layerBlend.frag shader that drives the live preview - with nothing holding
them together. They have already diverged once (the safeDiv operand), and a
divergence only shows up to the user as 'the render does not match the
preview'.
Adds compositor_blend_golden.json: every mode, at both endpoints, the
midpoint and inside each epsilon guard. Any implementation of these 26 modes
must reproduce it. compositor_blend_test.py pins the numpy side to it and
additionally spells out the boundary rules by hand, so the guards cannot be
re-broken by regenerating the fixture.
Diffing the shader against the numpy implementation over that grid leaves
exactly one mismatch: luminosity. safe_div guards the denominator and
returns 0, so a luminosity layer over a black or near-black backdrop
disappears. The backdrop has no hue or saturation to preserve there, so the
result should be a neutral grey at the layer's luminance - which is also the
analytic limit of i * lum(l)/lum(i) as the backdrop approaches black. The
matching four-line shader change is proposed on the frontend PR; with both
applied all 26 modes agree.
Also clamps layer opacity to [0, 1]. The layer state round-trips through the
saved workflow and is accepted verbatim on /prompt, so it is untrusted
input; the canvas is only clamped once, after the last layer, so an
out-of-range coverage multiplier changes the blend of every layer above it.
_parse_background already clamps the same field.
These were alll non-dynamic (some non-ModelPatcher) code path calling
FreeMemory for management requiring up-front memory freeing. Convert it
to dynamic to avoid legacy free behaviour mixing into otherwise
dynamic workflows.
* Add native Uni3C controlnet support for Wan models
* Dispatch double_block patches in all Wan model variants
* Remove unused grid_sizes assignment in CameraWanModel, WanModel_S2V, HumoWanModel, and AnimateWanModel
LoadTrainingDataset was the only torch.load call in the codebase without
weights_only=True; comfy/utils.py and comfy/sd1_clip.py already pass it.
Recent PyTorch defaults to weights_only=True, so this is defense-in-depth
for installs pinned to older PyTorch. Verified a typical shard (latents +
standard conditioning) round-trips cleanly under weights_only=True.