The batch merges grew Japanese 410 → 693, both Chinese catalogs 506 → 695,
Ukrainian 505 → 524, Czech 501 → 518 and Spanish 513 rows (its #996 landed)
without touching the doc's counts or shares; the overall figure was still the
10-October measurement. pt-br was the last count to update, so the row now
mirrors the code at this tree.
278 new rows (417 → 695), the Chinese catalogs' source list, keyed by the English
source string: panels, dialogs, tooltips and the Settings pages stop falling back
to English for every string the zh-hans catalog covers, including the six keys
the batch merges added on top. Terminology follows After Effects' Brazilian
Portuguese conventions; brand names stay English.
AI-assisted translation (OpenCode agents, GLM); not yet native-speaker proofread.
Startup check is opt-in for the desktop app, the Windows update helper is dispatched before argument parsing, and a successful update closes through the app's own close path.
Keeps the worker's failure visible instead of leaving the prompt installing, repaints while a check runs, and no longer calls process::exit past the unsaved-work prompt.
Integrate `craft-update` pinned to storytold/craft-libs fe61243 to check for
updates on startup, display the standard suite update modal with changelog notes,
and support one-click in-app updating and restart.
Ref: storytold/craft-libs#319
epaint exposes no baseline, but a capital without a descender has its ink bottom on
it, so the Latin half of the pair is load-bearing. Suggested by FeiLiuEM in #1309.
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Measured on a second machine (Noto Sans CJK JP), the ink-bottom gap after the
correction is 1.125 / 1.9375 / 1.71875 pt at 12 / 15 / 18 pt, so the old bound
failed at 15 and 18 where the interface is right. Without the correction the kana
sits 3-5 pt above the Latin ink, so the sign separates the two and the magnitude
only has to allow the face's own kana overshoot.
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
A failing atomic batch restored the project, history and state but left the
events its steps had already published, so the UI could open the comp the
rollback had just undone. Snapshot the event count with the other snapshots and
truncate before the rollback bumps.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
The Home quick-start tile clips its label to the tile; "სადემონსტრაციო
პროექტის გახსნა" was cut off after "პროექტის". "დემო-პროექტის გახსნა"
fits, like the Import Footage tile; the File menu item (file.openDemoProject)
keeps the full wording.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"შემოუერთდით ArtCraft-ის Discord-ს" ran past the right edge of the
fixed-width Discord button at the foot of the Home sidebar (v0.17.0,
macOS). "შემოგვიერთდით Discord-ზე" ("Join us on Discord") fits; the Help
menu item keeps its full wording.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS rfd put an unparented sheet on AppKit's main window, which after a
click in a floating panel's OS window is that window. With the sheet holding
the keyboard no app window was focused, so craft-ui hid the floating window,
the sheet ended without answering, and later sheets queued behind it: Save As
flashed and closed, and Cmd-S did nothing after. The commands' dialogs now
open at the end of the frame with the main window as parent (macOS; Linux's
portal dialog stays unparented). While one is up the dock area blocks
floating OS windows, as for a modal dialog, and a dialog that ends without
answering never leaves them blocked.
The op ran Session::edit once per selected layer, so each copy was a history
step of its own: undoing removed them one at a time. Collect the eligible
models first and create all the copies in a single edit — one undo (or redo)
removes (or restores) them all, and each copy keeps a name of its own.
solo_3d passed the layer to the 3D pipelines whatever the comp time, so an
instance (CC Composite referencing a model layer) rendered before the source
layer's in-point or after its out-point. Like any other layer reference, there
is nothing to read outside the range: return the transparent canvas.
Layer > Create > Create 3D Layer Instance, as After Effects (public docs):
3D model layers take no masks or effects, so the command puts a comp-sized
solid at the top of the layer stack with CC Composite referencing the
selected model, and switches the model off (back on to keep its shadow) -
a 2D copy of its render, through the comp's camera, that 2D effects can be
applied to.
- CC Composite (ec.channel.cccomposite) gains an optional Layer parameter:
with a layer chosen it takes the original's place; In Front replaces the
solid's pixels, alpha included, the other Composite Original modes blend
with them. The GPU compositor runs such an instance on the CPU.
- FxHost serves a referenced model or primitive layer as After Effects
does - the layer rendered on its own through the comp's camera
(Renderer::solo_3d); the owner keeps no cache entry for it (it moves with
the camera and lights, without a trace in its own properties).
- layer.create {op: instance3d} skips layers that are not 3D model or
primitive layers (the ones that take no effects); one undo step, the copy
selected, the model's video off.
Tests: tests_instance3d.rs (behaviour, pixels, undo/redo, serde, 2D effects
on the copy), a CC Composite layer-parameter unit test; docs/effects.md
regenerated (3033 parameters).
Bitrate mode coded the first frame four times with a fixed gain. Now the
first pass still gets the old correction (30 quantizer steps per halving
of the size, which fits the starting range), and from the second
correction on the next quantizer is placed in the log domain of the
quantizer step, where the frame size is close to a line: false position
between the nearest passes that enclose the target, otherwise a secant
step with the measured slope. The search stops as soon as the size is
within 3 % of the target, when no untried quantizer is left between the
enclosing passes, or when it would repeat a quantizer (a target the
content cannot reach, where the old code coded the same frame at the
lowest quantizer up to four times). At most four passes remain.
The first frame is within 3 % of the target on the same cells as before
and nearer to it on most of the others; two cells where the size jumps
by a factor of 4 or more between neighbouring quantizers (noisy content)
land further away than with the old search. Total passes over the 28
test cells: 112 -> 70.
Part of #2390
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Bitrate mode gave key frames keyint-dependent extra bits (about 6.8 frame
budgets at the 60 frame default) but left the inter frames at the full
frame budget, so every group of frames overshot by (boost - 1) / keyint,
9.6 % at keyint 60 (up to +187 % at keyint 5 on some content). The
accumulated deviation was tracked against the boosted budget, so the
feedback never saw it.
Now a group of keyint frames adds up to keyint * frame budget: inter
frames get (keyint - boost) / (keyint - 1) of it. Further changes to the
frame level control:
- The quantizer follows the size error with a measured slope (quantizer
steps per halving of the size, taken from consecutive inter frames)
instead of a fixed 12 steps per doubling. On noisy content the curve is
several times steeper than that and the old gain oscillated between
1 kB and 42 kB frames.
- Key frames have their own quantizer offset, adapted from key frame
sizes only, instead of feeding the key frame error into the inter
quantizer. A key frame after the first that misses its budget by more
than 19 % is coded again (at most twice more), with a corrected
quantizer found in the log domain of the quantizer step.
- A deviation from the budget is paid back over 16 frames, bounded to
0.35x..2x of the budget.
Constant quantizer mode is untouched (byte identical output). The
encoded stream in bitrate mode changes.
Adds a test that the rate stays within 5 % of the target over 180
frames for three 8 bit and one 10 bit setting.
Part of #2391
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
try_tx and reconstruct allocated six Vecs per candidate (residual, coefficients,
levels, dequantised coefficients, inverse residual, reconstruction), the
coefficient writer allocated its level map per transform block, and the loop
filter level search cloned the luma plane for every candidate level.
- Decider keeps a boxed TxScratch (32x32 arrays) that try_tx and reconstruct
write into; the levels and the reconstruction of a candidate stay there and
the caller copies out only what it keeps (the winning intra candidate, or the
levels that go into the leaf). intra_leaf uses stack arrays for its
predictions and a fixed array for the mode ranking and transform types.
- TileWriter reuses one level-map Vec, and the partition CDF at frame edges is
read through a slice instead of a copy.
- The level search filters one working copy of the luma plane and restores it
from the reconstruction before each candidate.
The encoded stream is unchanged.
Fixes#2374
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The decimator computed each output as one sequential f32 dot product over
167 taps, which is bound by the 4-cycle latency of the float addition. Use
eight independent partial sums (combined pairwise) so the compiler can
vectorise the loop.
This changes the input of the SILK encoder at rounding level, so the
bitstream differs from before; decoded quality and size are unchanged to
within noise (see the PR for SNR, segmental SNR and size at several
bitrates). A test checks the dot product against an f64 reference.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
pvq_search scored every coefficient in a serial compare-and-select chain per
pulse. Score all coefficients branch-free in 8-lane chunks instead (square
over energy, one division), take the maximum, and run the original exact
cross-multiplied test only on the entries within 1e-5 of it, which is
normally a single entry whose index comes from a lane-wise count. The chosen
pulse, and with it the bitstream, is unchanged; a test compares the result
with the old scalar search on 6000 random, sparse, tied and extreme-scale
vectors. The pre-projection also uses the truncating cast instead of a floor
libm call (the value is non-negative).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Decider::new built the half-sample planes (8,0), (0,8) and (8,8) with
three full 2-D 8-tap passes (subpel_plane), each with per-tap clamping.
half_planes computes them together: the horizontal half-sample pass is
shared by (8,0) and (8,8), the phase-0 kernels are exact shifts
(Round2(128*v, 11) = Round2(v, 4), Round2(16*s, 11) = Round2(s, 7)), and
interior pixels skip the clamps.
subpel_plane is kept as the test-only reference; a new unit test checks
half_planes against it bit for bit (including 1x1, 5x3 and 10 bit).
The only change in decide.rs is the call in Decider::new.
Output is bit-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every FFT stage was a generic DFT butterfly: p * p complex multiplications
per butterfly (16 for radix 4) and a modulo on every twiddle index. Add
kiss_fft-style kernels for radix 2, 3 and 4 (three twiddle multiplications
and eight additions for radix 4, a sqrt(3)/2 constant for radix 3, +-j done
by swapping parts). The generic butterfly stays for radix 5, which occurs
once per transform, with its roots precomputed. The modulo is dropped: the
twiddle index q * k * tw_stride is always below n.
The results differ from before at f32 rounding level, so a few packets
change on speech and hybrid input (CELT music stays byte-identical). A new
test checks the FFT against a direct DFT for every radix combination up to
the 480-point frame size.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The quantiser in try_tx called libm floorf for every coefficient, estimate_bits
and the motion-vector cost called f64::log2, and coefficients that quantise to
zero still went through an f32 division.
- (x / q + rnd).floor() as i32 becomes (x / q + rnd) as i32: the operand is
never negative, so truncation equals floor.
- log2 of small integers comes from a table built with the same f64::log2
call (estimate_bits and the MV cost in motion_search), so the values are
bit-identical.
- Coefficients below an exact per-(q, rounding offset) threshold are zero
without dividing. The threshold is found once per Decider by a binary
search over f32 bit patterns of the monotone quantiser expression.
The encoded stream is unchanged. Unit tests compare the new quantiser with the
old expression at the threshold boundary and on random values for every
quantiser index, and the table with f64::log2.
Fixes#2373
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The contour search tries 5 base lags times 34 codebook contours times 4
subframes, 680 normalised correlations, but only about 4 x 30 distinct
(subframe, lag) pairs occur. Cache them per call. The values are the same
f64 results as before, so the bitstream does not change (cmp on 24 SILK and
hybrid configurations of 90 s, mono and stereo, speech and music).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each root was refined with 40 bisection steps, which shrinks the bracket to
about 3e-15 rad. A Q15 NLSF step is 9.6e-5 rad, so 20 steps (3e-9 rad) are
far more than the quantiser can see. The saved cos calls are the remaining
bulk of the root search after the grid table.
The coded packet streams are byte-identical to the 40-step search on 24
configurations of 90 s: speech-like and music-like input, mono and stereo,
SILK NB 8k, MB 12k, WB 16k and 32k, hybrid SWB 32k and FB 64k.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The LPC to LSF conversion scans a 1024-point grid for sign changes of two
cosine series and called libm cos for every series term, about 21,000 calls
per channel and frame (30-40 % of SILK and hybrid encode time). Compute
cos(m * w) for every grid point and multiple once, with the same expression
as before, so the values are bit-identical and the bitstream does not change.
Only the bisection steps still call cos.
A test compares the new root search against the previous one bit for bit on
random symmetric polynomials; a2nlsf rejects orders beyond the table.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The inverse transform computed its bit-reversal permutations and loop
bounds at run time and went through one generic path for every size.
inverse_dct, the ADST permutations and inverse_2d are now const generic
over the size and inlined, so the permutations fold at compile time.
forward_2d zeroed a 4 KiB tmp buffer on every call, even for 4x4. It now
dispatches to a fixed-size version for 4, 8 and 16 (tmp is exactly n*n
floats); 32 keeps the run-time version, which measured faster than a
const 32 variant. The float operation order is unchanged.
Output is bit-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
encode_pulses rebuilt the full V(n, k) table with saturating u64 adds for
every coded band and summed the prefixes through a closure. Build the table
once (n <= 176, k <= 128, saturated at u32::MAX, 91 KB) and sum the prefixes
over a contiguous row slice. Shapes outside that range, which the band
layout never produces, fall back to a local table. The unused scratch Vec
is gone from BandEnc.
The bitstream is unchanged; a test keeps the old per-call encoder and
compares the coded bytes on random codewords over the whole (n, k) range.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts in project/src/lib.rs and ui-egui/src/frames.rs: Comp::same_settings
takes with_markers and is shared by same_pixels_with and changed_span (#1641);
frames identity uses same_pixels_with/reads_markers and drops
same_pixels_ignoring_markers and project_has_expressions.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>