Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,23 @@ The changelog is release-oriented and uses a small fixed set of categories:

### Added

- Immutable YED evaluators snapshot expression selection and skeleton defaults for repeated typed evaluation with explicit per-actor variable state.
- Compiled YCD clip samplers preserve the public phase/time evaluation interface while sharing immutable animation plans.
- Immutable compiled YCD animation samplers retain owned channel data for repeated native evaluation without stale caches after source edits.
- Per-track quaternion encoding overrides allow explicit facial rotations while keeping body and camera packing unchanged.

### Fixed

- YED program caches detect in-place operand and skeleton-default edits instead of relying on object identities and list lengths.
- Explicit quaternion authoring stores all four static components instead of discarding W and reconstructing it from rounded XYZ values.
- YCD precision diagnostics distinguish integer-frame and subframe component errors, report their frame locations and include quaternion angular error independently of component limits.
- YCD precision limits cover scalar, vector and quaternion components at subframes, including physical sequence overlaps; component-only policies no longer omit between-frame quaternion checks.
- Facial translation, rotation and scale honor the RETAIL channel policy with 16-bit compression, preserving static channels and per-track RAW_FLOAT overrides.

### Performance

- Up to 2x faster repeated YED evaluation with compiled evaluators, avoiding repeated expression resolution and Python input/output conversions.
- Up to 14x faster repeated YCD sampling with compiled animation samplers, keeping typed outputs and cached-quaternion interpolation order.
- YCD precision diagnostics distinguish integer-frame and subframe component errors, report their frame locations and include quaternion angular error independently of component limits.
- YCD precision limits cover scalar, vector and quaternion components at subframes, including physical sequence overlaps; component-only policies no longer omit between-frame quaternion checks.
- Facial translation, rotation and scale honor the RETAIL channel policy with 16-bit compression, preserving static channels and per-track RAW_FLOAT overrides.

## [0.5.0] - 2026-09-07

Expand Down
7 changes: 7 additions & 0 deletions docs/YCD_PLAYBACK.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,12 @@ sequence overlaps, out-of-range seeks, channel cycles and missing keys. They als
cover edits, detached lifetime, concurrent evaluation and output construction.
Performance cases are in `tests/performance/bench_ycd_playback.py`.

An external comparison of YCD names associated with indexed vanilla CUTs covered
10,275 Enhanced resources (195,904 animation clips) and 9,127 Legacy resources
(179,030 animation clips), including archive variants. The tested integer,
fractional, block-boundary and clip-time positions produced no numerical
differences from the mutable sampler. This is sampled differential coverage, not
a claim that every possible time or custom resource has been proven equivalent.

Library sampling improvements are not viewer FPS predictions: rendering,
skinning, expression evaluation and scheduling remain separate workloads.
45 changes: 45 additions & 0 deletions docs/YED_PLAYBACK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Compiled YED evaluation

For repeated playback, compile expression selection and skeleton defaults once:

```python
from fivefury import read_yed

yed = read_yed("expressions.yed")
evaluator = yed.compile(expression_names, skeleton=skeleton)
result = evaluator.evaluate(tracks, time=seconds, delta_time=delta_seconds)
```

`YedEvaluator` returns the same `YedEvaluationResult` as `evaluate_yed`: typed
tracks, output tracks, variables, expression names, issues and native-attachment
diagnostics. It is not a private native handle. The existing mutable convenience
function also uses the native typed boundary, but checks complete program/default
content before reusing its bounded program cache.

## Ownership

- A compiled evaluator owns a native snapshot of programs and skeleton defaults;
no source YED/skeleton is retained. Recompile after edits or expression-set changes.
- Compilation captures unresolved-expression and zero-signature diagnostics.
Those diagnostics describe the snapshot, not subsequent source edits.
- Evaluations have independent native frames and release the GIL during the VM
batch. There is no implicit per-actor state or cached last frame.
- Pass a separate mutable `variables` mapping per actor only when persistent
variable state is intended. Without it, evaluation starts with fresh variables.
- Input Vector3, Vector4, Quaternion and scalar/iterable values cross the boundary
directly. The native result contains final nominal Vector4 values, avoiding
repeated Python dictionary/tuple conversions.
- Output values may share immutable Vector4 objects with matching final tracks
within the same result. Result mappings and evaluation state are independent.

## Editable models

`evaluate_yed` detects changes to operands, instruction contents, stream metadata
and skeleton defaults, even when object/list identities and lengths stay unchanged.
This requires inspecting source content and is not the high-throughput playback
path. Use `compile()` once for repeated immutable playback instead of depending on
an incomplete cache signature or invoking compilation for every frame.

