Repository navigation
animaEngine 0.5.5 — hardening + engine correctness + frame pacing
animaEngine 0.5.5 — docs refresh + hardening + engine-correctness pass
Release driven by two external reviews of the post-0.5.4 tree, plus
an engine-correctness pass that came out of verifying their claims
against the code. The first review flagged stale docs, soft-fail
supply-chain checks, a second-granular cache key, and a missing
aggregate memory budget. The second caught two subtle
animation-timing bugs and a world-writable-/tmp weakness in the
XDG fallback dirs. Auditing those claims surfaced three more issues
both reviews missed: a quad-budget off-by-two, GPU textures leaking
on preset Replace, and a render loop redrawing at 60 Hz around the
clock for a fully static overlay. None of these are exploitable on
a cold start.
Idle-aware frame pacing
The biggest change in this release. The render loop previously
requested a new frame unconditionally — the overlay re-rendered an
unchanged scene at display refresh, 24/7, GPU and CPU awake the
whole time. Scheduling is now derived from live state at the end of
every frame:
| Scene state | Wake-up cadence |
|---|---|
| Edit mode / toasts / behaviors / physics / perf overlay | display refresh |
| Only sprite animations playing | soonest next-frame deadline (8 fps sprite → 8 wake-ups/s) |
| Fully static | 2 s hot-reload heartbeat, zero GPU work between |
Input (mouse, keyboard, drag-drop, tray, egui) wakes the loop
immediately; config hot-reload keeps applying even while the overlay
is hidden. The native-Wayland path is already compositor-paced by
frame callbacks and is unchanged.
Animation correctness
- Frame skip walked wrong on per-frame delays.
Animation::tickdivided the whole elapsed span by the current
frame's duration when skipping — correct for fixed-FPS, wrong for
GIF/WebP per-frame delays, where a stall landed on the wrong frame
with a desynced clock. Skips now walk frame by frame, each
consuming its own duration, capped at two loops before resyncing
(system suspend protection). - BounceOut silently shortened the loop. The bounce curve is not
monotonic, so raw boundary deltas went negative between bounces and
got clamped to 1 ms downstream. Intervals are now |delta|
normalised over the loop: all non-negative, sum exactly the loop
duration, monotonic curves unchanged to the bit.
Renderer fixes
MAX_QUADSoff-by-two. The quad budget equalled
MAX_ENTITIES, but the renderer reserves two UI slots mid-loop —
a legal 64-entity scene dropped its last sprites and warned at
60 Hz in journald. Budget is nowMAX_ENTITIES + 3; the overflow
warning fires once per episode.- GPU texture leak on preset Replace. None of the three
reset_to_configscall sites dropped replaced entities' textures;
VRAM grew on every Replace. Both render loops now prune textures
whose entity no longer exists (two-integer no-op in steady state).
Hardening
/tmpfallback ownership. When XDG resolution fails,
config/cache/data fell back to$TMPDIR/animaEngine-<uid>— a
path any local user could pre-create and own (sticky-bit/tmp
allows it), landing our atomic writes in a directory they control.
The fallback now prefers$XDG_RUNTIME_DIR(0700, uid-owned by
spec), and otherwise creates the tmpdir 0700 and verifies it: real
directory, not a symlink, owned by our uid, no group/other bits —
retrying once with a pid-suffixed sibling before giving up loudly.- Stale-index panic trap. Selection-driven keyboard actions
indexedentities[idx]directly, relying on every removal path
remembering to deselect. All dispatch arms now bounds-check and
no-op instead. - Video loader log redaction — three lines missed the M4/G.1
sweep; redacted filename atwarn!/info!, full path atdebug!.
Launcher icon (finally) renders
The app icon showed as a blank tile in the GNOME Shell app grid since
0.2.0. Root cause: the SVG began with an XML prolog and a long design
comment, and gdk-pixbuf — the pipeline GNOME Shell renders launcher
icons through — sniffs the file's first bytes for <svg (librsvg's
loader signature is anchored at the start). It never matched, so the
Shell silently rendered nothing. rsvg-convert, Inkscape and image
viewers all parse the XML properly, which is why every earlier check
"confirmed" the icon as fine. The SVG now starts with the literal
bytes <svg; the constraint is documented in
packaging/icon-variants/README.md so a future icon swap can't
regress it.
What's fixed in 0.5.5
Cache key was second-granular
hash(canonical_path + mtime_seconds) collapsed every sub-second
mtime into the same key. A cargo build && edit asset && cargo build
cycle inside one wall-clock second could therefore load stale RGBA on
the second run. The key now hashes
canonical_path + mtime_nanos + size + child_count:
- nanoseconds for ext4 / btrfs / xfs / APFS mtime resolution,
- size to disambiguate the rare cases where the filesystem floors
mtime to the second (FAT32 / SMB shares / restored backups), - child count so adding or removing a frame inside a PNG sequence
invalidates the key even when neither mtime nor total size moves
measurably.
Existing cache files become orphans on first upgrade — the next decode
rebuilds them at the new key. No user action needed; the cost is one
cache miss per asset on the first run.
Supply-chain checks were soft-fail
cargo audit and cargo deny check carried continue-on-error: true
in CI from the days when the exception policy was still being settled.
Both now hard-fail. The exception policy lives in
docs/threat-model.md §"Supply chain":
- Advisories without an upstream fix land in
deny.toml
(advisories.ignore) with a comment naming the upstream tracking
issue and the runtime-exposure rationale. cargo auditalso takes inline--ignore RUSTSEC-XXXX-XXXXflags
in.github/workflows/ci.yml; same documentation rule.- Both exception lists are reviewed each minor release.
The existing ignore for RUSTSEC-2024-0436 (paste unmaintained,
transitive through wgpu macOS metal + accesskit Windows AT bridge,
no runtime path on Linux X11/Wayland) stays in place with the
rationale documented inline.
What's added in 0.5.5
Aggregate memory budget across the scene
MAX_DECODED_ASSET_BYTES = 512 MB × MAX_ENTITIES = 64 allowed a
32 GB worst case when a hostile config (or accidental mass-drop)
loaded the maximum on every slot. A new aggregate cap defaults to
1 GB total across all loaded entities;
Scene::add_entity_from_path and Scene::append_character_config
reject loads that would push the running total above the cap and
surface an error toast:
memory budget exceeded (820 MB used + 270 MB incoming > 1024 MB cap);
set ANIMA_MEMORY_BUDGET_MB to raise
Override at startup with ANIMA_MEMORY_BUDGET_MB=<int> (high-RAM
machines that want headroom for many small assets, smoke tests that
want a tighter cap, etc.). The variable is read once per push and
validated for non-zero positive parses; anything malformed falls back
to the default.
A new helper Scene::total_decoded_bytes() reports the current
running total — useful for surfacing in the perf overlay later.
Documentation refresh
- README declares Status: 0.5.4, all
.deb/ AppImage example
commands use the matching filenames, and the Wayland section ships
a canonical Feature × Backend table marking the X11/XWayland path
as stable and the native Wayland path as opt-in beta with explicit
notes about wlroots coverage and the GNOME/KDE XWayland fallback. docs/architecture.mdNative-Wayland status table now reflects
E.1 (keyboard) / E.4 (drag-drop) / E.5 (egui events) shipping, and
flips global hotkeys from❌to⚠ via D-Bus + compositor bindingwith the rationale (Wayland refuses rawXGrabKey-style
grabs).- New
SECURITY.mddocuments the reporting channel (private GitHub
advisory), the supported-version policy (latest minor only), the
pointer to the threat model, and the disclosure timeline. - New
ROADMAP.mdenumerates shipped releases by theme (0.1 →
0.5.4), the in-flight 0.5.5 hardening, and the 0.6 candidates
under consideration (renderer polish / portal hotkeys / plugin
behaviors), plus an explicit non-goals list (macOS / FreeBSD
ports, network features, bundled assets).
Tests
252 pass in the lib suite — 21 added this release: 4 cache-key
fingerprint, 6 budget/decoded-bytes, 5 animation-tick walk (variable
delays, resync after stall, fixed-FPS regression), 2 easing
(non-negative intervals on every curve, BounceOut sum at awkward
frame counts), 4 private-dir verification (fresh create, lax mode,
symlink, wrong owner). cargo clippy --all-targets --all-features -- -D warnings clean. cargo fmt --all -- --check clean.
cargo audit and cargo deny check green locally. 6 s live smoke
test on XWayland with a 2-entity animated scene: clean start, paced
rendering, clean exit.
Upgrade notes
Configs decode unchanged. Observable user-facing changes:
- Idle CPU/GPU use drops to near zero when the scene is static; CPU
use scales with sprite FPS instead of display refresh when only
animations play. No configuration needed. - GIF/WebP assets with variable per-frame delays keep correct frame
timing across stalls (they could drift or land on the wrong frame
before). - The BounceOut easing now holds the configured loop duration
exactly; if you tuned an entity's FPS around the old (shorter)
loop, it will play slightly slower — re-tune the FPS slider. - First launch rebuilds the on-disk asset cache (one decode per
asset). Subsequent launches are unchanged. - Drag-drop / library "Add to scene" / preset Append refuse to add
new entities when the aggregate decoded RGBA would exceed 1 GB
(default). SetANIMA_MEMORY_BUDGET_MB=2048(or higher) to lift
the cap when you have a large scene of small assets. - CI now blocks PRs on
cargo audit/cargo denyfindings.
Contributors hitting one should either bump the offending dep or
add an explicit ignore with the rationale, per
docs/threat-model.md§"Supply chain".
# Debian / Ubuntu
sudo apt install ./anima-engine_0.5.5-1_amd64.deb
# AppImage
chmod +x animaEngine-0.5.5-x86_64.AppImage
./animaEngine-0.5.5-x86_64.AppImageAudit status post-0.5.5
All 0.5.0 audit findings closed in 0.5.1; 0.5.2 closed the M3/M4
sweep; 0.5.3 closed the second-round audit; 0.5.4 closed the
post-refactor audit. 0.5.5 picks up the four external-review items
(stale docs, soft-fail CI, cache key granularity, missing aggregate
budget) plus the SECURITY.md / ROADMAP.md additions. The remaining
LOW from 0.5.4 (KeyBindings.map per-action vector cap on
deserialize) stays accepted under the same-user threat model; the
informational items from earlier rounds (TOCTOU between
pre_validate and decoder, egui TextEdit without focus
restoration) remain documented invariants.