Design notes from epic
The compiler’s job is to generate the necessary data structures for efficient runtime evaluation. Sequencer supports a few more nuanced concepts which would be slow to compute for complex sequences at runtime such as Begin/End evaluation, restoring pre-animation state, hierarchical bias, evaluation priority, evaluation groups and sub sequences.
The primary job of the compiler is to determine what happens in the sequence, and when. It does this by generating a look-up table referred to as the Evaluation Field which is used to efficiently evaluate the necessary tracks in order for any given time
In this way we can handle incredibly complex trees of sequence and sub sequence data with relatively minimal overhead.
We added the idea of the Instance Data Object in 4.19 in order to support the global transform origin for transform tracks and it made sense to define it in such a way that you can inject any UObject you want with whatever interfaces/inheritance you want your tracks to interpret.
The sequence compiler has evolved in 4.19 to better support procedural changes to sequences at runtime by affording partial and iterative compilation.
Previously we would assume that templates are compiled on cook, and immutable at runtime, whereas now we can automatically re-compile segments of the template as and when they are evaluated, if the sequence has changed. We are also much better at only invalidating parts of the template that have actually changed - for instance if you add a new section to a track, only the time range that section occupies will need re-compiling, rather than the whole template.
You can now fully compile a sequence like so:
- UMovieSceneSequence* Sequence = …; 1.
- FMovieSceneSequencePrecompiledTemplateStore TemplateStore;
- FMovieSceneCompiler::Compile(*Sequence, *TemplateStore);
Bear in mind however:
That we will automatically re-compile segments of the template as they are evaluated if they have changed in any way, so you may be able to just remove the full upfront recompilation if you’re happy with this behaviour.
It’s worth noting that when there is no compiled data, it’s possible that very complex sequences may cause hitches when playing back due to the compiler periodically kicking in.
You can avoid this by compiling the entire template upfront (as above) if you want. We have plans to improve this further to support async compilation, and compiling particular ranges.
Sequence IDs are deterministically generated by recursively hashing together the names of sub sections that sequences are instanced within, child first. So to generate the sequence ID for a given sequence, we’d use the following approach:
FMovieSceneSequenceID CurrentID = MovieSceneSequenceID::Root;
UMovieSceneSubSection* OwningSection = …; // Get the child-most sub section
CurrentID = CurrentID.AccumulateParentID(OwningSection->GetSequenceID());
OwningSection = …; // Get the next parent sub section
You can also retrieve the currently focused sequence ID from ISequencer::GetFocusedTemplateID
One crucial point is that all evaluation of sequence assets (through evaluation templates) is immutable - every evaluation method is const.
Any storage or mutation of state occurs through transient data structures which are owned by the template instance (or player) that’s actually driving the evaluation.
This means it’s very cheap to start playing a precompiled template, and simple to play the same animation multiple times as we don’t have to create instances for all the tracks in the sequence before playing them back (which is especially costly for very large cinematics, but necessary also for UI that makes heavy use of animations).
The execution token stack using inline storage where possible is one place where using heap allocated tokens was showing up as a large cost for very big sequences. Typically execution tokens are small and mostly just define logic so they fit well into this optimization.
FMovieSceneSkeletalAnimationSectionTemplateParameters includes the section bounds which would not be appropriate to store on FMovieSceneSkeletalAnimationParams since that’s owned by the section itself. FMovieSceneSkeletalAnimationParams could have been added as a member to FMovieSceneSkeletalAnimationSectionTemplateParameters instead of using inheritance.
FMovieSceneExecutionTokens is owned by the instance playing back the sequence itself, and is populated and flushed throughout the evaluation. Using token stacks like this may also allow us to make progress in concurrent evaluation in future. IMovieScenePreAnimatedTokenProducer is a separate concept because we do have several tracks that need to cache and restore multiple types of pre-animated state at different times (some global, some bound to the section etc)