diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ea2a8..fb6f7fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/YCD_PLAYBACK.md b/docs/YCD_PLAYBACK.md index a8509c0..726c2a4 100644 --- a/docs/YCD_PLAYBACK.md +++ b/docs/YCD_PLAYBACK.md @@ -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. diff --git a/docs/YED_PLAYBACK.md b/docs/YED_PLAYBACK.md new file mode 100644 index 0000000..1e834a9 --- /dev/null +++ b/docs/YED_PLAYBACK.md @@ -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. diff --git a/fivefury/__init__.py b/fivefury/__init__.py index b044c0a..3420579 100644 --- a/fivefury/__init__.py +++ b/fivefury/__init__.py @@ -951,6 +951,7 @@ YedDictionary, YedEvaluationIssue, YedEvaluationResult, + YedEvaluator, YedExpression, YedFrameDof, YedFrameLayout, @@ -2114,6 +2115,7 @@ "YedDictionary", "YedEvaluationIssue", "YedEvaluationResult", + "YedEvaluator", "YedExpression", "YedFrameDof", "YedFrameLayout", diff --git a/fivefury/_native.py b/fivefury/_native.py index cad5143..b5fc53e 100644 --- a/fivefury/_native.py +++ b/fivefury/_native.py @@ -253,6 +253,7 @@ 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, @@ -260,6 +261,7 @@ def evaluate( variables, float(time), float(delta_time), + vector_type, ) diff --git a/fivefury/yed/__init__.py b/fivefury/yed/__init__.py index 75632d3..c3f3789 100644 --- a/fivefury/yed/__init__.py +++ b/fivefury/yed/__init__.py @@ -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, @@ -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, @@ -79,6 +80,7 @@ "YedDictionary", "YedEvaluationIssue", "YedEvaluationResult", + "YedEvaluator", "YedExpression", "YedFrameDof", "YedFrameLayout", diff --git a/fivefury/yed/evaluate.py b/fivefury/yed/evaluate.py index e95d88f..3c40f07 100644 --- a/fivefury/yed/evaluate.py +++ b/fivefury/yed/evaluate.py @@ -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( @@ -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, @@ -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, ) diff --git a/fivefury/yed/model.py b/fivefury/yed/model.py index e4e7e59..f5261c5 100644 --- a/fivefury/yed/model.py +++ b/fivefury/yed/model.py @@ -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 @@ -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: @@ -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): diff --git a/fivefury/yed/playback.py b/fivefury/yed/playback.py new file mode 100644 index 0000000..39cc23d --- /dev/null +++ b/fivefury/yed/playback.py @@ -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, + ) diff --git a/native/animation/yed_bindings.cpp b/native/animation/yed_bindings.cpp index bf8cb70..9ba7db0 100644 --- a/native/animation/yed_bindings.cpp +++ b/native/animation/yed_bindings.cpp @@ -58,11 +58,13 @@ Vec4 as_vec4(PyObject* object, const char* label) { } Vec4 as_track_vec4(PyObject* object) { - const auto size = PySequence_Size(object); - if (size < 0) { - PyErr_Clear(); + if (PyFloat_Check(object) || PyLong_Check(object)) { return {as_double(object, "track value"), 0.0, 0.0, 0.0}; } + PyHandle sequence(PySequence_Tuple(object)); + if (!sequence) throw std::invalid_argument("track value must be numeric or iterable"); + object = sequence.get(); + const auto size = PyTuple_Size(object); if (size >= 4) return as_vec4(object, "track value"); if (size == 3) { auto x = item(object, 0); @@ -323,7 +325,7 @@ void parse_variable_mapping(PyObject* mapping, std::unordered_map -PyObject* make_mapping(const Map& values) { +PyObject* make_mapping(const Map& values, PyObject* vector_type, + const Map* shared_values = nullptr, PyObject* shared_objects = nullptr) { PyObject* result = PyDict_New(); if (result == nullptr) return nullptr; for (const auto& [packed, vector] : values) { PyHandle key(make_pair_key(static_cast(packed >> 32U), static_cast(packed))); - PyHandle value(make_vec4(vector)); + PyObject* shared = nullptr; + if (key && shared_values) { + const auto found = shared_values->find(packed); + if (found != shared_values->end() && vec_max_component_error(vector, found->second, 4) == 0.0) { + shared = PyDict_GetItemWithError(shared_objects, key.get()); + if (!shared && PyErr_Occurred()) { Py_DECREF(result); return nullptr; } + } + } + PyHandle value(shared ? Py_NewRef(shared) : vector_type == Py_None ? make_vec4(vector) + : PyObject_CallFunction(vector_type, "dddd", vector.x, vector.y, vector.z, vector.w)); if (!key || !value || PyDict_SetItem(result, key.get(), value.get()) < 0) { Py_DECREF(result); return nullptr; @@ -426,7 +438,11 @@ PyObject* mod_yed_evaluate(PyObject*, PyObject* args) { PyObject* variables = nullptr; double time = 0.0; double delta_time = 0.0; - if (!PyArg_ParseTuple(args, "OOOdd", &capsule, &tracks, &variables, &time, &delta_time)) return nullptr; + PyObject* vector_type = Py_None; + if (!PyArg_ParseTuple(args, "OOOdd|O", &capsule, &tracks, &variables, &time, &delta_time, &vector_type)) return nullptr; + if (vector_type != Py_None && !PyCallable_Check(vector_type)) { + PyErr_SetString(PyExc_TypeError, "YED output vector type must be callable"); return nullptr; + } auto* program = require_yed_program(capsule); if (program == nullptr) return nullptr; try { @@ -438,9 +454,10 @@ PyObject* mod_yed_evaluate(PyObject*, PyObject* args) { evaluate_yed_program(*program, frame, time, delta_time); } - PyHandle result_tracks(make_mapping(frame.tracks)); - PyHandle result_outputs(make_mapping(frame.outputs)); - PyHandle result_variables(make_mapping(frame.variables)); + PyHandle result_tracks(make_mapping(frame.tracks, vector_type)); + if (!result_tracks) return nullptr; + PyHandle result_outputs(make_mapping(frame.outputs, vector_type, &frame.tracks, result_tracks.get())); + PyHandle result_variables(make_mapping(frame.variables, vector_type)); PyHandle result_issues(make_issues(frame.issues)); if (!result_tracks || !result_outputs || !result_variables || !result_issues) return nullptr; PyObject* result = PyTuple_New(4); diff --git a/tests/animation/yed/test_yed_evaluate.py b/tests/animation/yed/test_yed_evaluate.py index 4d298f3..7a53a36 100644 --- a/tests/animation/yed/test_yed_evaluate.py +++ b/tests/animation/yed/test_yed_evaluate.py @@ -800,7 +800,7 @@ def test_track_input_coercion_matches_the_public_vector_rules() -> None: assert result.output_tracks[(30, 0)] == Vector4(4.0, 5.0, 6.0, 0.0) -def test_compiled_program_is_reused_and_invalidated_by_stream_replacement() -> None: +def test_compiled_program_is_reused_by_content_and_invalidated_by_operand_edits() -> None: evaluator = importlib.import_module("fivefury.yed.evaluate") evaluator._PROGRAM_CACHE.clear() expression = YedExpression.create("cached") @@ -820,6 +820,10 @@ def test_compiled_program_is_reused_and_invalidated_by_stream_replacement() -> N expression.streams[0].instructions = list(expression.streams[0].instructions) evaluate_yed(yed, ("cached",), {}) + assert next(iter(evaluator._PROGRAM_CACHE.values())).program is first + expression.streams[0].instructions[1].operands["bone_id"] = 2 + result = evaluate_yed(yed, ("cached",), {}) + assert (2, 0) in result.output_tracks and (1, 0) not in result.output_tracks assert next(iter(evaluator._PROGRAM_CACHE.values())).program is not first diff --git a/tests/animation/yed/test_yed_playback.py b/tests/animation/yed/test_yed_playback.py new file mode 100644 index 0000000..92eae39 --- /dev/null +++ b/tests/animation/yed/test_yed_playback.py @@ -0,0 +1,155 @@ +from concurrent.futures import ThreadPoolExecutor +from types import SimpleNamespace + +from fivefury import ( + MetaHash, + Quaternion, + Vector3, + Vector4, + YedExpression, + YedInstruction, + YedStream, + create_yed, + evaluate_yed, +) +from fivefury import ( + YedInstructionType as Op, +) +from tests.support.yed import large_expression + + +def test_compiled_evaluator_matches_public_typed_results_and_diagnostics(): + yed = create_yed(large_expression()) + names = ("calibrated", "missing", "calibrated") + tracks = { + (t.bone_id, t.track): Vector4(0.1, 0.2, 0.3, 1) + for t in yed.expressions[0].tracks + if t.is_input + } + compiled = yed.compile(names) + for value in ( + Vector4(1, 2, 3, 4), + Quaternion(), + Vector3(1, 2, 3), + 1.5, + [1, 2], + (1,), + ): + tracks[123, 25] = value + expected = evaluate_yed(yed, names, tracks) + actual = compiled.evaluate(tracks) + assert actual == expected + assert all(isinstance(v, Vector4) for v in actual.tracks.values()) + before = compiled.evaluate(tracks) + yed.expressions.clear() + assert compiled.evaluate(tracks) == before + assert yed.compile(names).evaluate(tracks).output_tracks == {} + + +def test_compiled_evaluator_is_stateless_for_seeks_and_concurrent_actors(): + yed = create_yed(large_expression()) + compiled = yed.compile(("calibrated",)) + frames = [{(56462, 25): Vector4(i, 2, 3, 0)} for i in range(16)] + expected = [compiled.evaluate(frame) for frame in frames] + with ThreadPoolExecutor(4) as pool: + assert list(pool.map(compiled.evaluate, frames)) == expected + assert compiled.evaluate(frames[0], time=50) == compiled.evaluate(frames[0], time=0) + + +def test_snapshot_keeps_operand_values_and_zero_signature_diagnostics(): + expression = YedExpression.create("constant") + expression.streams = [ + YedStream( + MetaHash("main"), + 1, + b"", + b"", + b"", + instructions=[ + YedInstruction(Op.PUSH_FLOAT, operands={"value": 2.0}), + YedInstruction(Op.TRACK_SET, operands={"bone_id": 7, "track": 0}), + YedInstruction(Op.END), + ], + ) + ] + yed = create_yed(expression) + snapshot = yed.compile(("constant",)) + before = snapshot.evaluate({}) + assert before.native_diagnostics + expression.streams[0].instructions[0].operands["value"] = 5.0 + assert snapshot.evaluate({}) == before + assert yed.compile(("constant",)).evaluate({}) != before + assert evaluate_yed(yed, ("constant",), {}).output_tracks[7, 0].x == 5.0 + + +def test_mutable_cache_and_compiled_defaults_observe_their_ownership_contracts(): + expression = YedExpression.create("defaults") + expression.streams = [ + YedStream( + MetaHash("main"), + 1, + b"", + b"", + b"", + instructions=[ + YedInstruction( + Op.TRACK_GET, + operands={"bone_id": 7, "track": 0, "use_defaults": True}, + ), + YedInstruction(Op.TRACK_SET, operands={"bone_id": 8, "track": 0}), + YedInstruction(Op.END), + ], + ) + ] + yed = create_yed(expression) + skeleton = SimpleNamespace( + bones=[ + SimpleNamespace( + tag=7, + translation=Vector3(1, 2, 3), + rotation=Quaternion(), + scale=Vector3(1, 1, 1), + ) + ] + ) + compiled = yed.compile(("defaults",), skeleton=skeleton) + before = evaluate_yed(yed, ("defaults",), {}, skeleton=skeleton) + assert compiled.evaluate({}) == before + skeleton.bones[0].translation = Vector3(4, 5, 6) + assert compiled.evaluate({}) == before + changed = evaluate_yed(yed, ("defaults",), {}, skeleton=skeleton) + assert changed.output_tracks[8, 0].xyz == Vector3(4, 5, 6) + + +def test_variables_are_explicit_caller_state_and_outputs_remain_immutable(): + expression = YedExpression.create("state") + expression.streams = [ + YedStream( + MetaHash("main"), + 1, + b"", + b"", + b"", + instructions=[ + YedInstruction(Op.PUSH_FLOAT, operands={"value": 3.0}), + YedInstruction( + Op.SET_VARIABLE, operands={"variable": 12, "variable_index": 0} + ), + YedInstruction( + Op.GET_VARIABLE, operands={"variable": 12, "variable_index": 0} + ), + YedInstruction(Op.TRACK_SET, operands={"bone_id": 7, "track": 0}), + YedInstruction(Op.END), + ], + ) + ] + yed = create_yed(expression) + state = {} + compiled = yed.compile(("state",)) + result = compiled.evaluate({}, variables=state) + expected = evaluate_yed(yed, ("state",), {}) + assert result == expected + assert state == result.variables + assert result.tracks[7, 0] is result.output_tracks[7, 0] + state.clear() + assert compiled.evaluate({}) == expected diff --git a/tests/performance/bench_yed_playback.py b/tests/performance/bench_yed_playback.py new file mode 100644 index 0000000..c08d53b --- /dev/null +++ b/tests/performance/bench_yed_playback.py @@ -0,0 +1,28 @@ +import pytest + +from fivefury import Vector4, create_yed, evaluate_yed +from tests.support.yed import large_expression + +pytestmark = pytest.mark.performance + + +@pytest.fixture +def facial_program(): + yed = create_yed(large_expression()) + tracks = { + (t.bone_id, t.track): Vector4(0.1, 0.2, 0.3, 1) + for t in yed.expressions[0].tracks + if t.is_input + } + return yed, tracks + + +def test_mutable_yed_evaluation(benchmark, facial_program): + yed, tracks = facial_program + assert benchmark(evaluate_yed, yed, ("calibrated",), tracks).output_tracks + + +def test_compiled_yed_evaluation(benchmark, facial_program): + yed, tracks = facial_program + evaluator = yed.compile(("calibrated",)) + assert benchmark(evaluator.evaluate, tracks).output_tracks