Skip to content

Fork sync 2026-10-18: upstream and pending work into ours - #3

Merged
iannitram merged 149 commits into
oursfrom
sync/2026-10-18-hyperion
Oct 18, 2026
Merged

iannitram merged 149 commits into
oursfrom
sync/2026-10-18-hyperion

Conversation

@iannitram

Copy link
Copy Markdown
Owner

Daily fork sync 2026-10-18, from Hyperion: 143 upstream commits (to upstream/main 22072d9).

Conflicts, resolved:

Fork branches: no other branch is merged. feat/live-link-comp-flag stays parked (live link is not ready to land).

Checks: Hyperion runs fmt, clippy and the suites of what changed through the dev gate on this head; results are added here before merging. Record: Workspaces/_dev/runs/2026-10-18-satellites/fork-sync-hyperion.md

🤖 Generated with Claude Code

nanox333 and others added 30 commits October 13, 2026 10:09
…old#1181)

The pinning regression only exercised All Edges (and Left Edge in the CPU
unit test), so the reported 'does not work with Top/Bottom Edge' had no
coverage to catch or clear it. This holds the pinned first/last row in a
CPU test at Direction 90, and adds CPU/GPU parity cases for Top (4),
Bottom (6), diagonal directions, and single-edge pinning on shape layers.
CPU pinning verified correct on main: the guards in dst_wave_pin/wave_pin
agree; the gap was that neither edge path was tested.
…rytold#1174)

A Merge Paths item appended to a shape layer's Contents cleared every draw
collected so far, including fills and strokes inside groups above it, so
the layer's shapes vanished from the render. AE's rule is per group: only
paint items in the merge's own group above it are dropped. Draws now
record which contents level created them; the merge drops only its own
group's paint, and the group-bubbled draws keep rendering.

Adds a regression test (Merge Shapes and Add modes) that renders nothing
without the fix.
…d of a ring

