522 Commits
Author SHA1 Message Date
Dylan M. Taylor abeb16c96b paint: refuse a sequential-dab stroke whose working copy is too large (#11070)
* paint: refuse a sequential-dab stroke whose working copy is too large

apply_dab_stroke read the whole rectangle enclosing a stroke's dabs into
two dense f32 copies (and a third for the mix), sized by the area
between the dabs rather than what they paint. Two distant points asked
for gigabytes on a sparse document.

Check the three planes against a 4 GiB budget (1 GiB on 32-bit) before
reading, and paint nothing past it.

* address review: apply_dab_stroke returns Result so over-budget dab strokes fail with 'stroke too large'

* fix tests: apply_dab_stroke returns a Result

* fix tests: handle the Result of the dab-stroke functions
2026-11-11 16:31:47 -05:00
David Mann 00f9b7d60d Merge pull request #13106 from rmsff/fix/art-history-brush-depth-refused-12637
Art History Brush: refuse 16- and 32-bit documents with the reference texts
2026-11-11 14:25:30 +01:00
Tanish Manku 28d2b499b5 Merge pull request #11062 from yoriswind/codex/artboard-move-overflow
Reject cross-artboard moves whose required translation overflows
2026-11-11 00:27:05 -07:00
Tanish Manku 6c8140daf4 Merge pull request #10896 from yoriswind/codex/fill-dialog-mask-target
Fix Fill and Stroke for targeted masks
2026-11-11 00:24:24 -07:00
Tanish Manku dc855ae902 Merge pull request #6864 from hidoba/codex/curves-followup-fixes
Fix remaining grayscale Levels/Curves target mismatches
2026-11-10 23:57:32 -07:00
Tanish Manku c2e8c4db82 Merge pull request #6787 from ingling/spot-dot-gain
Color Settings › Spot: dot gain for spot channel and Multichannel ink previews
2026-11-10 23:57:09 -07:00
rmsff 6804a0b8da Art History Brush: refuse 16- and 32-bit documents with the reference texts
The brush works in 8-bit documents only: a 16-bit document with one
layer, a layered 16-bit document or a depth change after the source,
and a 32-bit document each get the reference application's alert text,
before anything is painted. A source of another colour mode gets the
reference text too. Tests that expected 16/32-bit painting now run at
8 bits.
2026-11-11 05:56:48 +03:00
David Mann 6a712d54c9 Merge pull request #12908 from hectorviov/fix/eraser-gray-background
Eraser and Fill: background swatch through the Grayscale document's grey profile (#11940)
2026-11-11 02:00:53 +01:00
David Mann d58afecac9 Merge pull request #12244 from ElNeMeR70/fix/gradient-live-drops-selection-12196
fix(engine): drop selection after masking a live Gradient Fill layer
2026-11-11 00:53:39 +01:00
Vlad Grankovsky 333566a616 Merge upstream main 609e2aa72 into curves follow-up 2026-11-10 19:51:17 +01:00
Hector Villarreal 7b20000052 Grayscale swatch through sGray, as agreed on the PR (#11940)
The reference's Eraser paints 173 for background RGB 0,200,0 in a Grayscale
document; the document's Dot Gain curve gives 153, so swatch_in_doc now
converts through the sRGB tone curve (sGray, 172-173) instead. Pixel
conversions keep the document's profile. The two Grayscale tests are no
longer ignored, and eraser_locked's Gray case checks the sGray value, since
the Brush's own conversion into Gray (not measured) is unchanged.
2026-11-10 08:37:07 -06:00
rmsff 6106c825e7 Red Eye: name the step Red Eye Removal Tool and record a click that changes nothing
A click that runs but changes no pixel (a blue pupil, a transparent area, an
empty layer, Darken Amount 0) now adds one unchanged history state instead of
the "no red-eye pixels" error, as in Photoshop. Non-RGB/Lab documents, channel
and mask targets, hidden layers and clicks outside the selection or the canvas
keep the error and add no state.
2026-11-10 13:56:00 +03:00
437cd7f0fe fix: brush hints keep a feathered mask's ring and the effects' reach (#9329) (#10104)
* fix: brush hints keep a feathered mask's ring and the effects' reach (#9329)

A command's damage hint (the bare dab) was intersected with the edit's
own damage, which `tile_damage` had grown by the mask's feather and the
layer's and enclosing groups' effects. The intersection cut that growth
off, so painting on a feathered layer mask left a stale soft ring around
each dab, and a dab's drop shadow was outside the reported damage.

`hint_damage` now grows the hint by the same rule first (`grow_hint`,
sharing `grow_in_layer` and `effects_reach` with `tile_damage` and the
group walk). The hint is still checked against the changed tiles as is.

The group-shadow test now cuts a hole at a tile's right edge, so its
shadow crosses into the next tile (it passed without the group's growth
before), and the clip-base test gains a dab at a tile edge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: brush hints skip the effects of layers that aren't drawn (#9329)

`grow_hint` grew a dab by the effects of every layer in the document,
hidden ones included, so a large drop shadow on a hidden layer widened
every `paint.stroke` dab elsewhere to all the tiles `step_damage`
reported, and each dab recomposited and uploaded that much.

It now skips layers the compositor skips, by the same rule as
`effect_layers`: a hidden layer, a clipped layer over a hidden base, and
everything inside a hidden group. A change there shows nowhere, and
every drawn layer still grows the hint as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: the group-shadow brush test goes through the hint (#9329)

`a_dab_inside_a_group_with_a_drop_shadow_damages_the_shadow` painted
with a plain `Session::edit`, so its damage came from `step_damage` and
never exercised `grow_hint`. It now paints with `paint.stroke`, and the
child carries a shadow of its own inside the shadowed group: the group's
shadow of the child's shadow lands past either shadow's reach alone, in
the next tile, so dropping the enclosing groups' reach from `grow_hint`
fails it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 16:36:58 +01:00
Eng. Mahmoud Bek ElNemer 5fa708967f fix(engine): drop selection after masking a live Gradient Fill layer
Fixes #12196
2026-11-09 05:07:35 +02:00
David Mann 55498d45ba Merge remote-tracking branch 'origin/main' into pr-9077
# Conflicts:
#	docs/gaps.md
2026-11-08 18:55:24 +01:00
Vlad Grankovsky 3d57a9afeb Merge upstream main 6ab991571 into curves follow-up
Keep recorded channel initialization shared with Record Again and retain upstream Photo Filter and Action playback behavior.
2026-11-08 14:13:01 +01:00
Hector Villarreal 60e45bb926 Color Replacement: refuse RGB/32, Grayscale, Bitmap and Indexed documents (#11923)
paint.colorReplacement and its live stroke refuse the modes and depths
the reference application refuses, with its words: 32 bit per channel
RGB, Grayscale and Bitmap (only full color modes), and Indexed Color.
Nothing is painted and no history is added. RGB/8, RGB/16, CMYK and Lab
paint as before; cases that weren't measured keep today's behaviour.
2026-11-07 21:00:56 -06:00
David Mann d95faebaa3 Merge remote-tracking branch 'origin/main' into pr-9077
# Conflicts:
#	docs/gaps.md
#	docs/target-app-parity.md
2026-11-08 03:04:43 +01:00
Vlad Grankovsky 2ed691bcc5 Merge upstream main e6ba4aca4 into curves follow-up 2026-11-07 20:12:52 +01:00
yoriswind 99653a4626 fix(actions): preserve the Equalize dialog on recorded playback 2026-11-07 10:24:40 -08:00
7f09b2013d Merge current main into #9077 (Average on Smart Objects)
Resolves the status-line conflicts in docs/gaps.md and
docs/target-app-parity.md (both sides kept) and the module list in
crates/engine/tests/it/main.rs.

Co-Authored-By: arjen <76012639+ArjenS-LA@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 18:48:12 +01:00
Trevor Brown 5c49acf7c0 Merge remote-tracking branch 'upstream/main' into fix/magnetic-lasso-composite-11686 2026-11-07 09:39:38 -05:00
David Mann e18900e5fb Merge pull request #10443 from hectorviov/fix/replace-color-add-subtract
Replace Color: Add to Sample and Subtract from Sample (#6280)
2026-11-07 14:44:38 +01:00
yoriswind 6e38ee5760 Reconcile Artboard overflow tests and documentation with current main 2026-11-06 20:16:57 -08:00
Vlad Grankovsky 283de2c506 Merge upstream main 8df2b7a2e into curves follow-up 2026-11-07 01:27:10 +01:00
yoriswind 2e210c0aae Scope Actions tool recording guard to the active take 2026-11-06 13:40:05 -08:00
Trevor BrownandClaude Fable 5.1 3014737225 Magnetic Lasso: tests for following the visible composite (#11686)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 15:49:58 -05:00
Tom InglingandClaude Opus 5.5 721e003b3f Merge main: spot inks in RGB/CMYK previews (#10276) keep the Spot dot gain
main moved the ink formula into `Ink::apply` / `overprint` and prints the
spot channels of RGB and CMYK documents as inks in the channel view instead
of a tint. The Spot dot gain now lives there: `overprint` (the Multichannel
composite and every on-screen ink preview) and the channel view's inks add
`spot_dot_gain()` through `Ink::apply_with_gain`; `Ink::apply` and the saved
PSD thumbnail print inks as stored. The new paged-canvas caller of
`texture_key` (viewport_pages.rs) passes the document, so its pages follow
the ink preview like the other textures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:47:16 -05:00
yoriswind 5f480238d0 Reconcile five Classic Actions commands with current main 2026-11-06 09:29:48 -08:00
FredApps aee46a2fc6 Merge remote-tracking branch 'origin/main' into fix/10164-final-prune-u2 2026-11-06 17:28:48 +01:00
David Mann ec5eb1e2d0 Merge commit 'refs/fixprs/main' into HEAD 2026-11-06 10:21:59 +01:00
Khawar Zamman Wani 123b2032c4 Web build: Place runs a slice per frame, so a large PNG no longer freezes the page (#10164, rebased onto main)
The remaining commits of the stack, applied on top of the squash-merged #10147:

- PNG decodes in resumable steps of rows (PngDecodeRows, shared with decode_rows)
- PNG imports as a document in resumable steps (io PngImport, text metadata and
  document build in bounded steps)
- Jobs can run a slice at a time where there are no threads (StepWork,
  cooperative sessions, Session::poll_jobs)
- Place decodes, flattens and resizes in slices (PlaceSteps), reading a path
  and keeping the frame, memory gate and typed memory-budget errors main has
- The web build places dropped and picked files a slice per frame, with a
  bounded place inbox
- A measurement of the stepped place; docs for jobs without threads
2026-11-06 08:51:17 +05:30
Hector VillarrealandClaude Opus 5.5 be4a915161 Replace Color: Lab bounds refuse subtracted samples; panic_hunt covers them (tanish2k09 review)
Main's Lab bounds (#10765) refuse RGB samples, subtractPoints and order
included. A test pins that for subtractPoints and order, and panic_hunt
now sends malformed and far-off subtractPoints, points and click orders,
also with Lab bounds, to every command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 21:15:00 -06:00
David MannandClaude Opus 5.5 99b93f82da Batch 18 (CodeRabbit nitpicks): crop test checks the active layer's covered pixels; Esc test goes through the shortcut handler
- #11458: the straight-crop rows also check that the active layer's pixels
  inside the old canvas are the plain crop's, so a fill that recolours them
  fails.
- #11295: the Esc case sends the key through shortcuts::handle instead of
  calling cancel directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 04:04:26 +01:00
yoriswind d58701ddff Merge current main into the Actions preservation candidate 2026-11-05 18:31:30 -08:00
David Mann 3af79c0cb7 Merge main 9b02eac2c4 into integration batch 18 2026-11-06 02:14:46 +01:00
yoriswind 84c41c5f10 Limit Actions panel changes to the five reviewed commands 2026-11-05 17:13:15 -08:00
David Mann d563b3100b Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts:
#	crates/engine/tests/it/svg_smart_object.rs
2026-11-06 01:46:48 +01:00
ArjenS-LA a38fc6ff5b Refresh Average Smart Filter stack and verify selection and channel values 2026-11-05 15:53:24 -08:00
e59dcc9c44 GPU plan: cache adjustment LUT rows between plans (#11498) (#11539)
* GPU plan: cache adjustment LUT rows between plans (#11498)

adjustment_program rebuilt the Levels/Curves tone tables on every plan
(twice per refresh), about 1-2 ms per Levels layer each time. Keep the
rows of Levels, Curves, Color Balance and Hue/Saturation ranges in a
32-entry most-recently-used cache (photocraft_compose::lut_rows, at
most 32 x 3 x 16 KiB = 1.5 MiB), built outside the lock.

Entries are keyed by a u64 hash of exactly what each row builder reads,
fields destructured in full and floats by bits (-0.0 and 0.0 stay
apart). Hue/Saturation master sliders don't add entries; Hue/Sat ranges
and Color Balance don't depend on depth. Edit > Purge > All drops the
cache, and a filled cache enables it.

Release, per call: Levels 8/16-bit 1.10 -> 0.003-0.018 ms, Curves
0.16 -> 0.018, Color Balance 0.11 -> 0.001-0.015, Hue/Sat ranges
0.05 -> 0.008-0.018. The tables are identical, so rendering is unchanged.

Tests: cached rows match fresh tables bit for bit at 8/16/32-bit, hits
and LRU eviction on a local cache, key coverage, and Purge All
releasing the cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* compose: rustfmt the LUT row cache tests (#11498)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU plan: test that plans read cached LUT rows and that curve points key them (#11498)

Plant rows no builder makes under a fresh key and check the planner returns
them, and check that moving a Curves point changes its key (review on #11539).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* compose: prevent in-flight LUT builds from undoing purge

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-06 00:32:49 +01:00
David Mann 1d053f5c2c Merge PR #11495: Actions: play recorded Stack Modes and Video Frame events 2026-11-06 00:28:47 +01:00
David Mann 1d5c69dda4 Merge PR #11458: Crop: fill the active layer and Background with Content-Aware Fill 2026-11-06 00:28:41 +01:00
David Mann f96e77e794 Merge PR #11553: Actions: replay recorded Perspective Warp steps 2026-11-06 00:28:38 +01:00
e55e2f3577 Place decodes and fits in the background: dropping a large file no longer freezes the window (#10147)
* Place decodes, fits and renders in a background job

File > Place Embedded / Linked started with Session::start, and
Session::start_place_bytes for bytes with no path, now run the decode,
flatten, resize and SVG render on a worker (prepare_place) and insert the
layer when the job is applied (apply_place). Inline callers (execute, the
CLI, headless MCP, actions) behave and return exactly as before. Part of
#10031.

* Dropped and placed files no longer freeze the window on desktop

With background jobs on, File > Place and files dropped on the canvas start
the place as a job (JobsUi.place). Free Transform begins when the layer has
landed, a failed or cancelled place is reported and the drop queue goes on,
and a drop that arrives meanwhile waits for it. The place shows its progress
in the status bar instead of the modal dialog, so the view stays usable.
Part of #10031.

* Tests: compare place job results without the layer id

Layer ids come from a counter shared by every session, so a job and an inline place in two sessions never get the same one.

* Dropped files stay with the document they were dropped on

Each queued canvas drop now carries its document's id. A file is placed
only while that document is active: files dropped on a document in
another tab wait until it is back in view, and are discarded with a
message when it is closed. Web drops, read by the shell after the drop,
go to the document the drop landed on.

Before, the next queued file went into whichever document was active
when the previous place finished, failed or was cancelled, in the
background and inline paths alike.

* Remove the unused file_open import

* fix: address background placement review feedback

* fix: preserve drop targets and interrupt placement pixel stages

* fix: qualify placement inbox document identity

* test: wait for background placement in app drop regression

* fix: retain queued drops while external document jobs run

* test: clone rendered pixels from drop-managed buffers

* test: cover band rows channels and queued-drop resumption

* Place decodes and fits in the background: dropping a large file no longer freezes the window

Placing a file decoded, flattened and resized on the UI thread; a large PNG froze the
window for seconds (#10031). `place_bytes` is split into a session read, `prepare_place`
(decode, flatten, fit, translate, SVG render at placement scale) and `apply_place`, and
runs through `jobs::run`, so File › Place and canvas drops run on a worker with progress
and cancel. The window follows the job: status-bar progress, Free Transform when the layer
lands, and drops queued per target document.

Review fixes on the way:
- a path and byte drop queue entry records its target DocId, so files dropped on one
  document never land in another after a tab switch, and closed documents discard theirs;
- the closed-document notice stays up for a frame instead of being cleared by the next
  queued placement;
- the queue is bounded (32 files / 256 MiB) so drops waiting on another tab cannot grow it
  without limit;
- flatten and resize stop at the job's interrupt, and a place skips the modal progress
  dialog only when the status bar actually shows its progress;
- the placement's `doc` is kept only for SVG input, through the importer's `is_svg_input`;
- the interrupted-composite test uses an opaque fixture that varies by row and channel, and
  checks the composite against the layer's stored pixels.

* Place reads its file in the job, and refuses a huge canvas as before

file.placeEmbedded and file.placeLinked read the whole file in the
command before the job started, so a path dropped on the canvas or
picked in File > Place still blocked the window for the read. The place
now carries the path and reads it in prepare_place: on the job's worker
in the background, on the calling thread inline (wasm, background jobs
off), with the same read_file limit and error text. Whether the source
is a vector one is decided once it has been read.

The job's flatten check returned Other("the document canvas is too large
to flatten"), where the place used to give flattened_checked's
BadParams for file.place naming the canvas size. Both now share
check_flattenable, so the old error type and message are back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Path drops aren't capped, and a discarded drop wakes the queue

The 32-file cap on the drop queue counted path drops, which hold no
file contents while they wait, so dropping 40 files on the canvas
placed 32 and refused 8 where main placed them all. The count cap now
applies to browser drops only (each holds its whole file); the byte
budget still bounds those.

When place_next_dropped discarded a closed document's files it returned
without asking for another frame, so another document's queued files
waited for the next input event. It now requests a repaint when files
remain, keeping the one frame that shows the notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:49:52 +01:00
6326117c53 Web build: big operations that won't fit fail with an error instead of trapping, and a trap says so (#10129)
* Raster: ask the allocator whether a big operation fits; PSD save reserves its whole output

New photocraft_raster::headroom: ensure(need) tries to reserve the need plus a margin and frees it
at once (web only; native always passes), below(bytes) tells whether less than that can be
allocated, and simulate_available fakes the answer for tests and diagnostics.

PsdFile::to_bytes reserved only the pixel data, so embedded smart-object data and other tagged
blocks grew the output by doubling, which on the web is one unrecoverable huge realloc. It now
reserves the exact encoded length.

* Big operations that won't fit in memory fail with an error instead of trapping

Place, Image Size, a smart-object re-render and Save now ask the allocator whether their estimated
need can be allocated before they start (photocraft_engine::memory_gate, estimates per operation,
photocraft_io::estimate_export_bytes for saves). A refusal changes nothing and says what the
operation needed and what to do (save, Edit > Purge > All, close documents).

A standing notice warns when less than 512 MiB can be allocated, with hysteresis, so the user can
save before anything traps. Notices can now be sticky, which keeps them past the auto-hide delay.

* Web build: a trap says so instead of freezing the page, and ?free-mb simulates low memory

index.html holds a hidden, static 'PhotoCraft stopped' message with a Reload link. web.rs reveals
it when the module traps (the window error event, or an unhandled rejection from a spawned
future), tells running out of memory from an internal error by the panic message a chained panic
hook kept, and skips the Leave site prompt once stopped. ?free-mb=N makes the memory gates and the
low-memory warning see only N MiB, for checking them in a browser.

* Docs: web build memory handling in the gaps list and the parity assessment

* Engine: use is_multiple_of in the megabyte formatter

* Cargo.lock: the web shell depends on photocraft-raster

* Web boot page: carry the stopped message too, as the test that keeps the two pages equal requires

* Save check counts one copy of the document, and runs once per save

The encoded file's buffer is reserved fallibly, so a save it doesn't fit
in fails with an error instead of trapping; counting it as well refused
saves that would have fitted. On the background-save path the check ran
in write_document and again in start_save, which other callers reach
directly; it now runs only in start_save there.

* Review fixes: warning check allocates nothing, PSD merged image counted, automation saves checked

The low-memory warning's periodic check reads the size of linear memory
instead of reserving a block: wasm memory never shrinks, so a trial every
few seconds raised the high-water mark by up to 768 MiB for nothing. The
operation checks keep their trial reservation.

PSD and PSB saves always build a canvas-sized merged image (a placeholder
without maximum compatibility) and hold it raw and encoded; the save
estimate now counts it. The synchronous automation save checks memory
before encoding, as File > Save does, and its test asserts the memory
error.

The trap listeners go in before the app is built, so a trap while
building it shows the stopped message too, and a memory access out of
bounds reads as unexpected rather than as running out of memory.

* Cover layered PSD preflight limitation

* Fix layered PSD test iteration

* Docs: layered PSD preflight limitation in the gaps list

* Web OOM gate: gate direct exports, truthful advice for unbounded PSD sources (#10024)

* Web OOM gate: estimate layered PSDs per layer instead of refusing every real PSD

The PSD check admitted a file only when its color-mode data, image
resources and layer section were all empty. Every Photoshop file has
image resources (at least the resolution), so on the web nearly every
real PSD was refused for Place, PSD smart objects could not render,
and Image Size was refused for documents holding one, whatever the
free memory (u64::MAX never fits).

The color-mode data and image resources hold no pixels, so they are
now only skipped. A layered file reads its layer count from the layer
info (signed, absolute value) and adds a canvas-sized surface and mask
per layer to the flat estimate. Files that are not 8-bit RGB or
grayscale, keep their layers in a Layr tagged block, or whose sections
can't be read stay unbounded and refused, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Web stopped page: match trap messages ignoring case

Safari reports the allocation-failure abort as "Unreachable code
should not be executed" and Firefox says "out of memory", which the
case-sensitive matching missed: the trap was not recognised, or was
shown as an unexpected stop instead of running out of memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Export estimate: count the merged image OpenRaster writes

The ORA writer keeps the layers but also flattens the document for
mergedimage.png, a 16 B/px composite of the canvas, which the
estimate left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:04:59 +01:00
yoriswind 8bc5399652 Keep unknown Stack Mode None descriptors inert 2026-11-05 12:57:30 -08:00
yoriswind 19ac2452a0 Merge Batch 17 while preserving Crop fill and Artboard sizing 2026-11-05 09:15:36 -08:00
yoriswind 8a126888f3 Merge upstream main after integration batch 17 2026-11-05 09:09:02 -08:00
yoriswind 9f393ebeb5 Require the null class for edited Stack Mode None provenance 2026-11-05 08:50:31 -08:00
yoriswind b6bb43bc19 Handle edited Stack Modes and cap video frame growth 2026-11-05 08:48:40 -08:00