Skip to content

animaEngine 0.5.5 — hardening + engine correctness + frame pacing

Choose a tag to compare

@Alexandru2984 Alexandru2984 released this 10 Jun 18:52
· 567 commits to main since this release

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::tick divided 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_QUADS off-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 now MAX_ENTITIES + 3; the overflow
    warning fires once per episode.
  • GPU texture leak on preset Replace. None of the three
    reset_to_configs call 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

  • /tmp fallback 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
    indexed entities[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 at warn!/info!, full path at debug!.

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 audit also takes inline --ignore RUSTSEC-XXXX-XXXX flags
    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.md Native-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 binding with the rationale (Wayland refuses raw XGrabKey-style
    grabs).
  • New SECURITY.md documents 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.md enumerates 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). Set ANIMA_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 deny findings.
    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.AppImage

Audit 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.