# fas-rs: watching the frames to decide how hard the CPU is allowed to work

> fas-rs is a Rust userspace implementation of frame aware scheduling for Android, built by shadow3aaa, that monitors frame rendering to hold a target frame rate while leaving the kernel alone, and it ships a plugin system, a web interface and a TOML configuration of per package target frame rates across four performance modes with margin and thermal thresholds. It is GPL-3.0 licensed, requires kernel eBPF support, and builds with the Android NDK and a nightly Rust toolchain.

**shadow3aaa/fas-rs** — Frame aware scheduling for android.

- Repository: https://github.com/shadow3aaa/fas-rs
- Stars: 1,116 · Forks: 91
- Language: Rust
- License: GPL-3.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/shadow3aaa-fas-rs

## Watching the frames to decide how hard the CPU may work

fas-rs is frame aware scheduling for Android, and the README's own framing of the idea is the most useful description of the project you will find anywhere.

The question it starts from is what to do if the picture you see could be reflected directly in the scheduling. In other words, if the scheduler were placed in the viewer's position and could decide how much performance to spend, would that give perfect performance control and a better experience. Frame Aware Scheduling, FAS, is the name for that concept: by watching frame rendering, the system tries to control performance, minimising overhead while keeping the render time within budget.

That framing explains the design choice that matters most. The project is a userspace implementation of FAS, fas-rs, and the README compares it with the kernel space MI FEAS, noting that fas-rs has an advantage in compatibility and flexibility that is close to universal on any device. The reasoning is straightforward: a kernel space implementation has to be merged into the kernel of every device, while a userspace implementation can run on the stock kernel.

The cost of that choice is that the scheduler is not in the kernel, so it observes and nudges rather than controlling, and the package description in the Cargo manifest makes the requirement explicit: frame aware scheduling for android, requires kernel ebpf support. Whatever it is doing, it needs eBPF.

The intended user is a person with a rooted Android device who wants a specific game to run at a specific frame rate without the device cooking itself, and who is comfortable with a TOML file, a sysfs node and a nightly Rust toolchain if they want to build from source. The primary language is Rust, the licence is GPL-3.0, and the recent releases are v4.9.1 from 2025-10-10, v4.9.0 from 2025-05-04 and v4.8.0 from 2025-03-23, with the last push on 2026-07-17.

## Plugins, a web interface and a sysfs node

Three extension points are visible in the repository, and they suit different kinds of contribution.

The plugin system exists to maximise userspace flexibility, and the README links a separate template repository for the plugins, the fas-rs-extension-module-template, with the development notes there. A plugin system with a separate template repository is a deliberate choice: the plugin author gets a starting point with the expected structure rather than reverse engineering the main tree.

The `module/` directory at the repository root is the plugin itself, and `webui/` is a separate front end, which is what makes the web interface possible. The `update/` directory handles the update path, and the `xtask/` directory is the build orchestration, with the Cargo workspace having `members = ["xtask"]` and the `cargo xtask build -r` command in the build instructions. The `webui/` directory and the `mlua` dependency with its `luajit` and `vendored` features point to a Lua scripting layer inside the runtime, which is a common way to make a performance tool extensible without shipping a plugin ABI.

The third extension point is a sysfs node. The README explains that if you have some experience programming on Linux, writing any of the four modes to the `/dev/fas_rs/mode` node switches to that mode, and reading it tells you which mode fas-rs is currently in. That is the interface a shell script or a home automation rule would use, and it means mode switching does not require the graphical interface at all.

The dependency list in the Cargo manifest is worth a look for what it implies. `frame-analyzer` at 0.3.4 and `dumpsys-rs` pulled from its own GitHub repository are the observation side of the system, the part that watches frame rendering. `nix` with the `sched` feature is the scheduling side. `inotify` is there to react to file changes, which is how the configuration reload works, and `sys-mount` and `quick-xml` suggest a mount and an XML surface, plausibly for reading system state. `mimalloc` with the `no_thp` and `override` features is a deliberate allocator choice, and `likely_stable` is a small crate whose name suggests it marks APIs as likely to change.

## games.toml: target frame rates per package and four modes

The configuration lives at `/sdcard/Android/fas-rs/games.toml` and it has two sections that matter.

The `[config]` section holds two booleans. `keep_std` decides whether the standard configuration profile is always kept when configurations are merged, preserving the local configuration's application list, with the rest behaving the same as `false`. `scene_game_list` decides whether the scene game list is used. Both are marked as the default configuration.

