Unity games
Unity engine being one of the most used game engine, and having Linux support, a lot of work has been performed to support Unity games in libTAS. Running games in libTAS has been possible for a long time, but movies desynced very easily.
Here is an overview of what is required to run Unity games in libTAS.
Porting
If the game does not have a Linux version, you may be able to port from a Windows or (even better) Mac version. ikuyo has made a nice documentation about it.
There is also the Unify project to automatically port games.
Running
Running Unity games in libTAS does not require specific options. For determinism,
however, you will need to add -force-gfx-direct to the command-line options
field. libTAS tries to automatically detect Unity games and add this option,
but it may failed, so it’s better to add it manually.
For recent games that will default to using Vulkan, you may want to
enforce OpenGL which is more stable, by adding -force-opengl to command-line
options as well.
Determinism
Debug version
Unity engine has been made more and more optimized by heavily relying on multi-threading. This made games highly non-deterministic sadly. To overcome this, libTAS hooks a bunch of internal Unity functions that handle the job system, the asynchronous reads and preloading operations. The goal is to make all those systems sequential: they still rely on threads, but their execution is performed sequentially in a defined order.
To be able to hook internal functions, the main hooking mechanism is helpless. We need to rely on patching code functions, meaning that we need to know the location of those functions in the game memory. To do that, we rely on a nice offer from Unity: they distribute a version of each Unity runtime with debug symbols! By replacing the runtime program with the version with symbols, we can still run the game and get the location of the functions we want.
So, the procedure is the following:
- Obtain the runtime version: this is performed by libTAS when running the game
(look at what is displayed on the terminal), if it detected the game as using
Unity engine. Alternatively, you can run this command:
strings /path/to/game/data/Resources/unity_builtin_extra | head -n 1 - Download the Linux runtime for this version. You can find those on the
following sources depending on the version:
-
contains what you want. Download the file
̀Component installers>macOS>Linux Build Support, which can be extracted on Linux. For recent Unity version, you will need to pick the correct one betweenLinux Build Support (IL2CPP)andLinux Build Support (Mono). You can see easily if your game is compiled with IL2CPP, by looking if there is ail2cppfolder inside the gameDatadirectory.
-
- Extract the archive and grab the runtime, which should be located inside
Data/PlaybackEngines/linuxstandalonesupport/Variations/. You will be interested inlinux[32|64]_[withgfx|player]_development_[mono|il2cpp]directory. - For newer Unity versions (~2019+), copy file
UnityPlayer_s.debugto the game directory. This will be used by libTAS to find symbols (do not rename). For old Unity versions, copyLinuxPlayerexecutable to the game directory and use it as your new game executable. You will need to rename this file like the original executable (minus the extension which does not matter), for the game to work correctly.
If all goes well, libTAS should find the function addresses it wants, and print each one on the terminal at the game startup.
If the Unity version is not available, e.g. the devs are using a custom build (hi Silksong), there is still an alternative to locate functions, using function signatures. One function signature will only be working on a small set of Unity versions, so these need to be found and added manually.
Features
Options to enable or disable deterministic behaviour for Unity features are
located inside Settings > Game-specific. They must be enabled before the
game is launched to be effective.
Jobs
Unity jobs
are small tasks that are designed to be run in parallel, with possible
dependencies. To enforce determinism, when one job is scheduled, it immediately
waits for the job to complete. When multiple independent jobs are scheduled (in
a parallel for design), we schedule each job in order and wait for one job to
finish before scheduling the next.
Preload operations
When tasks can last more than one frame, Unity uses a Preloader that performs async operations (scene loading, assets loading, etc.). The lifetime of an async operation is roughly:
- the (usually main) thread pushes the operation into the preloader queue
- the preloader thread pulls the operation with highest priority, and calls
op->Perform() - once per frame, the main thread calls
op->IntegrateTimeSliced() - if the operation is completed (
op->IsDone()), it callsop->IntegrateMainThread()and optionallyop->InvokeCoroutine(). Then the operation is pulled
To enforce determinism, each time an operation is pushed, we wait for the preloader
thread to finish calling op->Perform().
Async reads
Some files are read inside another thread, to not block the main thread. To enforce determinism, we swap it with the sync variation of file read, that exists in every version of Unity
Visualization
Jobs and preload operations can be seen from the OSD in Debug > Unity.
They can be seen also in the profiler (in Debug > Profiler).