- automation / cli autosave restart tests: the MCP instructions embed the
auto-save state as JSON, so a Windows path's backslashes are escaped there;
look for the JSON form of the path.
- ui_gpu: egui_kittest prefers a CPU adapter, which on Windows is WARP on
DirectX 12, where FXC fails to compile the compositor's `pointwise` kernel
("compilation aborted unexpectedly"), so the viewer stayed on the CPU and
the test failed before comparing anything. Build the harness on the adapter
the headless compositor picked (falling back to egui_kittest's choice).
- ui_audio_scrub (#103 test): with a fixed 2 s comp, a machine with many
render threads cached the whole preview before the first step, so the sound
rightly started at once and no silent frame was counted (3 of 24 runs
failed with 8 copies in parallel). Size the comp to four times the frames
playback requests per step, so caching takes at least four steps (0 of 24).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GIFs went through the still-image path, so an animated GIF came in as a
single frame with zero duration. A GIF with two or more frames is now
probed as a movie: its frame rate is chosen so every per-frame delay is
a whole number of frames (uneven timing is kept exactly), delays of 0
or 1 hundredths count as 10 like in browsers, and the pool decodes the
frame shown at the requested time. A single-frame GIF stays a still.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
LayerCollection.addSolid dropped its fifth argument, so layer.newSolid
fell back to the composition's pixel aspect. The wrapper now passes
pixelAspect through; layer.newSolid already validates it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause: quantize_px rounded with `(v + 0.5) as u32 as f32`. Converting back
from u32 is several instructions in V8's wasm code on x86 (no unsigned
conversion), so quantising a 1080p layer took about 15 ms in the browser:
every 8/16 bpc layer is quantised after its source and every effect, and
the CPU compositor quantises the comp after every layer.
Fix: round_half_up uses floor in wasm (one instruction there) and keeps the
integer conversion elsewhere (x86-64's baseline has no floor instruction,
where floor is slower). The values are the same: both round non-negative
values half up. About 1.5x faster quantising in the browser (15 -> 10 ms
per 1080p layer in a wasm micro-benchmark), unchanged on the desktop.
Tests: color::tests::rounding_forms_agree: both forms agree on 0..=levels
in sixteenths (exact halves included) and on 100 001 products, at 8 and
16 bpc.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cause: at Half, Third and Quarter resolution (the viewer's Auto resolution
in most windows) every movie frame was converted from Y'CbCr to f32 RGBA at
full size and then resampled down. In the browser (wasm, one thread) that
was about 8 ms converting a 1080p frame, 6 ms resampling it and 2 ms
zeroing a 33 MB buffer, per layer and frame, for a frame shown at a
quarter or a ninth of its pixels; the footage frame cache held the
full-size frames.
Fix: FootageSource::frame_at_size asks for a frame shown at a given size
(default: the full frame, as before). The renderer asks for it where it
resampled footage (below 3/4 scale; not for proxies, fields or Interpret
Footage's pixel options, which need the full frame). MediaPool makes opaque
4:2:0 movie frames of the footage's own size there: it box-filters them to
half size in Y'CbCr before converting (each 2x2 block of luma, and the
[1 6 1]/8 neighbourhood of its chroma sample, which is the mean of the
upsampled chroma its pixels get), resamples the rest of the way and caches
the result per size. Conversion is linear until colours clip, so this is
the RGB box filter resample starts with, except in blocks where some
colours clip. Other frames convert at full size and resample as before.
In the browser the conversion and resampling take about 4 ms instead of
16. Docs (architecture).
Tests:
- convert::scaled_conversion_matches_resampling_the_converted_frame: half,
third, quarter and odd sizes of an unclipped 4:2:0 frame are within 1e-5
of resampling the converted frame; larger sizes, odd frame sizes and
4:4:4 convert at full size.
- tests_footage_size::reduced_renders_take_footage_resampled_by_the_source:
a source that resamples frames itself renders the same frames as one
that doesn't, at 8 and 32 bpc and every resolution, and is asked only
below 3/4 scale, and not with Invert Alpha.
- footage::h264_frames_at_reduced_sizes (ffmpeg fixture): 1080p H.264
testsrc2 frames, whose saturated colours clip, at 960x540, 640x360,
480x270 and 323x182 are 58-62 dB from the resampled full frames, and
are cached per size.
- convert's ignored 1080p bench also times the conversion at a third.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cause: the browser build kept the desktop's cache budgets (1 GB layer
cache, 1 GB footage frame cache) inside the page's memory, which wasm32
caps at 4 GiB. FilmCraft's decoders keep up to 384 MB of decoded frames
per open movie beside them, so a comp of eight stacked 1080p H.264 clips
grew the page by about 55 MB/s while playing and ran it out of memory
after about 50 s ("unreachable": a failed allocation).
Fix: in the browser Prefs::cache_budgets keeps the Layer Cache within
512 MB and the Footage Frame Cache within 256 MB (WEB_LAYER_CACHE,
WEB_MEDIA_CACHE); smaller settings and the desktop are unchanged, and
the RAM preview keeps its budget. Docs (web, preferences) say so.
Tests: tests_m145::web_cache_budgets_leave_room_for_the_decoders: with
4 GB settings the web budgets are the caps, the desktop's are the
settings, and smaller settings stay as they are.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Convert Position values and spatial tangents into the new parent space.
Preserve supported static Orientation and Scale in constant uniform
parent spaces, and convert spatial Position ease speeds for uniform
transforms. Retain the compensation and Undo/Redo regressions.
Refs #348.
Co-authored-by: Codex <codex@openai.com>
The freeze and quit during a FLAC preview did not reproduce on current
main (Windows 11): a 4-minute stereo and a 10-minute 24-bit 6-channel
96 kHz FLAC played through a RAM preview with a real-time audio pull,
after trimming and zooming, with no stall, no growing queue and memory
bounded by the RAM preview budget; 150 cpal output start/stop cycles
ran clean. Three defects in that path were found and fixed:
- FilmCraft decodes FLAC, MP3, Ogg and AIFF files whole when they are
opened (8 s and 1.4 GB for the 10-minute 6-channel file). MediaPool
opened a file outside its lock, so every caller asking before the
first open finished decoded the whole file too: a preview's frame
workers, audio feeder, conform and waveform threads at once (8 reads
at once: 8 decodes). Each path now opens once; the others wait for it.
- AudioPlayback::start mixed its first 50 ms on the calling (UI)
thread, so the first read of such a file froze the UI for the whole
decode (audio-only previews start the sound right away). All mixing
is on the feeder thread now; the device plays silence until the first
block arrives, and the clock (frames actually played) waits for it.
- Conforming (Settings > Disk > Conformed Audio Folder, on by default)
held the whole conformed track in memory before writing it, sized by
the footage duration from the file's header: a header claiming a
month of audio aborted the app on a failed 995 GB allocation. The
conformed file is now written a chunk at a time, and footage longer
than 24 hours is read from its decoder instead of conformed.
Tests: media concurrent_first_reads_open_a_file_once (8 threads: 8
opens before, 1 now; PoolStats::opened), pool
a_header_claiming_a_month_of_audio_is_not_conformed (aborted before),
ui-egui audio starting_audio_does_not_wait_for_slow_footage (start took
the footage's 2 s before), conformed_flac_audio_reads_back_like_the_
decoder, and the robustness test flac_audio_of_any_shape_reads_in_
device_blocks (2 minutes of 24-bit 6-channel 96 kHz, mono 8 kHz,
11.025 kHz, 192 kHz and 8-channel FLAC read in 1024-frame blocks at
48 kHz through and past the end: level, silence after the end, opened
once). FLAC fixtures come from ffmpeg (skipped without it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause: two things kept Arabic out of a layer name on Windows.
- Windows switches the keyboard layout with Alt+Shift. Releasing Shift
before Alt left Alt held alone, and the in-window menu bar (#279)
counted the release as an Alt tap: it took the keyboard focus, the
rename field closed, and the letters typed next were swallowed by the
menu bar. The Project panel's rename field lost them the same way.
- The UI fonts are Inter plus the CJK system fallbacks, so Arabic,
Hebrew, Indic and Thai names that did arrive (pasted, or from a
project) showed as missing-glyph boxes.
Fix:
- An Alt tap only counts when Alt went down with no other modifier
held, so Alt+Shift in either release order is no tap; a plain Alt tap
still focuses the menu bar.
- The UI registers installed system faces for Arabic, Hebrew, Devanagari,
Bengali, Gurmukhi, Gujarati, Tamil, Telugu, Kannada, Malayalam and Thai
after the CJK ones, the same way (native only; the web build has no
system fonts). A face covering several scripts is registered once
(Segoe UI: Arabic and Hebrew; Nirmala UI: every Indic script).
- The text engine's fallback tries each script's plain platform families
(Segoe UI, Nirmala UI, Leelawadee UI, Geeza Pro, Kohinoor, Noto…)
before any covering face, so neither the UI nor a text layer gets a
calligraphic face that merely sorts first.
- The Timeline's rename field registers `timeline.layer.<id>.nameEdit`.
egui 0.36 shapes each run of one face, so Arabic letters join and a word
reads right to left; it has no bidirectional layout, so the words of a
multi-word Arabic name (and mixed Arabic/Latin) still follow each other
left to right.
Tests: ui_inline_edit a_layout_switch_keeps_the_name_field_and_arabic_
names_are_kept (Alt+Shift both ways in the layer rename, Arabic typed and
stored; Hebrew/Devanagari/Thai in the Project panel; a plain Alt tap
still focuses File; failed before: the field closed), theme
installed_script_fallbacks_draw_arabic_hebrew_indic_and_thai_names
(failed before: replacement glyph for Arabic) and
arabic_words_are_joined_and_read_right_to_left, text
other_scripts_fall_back_to_a_plain_system_family and the script
classification.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause: CC Particle World projected its particles through a fixed camera
(distance 2 layer widths, looking at the layer centre) and never asked the
host for the comp camera, so moving or orbiting a camera layer left the
particles flat on the layer. It also had no Extras > Effect Camera. Separately,
the layer cache key ignored the comp camera and lights, so the effects that
read them (Card Dance / Shatter / Card Wipe Comp Camera, First Comp Light,
Caustics) kept a stale frame when the camera, a light or the layer moved.
Fix: as the Cycore manual describes, Particle World sees its particles through
the comp's active camera layer when there is one (EffectHost::comp_scene now
says whether its camera is a camera layer's, CompScene::camera_layer), through
card3d's comp-camera projection (factored out of Card Dance as comp_proj, with
Proj::view for depth, size scale and screen velocity). Otherwise it uses the
new Extras > Effect Camera: Distance (2), Rotation X / Y / Z (0) and FOV (50)
orbit the world origin; FOV moves the camera so the origin's plane keeps the
scale Distance gives it, so the defaults reproduce the old fixed camera bit for
bit (golden hashes unchanged) and old projects render as before. The Effect
Camera is left out of the simulation cache key, so animating it doesn't
restart the simulation. The plan is shared, so the GPU compositor follows too.
The layer cache folds the comp scene into the key of layers running an effect
in effects::READS_COMP_SCENE (render::comp_scene is shared with the host).
Tests: sim particle_world_follows_the_comp_camera (panning a camera layer moves
the particles, zooming spreads them, the Effect Camera is ignored then, the
default view keeps the Effect Camera, cameras facing away / zero / NaN draw
nothing) and particle_world_effect_camera (missing params = defaults, rotation,
distance and FOV change the view, extremes stay finite, the sim key ignores the
camera); render particle_world_follows_the_comp_camera (a real camera layer
panned 40 px moves the particles left, and the cached render after the move
equals an uncached one: it failed with the old cache key); gpu
particle_world_through_the_comp_camera (CPU vs GPU at 8 / 32 bpc, full and
half resolution). particle_golden / sim_golden unchanged. effects.md
regenerated; parity.md and gaps.md describe the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>