CPU evaluation and native attachment remain separate checks. A numerical output
does not prove that the expression can attach to a runtime actor. Compilation does
not erase diagnostics, repair malformed assets or certify in-game behavior.
2 changes: 2 additions & 0 deletions fivefury/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,7 @@
YedDictionary,
YedEvaluationIssue,
YedEvaluationResult,
YedEvaluator,
YedExpression,
YedFrameDof,
YedFrameLayout,
Expand Down Expand Up @@ -2114,6 +2115,7 @@
"YedDictionary",
"YedEvaluationIssue",
"YedEvaluationResult",
"YedEvaluator",
"YedExpression",
"YedFrameDof",
"YedFrameLayout",
Expand Down
2 changes: 2 additions & 0 deletions fivefury/_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,15 @@ def evaluate(
variables: object,
time: float,
delta_time: float,
vector_type: type | None = None,
) -> tuple[dict, dict, dict, list]:
return _ffi.yed_evaluate(
self._capsule,
tracks,
variables,
float(time),
float(delta_time),
vector_type,
)


Expand Down
4 changes: 3 additions & 1 deletion fivefury/yed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
YED_FACIAL_ROOT_BONE_ID,
YED_FACIAL_ROOT_BONE_NAME,
)
from .enums import YedInstructionType, YedTrackFormat
from .contract.frame import YedFrameDof, YedFrameLayout
from .enums import YedInstructionType, YedTrackFormat
from .evaluate import (
DofKey,
VariableKey,
Expand Down Expand Up @@ -47,6 +47,7 @@
set_ped_expression_binding,
validate_ped_expression_binding,
)
from .playback import YedEvaluator
from .reader import read_yed, read_yed_dictionary
from .runtime_headers import (
GEN9_YED_DICTIONARY_VFTS,
Expand Down Expand Up @@ -79,6 +80,7 @@
"YedDictionary",
"YedEvaluationIssue",
"YedEvaluationResult",
"YedEvaluator",
"YedExpression",
"YedFrameDof",
"YedFrameLayout",
Expand Down
95 changes: 46 additions & 49 deletions fivefury/yed/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,21 +247,7 @@ def _resolve_expressions(


def _program_signature(expressions: tuple[Any, ...], skeleton: object | None) -> tuple[Any, ...]:
bones = getattr(skeleton, "bones", ())
return (
tuple(
(
id(expression),
tuple(
(id(stream), id(stream.instructions), len(stream.instructions))
for stream in expression.streams
),
)
for expression in expressions
),
id(bones),
len(bones),
)
return _program_spec(expressions), _skeleton_defaults(skeleton)


def _get_program(
Expand All @@ -282,7 +268,7 @@ def _get_program(
):
_PROGRAM_CACHE.move_to_end(cache_key)
return cached.program
program = NativeYedProgram(_program_spec(expressions), _skeleton_defaults(skeleton))
program = NativeYedProgram(*signature)
_PROGRAM_CACHE[cache_key] = _CachedProgram(
yed=yed,
skeleton=skeleton,
Expand All @@ -309,53 +295,64 @@ def _native_issues(values: Iterable[tuple[Any, ...]]) -> list[YedEvaluationIssue
]


def evaluate_yed(
yed: Any,
expression_names: Iterable[str | int],
def _native_attachment_diagnostics(expressions) -> tuple[Diagnostic, ...]:
from .contract.validation import executable

return tuple(
Diagnostic("yed.native.signature.zero", "Numerical evaluation does not establish native attachment: this expression has a zero signature", path=expression.name)
for expression in expressions if not expression.signature and executable(expression)
)


def _evaluate_program(
program: NativeYedProgram,
expression_names: tuple[str, ...],
resolution_issues: tuple[YedEvaluationIssue, ...],
native_diagnostics: tuple[Diagnostic, ...],
tracks: Mapping[DofKey, object],
*,
skeleton: object | None = None,
time: float = 0.0,
delta_time: float = 0.0,
variables: MutableMapping[VariableKey, Vector4] | None = None,
) -> YedEvaluationResult:
"""Evaluate selected serialized RAGE expression streams against typed DOFs."""
from .contract.validation import executable

names = tuple(expression_names)
expressions, resolution_issues = _resolve_expressions(yed, names)
program = _get_program(yed, skeleton, names, expressions)
variable_values: MutableMapping[VariableKey, Vector4] = (
variables if variables is not None else {}
)
native_tracks = {key: _native_vector(value) for key, value in tracks.items()}
native_variables = {
key: _native_vector(value) for key, value in variable_values.items()
}
result_tracks, outputs, result_variables, native_issues = program.evaluate(
native_tracks,
native_variables,
tracks,
variables if variables is not None else {},
time,
delta_time,
vector_type=Vector4,
)
typed_tracks = {key: Vector4.from_iterable(value) for key, value in result_tracks.items()}
typed_outputs = {key: Vector4.from_iterable(value) for key, value in outputs.items()}
typed_variables = {
key: Vector4.from_iterable(value) for key, value in result_variables.items()
}
if variables is not None:
variables.clear()
variables.update(typed_variables)
variables.update(result_variables)
return YedEvaluationResult(
tracks=typed_tracks,
output_tracks=typed_outputs,
variables=typed_variables,
evaluated_expressions=[expression.short_name for expression in expressions],
tracks=result_tracks,
output_tracks=outputs,
variables=result_variables,
evaluated_expressions=list(expression_names),
issues=[*resolution_issues, *_native_issues(native_issues)],
native_diagnostics=tuple(
Diagnostic("yed.native.signature.zero", "Numerical evaluation does not establish native attachment: this expression has a zero signature", path=expression.name)
for expression in expressions if not expression.signature and executable(expression)
),
native_diagnostics=native_diagnostics,
)


def evaluate_yed(
yed: Any,
expression_names: Iterable[str | int],
tracks: Mapping[DofKey, object],
*,
skeleton: object | None = None,
time: float = 0.0,
delta_time: float = 0.0,
variables: MutableMapping[VariableKey, Vector4] | None = None,
) -> YedEvaluationResult:
"""Evaluate current editable expression state; compile for repeated playback."""
names = tuple(expression_names)
expressions, resolution_issues = _resolve_expressions(yed, names)
program = _get_program(yed, skeleton, names, expressions)
return _evaluate_program(
program, tuple(expression.short_name for expression in expressions),
tuple(resolution_issues), _native_attachment_diagnostics(expressions),
tracks, time=time, delta_time=delta_time, variables=variables,
)


Expand Down
9 changes: 8 additions & 1 deletion fivefury/yed/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import dataclasses
import struct
from collections.abc import Iterator, Sequence
from collections.abc import Iterable, Iterator, Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Any

Expand All @@ -23,6 +23,7 @@
if TYPE_CHECKING:
from ..authoring.context import BuildContext
from .contract.frame import YedFrameLayout
from .playback import YedEvaluator


def _coerce_track_format(value: YedTrackFormat | int) -> YedTrackFormat:
Expand Down Expand Up @@ -430,6 +431,12 @@ def recalculate_runtime_contract(self) -> Yed:
self._standalone_data = None
return self

def compile(self, expression_names: Iterable[str | int], *, skeleton: object | None = None) -> YedEvaluator:
"""Snapshot expression programs and skeleton defaults for repeated evaluation."""
from .playback import YedEvaluator

return YedEvaluator(self, expression_names, skeleton=skeleton)

def validate_runtime_contract(self) -> ValidationReport:
report = ValidationReport()
for index, expression in enumerate(self.expressions):
Expand Down
72 changes: 72 additions & 0 deletions fivefury/yed/playback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from __future__ import annotations

from collections.abc import Iterable, Mapping, MutableMapping
from dataclasses import dataclass
from typing import TYPE_CHECKING

from .._native import NativeYedProgram
from ..authoring.diagnostics import Diagnostic
from ..vector import Vector4
from .evaluate import (
DofKey,
VariableKey,
YedEvaluationIssue,
YedEvaluationResult,
_evaluate_program,
_native_attachment_diagnostics,
_program_signature,
_resolve_expressions,
)

if TYPE_CHECKING:
from .model import Yed


@dataclass(frozen=True, slots=True, init=False)
class YedEvaluator:
"""Owned immutable program/defaults snapshot with no implicit frame state."""

expression_names: tuple[str, ...]
_program: NativeYedProgram
_issues: tuple[YedEvaluationIssue, ...]
_diagnostics: tuple[Diagnostic, ...]

def __init__(
self,
yed: Yed,
expression_names: Iterable[str | int],
*,
skeleton: object | None = None,
):
expressions, issues = _resolve_expressions(yed, tuple(expression_names))
object.__setattr__(
self, "expression_names", tuple(e.short_name for e in expressions)
)
object.__setattr__(
self,
"_program",
NativeYedProgram(*_program_signature(expressions, skeleton)),
)
object.__setattr__(self, "_issues", tuple(issues))
object.__setattr__(
self, "_diagnostics", _native_attachment_diagnostics(expressions)
)

def evaluate(
self,
tracks: Mapping[DofKey, object],
*,
time: float = 0.0,
delta_time: float = 0.0,
variables: MutableMapping[VariableKey, Vector4] | None = None,
) -> YedEvaluationResult:
return _evaluate_program(
self._program,
self.expression_names,
self._issues,
self._diagnostics,
tracks,
time=time,
delta_time=delta_time,
variables=variables,
)
Loading