The `[game_list]` section is where the intent becomes concrete. Each key is a package name and the value is either an array of frame rates or a single integer, representing the target frame rate the game will render at, which fas-rs matches dynamically at runtime. A working example from the README:

```toml
[config]
keep_std = true
scene_game_list = true

[game_list]
"com.hypergryph.arknights" = [30, 60]
"com.miHoYo.Yuanshen" = [30, 60]
"com.miHoYo.enterprise.NGHSoD" = [30, 60, 90]
"com.kurogame.mingchao" = [24, 30, 45, 60]
"com.mojang.minecraftpe" = [60, 90, 120]
"com.tencent.tmgp.sgame" = [30, 60, 90, 120]
```

The array form matters. Where a game can run at several refresh rates, listing them lets fas-rs pick the one that matches what the display is actually running, rather than forcing a single target.

The four mode blocks follow, and each is a pair of parameters. The example in the README:

```toml
[powersave]
margin_fps = 3
core_temp_thresh = 80000

[balance]
margin_fps = 1
core_temp_thresh = 90000

[performance]
margin_fps = 0
core_temp_thresh = 95000

[fast]
margin_fps = 0
core_temp_thresh = 95000
```

There is one wrinkle in mode switching that the README is careful about. fas-rs does not have an official mode manager of its own, and instead plugs into the configuration interface of `scene`, the vtools.omarea.com project. If you do not use scene, the default is the `balance` configuration. Writing to `/dev/fas_rs/mode` switches modes, and reading it tells you the current one.

So the mode you get is partly determined by a third party tool's configuration, which is a design that has to be understood before you rely on it. It also means the practical set of people who can switch modes comfortably is the set of people who already use scene.

## margin_fps and core_temp_thresh, the two knobs per mode

Both parameters are worth understanding, because they are the whole tuning surface.

`margin_fps` is the extra amount of dropped frames permitted, expressed in frames per second. It supports two formats. The full format is `margin_fps = { base = <float>, <target_fps margin override> = <float> }`, where the override can be given for several target frame rates. The shorthand is `margin_fps = <float>`, which is equivalent to `margin_fps = { base = <float> }`.

The scaling rule is the part that makes the shorthand sensible. Unless a `target_fps margin override` forces a specific value, the margin is scaled by the formula `target_fps / 60 * base`. So a base of 3 means three frames of slack at 60 fps, six at 120 fps, and one and a half at 30 fps. A constant margin would be far too generous on a 120 Hz display and too tight on a 30 Hz one, and the scaling formula is the mechanism that makes a single number work across refresh rates.

`core_temp_thresh` is the core temperature at which fas-rs triggers thermal control, in units of 0.001 degrees Celsius, so 80000 is 80 degrees. It also accepts the string `disabled`, which turns off the built-in thermal control entirely.

Looking at the four modes together, the shape of the tuning is clear. `powersave` has the largest margin at 3 and the lowest threshold at 80 degrees, so it tolerates the most dropped frames and backs off the earliest. `balance` halves the margin to 1 and raises the threshold to 90. `performance` and `fast` both go to a margin of 0 and a threshold of 95, meaning no frames may be dropped and thermal control only intervenes near the top of the range.

The two parameters interact in a way the documentation does not spell out. A margin of 0 in `performance` means the target is absolute, so the only thing standing between you and a frame time limit is the thermal threshold. That is a coherent design, and it is also the setting most likely to make a device hot, which is why the threshold is configurable per mode rather than global.

## The configuration merge system, and what it takes from you

fas-rs has a built-in configuration merging system, and the README describes it as a way to solve the problem of future configuration feature changes. Its behaviour is three rules: delete from the local configuration the entries that do not exist in the standard configuration, insert into the local configuration the entries that are missing but exist in the standard configuration, and keep the entries that exist in both the standard and the local configuration.

So the local file is always a subset of the standard one, and the merge is total in both directions. That is a sensible way to handle a standard configuration that gains new options over time, and it is also a way of taking control away from the person who edited the file, which is why two notes matter.

The first note is that the merge is implemented with automatic serialisation and deserialisation, so it cannot preserve comments or other information that is not part of the serialised data. A `games.toml` with careful comments in it will come back from a merge without them.

The second note is about timing. The automatic merge at install time is not applied immediately, because that might affect the running version, and instead the local configuration is replaced with the merged new configuration at the next reboot. So a configuration change lands one reboot later than you might expect, and the state in between is the old one.

The merge also runs automatically every time a module is installed, and the manual form is a single command:

```bash
fas-rs merge /path/to/std/profile
```

The practical advice for a user is to keep anything you care about in the standard profile rather than in a local file, because a local file is a derived artefact here, not a source of truth.

