clicks_queued_while_reading_are_one_step_each_and_undo_in_order relied on
a blurred 4K plate being slow to read, and failed when the IDs were read
while the clicks were still being pressed. The probe's ID read is now held
(the Held footage from ui_crypto_pick) until the clicks are checked, and
the test asserts that the build reached that read and that nothing was
made while it was held.
The Esc that closed New Solid or Composition Settings also disarmed the
eyedropper. An open dialog now takes Esc like a focused text field or an
open popup, so that Esc only closes the dialog and the next one disarms.
long_sequence asserts both instead of logging a finding.
A solve with unsolved frames drew the same block as a full one, so it
looked complete. It now gets one more line in the warning colour,
"Partial solve: N of M frames solved", under the status line, and the
editor grows by that line only then. The line is an automation element,
so agents can read it too.
effects-parity.md and gaps.md: the Selection is picked in the viewer. control-protocol.md: the
viewer.cryptoPick element ids. footage.md: a paragraph on the eyedropper.
An eyedropper next to the Cryptomatte effect's Selection arms the viewer pick, and the status
line says how to use it. It stays armed until Esc or the button (Esc in a text field or a menu
only ends that); disarming, New, Open and Revert end it and let go of the session's probe.
The app runs the pick's bookkeeping at the start and end of each pass, the viewer hands the
pointer to the pick while it is armed and opens its context menu for a right-click the pick
forwarded, and a Selection cut off by a narrow Effect Controls panel shows whole on hover.
UI tests: ui_crypto_pick (arming, hover, clicks, modifiers, drags, waiting clicks, status,
highlight) and ui_crypto_pick_undo (one undo step per pick, the probe kept over undo).
panels::crypto_pick is the eyedropper's viewer side; it is wired in by the next commit. Armed,
the hover names the ID under the cursor and highlights its coverage, a click replaces the
Selection with it, Shift adds, Alt or Ctrl (Option or Cmd on a Mac) removes. Each pick is one
undo step, "Pick Cryptomatte".
The probe the hover needs is built off the UI thread, one at a time for the app
(EffectcraftApp::crypto_probe_busy, re-arming included), not while playing and not until a
scrub has settled for 100 ms; meanwhile the chip says so. Session::crypto_probe_ready asks for
readiness without changing anything; a job is asked for only when it starts.
Clicks made meanwhile are queued, own the build and are made in order at the end of the pass,
after its keys and edits. A click is the frame, point, resolution and project revision its
press saw. If any of them changed before it is made (an edit, Ctrl+Z, a nudge, a frame step),
it and every click waiting with it are dropped and the status says "Pick not made". A pick's
own change of the revision doesn't count as an edit for a click still held. Losing the window
focus drops a held press. A click that takes the focus from the Selection field waits for the
field to commit and is made on top of it.
The overlay takes drags too: a click that drifts picks at its press, and one dragged further
than 24 points picks nothing ("Drag cancelled the pick"). Presses and releases are read from
the pass's events in order rather than from egui's end-of-pass click state, since a slow pass
can hold a whole click. Space, the Hand and the Zoom tool keep their clicks, the middle button
pans, and a right-click is passed on under viewer::FORWARDED_SECONDARY for the viewer to open
its context menu (the viewer side is in the next commit). Errors show in the chip in red,
without the command's prefix, wrapped to the viewer's width.
The hovered ID's coverage (Session::crypto_coverage) is drawn as a texture through the layer's
effect-space transform, rebuilt only when the hovered ID, frame or revision changes, and
exposed to the test harness as viewer.cryptoPick.highlight.
Supporting changes:
- EffectcraftApp::say sets the status line and restarts its 5 s; each status message now gets
its own 5 s, also when the same text is shown again.
- Points snap to the centre of the shown frame's pixel at the exact 1/n scale the viewer
renders (frames::render_scale; the frame key keeps 0.333 for Third, which named the next
row's ID). Cache Frames When Idle renders at that exact scale too, so idle-cached frames are
on the grid the pick reads.
Session::crypto_coverage returns the coverage of one Cryptomatte ID over the layer, read from
the probe Session::crypto_probe_job keeps: at most max_side samples on the long side, each at
the centre of its cell, or None until the probe is built. It is what the Cryptomatte
eyedropper's hover highlight will draw.
Tests cover the coverage of an ID from the kept probe and the cost of a highlight with six
ranks.
One comp-space path: the pivot is the anchor where it shows and a flipped
parent reverses the turn, so the separate parent-space frame and the moved
parent_to_comp go. Under uneven or skewed parents the layer's edge tracks the
pointer slightly less exactly. The empty-layer marker goes back to main's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text listing printed enum values as bare numbers, which are 0-based
where After Effects scripting counts from 1, so Range Selector Shape 2
read as Ramp Up was Ramp Down. Print the label from the property's
options, as props --json already lists them: "0 (Square)".
- A watch-folder render gives the host session's set back after its own
project replaced it on the shared footage source.
- Guard gpu fx_depth::IDS against reads_aux drifting apart.
- Count picture and channel decodes per thread instead of reading the
shared channel cache, so parallel tests can't make the checks flaky.
- The engine test fails when no set was ever sent.
- Docs: Media Replacement items are learned at their first frame.
Refs #474
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bitstream check read ffprobe's first-frame tags from the MP4, which
ffmpeg can fill from the colr box when a stream says nothing itself. Copy
the video out as a bare h264, hevc or obu stream first, so only the SPS VUI
or AV1 color_config counts.
The pool learned that an effect reads a footage item's channels only at the
first aux() call, so the first frame was decoded twice (picture, then in
full), and the hint was never cleared after the effect was removed.
The session now gives the footage source the whole set of files read by
enabled 3D Channel effects (FootageSource::set_aux_footage) with every
project change (Session::bump: edits, undo, redo, history jumps, rollbacks,
project open), sending it only when it changed. The pool keeps that set and,
as a safety net, what aux() learns; a new set drops what was learned.
Refs #474
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A source with no box-sized area clear of transparent pixels (a small
layer inside a blur's padding) got no sample boxes, and
grain_stats_boxes then measured the whole image, edge included. With no
clean box Match Grain now matches no grain, as After Effects applies
none when its samples hold transparent areas. Compensate for Existing
Noise took the same route through the layer's own boxes, so a layer
with no clean box now has nothing taken off. grain_stats_boxes keeps
its empty-list fallback.
A 3D model layer keeps its file's materials, so it has no Base Color,
Metallic, Roughness or Emissive. material.set refused them with "no
Material Options property matched (..., baseColor, metallic, roughness,
...)", naming the parameters it had just refused, and next to a
parameter the layer has it dropped them and reported success.
A parameter that none of the given layers has now fails the command
(nothing is set), naming it and the properties the first layer has. A
value of the wrong type is its own error. The parameter list says which
layer kinds take which.
Cut the fix down: a click off every mesh makes nothing only when the art it resolves to
already has a mesh with that outline. The nearest-art fallback in `region` stays, so a
near miss beside separate art still meshes it. `mesh::outline` is as it was and the
`newMesh` option of puppet.addPin is back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A box's own pixels had to be opaque, but the high-pass that measures its
grain reads 3 px around it, and a transparent pixel there is black once
unpremultiplied. A blur earlier in the stack pads the layer with
transparent pixels, so boxes along that border measured the edge as
strong, smooth colour grain, and Match Grain drew 12 px colour cells
from a clean source on the frames that picked them. Boxes now need their
3 px surround opaque too (a summed-area table keeps the check O(1) per
candidate). Remove Grain picks its boxes the same way.
The drag branch asked what was under the pointer when the drag was recognised, by which
time a quick drag has already reached the art, so no gesture started at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups:
- Files in use are never dropped from the channel cache: planes an effect
holds, the files of the last 4 pictures made of their planes until their
effects ask for the channels, and decodes in progress, which now count
the planes they will allocate (from the header) against the budget. With
4 frames at once over a budget of one file, each file decodes once (test).
- A decode that panics removes its entry instead of leaving it decoding.
- One decode allocates at most 8 GiB of planes (512 MiB on 32-bit and
wasm); a header asking for more is refused with the reason.
- A shared picture whose file has no cached channels (a failed read, a
broken part it doesn't use) is decoded from its own channels.
- Comment why waiting on a decode may block a rayon worker; drop the
subsampling check and test (the exr crate refuses subsampled files).
- Docs: the cache is a soft bound; a single-part file still decompresses
every channel; the first frame and the image crate's route decode twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds tiled (ZIP16, RLE), tiled mip-mapped (ZIP16, PIZ), decreasing line
order, uncompressed, a uint R/G/B picture and a multi-part file whose first
R/G/B part is depth-named (the planes can't stand in for the image crate's
picture there). The new hashes were taken with main's crates/media/src at
a646bdcc (extracted with git archive) and the same test, before the decode
rework.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The channel cache decodes a file once however many callers ask for it at
the same time (the others wait for that decode), and is bounded by the
bytes of its planes (1 GiB, least recently used out first, a file counting
as used when its decode finishes, the newest file always kept) instead of
four files of any size.
- Once an effect has read a footage item's channels, its pictures are made
of the cached planes, also for files with an unnamed RGB layer (bit for
bit what the image crate shows, which still decodes the files the planes
can't stand in for); footage alone decodes only its picture's channels
(a multi-layer file's were all decoded and cached for every frame).
- Channels are decoded straight into their display-window planes, and the
picture is converted from the planes without two interleaved copies.
Measured on a 30-frame 2560x1440 sequence of 32 channels (DWAA), one frame
at a time: footage alone 0.185 -> 0.073 s per frame and 4.4 -> 1.8 GB peak;
with an unnamed RGB layer plus Cryptomatte 0.244 -> 0.159 s; multi-part
plus EXtractoR 0.194 -> 0.127 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rename RenderOpts::scene_output to skip_hdr_step (Pipe::without_hdr_step):
the consumer decodes with Pipe::output_linear or applies the step itself.
- Alpha-only EXRs hold the alpha as it is in every output space (it is
coverage, not encoded colour; it used to be sRGB-decoded).
- Tests: a half-opacity layer over a coloured background (premultiplied and
RGB-over-background paths, golden hashes taken before the change), Rec.
2100 PQ and HLG EXR outputs, raw alpha, and a visible skip of the GPU test
without an adapter.
- docs/architecture.md: the Working Gamma curve for unmanaged projects, and
no HDR step for unmanaged projects or PQ / HLG outputs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
B44 and B44A pack only half floats, so at 32-bit float the menu and the
render log say the samples stay uncompressed. Half-float frames hold samples
above 65 504 at that limit instead of writing infinity. The EXR parameters
move into their own helper; exr_out is public for other writers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenEXR renders (Render Queue sequences and Save Frame As ProEXR) received
the frame after Project Settings > HDR, so Compand and Tone Map (the default
of every OCIO project) compressed highlights and changed values below 1.0
before the writer decoded them, always with the sRGB curve.
- RenderOpts::scene_output: the top-level comp converts to the output space
without the HDR step (CPU and GPU walks, via Renderer::output_pipe).
- Pipe::output_linear: the writers decode with the output space's curve
(the Working Gamma's when unmanaged) instead of a fixed sRGB curve.
- Render Queue EXR jobs and ProEXR set the flag; PSD layers, 8-bit stills,
movies and the viewer keep the HDR step. The flag is part of the viewer
frame and disk cache keys (opts_hash).
Golden hashes, taken before the change, pin PNG/TIFF/Targa bytes for
8/16/32 bpc x Clip/Compand/ToneMap, Clip-mode EXR samples with an sRGB
output, and the layered PSD bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenEXR sequence output modules get two settings, exrCompression (none,
rle, zips, zip, piz, pxr24, b44, b44a, dwaa, dwab; scan lines in each
method's own block height) and exrBitDepth (half or float). The defaults
are ZIP and 32-bit float, so existing projects render the same bytes as
before. They go through the project file, Output Module templates, the
renderQueue commands and renderQueue.formats, the CLI (--exr-compression,
--exr-depth), the Output Module menu (lossy methods are marked) and the
render log. Half rounds each sample to the nearest half float.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
animator_uid matches a uid before an index, so an animator whose uid
equals an index is picked over the one at that position. The four
commands that take `animator` (layer.addTextAnimatorProperty,
layer.addTextSelector, text.addSelector, text.animatorFontAxes) now say
so; text.addSelector also said only `uid|index` before. The test gives a
second animator uid 1 and checks that `animator: 1` picks it.
- `dialog.comp.view` is the scroll area's viewport (its clip rect). Cut
by the content's max rect, it lost the scrolled amount at the bottom.
- When the dialog's width changes, the pass laid out at the old width is
discarded and a repaint requested.
- The measured width keeps room for a vertical scroll bar. Without it,
switching the language with the dialog open left the two scroll bars
trading places every frame: the UI never settled and OK was clipped
every other frame.
- `content_width` becomes `dialog_width`, which returns the dialog's
width, so the 600-point rule is in one place.
Tests: the view keeps its place when the dialog scrolls, and switching
to French and back with the dialog open settles with OK inside it.
layer.addTextAnimatorProperty, layer.addTextSelector and
text.animatorFontAxes take `animator: uid|index`, counted from 1 as the
code and `#n` references do, but the parameters didn't say so and
`animator: 0` failed with only "no animator 0". The parameters now say
1-based, and the error says what the value can be.
Synthetic single-layer, multi-layer, multi-part and windowed files in
half, float and uint, with RLE, ZIP, PIZ and DWAA, decoded through the
media pool picture first and channels first. The hashes are taken on
main before the decode is reworked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comp::unique_layer_name and Project::numbered_item_name each stripped
every trailing digit before numbering. Both use one helper now,
numbered_name, which replaces a trailing number only when a space sets
it off (Red Solid 1 -> Red Solid 2); digits that end a word stay.
SAM 2's instance now reads wgpu's environment variables, as the render context's does, so a driver such as
Mesa's Dozen (WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER=1) is visible to it, and WGPU_POWER_PREF applies.
An adapter without compute shaders is refused before a device is made on it, so the CPU takes over with one
line instead of a validation error per kernel. The refused adapter is kept for the life of the process, as
the device on it was: dropping it crashed the tests on WSL2's GL driver. Gpu::shared no longer repeats
"SAM 2 compute:" in its errors.
The dialog was a fixed 600 points wide inside a scroll area, so a row
wider than that in another language (French "Conserver la fréquence
d'images en cas d'imbrication ou dans la file d'attente de rendu", the
French and Spanish Lock Aspect Ratio row) pushed OK past the right edge,
reachable only by scrolling sideways.
The dialog now measures the widest row of its three tabs (as it already
measured their heights) and takes that width, or 600 points if that is
more, capped by the window as before. English keeps 600 points.