Selecting a non-path property (Position) or a stale selection disabled Closed, unlike
RotoBezier and the other Mask and Shape Path commands, which fall back to every mask of
the selected layers. A selected mask property (Feather, Opacity, Expansion) now selects
its mask. A selected locked mask still counts as a match, so it never falls back to its
unlocked siblings.
* fix(model): share decoded textures and skin matrices, bound texture pixels, index OBJ materials
Texture::decode expanded any image to float RGBA without a size check. Repeated mtllib lines
reparsed the library and decoded its textures again; glTF textures with a non-default sampler
each copied their image's pixels; every instance of a skin built its own joint matrices. OBJ
usemtl and faces scanned the growing material and primitive lists, and contour grouping kept
every ancestor of every contour.
* address review: keep largest contours and dedupe mtllib by set
* address review: contour depth counts containers, glTF wrap copies and FBX textures share the import pixel budget
* address review: holes go to the innermost outline; glTF wrap variants keyed by decoded image
* address review: create every outline slot before attaching holes
* Import: a model's left-out parts reach the user as an import warning (Model::warnings, Importer::probe_noting)
* Models: left-out textures are import warnings; the import texture budget is 2^29 pixels
* Import: a model's left-out parts reach the user as an import warning (Model::warnings, Importer::probe_noting)
* Import: a model's left-out parts reach the user as an import warning
* FBX textures move to #2955 (it reworks the same code)
* fix(model): FBX records stay within their parent, and textures, materials and links are indexed
A binary child record could end past its parent: the parent's end was then restored and the
child's tail parsed again. A failed array decompression printed the decompressed bytes in the
error. Texture objects aliasing one embedded Video or file each decoded and kept their own
pixels, outside the import budget. Each polygon scanned the primitive groups for its material,
and each object scanned every connection for its children.
* address review: FBX textures: key image cache by the source used, charge decoded size before decoding, propagate budget errors
* Import: a model's left-out parts reach the user as an import warning (Model::warnings, Importer::probe_noting)
* Import: a model's left-out parts reach the user as an import warning
* FBX: textures past the limits are left out with an import warning, not a failed import; 2^29-pixel allowance
* Render: memoized audio detection, bounded stretch, preroll, waveform, cache entries, 3D bounds, solids and selector time
comp_has_audio walked a precomp again for every layer using it; each comp
is answered once. A tiny stored stretch made one audio block fetch a vast
source range; playback speed is capped at 100x. Effect preroll is capped at
10 s. Waveforms stop where the audio ends and summarise at most a day.
Disk cache entries are decompressed only when their stated size matches
what their dimensions need (and at most 1 GiB). Advanced 3D triangle bounds
are clamped on both sides before narrowing. Solids larger than 16 Mpx are
made at a lower scale. A camera-facing layer's parent recursion is bounded.
A layer's Expression Selectors share one time allowance. A disk cache
folder under the temp folder is made private and may not be a link.
* address review: audio detection by breadth-first search, private cache folder resolved before the temp check, oversized layers not written, solid cap after rounding, selector static amount, viewport i32 clamp
* address review: private-folder check skips the temp folder itself; override value in expression fallback; assert alias cache opens
* address review: refuse the shared temp folder itself as a disk cache folder
* address review: shared counted selector budget, 30 s preroll, cache hardening (shared temp roots, matte discard, exact LZ4 length, render version 7), orient nesting guard
* fix(media): check still, EXR channel, GIF and WebP canvas sizes before allocating
* address review: GIF frame allocation limit, lower wasm32 still limit, no EXR fallback after a size-limit error
* address review: the 2^25-pixel still limit also applies on 32-bit native targets
* Media: raise the still pixel limit to 2^28 (16384 x 16384) on 64-bit targets
* fix(engine): refuse to convert an expression on a layer of more than 2^18 frames
* address review: refuse a non-positive frame duration; count a partial last frame; check every selected layer before converting
* Convert Expression to Keyframes: cap at 2^20 frames (a 3-hour 60 fps comp is 648,000)
* Timewarp: bound the motion search's total comparisons by coarsening the vector grid
* Timewarp: coarsen within the frame, keep the pyramid depth and the window inside the frame
* Import: a model's left-out parts reach the user as an import warning (Model::warnings, Importer::probe_noting)
* Import: a model's left-out parts reach the user as an import warning
WGSL's select evaluates both of its arguments, so the previous commit still
ran the overflowing pow and only discarded it. An explicit branch now skips
the power for a curve past the bound.
WGSL leaves the result of an overflowing pow indeterminate, so clamping
after it did not guarantee the CPU's 1e30 bound on every backend. A curve
past the bound (log2|t| / gamma > 99.7) now saturates to +-1e30 without
calling pow; between 1e30 and 2^99.7 the clamp gives the same 1e30, so the
GPU matches the CPU everywhere.
CPU / GPU comparisons at the new ends for Venetian Blinds (width 1, width
and feather 32000), Radial Wipe (feather 32000), Grid (border 2000),
Scatter (1000), Beam (4000), Change Color (1710 degrees) and Turbulent
Displace (seed 100000). The comparison counts infinity on both sides as
equal, so Levels (Individual Controls) gets a direct check that the GPU's
output is finite at the ends of its ranges. The Noise HLS test asserts the
spec's ranges, so it fails without them.
The 1e6 bound also capped curves that were finite before (gamma 0.1 on an
HDR value 4 x Input White is 4^10, about 1.05e6). The bound is now 1e30:
1e30 times the widest output range (2e4) is still finite, and every curve
below it renders as before. A test pins 4^10.
Random Seed takes 0..100000, After Effects' recorded valid range (and what
the other effects' Random Seed already takes). The CPU and GPU hash it the
same way with wrapping u32 arithmetic, so any seed costs the same.
Default and slider are unchanged.
Scale Height and Scale Width take -30000..30000 %, After Effects' recorded
valid range in the same units. The CPU and GPU share the matrix
(transform_matrix_px) and the buffer growth, which is capped at 16 Mpx
(transform_grow; the CPU test already renders 100000 %), so larger scales
stay bounded.
Default and slider are unchanged.
Starting and Ending Thickness take 0..4000, After Effects' recorded valid
range in the same units (pixels). The CPU and GPU renderers draw the beam
per pixel from its distance to the segment, within the layer's buffer, so
a thicker beam costs the same.
Default and slider are unchanged.
Scatter Amount takes 0..1000, After Effects' recorded valid range in the
same units (pixels); the slider stays 0..127. The CPU and GPU renderers move
each pixel by one random offset of up to the amount, so a larger amount
costs the same; samples outside the layer are transparent.
Default and slider are unchanged.
Feather takes 0..32000, After Effects' recorded valid range in the same
units. The CPU and GPU renderers evaluate the feather per pixel in closed
form (a Gaussian of the distance to the wedge, radial_wipe_alpha), so a
wide feather costs the same and stays finite.
Default and slider are unchanged.
Width takes 1..32000 and Feather 0..32000, After Effects' recorded valid
ranges in the same units (pixels). The work stays bounded: the renderers
floor the width at 1 buffer pixel, sum at most 40 Gaussian terms per
pixel, and above 8 widths the feather is the even veil
(venetian_cover; the GPU hands that case to the CPU, as before).
Default and slider are unchanged.
Border takes 0..2000, After Effects' recorded valid range in the same units
(pixels). The CPU and GPU renderers compute the border per pixel in closed
form, so a wider border costs the same; one wider than the cells covers
them.
Default and slider are unchanged.
Hue Transform takes -1800..1800 degrees, After Effects' recorded valid range
in the same units. The CPU and GPU renderers wrap the hue (rem_euclid /
fract_euclid), so five turns render like the hue they end on, at the same
cost.
Default and slider are unchanged.
Hue, Lightness and Saturation go up to 1000 % and Grain Size down to 0 (it
keeps its wider maximum, 100), After Effects' recorded valid ranges in the
same units. Noise HLS Auto shares the controls and widens with them.
Defaults and sliders are unchanged.
The renderers need no change: hue wraps, lightness and saturation clamp per
pixel after the noise is added, and Grain Size is floored at 0.1 px on the
CPU and the GPU, so the new values cost the same and stay finite.
The amounts were written as "percent" in keyframe text because their range
was 0..100, and Grain Size would become "percent" at 0..100; UNITS rows keep
the units they had.
Blend goes up to 10000 (it keeps its minimum of 1, below After Effects' 5)
and Jitter up to 500 %, After Effects' recorded valid ranges in the same
units. Defaults and sliders are unchanged.
The CPU and GPU renderers capped Jitter's dither at 100 %, so higher values
would be stored but render as 100. Both now take it from one function,
four_color_jitter, which keeps the dither linear up to 500 %. Blend 10000
is a finite softening (Blend squared) at the same cost.
Jitter was written as "percent" in keyframe text because its range was
0..100; a UNITS row keeps that unit.
Input/Output Black and White of every channel take -10000..10000 and Gamma
goes down to 0 (it keeps its wider maximum, 10), After Effects' recorded
valid ranges in the same units (the .aep mapping is Conv::Plain). Defaults
and the 0..1 / 0.1..3 sliders are unchanged.
Gamma 0 renders as the existing 0.01 floor, an exponent of 100, so an HDR
value a few times Input White would overflow f32 and an infinite curve
times an empty output range is NaN. The curve is now bounded at +-1e6 on
the CPU (Lv::apply) and the GPU (fxt_lv), so the output stays finite over
the whole range; values below the bound are unchanged.
The engine regression (tests/effect_ranges.rs) sets both ends of each row
through prop.set, checks undo/redo, save and reopen, and that a project
saved with the narrower ranges opens with the new ones and its values.
Reload Footage Changed on Disk of a file whose metadata didn't change keeps the
project's revision; PurgeCaches emptied the viewer frame cache but the new
frame came back under the key of the one on screen, so neither the Composition
viewer nor the passive viewers uploaded it. Count purges and mix the count into
the frame key's options, as #3493 does with the LUT generation.
A test walks every effect parameter the .aep table maps to a recorded After
Effects definition (fx_defs) and checks that the effect's hard range
(ParamUi::Slider min..max, what prop.set and the value editors clamp to)
covers After Effects' valid range, converted to EffectCraft's units the way
the importer converts values. Conv::number_scale is that conversion, now
shared by the importer and the test.
The parameters still narrower are allow-listed by issue (#3460-#3492); the
test fails on a new narrower parameter and on a row that is no longer
needed, so the list shrinks as the issues are fixed.
On a precomp layer the key covered only the nested comp's size, rate and
duration, so an edit inside the precomp kept a stale analysis. The key now
writes the precomp layer's Essential Properties and what its comp draws, field
by field and recursively. Each comp is written once per key and nesting is
capped, so shared, deep or cyclic precomps are keyed in bounded time. Docs say
what clears an analysis.
Their analyses are made from the layer's input to the effect (masks, then the
effects above it), but the key covered only the layer's frames, so a mask or
effect edit kept a stale analysis. frames_key::analysis_key adds the masks,
the effects above the instance and the Effects switch. Keys saved before
(frames only) stay valid while each edit keeps the input.
Move the LUT cache that follows files by size and modification time into
effects::file_cache and use it for ocio::lut::load_file and
ocio_config::load_config. A config's entry depends on the .ocio file, the LUTs
its FileTransforms read and the search-path places it looked for missing ones.
check_lut_files and lut_generation now cover all three caches.
Expressions find effects by name and read names (thisProperty.name,
propertyGroup(1).name), and seed random/wiggle with the property uid, so
both can change the frames. The Debug-based key had them.