## Building it: NDK, nightly Rust and cargo xtask

The build section is compact and assumes you are comfortable with cross compilation, which is reasonable for the audience.

On Ubuntu, with the NDK required, the README installs the compiler and Git LFS:

```bash
apt install gcc-multilib git-lfs
```

Rust has to be nightly:

```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup default nightly
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
rustup component add rust-src
```

The four targets are the four Android ABIs, and the `rust-src` component is needed because the build compiles against the standard library source. The nightly requirement is the one to note, since it means the build is not pinned to a stable toolchain and can change under you.

Then cargo-ndk, the clone, and the build:

```bash
cargo install cargo-ndk

git clone https://github.com/shadow3aaa/fas-rs
cd fas-rs

cargo xtask build -r
```

The `-r` flag is the release build, and the `xtask` mechanism is how a Cargo workspace with one member, the `xtask` package, drives a multi step build. The `build.rs` at the root, the `rust-toolchain.toml` that pins the toolchain, and the `NOTICES` file alongside `LICENSE_HEADER` and `licenserc.toml` show a project with attention to reproducible builds and to licence headers in every source file.

The release cadence is steady rather than fast. Three releases across 2025, v4.8.0 in March, v4.9.0 in May and v4.9.1 in October, with a push in July 2026 that is more recent than the newest tag, which is the usual pattern for a project that ships when the maintainer is ready rather than on a schedule.

The alternative approach, and the one to weigh against, is to leave the scheduling to the kernel and the vendor. Modern Android already has its own frame rate and thermal management, and a tuning tool on top of it competes with the system's own decisions, and the sysfs mode node and the scene integration are both indications that fas-rs expects to work alongside those rather than replace them. The difference in approach is that a kernel space implementation such as MI FEAS sits where the scheduling decisions are actually made, at the cost of needing kernel support on every device, while fas-rs runs on the stock kernel with eBPF and reaches the same result from outside.

## Conclusion

Adopt fas-rs if you run a rooted Android device and want the frame rate of a specific game held to a target while the device stays cool, since the target frame rates and the thermal thresholds are expressed in a single readable TOML file and the mode switching is scriptable through a sysfs node. Do not adopt it if you need a supported solution on an unrooted device, since the package description states that it requires kernel eBPF support, and treat it as a tuning tool rather than a scheduler you can reason about. Verify three things first: that your kernel exposes the eBPF support the project needs, that the `margin_fps` and `core_temp_thresh` values in your mode blocks are appropriate for your thermal envelope, and that you understand what the configuration merge does to a local `games.toml`, because it is applied automatically on install and comments in your own file are not preserved.

## FAQ

### What does fas-rs do on Android?

It is a userspace implementation of frame aware scheduling for Android that monitors frame rendering to control performance, holding a target frame rate while keeping overhead low. The Cargo manifest states that it requires kernel eBPF support.

### Where is the fas-rs configuration file and what is in it?

The configuration is at /sdcard/Android/fas-rs/games.toml. It has a [config] section with keep_std and scene_game_list booleans, a [game_list] section mapping package names to one or more target frame rates, and four mode blocks for powersave, balance, performance and fast.

### How do the margin_fps and core_temp_thresh settings work in fas-rs?

margin_fps is the extra dropped frames allowed, given either as a float or as a table with a base value and per target frame rate overrides, scaled by target_fps divided by 60 times base unless overridden. core_temp_thresh is the core temperature in units of 0.001 degrees Celsius that triggers thermal control, or the string disabled to turn that control off.

### How do I switch the fas-rs mode?

fas-rs has no official mode manager of its own and plugs into the configuration interface of the scene project, with balance as the default if you do not use scene. You can also write any of the four mode names to the /dev/fas_rs/mode node, and read that node to see the current mode.

### How do I build fas-rs from source?

You need the Android NDK, a nightly Rust toolchain with the four Android targets and the rust-src component, and cargo-ndk. On Ubuntu the README installs gcc-multilib and git-lfs, then the build is `cargo xtask build -r` after cloning the repository.

## Sources

- [Issues](https://github.com/shadow3aaa/fas-rs/issues)
- [License: GPL-3.0](https://github.com/shadow3aaa/fas-rs/blob/master/LICENSE)
- [README](https://github.com/shadow3aaa/fas-rs/blob/master/README.md)
- [Releases](https://github.com/shadow3aaa/fas-rs/releases)
- [shadow3aaa/fas-rs on GitHub](https://github.com/shadow3aaa/fas-rs)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/shadow3aaa-fas-rs