Expansion was a fill plus a stroke 2·|expansion| wide, blended per pixel. Once the
expansion passed the shape's curvature radius the stroke's inner edge inverted: a
20 px circle expanded by 60 left a ring with the layer showing through the middle,
+600 cut only the original dot, and negative values left islands. The outline is
now offset with Offset Paths' geometry (round joins) and filled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preset rows (text, built-in and user .ecpreset) were click-only, so dragging
one onto a layer did nothing. They now start a DragPayload::Preset drag with
a ghost, and the Timeline, Composition viewer and Effect Controls apply the
preset to the layer it is dropped on, as they already do for effects and as
After Effects does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eyframes (storytold#1329)

The panel eased only pairs of neighbouring selected keyframes, so a single
selected key (or keys that aren't neighbours) changed nothing.

An Ease selector (In, Out, In-Out; In-Out by default) now chooses the
sides of each selected key to ease. In eases a key's incoming side with
the curve's in side, Out its outgoing side with the curve's out side. A
side is eased when it has a neighbouring key, selected or not; the
neighbour is left as it is. Each side's speeds scale to the segment on
that side, so a key between a +100/s and a -40/s segment gets each its
own. The side not eased keeps playing as it did (easeSide, unchanged).

Sides are measured before any key changes, and no undo step opens when
no selected key has a segment on the chosen side; the status says which
neighbour is missing. A selected pair in In-Out still gets the whole
curve; its outer sides now ease too when they face an unselected key.
From Keys still reads the curve of a selected pair.
…torytold#1329)

One middle key between unequal segments in each mode, with its
neighbours, times, values and selection unchanged and one undo restoring
it; first and last keys, keys that aren't neighbours and keys of several
properties; no undo step when nothing can ease; a Hold and an
auto-Bezier side kept as they play; per-dimension and motion-path
speeds on Scale and Position; and the pair's outer sides under In-Out
and Out. All but the unchanged API tests fail on main.
…u, as in After Effects

Guide layers render only in their own comp's viewer: a nested renderer (precomp, collapsed
precomp in 2D, Classic 3D and Advanced 3D, comps read by effects, aux passes, the GPU walk's
resident precomps) now runs with guides off (Renderer::nested_opts), so a precomp's guide
layers no longer show in the parent comp's viewer or in a "Current Settings" render.

The Timeline layer and Composition viewer right-click menus (shared layer_menu) gain a
Guide Layer item (a plain toggle, no check mark) acting on the selection, and guide layers show a guide mark
before their name in the Timeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… them eased (storytold#1329)

A roving key passes speed straight through: the engine keeps both its
sides linear whenever the keys are edited (retime_roving), so an ease set
on one never plays. The panel still counted it as eased, so a selected
roving key showed "Eased 1 keyframe(s)" and stayed linear. The previous
pair-only panel did the same to a pair holding a roving key.

Selected roving keys are now left out of the work and counted. The
status names how many were skipped and that turning off Roving lets them
ease; when nothing else was selected, nothing changes and no undo step
opens. Fixed keys next to a roving key still ease.
… key on a curved path (storytold#1329)

Out on an auto-Bezier key leaves its in side playing the same values,
and undo and redo restore each state. On a curved Position path with
unequal segment lengths and durations, In sets the key's speed from the
incoming path length, playback arrives faster than a constant speed, and
the out side plays as before.
…pts (storytold#1248)

Every socket operation locked one global table and held it through the
blocking write_all/flush, and streams had no write timeout (accepted
ones were explicitly blocking). A peer that stopped reading kept the
script blocked for good, and every other socket call, close included,
waited behind it.

- The table maps handles to Arc<Entry>, each with its own Mutex<Sock>.
  The table is locked only to look a socket up, add or remove one,
  never during network I/O, so one stalled socket doesn't hold up the
  others.
- `write` sets a write timeout from the socket's `timeout` (10 s by
  default, as reads) and reports "write timed out … the other side
  isn't reading" instead of blocking. Failures to set the read or write
  timeout, or to make an accepted stream blocking, are reported instead
  of ignored.
- `close` removes the socket and shuts its stream down through a second
  handle, so a call blocked on it fails at once.

Tests (socket.rs, native only): a 64 MB write to a loopback peer that
never reads fails with "timed out" after a 1 s timeout; while one socket
is blocked writing, another answers at once, and closing the blocked one
returns at once and ends its write. Both fail on main (the write never
gives up; the second socket waits on the table lock).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cles (storytold#1236)

Depth-limited <use> expansion still let a few kilobytes of branching
references expand to 2^N elements. The parser now counts every element it
converts, <use> copies included, against a budget of max(500 000, twice the
file's own elements) and fails the import with Error::Xml once it is spent.
Targets currently being expanded are tracked, so mutual references stop at
the first repeat instead of nesting to the depth limit.

Fixes storytold#1236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
…rithmetic (storytold#1235)

Accessor counts sized the f32/u32 output before the buffer view was looked
at, and offsets, strides and count * components used unchecked arithmetic,
so a few hundred bytes of JSON could request a multi-gigabyte allocation
(aborting the process) or overflow.

- Doc::layout checks count * components, adds the view and accessor offsets,
  rejects a byteStride smaller than an element, and checks that the last
  element ends inside the buffer, all with checked arithmetic, before
  floats() or ints() allocate anything.
- Accessors without a buffer view (all zeros) are capped at 2^24 components.
- idx() and node index lists use usize::try_from instead of 'as usize'; an
  image bufferView's end and a GLB chunk's end are computed with checked_add.

Fixes storytold#1235

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
…hes (storytold#1234)

The 1 GiB array limit counted wire bytes, but arrays are kept as f64/i64,
so boolean arrays grew 8x and 32-bit arrays 2x past it, and nothing bounded
the total across arrays. Every Model linked to a Geometry also built its own
copy of the mesh.

- A per-import Budget (2 GiB) is charged 8 bytes per array element before
  the array is decompressed or widened, ASCII files are charged for their
  parsed values, and each built mesh is charged for its vertex and index
  data. Going past it fails the import with a clear error instead of an
  allocation abort. parse_within/read_within take a lower limit for tests.
- Models instancing the same geometry with the same materials share one
  mesh; a different material mapping still gets its own.

Fixes storytold#1234

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
…age to (storytold#1442)

Phones and messaging apps store frame times rounded to whole milliseconds
(33/34 ms at 30 fps, 16/17 ms at 60 fps). FilmCraft's reader takes the
median duration, so such clips imported at 30.303 or 58.824 fps, and New
Comp from Selection and renders followed that rate. When the median is no
standard rate, the durations near it are averaged and snapped to the
standard rate within 0.5 %; exact and variable rates keep the reader's
estimate.
…import's limits

The EPS interpreter counted one operation against its budget for each
name it ran, whatever the operator did, and paths had no length limit.
Parsing one arc at a radius of 10^300 passed 3 GiB of resident memory
within 43 seconds. A loop of arc at a radius of 2.2 x 10^18 was still
running after 60 seconds, and a loop of pathbbox on a path of 100,001
elements and { {} loop } loop did not finish within 20 seconds. PDF
content had no path limit either: parsing a page that repeats
0 0 1 1 re 4,000,000 times peaked at 2.3 GiB of resident memory.

A path now holds at most 1,048,576 elements (MAX_PATH, the limit of
VectorCraft's EPS interpreter). In EPS, moveto, lineto, curveto,
closepath, arc, charpath and the rect operators check it, and an
operator that would take a path past it stops the program with
"PostScript (path limit)". In PDF, m, l, c, v, y, h and re (each adds
at most five elements) stop the content stream with
"content (path limit)" when the path already holds more than 1,048,571.
Each path element an operator makes, reads or copies (arc, rectangle
arrays, charpath, pathbbox, clip, eoclip, gsave, save) now counts
against the EPS budget. Each pass of repeat, for, loop and forall
counts too, and so does each procedure item that is not a name. A
moveto right after a moveto replaces it, as the PostScript Language
Reference describes, so text placed with moveto and show leaves one
element.

Five tests in tests_import.rs cover the loops, the path limit in both
interpreters and runs of moveto and show.
…t component of the other selected layers (storytold#1434)

With several layers selected, typing a value into one dimension (say Z
Position) sent the edited layer's whole value with selected: true, and
prop.set gave every selected layer that same value: they all took the first
layer's X and Y and stacked on top of it. Scrubbing was fine (it sends an
offset).

prop.set takes dims: with selected and a typed value, the other selected
layers get only those components and keep the rest of their own value (Position
with Separate Dimensions included). The three panels send the components the
edit changed (both for linked Scale). Without dims, agents still set the whole
value on every selected layer.

Fixes storytold#1434

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
echelon and others added 22 commits October 18, 2026 09:35
# Conflicts:
#	docs/gaps.md
# Conflicts:
#	contributors/people.toml
# Conflicts:
#	crates/engine/src/commands/prop_groups.rs
…old#1413 changed layer_menu

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…late media values (storytold#1787)

- render --progress with a piped stderr writes well-formed
  `progress <done> <total> <elapsed> <left|->` lines, one per change, and
  the same render without the flag writes none.
- A template's Media Replacement control takes {"path"|"file"|"media": …},
  is found by name ignoring case, and a path that cannot be imported fails
  with an error naming the control and the path.
# Conflicts:
#	crates/ui-egui/src/panels/effects_presets.rs
#	crates/ui-egui/src/panels/timeline.rs
# Conflicts:
#	Cargo.lock
#	apps/effectcraft/src/main.rs
#	crates/engine/src/commands/file.rs
#	crates/render/src/lib.rs
#	crates/render/src/masks.rs
#	crates/ui-egui/src/menus.rs
…t-log is byte-identical again)

The ring is one per process and tests run in parallel: the ring test's 205 lines could push the
crash test's own line out (seen on FreeBSD CI in PhotoCraft). Owed since the 10-16 sync.
@iannitram

Copy link
Copy Markdown
Owner Author

Fork sync 2026-10-18, Hyperion leg 2 (13:12 MDT): left open, not merged.

  • Local checks (Windows, rustc 1.95.0): layers pass; render 237 pass; ui-egui 775 pass; app 44 pass; craft-log + craft-stage 59 pass. Engine lib 665 pass, 2 fail at c473ce96.
  • Fail 1, ours: media_anchor::tests::footage_sequences_and_proxies_follow_a_moved_show_folder. Fix committed on Hyperion (99e8dbd2, the anchor follow runs before upstream's resolve_footage), NOT pushed: its re-run never got the build slot.
  • Fail 2: tests_roto::object_matte_mattes_persist_in_the_disk_cache (1 disk-cache write, 0 hits). It passes on plain upstream/main 22072d9f when run alone; not yet run alone on ours, or in the full suite on upstream. Blame still open.
  • Clippy: 1 error, struct Stills is never constructed in crates/engine/src/doc_path/tests.rs, byte-identical to upstream's (used only under cfg(unix)).
  • Still owed before merge: the two-test re-run on 99e8dbd2, then push it, then the full engine suite.

Upstream's resolve_footage now writes stored paths the platform's way, so on
Windows `/elsewhere/table.csv` comes back rooted on the drive with backslashes.
The test still checks that it is left where it was.

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

Copy link
Copy Markdown
Owner Author

Leg 3 (Hyperion, 13:45 MDT) status: NOT merged, needs one more look.

Done on this head (cb26229f): 99e8dbd2 (anchor follow runs before upstream's resolve_footage) and cb26229f (the anchor test compares the Data footage path by its tail; upstream now writes rooted paths the Windows way). media_anchor:: 7 pass.

Left: tests_roto::object_matte_mattes_persist_in_the_disk_cache fails in the full effectcraft-engine lib suite on ours (3 of 3 parallel runs, also with our 10 added tests skipped) and passes alone, with tests_roto:: + tests_disk_cache::, and in the whole suite single-threaded. Plain upstream/main 22072d9f passes the full suite 2 of 2. Cause: the Roto/Object Matte segment store is one process-global (effects/src/roto.rs set_seg_store), replaced by any session that enables a disk cache; that code and the test are byte-identical to upstream, but ours' suite runs 21 s vs upstream's 8.5 s and the race lands. Not upstream's by the fork-sync rule (it does not fail on plain upstream), so this stays open: find which of ours makes a test slow, or make the test not depend on the global. Other checks: render, ui-egui, app, craft-log pass (leg 2); clippy's one error (Stills) is in a file byte-identical to upstream.

The Roto/Object Matte segment store is one per process (roto::set_seg_store),
and every Session::configure_disk_cache that opens a cache takes it over. The
roto and disk-cache tests already serialise on hold_roto_cache_test_lock(), but
many other engine tests build a Session with a settings store, which opens the
disk cache at the platform folder and takes the store; once that session drops,
the weak store is dead and object_matte_mattes_persist_in_the_disk_cache sees
no disk writes. It failed 3 of 3 in the full parallel suite here.

configure_disk_cache now takes that lock under cfg(test) before opening a
cache, so every such test runs apart from the roto tests. Re-entry on the same
thread is a no-op. Product code is unchanged: only the app's one session ever
opens a disk cache, so users cannot hit the race.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iannitram
iannitram merged commit b511fea into ours Oct 18, 2026
2 checks passed
@iannitram
iannitram deleted the sync/2026-10-18-hyperion branch October 18, 2026 20:49
iannitram added a commit that referenced this pull request Oct 18, 2026
Fork sync 2026-10-18 (Hyperion): upstream and pending work into ours
@iannitram iannitram changed the title Fork sync 2026-10-18 (Hyperion): upstream and pending work into ours Fork sync 2026-10-18: upstream and pending work into ours Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.