This is the full developer documentation for Auriya
# Auriya Wiki
> Technical documentation for Auriya and personal optimization experiments.
## Core Features
[Section titled “Core Features”](#core-features)
Rust
Runs as a background userspace process to handle system state, periodic tick evaluation, and hardware node writes.
Kala
Directly taps into surface frame timings and rendering buffers to extract real-time FPS and frame drops.
Dynamic Governor
Dynamically adjusts CPU and GPU frequency clusters based on active render headroom and target frame pacing.
Per-Package Profiling
Applies distinct scheduling profiles, refresh rate locks, and system tweaks automatically when configured games open.
CLI Tools
Exposes an IPC socket for live status queries and includes `auriyactl` for terminal-based scripting and diagnostics.
Kotlin Companion App
Android manager application for adjusting configuration files, monitoring live telemetry, and managing module state.
## AI & Machine-Readable Docs
[Section titled “AI & Machine-Readable Docs”](#ai--machine-readable-docs)
llms.txt Catalog
Standardized discovery catalog following the llmstxt.org specification. Access [`/llms.txt`](/llms.txt) for machine-readable navigation.
Full Context Bundles
Single-file markdown datasets ([`llms-small.txt`](/llms-small.txt) & [`llms-full.txt`](/llms-full.txt)) containing complete architecture, IPC specs, and tuning guides.
Agent & IDE Ready
Directly pluggable into Cursor, Windsurf, Claude Code, Antigravity, and Copilot. View the [LLM Integration Guide](/reference/llms/).
# Components
Auriya is three runtime planes plus shared code. This page names each component, where it lives, and what it owns. For how they interact at runtime see [Data flow](/architecture/data-flow/); for the whole-system flow see [Architecture overview](/architecture/overview/).
## Android manager — `android/app/`
[Section titled “Android manager — android/app/”](#android-manager--androidapp)
The user-facing app (package `dev.auriya.app`). Renders the Compose UI, persists appearance/onboarding preferences, requests root, edits `settings.toml` / `gamelist.toml`, and displays live daemon status. It is a **client** of the daemon over the Unix socket — it does not itself apply tweaks. Installed by `customize.sh` via `pm install` ([Installation](/getting-started/installation/)).
## Companion service — `android/service/`
[Section titled “Companion service — android/service/”](#companion-service--androidservice)
A headless service (process `AuriyaSysMon`, launched via `app_process`, package identity `dev.auriya.service`). It bridges Android-only capabilities the root daemon cannot reach:
* **Sensors** → writes the foreground app/PID, screen state, battery-saver, and Zen/DnD state to `/data/adb/.config/auriya/system_status`.
* **Actuators** → replays daemon-requested DnD and refresh-rate changes through Android framework APIs, driven by the `auriya_cmd` file ([System tweaks → CmdWriter](/internals/system-tweaks/#actions-routed-through-android--cmdwriter)).
Its liveness is tracked via `companion.lock` (see [Architecture overview](/architecture/overview/#control-and-status-paths)). Full internals — sensors, actuators, atomic file IO — are documented in [Companion service](/internals/companion/).
## Shared Kotlin — `android/shared/`
[Section titled “Shared Kotlin — android/shared/”](#shared-kotlin--androidshared)
Models and codecs used by both the app and the companion: the `Settings` / `GameProfile` / `SystemStatus` data classes, the TOML parser/serializer (`TomlParser.kt`), and the command/status wire formats. This is where the app’s view of `settings.toml` is defined — and why config keys must stay in sync between here and the Rust `Settings` struct ([settings reference](/reference/settings/#schema-sync-rust--app)).
Note
`android/shared/bin/` mirrors the shared Kotlin for tooling and is **not** the source of truth; edit `android/shared/src/`.
## Rust daemon — `src/main.rs` + `src/daemon/` + `src/core/`
[Section titled “Rust daemon — src/main.rs + src/daemon/ + src/core/”](#rust-daemon--srcmainrs--srcdaemon--srccore)
The long-running root process (binary `auriya`). Loads config, runs the event/tick loop, serves the IPC socket, observes foreground/FPS/telemetry, selects a profile, and applies tweaks. Two binary targets are declared in `Cargo.toml`:
| Binary | Entry | Role |
| ----------- | ------------- | ---------------- |
| `auriya` | `src/main.rs` | The daemon. |
| `auriyactl` | `src/ctl.rs` | The control CLI. |
Core subsystems (`src/core/`): `config/` (settings + game profiles), `system_status/` (companion snapshot cache), `pid_tracker.rs` (foreground liveness), `fps_meter/` (FPS telemetry), `fas/` + `daemon/fas.rs` (frame-aware scheduling), `telemetry/` (CPU/GPU/thermal), `tweaks/` (kernel writes), `cmd_writer/` (companion command file), `display.rs` (supported modes).
## Control CLI — `src/ctl.rs` + `src/cli/`
[Section titled “Control CLI — src/ctl.rs + src/cli/”](#control-cli--srcctlrs--srccli)
`auriyactl` — a line-oriented client for the same Unix socket ([Command reference](/reference/commands/)). As of this revision it is a secondary control surface; the app is primary.
## Kernel/device boundary — `src/core/tweaks/`, telemetry, eBPF
[Section titled “Kernel/device boundary — src/core/tweaks/, telemetry, eBPF”](#kerneldevice-boundary--srccoretweaks-telemetry-ebpf)
Best-effort reads and guarded writes to vendor-dependent `/proc` and `/sys` nodes, plus the eBPF frame probe. Missing nodes are skipped ([System tweaks](/internals/system-tweaks/)).
## Architecture tree
[Section titled “Architecture tree”](#architecture-tree)
```
flowchart TD
root["Auriya Root Architecture"]
subgraph android_plane ["Android Plane (android/)"]
app["Manager App (android/app)
Compose UI, root cmds, overlay"]
comp["Companion Service (android/service)
Sensors & actuators (app_process)"]
shared["Shared Module (android/shared)
Models, TOML parser, types"]
end
subgraph rust_plane ["Rust Plane (src/)"]
daemon["auriya Daemon (src/main.rs, src/daemon, src/core)
Event loop, watchers, scheduler"]
fas["FAS & FPS Meter (src/core/fas, fps_meter)
eBPF frame pacing & scaling"]
tweaks["System Tweaks (src/core/tweaks)
Governors, frequencies, memory, vendor"]
ctl["auriyactl (src/ctl.rs, src/cli)
Unix domain socket CLI client"]
end
subgraph module_plane ["Module Plane (module/)"]
scripts["Root Lifecycle Scripts
customize.sh, service.sh, uninstall.sh"]
end
subgraph doc_plane ["Documentation Plane (website/)"]
wiki["Docusaurus Technical Wiki"]
end
root --> android_plane
root --> rust_plane
root --> module_plane
root --> doc_plane
daemon --> fas
daemon --> tweaks
```
For the exact source-tree layout of files on disk, see [Project structure](/development/project-structure/).
# Data Flow
Two independent flows cross the same three planes: **commands** (a client asks for a state change) and **telemetry/state** (observed state flows back). They must be read separately — a command is a request, telemetry is a report — and a failure at either boundary must stay visible to the caller.
## End-to-end: everything running
[Section titled “End-to-end: everything running”](#end-to-end-everything-running)
The full picture once the module is installed and a game is in the foreground — who talks to whom, over which channel, with the real path/command. Every arrow is one of the four channels in the [table below](#the-four-channels-concretely).
```
flowchart TD
app["Manager App (Compose) / auriyactl
dev.auriya.app"]
sock["Unix Domain Socket
/dev/socket/auriya.sock"]
daemon["Rust Daemon (auriya)
Tokio Async Event Loop"]
comp["Companion Service (AuriyaSysMon)
app_process (root uid)"]
kernel["Kernel Interfaces
/proc · /sys"]
subgraph config_files ["Persisted Config & Status Files"]
cfg[("settings.toml
gamelist.toml")]
status["system_status"]
cmd["auriya_cmd"]
end
app -->|"writes config (root)"| cfg
cfg -.->|"watched by"| daemon
app -->|"IPC commands: STATUS, SET_PROFILE, GET_STATS"| sock
sock <-->|"request / JSON reply"| daemon
comp -->|"writes Android state"| status
status -.->|"watched by"| daemon
daemon -->|"writes DnD & refresh rate"| cmd
cmd -.->|"watched & replayed via Android APIs"| comp
daemon -->|"guarded writes: governors / ceiling / FAS"| kernel
kernel -->|"best-effort telemetry reads: freq / load / temp"| daemon
```
The app and `auriyactl` talk to the daemon **directly** over the socket for commands and status. The companion is a *separate* participant: it feeds the daemon observed Android state (`system_status`) and executes the Android-framework actions the root daemon cannot (`auriya_cmd`) — see [System tweaks → CmdWriter](/internals/system-tweaks/#actions-routed-through-android--cmdwriter).
## Boot sequence (cold start → first tick)
[Section titled “Boot sequence (cold start → first tick)”](#boot-sequence-cold-start--first-tick)
What happens from power-on until the daemon is serving requests, per `module/service.sh` and `src/daemon/run.rs` (full detail: [overview → binary execution](/architecture/overview/#binary-execution-workflow)):
```
flowchart TD
boot([Android boot_completed]) --> svc["service.sh: stop stale procs,
rm stale socket/status/lock"]
svc --> comp["app_process → start Companion (AuriyaSysMon)"]
comp --> cw["Companion writes first system_status"]
cw --> wait{"system_status
appears within 10s?"}
wait -->|no| fail1["boot aborts — daemon not started"]
wait -->|yes| exec["exec auriya --settings … --gamelist …"]
exec --> load{"load settings.toml
+ gamelist.toml"}
load -->|parse error| fail2["main returns — no daemon"]
load -->|ok| trace["init tracing (log_level)"]
trace --> ebpf["init eBPF frame stream
(or fall back: sysfs FPS, FAS off)"]
ebpf --> build["build Daemon: whitelist,
FasController(FasTuning), ceiling, telemetry"]
build --> bind["bind /dev/socket/auriya.sock
+ spawn IPC listener"]
bind --> watch["start watchers: settings, gamelist,
module-update, companion.lock"]
watch --> tick0["run one immediate tick"]
tick0 --> loop([enter adaptive event loop])
```
## Steady-state: one game session, tick by tick
[Section titled “Steady-state: one game session, tick by tick”](#steady-state-one-game-session-tick-by-tick)
The command/telemetry round trip while a whitelisted game runs and the app polls:
```
sequenceDiagram
autonumber
actor User as User / Game
participant Comp as Companion (AuriyaSysMon)
participant Daemon as Rust Daemon (tick loop)
participant Kernel as Kernel (/proc, /sys)
participant App as App (Manager UI ~1Hz)
User->>Comp: Game enters foreground
Comp->>Daemon: Write system_status (pkg, pid)
Note over Daemon: Watcher fires → instant tick
Lock vendor, apply profile, attach eBPF
Daemon->>Comp: Write auriya_cmd (DnD, refresh rate)
Comp->>User: Replay via Android Framework APIs
loop Each Tick (~500ms active game session)
Daemon->>Daemon: Drain frames → FAS scaling decision
Daemon->>Kernel: Write ScalingAction (CPU/GPU frequencies)
Daemon->>Daemon: Refresh CurrentState (FPS, telemetry)
App->>Daemon: GET_STATS (Unix socket)
Daemon-->>App: JSON (FpsStats, thermals, battery)
App->>App: Render telemetry & benchmark cards
end
User->>Comp: Game leaves foreground
Comp->>Daemon: Write system_status (home/launcher)
Note over Daemon: Instant tick → clear game state,
restore default profile, detach eBPF
App->>Daemon: GET_STATS
Daemon-->>App: JSON (fps: null, standby)
```
The eBPF worker only drains frames while a PID is attached, so it costs nothing outside a game session. `GET_STATS` computes on request — see [Stats API](/reference/stats-api/).
## The four channels concretely
[Section titled “The four channels concretely”](#the-four-channels-concretely)
| Direction | Mechanism | Payload | Reference |
| ------------------ | ------------------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Client → daemon | Unix socket, newline text | commands: `STATUS`, `SET_PROFILE`, `ADD_GAME`, `GET_STATS`, … | [IPC protocol](/internals/ipc-protocol/) |
| Companion → daemon | `system_status` file (watched) | foreground app/PID/UID, screen, battery-saver, Zen | below |
| Daemon → companion | `auriya_cmd` file (watched) | DnD filter, refresh rate | [System tweaks → CmdWriter](/internals/system-tweaks/#actions-routed-through-android--cmdwriter) |
| Daemon → kernel | `/proc`, `/sys` writes | governors, ceilings, tweaks | [System tweaks](/internals/system-tweaks/) |
The exact struct/field shapes of these payloads are in the [data model](/architecture/data-model/).
## `system_status` — companion → daemon
[Section titled “system\_status — companion → daemon”](#system_status--companion--daemon)
The companion writes `/data/adb/.config/auriya/system_status` whenever the foreground app, screen, battery-saver, or Zen state changes. The wire format is line-oriented (`src/core/system_status/mod.rs:8-11`):
```text
focused_app
screen_awake <0|1>
battery_saver <0|1>
zen_mode <0|1|2|3>
```
The daemon’s watcher reloads this file and merges it into a `CurrentState` snapshot that IPC clients read. Fields are optional — a partial write updates only the lines present (`SystemStatus`, `mod.rs:27-56`). The daemon uses `focused_app`
* `focused_pid` for [game detection](/internals/game-detection/), and `screen_awake` + `battery_saver` to force the power-save branch of the [scheduler](/internals/profile-scheduler/#decision-order).
## Tick flow
[Section titled “Tick flow”](#tick-flow)
The daemon runs a variable-cadence tick (see [Architecture overview → Event loop](/architecture/overview/#event-loop-and-execution-cadence) for the exact event table):
* **≈ 500 ms** while a validated game session is active,
* **`daemon.check_interval_ms`** (default 2 s) in normal foreground operation,
* **10 s** when screen-off / battery-saver suspends normal work.
Each tick reads the cached companion snapshot, handles power-saving overrides first, resolves the package/PID (or an `INJECT` override), then either runs FAS for a known game or applies the appropriate profile ([Profile scheduler](/internals/profile-scheduler/)). A copy-on-write game-list snapshot avoids holding a lock across async work. A tick can also be triggered early — outside the timer — by a companion update, a config change, or a tracked PID exiting ([game detection](/internals/game-detection/#liveness-tracking-and-instant-exit)).
## Failure visibility
[Section titled “Failure visibility”](#failure-visibility)
* IPC errors are returned to the client as `ERR …` lines ([IPC protocol → response conventions](/internals/ipc-protocol/#response-conventions)).
* Kernel-write failures are best-effort and logged, not fatal ([System tweaks](/internals/system-tweaks/#guarded-best-effort-writes)).
* A dead companion is detected via `companion.lock`; display/DnD then fall back to Android `settings put` ([overview](/architecture/overview/#control-and-status-paths)).
# Data Model
Auriya has no database — its “entities” are configuration files, in-memory snapshots, and wire payloads that cross the Rust ↔ Android boundary. This page maps them: what each entity is, **where it lives**, and **which direction it syncs**. The single most important property here is **Rust ↔ Kotlin schema parity** for the config entities — the two sides must agree or a field is silently dropped (there is no `deny_unknown_fields`).
Rust: `src/core/config/`, `src/core/system_status/`, `src/core/cmd_writer/`, `src/daemon/state.rs`, `src/core/stats/`, `src/core/telemetry/`. Kotlin: `android/shared/src/main/kotlin/dev/auriya/shared/model/` + `.../config/TomlParser.kt`.
## Entity map (who owns what, sync direction)
[Section titled “Entity map (who owns what, sync direction)”](#entity-map-who-owns-what-sync-direction)
```
flowchart LR
subgraph persisted ["Persisted config - Rust and Kotlin must agree"]
direction LR
kt["Kotlin models (app)
Settings.kt, TomlParser.kt"]
toml[("settings.toml
gamelist.toml")]
rs["Rust structs
settings.rs, gamelist.rs (serde)"]
kt -->|write| toml
toml -->|read| rs
rs -.->|"daemon rewrites gamelist on IPC mutate"| toml
toml -->|read| kt
end
kt <-->|"schemas must match field-for-field,
or a field is silently dropped"| rs
subgraph runtime ["Runtime / wire - no persistence"]
direction LR
comp["Companion"] -->|writes| ss["SystemStatus
(system_status file)"]
ss --> cs["CurrentState
(per-tick, in-memory)"]
cs --> stats["StatsSnapshot
(GET_STATS, on request)"]
stats -->|"JSON over socket"| app["App (cards)"]
fb["FrameBuffer
(FAS deque)"] --> fps["FpsStats (computed)"]
fps --> stats
daemon["Daemon"] -->|"DnD, refresh rate"| cmd["Cmd
(auriya_cmd file)"]
cmd --> comp
end
```
## Config entities (Rust ↔ Kotlin — must stay in sync)
[Section titled “Config entities (Rust ↔ Kotlin — must stay in sync)”](#config-entities-rust--kotlin--must-stay-in-sync)
These have **two authoritative definitions** — a Rust struct (what the daemon consumes) and a Kotlin data class (what the app reads/writes). They are joined by the TOML file. Keeping them equal is a hard requirement.
### `Settings` ↔ `settings.toml` ↔ `Settings.kt`
[Section titled “Settings ↔ settings.toml ↔ Settings.kt”](#settings--settingstoml--settingskt)
| Group | Rust (`settings.rs`) | Kotlin (`Settings.kt`) | Notes |
| -------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------- |
| `[daemon]` | `DaemonConfig { log_level, check_interval_ms, default_mode }` | `DaemonConfig(logLevel, checkIntervalMs, defaultMode)` | Parity |
| `[cpu]` | `CpuConfig { default_governor }` | `CpuConfig(defaultGovernor)` | Parity |
| `[dnd]` | `DndConfig { default_enable }` | `DndConfig(defaultEnable)` | Parity |
| `[fas]` | `FasConfig { enabled, default_mode, thermal_threshold, poll_interval_ms, target_fps }` | `FasConfig(enabled, defaultMode, thermalThreshold, pollIntervalMs, targetFps)` | Parity (all 5) |
| `[dynamic_governor]` | `DynamicGovernorConfig { enabled, cv_threshold, debounce_frames }` | `DynamicGovernorConfig(enabled, cvThreshold, debounceFrames)` | Parity |
| `[modes.*]` | `HashMap` | `Map` | Parity |
Full per-key meaning + which the daemon consumes: [settings reference](/reference/settings/).
### `GameProfile` ↔ `[[game]]` ↔ Kotlin
[Section titled “GameProfile ↔ \[\[game\]\] ↔ Kotlin”](#gameprofile--game--kotlin)
| Field | Type | Required | Notes |
| -------------- | ----------------- | -------- | ------------------------------------------ |
| `package` | string | Yes | whitelist key |
| `cpu_governor` | string | Yes | |
| `enable_dnd` | bool | Yes | |
| `target_fps` | int **or** int\[] | — | custom deserializer (`TargetFpsConfig`) |
| `refresh_rate` | int | — | |
| `mode` | string | — | `powersave`/`balance`/`performance`/`fast` |
| `ceiling` | string | — | |
Full detail: [gamelist reference](/reference/gamelist/).
There is no `#[serde(deny_unknown_fields)]`. If you add a key to one side only:
* key in TOML but not the Rust struct → **silently dropped** on load;
* key the app writes but the daemon doesn’t read → **dead config** (parsed, no effect).
So a new setting is a **three-place change**: Rust struct (+ consume it), `settings.toml`/`gamelist.toml` default, and the Kotlin model + `TomlParser`. Do **not** add a field only one side uses. This is why per-game *UI-only* flags (e.g. auto-record enable) live in app SharedPreferences, **not** in `gamelist.toml`.
## Runtime / wire entities (no persistence)
[Section titled “Runtime / wire entities (no persistence)”](#runtime--wire-entities-no-persistence)
These are transient — computed per tick or per request, never stored.
### `SystemStatus` (companion → daemon)
[Section titled “SystemStatus (companion → daemon)”](#systemstatus-companion--daemon)
`src/core/system_status/mod.rs`. Parsed from the `system_status` file; all fields `Option` (partial writes allowed).
| Field | Type |
| --------------- | ---------------- |
| `focused_app` | `Option` |
| `focused_pid` | `Option` |
| `focused_uid` | `Option` |
| `screen_awake` | `Option` |
| `battery_saver` | `Option` |
| `zen_mode` | `Option` |
### `Cmd` (daemon → companion, `auriya_cmd`)
[Section titled “Cmd (daemon → companion, auriya\_cmd)”](#cmd-daemon--companion-auriya_cmd)
`src/core/cmd_writer/mod.rs`. Stateful writer re-emits full state each write.
| Field | Type |
| -------------- | ------------------------------------ |
| `dnd` | `Option` (All / Priority) |
| `refresh_rate` | `Option` (0 = restore) |
### `CurrentState` (in-memory, per-tick)
[Section titled “CurrentState (in-memory, per-tick)”](#currentstate-in-memory-per-tick)
`src/daemon/state.rs`. Refreshed every tick; read by IPC `STATUS` / `GET_STATS`. Holds `pkg`, `pid`, `screen_awake`, `battery_saver`, `profile`, `companion_alive`, `cpu_telemetry`, `gpu_telemetry`, `thermal_telemetry`, `fps`, `fps_source`, and `game_session` (true only for a whitelisted game — the record trigger).
### `StatsSnapshot` (`GET_STATS`, computed on request)
[Section titled “StatsSnapshot (GET\_STATS, computed on request)”](#statssnapshot-get_stats-computed-on-request)
`src/core/stats/mod.rs`. Assembled per request from `CurrentState` + a fresh `BatterySnapshot` + FPS stats from the FAS `FrameBuffer`. Serialized to JSON, grouped one-per-UI-card. This is the **stable contract for the app** — schema and null rules in [Stats API](/reference/stats-api/).
### Telemetry snapshots (point reads, per tick or per request)
[Section titled “Telemetry snapshots (point reads, per tick or per request)”](#telemetry-snapshots-point-reads-per-tick-or-per-request)
`CpuSnapshot`, `GpuSnapshot`, `ThermalSnapshot` (`src/core/telemetry/`) — sampled each tick into `CurrentState`. `BatterySnapshot` (`telemetry/battery.rs`) — read fresh on each `GET_STATS`. All fields best-effort `Option`.
## Lifecycle at a glance
[Section titled “Lifecycle at a glance”](#lifecycle-at-a-glance)
| Entity | Created by | Lives in | Read by | Persisted? |
| -------------------------- | ------------------------- | -------------------- | ---------------------- | --------------------- |
| `Settings` / `GameProfile` | app (or install defaults) | TOML on disk | daemon (startup/watch) | Yes (file) |
| `SystemStatus` | companion | `system_status` file | daemon (watch) | Yes (file, transient) |
| `Cmd` | daemon | `auriya_cmd` file | companion (watch) | Yes (file, transient) |
| `CurrentState` | daemon tick | RAM | IPC handlers | No (RAM only) |
| `StatsSnapshot` | IPC handler | RAM → JSON | app | No (per-request) |
| `current_profile` | daemon | file (`1`/`2`/`3`) | legacy readers | Yes (file) |
## See also
[Section titled “See also”](#see-also)
* [Data flow](/architecture/data-flow/) — how these entities move at runtime.
* [Components](/architecture/components/) — the processes that own them.
* [settings](/reference/settings/) · [gamelist](/reference/gamelist/) · [Stats API](/reference/stats-api/) — the field-level specs.
# Module Lifecycle
From CI build to boot to uninstall — the life of the module payload. Each stage cites the script that implements it. For install *details* see [Installation](/getting-started/installation/); for the boot *execution* see [Architecture overview](/architecture/overview/#binary-execution-workflow).
Traced to commit `10fe7c6`: `module/customize.sh`, `module/service.sh`, `module/uninstall.sh`, and `.github/actions/package-module` ([CI/CD workflows](/development/ci-cd/)).
## 1. Package (CI)
[Section titled “1. Package (CI)”](#1-package-ci)
CI builds **one** self-contained ZIP: lifecycle scripts, `module.prop`, the default `settings.toml` / `gamelist.toml`, the aarch64 `auriya` (+ optional `auriyactl`) with a `checksums.sha256`, and both APKs under `libs/companion/`. Nothing is fetched at boot. The exact packaging (APK search order, versioning, 7-zip invocation) is documented in [CI/CD → package-module](/development/ci-cd/#package-module). The ZIP name is `auriya----.zip`.
## 2. Extract (root manager)
[Section titled “2. Extract (root manager)”](#2-extract-root-manager)
Magisk / KernelSU / APatch extracts the ZIP into `/data/adb/modules/auriya`. The repository’s `module/` directory **is** the ZIP root, so there is no nested `module/module/` on device.
## 3. Install (`customize.sh`)
[Section titled “3. Install (customize.sh)”](#3-install-customizesh)
Runs at flash time (see [Installation → what the installer does](/getting-started/installation/#what-the-installer-actually-does)):
1. Abort unless `$ARCH` is `arm64`.
2. Verify the daemon binary against `checksums.sha256` (mismatch aborts).
3. Copy the daemon → `system/bin/auriya`, CLI → `system/bin/auriyactl` (if present).
4. Copy the companion APK → `system/etc/auriya/service.apk` (**required**; missing aborts).
5. `pm install` the manager app `auriya-app.apk` (**best-effort**; failure warns and continues).
6. Delete the staging `libs/` directory.
7. Move `settings.toml` / `gamelist.toml` into `/data/adb/.config/auriya` **only if absent** (never overwrites user config).
8. Create KernelSU/APatch `bin` symlinks where those managers are present.
The runtime-vs-staging path distinction is in the [Filesystem reference](/reference/filesystem/#zip-staging-paths-inside-the-archive--during-install-only).
## 4. Boot (`service.sh`)
[Section titled “4. Boot (service.sh)”](#4-boot-servicesh)
On every boot, via the root manager’s `service.d` hook: wait for `sys.boot_completed`, stop any stale companion/daemon, remove stale socket/status/lock files, launch the companion with `app_process`, wait up to 10 s for its `system_status`, then start the daemon with explicit `--settings` / `--gamelist` paths, teeing output to logcat and `/data/adb/auriya/daemon.log`. The full boot sequence is in [Architecture overview → Binary execution workflow](/architecture/overview/#binary-execution-workflow).
## 5. Run
[Section titled “5. Run”](#5-run)
The daemon’s tick loop selects profiles and publishes status; clients connect over the socket. See [Data flow](/architecture/data-flow/) and [Profile scheduler](/internals/profile-scheduler/).
## 6. Update
[Section titled “6. Update”](#6-update)
`update.json` (committed to `main` by the release workflow) advertises the latest version, `versionCode`, release-asset URL, and changelog URL; `module.prop`’s `updateJson` points root managers at it. See [CI/CD → release.yml](/development/ci-cd/#releaseyml).
## 7. Uninstall (`uninstall.sh`)
[Section titled “7. Uninstall (uninstall.sh)”](#7-uninstall-uninstallsh)
Stops `auriya` and `AuriyaSysMon`, `pm uninstall`s the packages, and deletes the socket, `/data/adb/.config/auriya`, `/data/adb/auriya`, and the symlinks. Can also be triggered at boot by a `remove` flag. Details: [Uninstall](/getting-started/uninstall/).
# Architecture Overview
Auriya is a rooted Android performance module composed of three runtime planes: a Compose manager app, a companion Android service, and an aarch64 Rust daemon. The daemon is the owner of observation, profile transitions, telemetry, and writes to `/proc`/`sys`; Android components provide lifecycle integration and user-facing control.
```
flowchart TD
app["Android Manager (Compose)"]
comp["Companion Service (AuriyaSysMon)"]
daemon["Rust Daemon (auriya)"]
det["Process / Game Detection"]
fps["FPS Meter (eBPF / sysfs)"]
sched["Profile Scheduler"]
tweaks["System Tweak Layer"]
kernel["/proc & /sys (Kernel Interfaces)"]
app -->|commands & status| comp
comp -->|local IPC / system_status| daemon
daemon --> det
daemon --> fps
det --> sched
fps --> sched
sched --> tweaks
tweaks --> kernel
```
The Android manager owns user interaction and presentation. The companion service bridges Android lifecycle constraints. The Rust daemon owns long-running observation, scheduling, telemetry, and system writes.
The control CLI in `src/ctl.rs` provides a second entry point for querying or controlling the daemon without the Compose UI.
## Binary execution workflow
[Section titled “Binary execution workflow”](#binary-execution-workflow)
The installed module does not launch the daemon through the Android app. At boot, `module/service.sh` waits for Android’s `sys.boot_completed`, starts the bundled companion APK with `app_process`, waits up to 10 seconds for `/data/adb/.config/auriya/system_status`, then executes `/data/adb/modules/auriya/system/bin/auriya` with the installed settings and gamelist paths. Standard output/error are piped into logcat and `/data/adb/auriya/daemon.log`. See [module lifecycle](/architecture/module-lifecycle/) for the installation paths.
```
flowchart TD
boot([Android boot completed]) --> svc["module/service.sh"]
svc --> c1["Stop stale companion & daemon processes"]
svc --> c2["Remove stale socket / status / lock files"]
svc --> c3["app_process service.apk → dev.auriya.service.Main"]
svc --> c4{"Wait for system_status
(≤ 10s timeout)"}
c4 -->|ok| daemon["exec auriya (Rust Binary)"]
c4 -->|timeout| abort["Abort daemon startup"]
daemon --> d1["Load settings.toml + gamelist.toml"]
daemon --> d2["Initialize logging & tracing"]
daemon --> d3["Create daemon state, telemetry, eBPF & FAS"]
daemon --> d4["Bind /dev/socket/auriya.sock"]
daemon --> d5["Start config, module & companion watchers"]
daemon --> d6["Run one immediate tick"]
d6 --> loop([Enter adaptive event loop])
```
The Rust entry point loads both configuration files before initializing tracing; a load/parse error returns from `main` and prevents daemon startup ([`main`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/main.rs#L8-L49)). `run_with_config` refuses to continue when the companion status file is not populated within 10 seconds. Failure to enumerate display modes is non-fatal and produces an empty supported-mode list ([`run_with_config`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/run.rs#L574-L607)).
## Event loop and execution cadence
[Section titled “Event loop and execution cadence”](#event-loop-and-execution-cadence)
The daemon is a single-thread Tokio runtime for orchestration, with background threads/tasks for watchers, eBPF, and blocking device work. After one immediate tick, `tokio::select!` waits for the first available event ([event loop](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/run.rs#L624-L681)):
| Event | Result |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| timer | run a tick after 500 ms in a validated game session, 5 seconds normally, or 10 seconds when screen-off/battery-saver state suspends normal work |
| companion status update | run an immediate tick; no timer wait |
| settings update | reload default governor/default mode; reapply the governor immediately only when the current profile is Balance |
| gamelist update | rebuild the whitelist, clear tracked package/PID, then run an immediate tick |
| tracked PID exit | run an immediate tick |
| companion lock release | mark the companion dead and attempt a rate-limited restart |
| staged module update or Ctrl-C | release vendor locks and ceiling/core overrides, then exit cleanly |
Tick errors do not terminate the loop. Identical errors are log-debounced for 30 seconds; successful ticks refresh the shared IPC status with package, PID, profile, power state, FPS source/value, and CPU/GPU/thermal telemetry ([`Daemon::tick`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/tick.rs#L24-L88)).
## Profile decision workflow
[Section titled “Profile decision workflow”](#profile-decision-workflow)
The profile scheduler evaluates conditions in strict priority order. A lower branch is not evaluated after a higher branch returns.
```
flowchart TD
tick([Tick Triggered]) --> check_screen{"Screen OFF or
Battery Saver ON?"}
check_screen -->|yes| p_powersave["POWERSAVE + Low ceiling
+ detach eBPF + disable game DnD"]
check_screen -->|no| check_inject{"Foreground override
from IPC exists?"}
check_inject -->|yes| use_inject["Use injected package"]
check_inject -->|no| check_fg{"Companion has
focused package?"}
check_fg -->|no| default_mode["Apply default mode
+ release game-owned state"]
check_fg -->|yes| check_same{"Same package &
tracked PID still alive?"}
check_same -->|yes| check_fas{"FAS available &
whitelisted?"}
check_fas -->|yes| run_fas["Run FAS scaling decision"]
check_fas -->|no| skip_reapply["Skip profile reapplication"]
check_same -->|no| check_white{"Package is
whitelisted?"}
check_white -->|no| default_mode
check_white -->|yes| check_pid{"Validate PID
against /proc/package"}
check_pid -->|invalid / missing| default_mode
check_pid -->|valid| enter_game["Enter / update game session
(lock vendor, profile, ceiling, eBPF, DnD)"]
```
This order is implemented by `Daemon::process_tick_logic` ([source](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/tick.rs#L91-L192)). Screen-off or battery saver always wins, even if a game remains foregrounded. The daemon only calls a full profile application when `last.profile_mode` differs from the target mode; repeated ticks do not rewrite every kernel node.
### Entering a whitelisted game
[Section titled “Entering a whitelisted game”](#entering-a-whitelisted-game)
`handle_whitelisted_app` validates the focused PID before applying game state ([source](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/tick.rs#L194-L307)). On a new game session it:
1. Locks vendor-owned controls so external vendor services cannot immediately overwrite Auriya’s values.
2. Broadcasts `dev.auriya.app.ACTION_SHOW_TOAST` with the chosen mode.
3. Reads the game profile. Missing/unknown `mode` defaults to Performance; recognized values are `fast`, `performance`, `balance`, and `powersave`.
4. Applies the target profile only when it differs from the current mode.
5. Applies the game ceiling override, or the configured default ceiling when none exists.
6. Requests the configured refresh rate when it differs from the active override.
7. Attaches the Kala eBPF frame probe to the validated game PID.
8. Requests Priority DnD when `enable_dnd=true`, otherwise All/normal notifications.
9. Stores the package and creates a PID tracker.
If PID validation fails, the daemon does not apply the game profile; it falls back to the configured default mode and clears game-specific state.
### What each static profile changes
[Section titled “What each static profile changes”](#what-each-static-profile-changes)
These are the direct actions in `src/core/profile.rs`; individual tweak modules decide which device paths exist.
| Profile | Actions |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fast | aggressive performance profile: sets requested governor, enables CPU boost, onlines cores, applies MediaTek/Snapdragon hooks, sets GPU performance mode, enables touch game mode, applies general/scheduler/storage/memory tweaks, drops caches, and sets game affinity/priority |
| Performance | set requested CPU governor, enable CPU boost, online cores, apply MediaTek/Snapdragon performance hooks, set GPU performance mode, enable touch game mode, apply general/scheduler/storage/memory tweaks, drop caches, and optionally set game CPU affinity/priority |
| Balance | set the configured governor, disable CPU boost, restore vendor normal mode, set balanced GPU mode, disable touch game mode, restore scheduler/storage/memory defaults |
| Powersave | set CPU governor to `powersave` and request swappiness `60`; it does not run the Balance restoration sequence first |
Fatal `?` operations return an error and the daemon leaves `last.profile_mode` unchanged. Operations wrapped by `warn_on_err` log a warning but allow the profile call to succeed. DnD is deliberately not owned by these functions; the daemon synchronizes it from game-session state so an FAS mode reduction does not incorrectly re-enable notifications ([profile functions](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/core/profile.rs#L96-L251)).
### FAS dynamic changes inside the same game
[Section titled “FAS dynamic changes inside the same game”](#fas-dynamic-changes-inside-the-same-game)
When the package and PID are unchanged, the daemon avoids full re-entry logic. If Frame-Aware Scheduling (FAS) exists, it consumes the shared Kala frame stream and chooses one `ScalingAction` ([`run_fas_tick`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/tick.rs#L442-L515)):
| FAS action | Applied change |
| --------------- | ------------------------------------------------------------------------------------------------------------- |
| `BoostGpu` | GPU performance mode only; CPU settings remain untouched |
| `BoostCpu` | game governor, CPU boost, online cores, performance scheduler, process affinity/priority; GPU is set balanced |
| `BoostBalanced` | full Performance profile unless already marked Performance |
| `Maintain` | no system write |
| `Reduce` | return to `daemon.default_mode` unless already there |
FAS errors are warnings in the caller and do not stop the tick loop. The eBPF measurement method and its limitations are documented once in [Kala eBPF frame probe](/internals/kala-research/).
### Leaving a game or losing foreground state
[Section titled “Leaving a game or losing foreground state”](#leaving-a-game-or-losing-foreground-state)
For a non-whitelisted package, invalid/missing PID, or no foreground package, the daemon applies `daemon.default_mode` only when required, restores the default ceiling, detaches eBPF, requests normal notifications, clears the PID tracker, unlocks vendor controls, and releases any refresh-rate override by requesting `0` Hz ([clear path](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/tick.rs#L309-L440)).
## Control and status paths
[Section titled “Control and status paths”](#control-and-status-paths)
`auriyactl` and Android clients connect to `/dev/socket/auriya.sock`; they do not invoke profile functions directly. The IPC handler parses a command, then operates on daemon state or calls a serialized profile function. Companion-originated state travels in the opposite direction through `/data/adb/.config/auriya/system_status`. Display and DnD requests normally go through the companion command writer; when the companion is considered dead, refresh rate and Zen mode use Android `settings put` fallbacks. See [IPC protocol](/internals/ipc-protocol/) and [filesystem reference](/reference/filesystem/).
## Runtime boundaries
[Section titled “Runtime boundaries”](#runtime-boundaries)
* **Manager app (`android/app`)**: Compose UI, settings/game-list editing, root shell access, widget, tile, overlay, and status presentation.
* **Shared Kotlin (`android/shared`)**: TOML paths/parser plus `Settings`, `GameProfile`, `SystemStatus`, and command/status wire models.
* **Companion service (`android/service`)**: foreground/task-stack, power, and Zen/DnD sensors; writes the daemon status snapshot and consumes daemon commands for display/DnD actions.
* **Rust daemon (`src/main.rs`, `src/daemon`)**: loads config, starts the event/tick loop, serves `/dev/socket/auriya.sock`, detects the foreground PID, samples FPS/telemetry, selects a profile, and applies tweaks.
* **Rust CLI (`src/ctl.rs`, `src/cli`)**: line-oriented client for the same Unix socket.
* **Kernel/device boundary (`src/core/tweaks`, telemetry, eBPF)**: best-effort reads and guarded writes to vendor-dependent nodes.
# Use Cases
Who does what with Auriya, and the exact path each capability takes through the system. Actors are the five participants from [Components](/architecture/components/); every flow below is grounded in the runtime paths described in [Data flow](/architecture/data-flow/).
## Actors
[Section titled “Actors”](#actors)
| Actor | Role |
| --------------- | -------------------------------------------------------------------------------- |
| **User** | Person operating the phone — installs, tweaks, plays. |
| **Manager app** | Compose UI (`dev.auriya.app`). Writes config, sends commands, renders telemetry. |
| **Companion** | `AuriyaSysMon`. Observes Android state, executes framework actions. |
| **Daemon** | `auriya` (root). Owns scheduling, tweaks, IPC, telemetry. |
| **Kernel** | `/proc` + `/sys` nodes the daemon reads/writes. |
## Use-case map
[Section titled “Use-case map”](#use-case-map)
```
flowchart LR
user(("User"))
user --> uc1["Install / flash"]
user --> uc2["Set profile"]
user --> uc3["Add / edit game"]
user --> uc4["Tune FAS / settings"]
user --> uc6["View live stats"]
user --> uc7["Auto-record FPS"]
uc1 --> life["module lifecycle (customize.sh)"]
uc2 --> app["Manager app"]
uc3 --> app
uc4 --> app
uc6 --> app
uc7 --> app
app -->|SET_PROFILE| daemon["Daemon"]
app -->|ADD / UPDATE_GAME| daemon
app -->|writes settings.toml| daemon
app -->|GET_STATS| daemon
uc7 -.->|watches session.active,
records GET_STATS samples| app
daemon -->|"governor / tweaks"| kernel["Kernel /proc,/sys"]
daemon -->|rewrites| gl[("gamelist.toml")]
daemon -->|JSON| app
comp["Companion"] -->|"detects foreground,
writes system_status"| daemon
play(("Play a game")) --> comp
daemon -->|"profile + FAS + eBPF attach"| kernel
boot(("Boot")) --> svc["service.sh starts
Companion + Daemon (no user action)"]
```
## Flows
[Section titled “Flows”](#flows)
### UC-1 · Install & first run
[Section titled “UC-1 · Install & first run”](#uc-1--install--first-run)
**Actor:** User → root manager → `customize.sh` → app.
1. Flash the module ZIP; `customize.sh` verifies arch/checksum, installs daemon + companion APK, `pm install`s the app, seeds default TOMLs.
2. Reboot. `service.sh` starts companion + daemon automatically.
3. Open the app, grant root. See [Installation](/getting-started/installation/), [First run](/getting-started/first-run/).
### UC-2 · Set a global profile
[Section titled “UC-2 · Set a global profile”](#uc-2--set-a-global-profile)
**Actor:** User → App → Daemon → Kernel.
1. User taps a profile (or tile/widget).
2. App: `echo 'SET_PROFILE PERFORMANCE' | nc -U …sock` (`UiViewModel.kt`).
3. Daemon takes the profile lock, applies governor/GPU/tweaks → `/proc`,`/sys`.
4. Reply `OK SET_PROFILE Performance`. See [IPC](/internals/ipc-protocol/#profile-control).
### UC-3 · Add / edit a game
[Section titled “UC-3 · Add / edit a game”](#uc-3--add--edit-a-game)
**Actor:** User → App → Daemon → `gamelist.toml`.
1. User adds a package or edits its overrides on the Games screen.
2. App sends `ADD_GAME ` / `UPDATE_GAME [k=v…]`.
3. Daemon mutates the in-memory list, **atomically rewrites** `gamelist.toml`, rebuilds the whitelist. See [gamelist](/reference/gamelist/#how-entries-are-added-and-changed).
### UC-4 · Tune FAS / settings
[Section titled “UC-4 · Tune FAS / settings”](#uc-4--tune-fas--settings)
**Actor:** User → App → `settings.toml` → Daemon.
1. User changes a setting in the app (recommended — no manual file editing; see [Configuration](/getting-started/configuration/)).
2. App writes `settings.toml`. Live keys (`cpu.default_governor`, `daemon.default_mode`, `check_interval_ms`) apply on reload; FAS keys apply on daemon restart. See [Performance tuning](/getting-started/performance-tuning/).
### UC-5 · Play a game (automatic, no user action)
[Section titled “UC-5 · Play a game (automatic, no user action)”](#uc-5--play-a-game-automatic-no-user-action)
**Actor:** Companion → Daemon → Kernel.
1. Game enters foreground; companion writes `system_status`.
2. Daemon watcher fires an instant tick; if the package is whitelisted with a live PID it enters a game session: lock vendor nodes, apply profile, attach the eBPF frame probe, request DnD/refresh via `auriya_cmd`.
3. Each tick FAS reads frames and nudges CPU/GPU. On exit, state is cleared and the default profile restored. See [Profile scheduler](/internals/profile-scheduler/), [Game detection](/internals/game-detection/).
### UC-6 · View live telemetry
[Section titled “UC-6 · View live telemetry”](#uc-6--view-live-telemetry)
**Actor:** User → App → Daemon.
1. App opens the stats screen and polls `GET_STATS` (\~1 Hz, root `nc`).
2. Daemon computes FPS stats from the FAS buffer + a battery snapshot, returns grouped JSON.
3. App renders one card per group. `fps` is `null` when no game runs. See [Stats API](/reference/stats-api/).
### UC-7 · Auto-record FPS per game (app-side)
[Section titled “UC-7 · Auto-record FPS per game (app-side)”](#uc-7--auto-record-fps-per-game-app-side)
**Actor:** App (foreground service) driven by daemon signal.
1. User enables auto-record for a whitelisted game (app preference).
2. App watches `session.active` from `GET_STATS`; on `false → true` for that game it starts accumulating poll samples, and finalizes a session summary on `true → false`.
3. Recording is stored in the app’s own sandbox. The daemon provides the *signal* (`session.active`) and *data* (`GET_STATS`); the recording logic is app-side — see [Stats API → auto-record](/reference/stats-api/#fps-auto-record-app-side).
## See also
[Section titled “See also”](#see-also)
* [Data flow](/architecture/data-flow/) — the channels these flows travel on.
* [Data model](/architecture/data-model/) — the entities they move.
* [Components](/architecture/components/) — the actors in detail.
# Building
How to build each part of Auriya. The Rust build has a **repo-specific gotcha** you must know before running any cargo command.
Traced to commit `10fe7c6`: [`Cargo.toml`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/Cargo.toml), [`.cargo/config.toml`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/.cargo/config.toml), `android/*/build.gradle.kts`, `website/package.json`. The authoritative CI recipe is [CI/CD workflows](/development/ci-cd/).
## Cargo is pinned to cross-compile for Android
[Section titled “Cargo is pinned to cross-compile for Android”](#cargo-is-pinned-to-cross-compile-for-android)
`.cargo/config.toml` sets:
```toml
[build]
target = "aarch64-linux-android"
[target.aarch64-linux-android]
linker = "/opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang"
runner = "bash .cargo/adb-runner.sh"
```
Consequences for a first-time contributor:
* **`cargo build` does not build for your host.** It cross-compiles an `aarch64-linux-android` binary and needs an Android NDK at the **hardcoded path** `/opt/android-ndk`. Without that NDK, the build fails at the linker.
* **`cargo test` runs on a device.** The `runner` ships each test binary to a connected device over adb (`.cargo/adb-runner.sh`) — tests execute on real hardware, not your workstation. Several tests (`pid_tracker`, `cmd_writer`) assume a real Android/Linux environment.
* CI installs the NDK and uses `cargo ndk` rather than relying on this hardcoded path — see below.
To build for your host instead (e.g. to run a unit test locally), override the target explicitly:
```bash
cargo build --target x86_64-unknown-linux-gnu
cargo test --target x86_64-unknown-linux-gnu
```
(Device-only tests will not be meaningful on the host.)
## Rust — the CI recipe (reproducible)
[Section titled “Rust — the CI recipe (reproducible)”](#rust--the-ci-recipe-reproducible)
The exact command CI uses to produce the shipped binaries ([CI/CD → rust-binary](/development/ci-cd/#rust-binary)):
```bash
# Requires: Android NDK, Rust nightly with target aarch64-linux-android,
# rust-src, and cargo-ndk.
cargo ndk -t aarch64-linux-android --platform 26 -- build --release --bin auriya --bin auriyactl
```
`cargo ndk` supplies the NDK toolchain paths, sidestepping the hardcoded `/opt/android-ndk` in `.cargo/config.toml`. The release profile is size-optimized (`Cargo.toml`): `opt-level = "z"`, `lto = "fat"`, `codegen-units = 1`, `panic = "abort"`, `strip = true`.
Note
CI uses **nightly** Rust with `edition = "2024"` and installs `rust-src` ([CI/CD → setup-tools](/development/ci-cd/#setup-tools)). The Kala eBPF dependency is a git dependency (`Cargo.toml`); its own eBPF object is prebuilt, so you do not need `bpf-linker` to build Auriya ([Kala eBPF frame probe](/internals/kala-research/#auriya-integration)).
## Lints and formatting
[Section titled “Lints and formatting”](#lints-and-formatting)
The repo enforces strict lints (`Cargo.toml` `[lints]`):
```bash
cargo fmt --all -- --check # formatting
cargo clippy --all-targets --all-features -- -D warnings # deny-level lints
```
`clippy` groups `all`/`correctness`/`suspicious`/`perf`/`complexity` are set to **deny**; `style` is `warn`. Rust `unsafe_op_in_unsafe_fn` and `unused_must_use` are denied.
## Android
[Section titled “Android”](#android)
Two Gradle modules produce the APKs (`android/app`, `android/service`; `minSdk = 30` / Android 11, `android/app/build.gradle.kts:54`). Signed release builds need `android/signing.properties` (template: `android/signing.properties.example` — never commit real keys):
```bash
cd android
./gradlew build # all modules, debug + release
./gradlew :app:assembleRelease :service:assembleRelease # what CI builds
./gradlew test # JVM unit tests
```
## Documentation site
[Section titled “Documentation site”](#documentation-site)
The website uses **Bun** (`website/bun.lock`, `website/package.json`):
```bash
cd website
bun install
bun run start # local dev server with live reload
bun run build # static production build into website/build
```
## Full pipeline
[Section titled “Full pipeline”](#full-pipeline)
The complete trigger, job DAG, per-step commands, artifacts, secrets, cache keys, failure behavior, and external side effects are documented in [CI/CD workflows](/development/ci-cd/).
# CI/CD Workflows
Verified against Auriya commit `10fe7c6b56474a00513fec34ebac1376b30e95e6`. Workflow/action references below point to that revision. Re-verify this page after changing `.github/workflows/`, `.github/actions/`, `Cargo.toml`, Android output names, or module layout.
## Inventory
[Section titled “Inventory”](#inventory)
| File | Trigger |
| ------------------------------- | ----------------------------------------------------------------------------- |
| `.github/workflows/build.yml` | `workflow_dispatch` only. No branch, tag, or path filter. |
| `.github/workflows/release.yml` | Push of any tag matching `v*`, or `workflow_dispatch`. No branch/path filter. |
There is no `pull_request`, branch-push, schedule, release-event, or matrix workflow. `.github/dependabot.yml` is separate weekly dependency-update automation for Cargo `/`, Gradle `/android`, and GitHub Actions `/`; it does not execute either workflow.
## Shared composite actions
[Section titled “Shared composite actions”](#shared-composite-actions)
### `setup-tools`
[Section titled “setup-tools”](#setup-tools)
Every invocation executes the same sequence ([source](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/.github/actions/setup-tools/action.yml)):
1. `actions/setup-java@v4`: Temurin Java 26.
2. `nttld/setup-ndk@v1`: Android NDK r29, added to `PATH` and exposed through the action’s environment.
3. `dtolnay/rust-toolchain@nightly`: nightly Rust, target `aarch64-linux-android`, component `rust-src`.
4. `rustup default nightly-x86_64-unknown-linux-gnu`.
5. `curl -sL | tar --zstd -x -C "${HOME}/.cargo/bin"`.
6. `cargo install cargo-ndk --locked`.
7. `sudo apt-get update`, then `sudo apt-get install -y p7zip-full zstd`.
Failure of any command/action stops the current job. Downloads and package installation access external systems. The `curl` pipeline does not enable `pipefail`, so a failed download can be masked if `tar` exits successfully.
### `package-module`
[Section titled “package-module”](#package-module)
The composite action runs one Bash block with `set -e` ([source](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/.github/actions/package-module/action.yml)):
1. Creates `build/release/module` and copies `module/*` into it.
2. Copies repository-root `settings.toml` and `gamelist.toml`; failures are explicitly ignored with `|| true`.
3. Attempts to copy `module/system`; failure is ignored.
4. Searches the downloaded artifacts for the manager APK in this order: exact `app-arm64-v8a-release.apk`, any `*-arm64-v8a-*.apk`, then any APK under an `app` path.
5. Searches for exact `service-release.apk`, then any APK under a `service` path.
6. Missing APKs print warnings but do not fail packaging. Found APKs become `libs/companion/auriya-app.apk` and `libs/companion/service.apk`.
7. Requires `target/aarch64-linux-android/release/auriya`; absence exits `1`. `auriyactl` is optional and only produces a warning when absent.
8. Copies binaries into `libs/aarch64/` and runs `sha256sum * > checksums.sha256` there.
9. Reads `VERSION` from the first `version =` line in `Cargo.toml`, `COMMIT_HASH` from `git rev-parse --short HEAD`, and `VERSION_CODE` from `git rev-list --count HEAD`.
10. Rewrites `module.prop` version fields, removes `.placeholder` files, then runs `7z a -tzip -mm=Deflate -mx=9 -mfb=258 -mpass=15` from inside the module staging directory.
11. Exposes `zip_name`, `version`, and `version_code` through `$GITHUB_OUTPUT`.
Output name: `auriya----.zip`. The input is `debug` in `build.yml` and `release` in `release.yml`; both workflows still compile Rust and Android with release build commands.
### `telegram-notify`
[Section titled “telegram-notify”](#telegram-notify)
The action reads the current commit subject, HTML-escapes `&`, `<`, and `>`, then chooses a Telegram Bot API request from `NOTIFY_TYPE` ([source](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/.github/actions/telegram-notify/action.yml)):
* `start`: `curl -s -X POST .../sendMessage`; parses `message_id` with `grep`/`cut` and writes it only when found.
* `complete`: exits successfully when the ZIP path is empty/missing; otherwise `curl -s -X POST .../sendDocument` with the ZIP and caption.
* `failure`: `curl -s -X POST .../sendMessage` with a failure message.
* When `DELETE_MSG_ID` is non-empty, calls `deleteMessage`; errors are ignored with `|| true`.
**High-risk external side effect:** these calls send messages/documents and delete messages in Telegram. `curl` uses `-s` without `-f`, and the JSON response is not validated, so an HTTP/API rejection can leave the step green. The action also prints the full Telegram JSON response to the Actions log.
## `build.yml`
[Section titled “build.yml”](#buildyml)
**Trigger:** manual `workflow_dispatch` only ([source](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/.github/workflows/build.yml)). Workflow permission is `contents: read`.
### Job DAG
[Section titled “Job DAG”](#job-dag)
```
flowchart LR
setup["setup"]
rust["rust-binary"]
apk["android-apk"]
pkg["package"]
notify["notify"]
setup --> rust
setup --> apk
rust --> pkg
apk --> pkg
setup -.-> notify
pkg --> notify
```
`rust-binary` and `android-apk` run in parallel after `setup`. `package` requires both. `notify` declares `needs: [setup, package]` and `if: !cancelled()`, so it is allowed to start after a dependency failure/skipped result unless the run was cancelled.
| Job | Runner | Needs | Actual purpose |
| ------------- | --------------- | ---------------------------- | ------------------------------------------------------------------------------------------------ |
| `setup` | `ubuntu-latest` | none | optional start notification, writes Git credentials, verifies the shared tool setup can complete |
| `rust-binary` | `ubuntu-latest` | `setup` | cross-compiles and strips two arm64 Android Rust binaries |
| `android-apk` | `ubuntu-latest` | `setup` | checks out private signing material and builds two signed release APKs |
| `package` | `ubuntu-latest` | `rust-binary`, `android-apk` | downloads both artifact sets and builds the flashable ZIP |
| `notify` | `ubuntu-latest` | `setup`, `package` | sends success ZIP or failure notification when a start message exists |
### Steps per job
[Section titled “Steps per job”](#steps-per-job)
#### `setup`
[Section titled “setup”](#setup)
1. `actions/checkout@v7` with full history (`fetch-depth: 0`). This full history is local to `setup`; jobs run on separate runners and do not inherit its checkout or credential file.
2. If `BOT_TOKEN` is non-empty, invoke `telegram-notify` with `type: start`; its output becomes `notify_message_id`.
3. Execute `git config --global credential.helper store`, then write `https://pavelc4:${GH_PAT}@github.com` to `~/.git-credentials`.
4. Execute all `setup-tools` steps listed above.
The credential file contains `GH_PAT` in plaintext for the lifetime of the hosted runner. This is a high-risk credential side effect outside the repository checkout.
#### `rust-binary`
[Section titled “rust-binary”](#rust-binary)
1. Checkout source with the action default (the workflow does not set `fetch-depth`, so the runner receives a shallow checkout).
2. Execute `setup-tools`.
3. Restore/save `~/.cargo/registry` and `~/.cargo/git` with exact key `cargo-${hashFiles('Cargo.lock')}` and fallback prefix `cargo-`.
4. Set `TARGET=aarch64-linux-android`.
5. Replace `/opt/android-ndk` in `.cargo/config.toml` with `${ANDROID_NDK_HOME}`.
6. Run `cargo ndk -t aarch64-linux-android --platform 26 -- build --release --bin auriya --bin auriyactl`.
7. Strip both binaries with NDK `llvm-strip` when that file exists; otherwise use host `strip`.
8. Generate separate `.sha256` files with `sha256sum`.
9. Upload artifact `rust-binary` from `target/aarch64-linux-android/release/auriya*`.
#### `android-apk`
[Section titled “android-apk”](#android-apk)
1. Checkout Auriya and execute `setup-tools`.
2. Checkout private repository `pavelc4/keystores` into `keystores-private` using `KEYSTORES_SSH_KEY` as the checkout token.
3. Generate `android/signing.properties` containing `KEYSTORE_PATH`, `KEYSTORE_PASSWORD`, `KEY_ALIAS`, and `KEY_PASSWORD` from secrets.
4. From `android/`, run `chmod +x gradlew` and `./gradlew :app:assembleRelease :service:assembleRelease`.
5. Upload artifact `android-apks` from both modules’ `build/outputs/apk/release/*.apk` paths.
#### `package`
[Section titled “package”](#package)
1. Checkout source with the action default (shallow checkout).
2. Download `rust-binary` into `target/aarch64-linux-android/release/`.
3. Download `android-apks` into the workspace root.
4. Run `package-module` with `build_type: debug`.
5. Upload `build/release/*.zip` as artifact `auriya-aarch64` with artifact compression disabled (`compression-level: 0`) because the file is already ZIP-compressed.
#### `notify`
[Section titled “notify”](#notify)
1. Checkout source.
2. Only when `package.result == 'success'`, download `auriya-aarch64` into `build/release/`.
3. Only when package succeeded and `notify_message_id` is non-empty, invoke `telegram-notify` with `type: complete`, attach the ZIP, and request deletion of the start message.
4. Only when package did not succeed and `notify_message_id` is non-empty, invoke it with `type: failure` and request deletion of the start message.
If the initial Telegram call returned no parsed message ID, neither final notification step runs.
### Artifacts produced
[Section titled “Artifacts produced”](#artifacts-produced)
| Name | Source path | Destination |
| ---------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `rust-binary` | `target/aarch64-linux-android/release/auriya*` | GitHub Actions artifact store; consumed by `package` |
| `android-apks` | Android app/service release APK output directories | GitHub Actions artifact store; consumed by `package` |
| `auriya-aarch64` | `build/release/*.zip` | GitHub Actions artifact store; downloaded by `notify` and optionally uploaded to Telegram |
### Secrets and environment
[Section titled “Secrets and environment”](#secrets-and-environment)
Secrets: `BOT_TOKEN`, `CHAT_ID`, `GH_PAT`, `KEYSTORES_SSH_KEY`, `KEYSTORE_PASSWORD`, `KEYSTORE_ALIAS`, `KEYSTORE_KEY_PASSWORD`. Environment/context affecting commands: `CARGO_TERM_COLOR`, `ANDROID_NDK_HOME`, `HOME`, `github.workspace`, run/commit/repository URLs.
### Failure behavior
[Section titled “Failure behavior”](#failure-behavior)
* Failure/cancellation of `setup` prevents both build jobs from starting.
* Failure of either parallel build job prevents `package` under the default success condition.
* Missing daemon binary fails packaging; missing CLI/APKs and missing default TOML copies do not.
* `notify` runs after non-cancellation dependency failure because of `if: !cancelled()`, then selects success/failure behavior from `package.result`.
* Telegram HTTP/API failure may remain green as described above.
* There is no workflow tied to pull requests, so this file alone does not block merges unless repository rules invoke it manually or require an external check not present here.
## `release.yml`
[Section titled “release.yml”](#releaseyml)
**Trigger:** a pushed tag matching `v*`, or manual `workflow_dispatch`. Permission is `contents: write` ([source](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/.github/workflows/release.yml)).
### Job DAG
[Section titled “Job DAG”](#job-dag-1)
```
flowchart LR
setup["setup"]
rust["rust-binary"]
apk["android-apk"]
pkg["package"]
rel["release"]
setup --> rust
setup --> apk
rust --> pkg
apk --> pkg
setup -.-> rel
pkg --> rel
```
The first four jobs execute the same commands and dependencies as `build.yml`, except packaging receives `build_type: release`. The final job is named `release`, needs `setup` and `package`, and uses `if: !cancelled()`.
| Job | Runner | Needs | Actual purpose |
| ------------- | --------------- | ------------------ | ----------------------------------------------------------------- |
| `setup` | `ubuntu-latest` | none | start notification, credential setup, tool installation |
| `rust-binary` | `ubuntu-latest` | `setup` | build/strip/checksum arm64 Rust binaries |
| `android-apk` | `ubuntu-latest` | `setup` | build signed release APKs |
| `package` | `ubuntu-latest` | both build jobs | build release-labelled module ZIP |
| `release` | `ubuntu-latest` | `setup`, `package` | publish GitHub Release asset, push `update.json`, notify Telegram |
### Steps per job
[Section titled “Steps per job”](#steps-per-job-1)
`setup`, `rust-binary`, and `android-apk` execute the same ordered commands documented for `build.yml`. `package` also matches except `package-module` receives `build_type: release` and exports `zip_name`, `version`, and `version_code`.
#### `release`
[Section titled “release”](#release)
1. Checkout full history and download `auriya-aarch64` into `build/release/`.
2. Resolve tag: for a tag-triggered run, write `github.ref_name`; for manual dispatch, require a non-empty packaged Cargo version and synthesize `v`.
3. **High-risk publish:** `softprops/action-gh-release@v3` creates/updates that GitHub Release and uploads the exact packaged ZIP using `GITHUB_TOKEN`.
4. Build variables from package outputs. Extract up to 20 non-empty lines from the first version section of `CHANGELOG.md` through `awk`, multiple `sed` filters, `head`, and `jq`; the resulting `CHANGELOG` variable is calculated but never inserted into `update.json`.
5. Overwrite `update.json` with version, numeric commit-count versionCode, release asset URL, and the raw `main/CHANGELOG.md` URL.
6. Configure the bot identity, stage `update.json`, commit it (commit failure is suppressed with `|| echo "No changes"`), then **high-risk publish** with `git push origin HEAD:main`.
7. On normal step success and non-empty initial message ID, send the ZIP to Telegram with a custom release caption and delete the start message.
8. If an earlier step in this job failed and the message ID is non-empty, send the failure message and delete the start message.
### Artifacts produced
[Section titled “Artifacts produced”](#artifacts-produced-1)
The intermediate Actions artifacts are identical to `build.yml`. The final ZIP is additionally uploaded as a GitHub Release asset under the resolved tag. `update.json` is committed and pushed directly to branch `main`. Telegram receives the same ZIP only when the initial notification produced a message ID.
### Secrets and environment
[Section titled “Secrets and environment”](#secrets-and-environment-1)
All `build.yml` secrets plus `GITHUB_TOKEN`. `contents: write` is required for the release asset and repository push. Cache paths and key are identical to `build.yml`.
### Failure behavior
[Section titled “Failure behavior”](#failure-behavior-1)
* Setup/build/package blocking behavior matches `build.yml`.
* Because `release` uses `if: !cancelled()`, a failed/skipped package can still start the job; artifact download or empty outputs then fail it.
* Tag resolution fails only for manual dispatch with an empty package version.
* Release upload failure blocks `update.json`, success notification, and produces the conditional failure notification when a message ID exists.
* `git commit` failure is suppressed, but `git push` failure is not and makes the job red.
* Telegram API rejection can remain green because the composite action does not validate it.
* A successful run changes external state: GitHub Release assets, branch `main`, `update.json`, and Telegram messages/documents. Review those steps before modifying permissions, tags, artifact names, or notification inputs.
## Reproduction checklist
[Section titled “Reproduction checklist”](#reproduction-checklist)
There are no matrix combinations. The only compiled target/configuration is `aarch64-linux-android`, Android API 26, Rust `--release`, and Gradle `assembleRelease` for `app` plus `service`. The workflow’s `package` checkout does not request full history; with the default shallow checkout, `git rev-list --count HEAD` normally produces the shallow history count (typically `1`), not the repository’s lifetime commit count. A manual reproduction that needs a full historical commit count must explicitly use `git fetch --unshallow` before running `package-module`, which will change the generated `versionCode` and ZIP filename from the workflow result.
# Contributing
Thank you for your interest in contributing to Auriya! Contributions of all kinds are welcome — from fixing bugs and optimizing code to improving documentation or adding game profile presets.
## How to Contribute
[Section titled “How to Contribute”](#how-to-contribute)
1. **Fork & Branch**: Fork the [Auriya repository](https://github.com/pavelc4/auriya) and create your feature branch:
```bash
git checkout -b feat/my-new-feature
```
2. **Make Changes**: Implement your changes cleanly and write tests if applicable.
3. **Validate**: Make sure tests and linter checks pass locally.
4. **Submit a PR**: Open a Pull Request on GitHub with a clear description of your changes.
## Validate Before Submitting
[Section titled “Validate Before Submitting”](#validate-before-submitting)
Run the checks corresponding to what you modified:
```bash
# Rust Daemon & CLI
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test
# Android Manager & Companion
cd android && ./gradlew test
```
## Coding Conventions
[Section titled “Coding Conventions”](#coding-conventions)
* **Rust**: Default `rustfmt` formatting (4-space indentation), `snake_case` for functions/modules, `CamelCase` for types, `SCREAMING_SNAKE_CASE` for constants.
* **Kotlin**: 4-space indentation, `PascalCase` for composables/classes, `camelCase` for functions/members.
* **Commits**: Clear and concise commit messages, optionally following Conventional Commits (e.g. `feat(daemon): ...`, `fix(app): ...`, `docs: ...`).
## Security & Privacy
[Section titled “Security & Privacy”](#security--privacy)
Treat root permissions, sysfs nodes, and shell operations as trust boundaries. Never commit private credentials, personal signing keys, or device-specific sensitive paths.
# Debugging
A boundary-first playbook. Auriya spans five boundaries — Android UI, companion service, the Unix socket, daemon subsystems, and kernel nodes — and a symptom usually points at one. Identify the boundary, then use the matching tool below.
## Logs first
[Section titled “Logs first”](#logs-first)
| Log | Path | What it holds |
| --------- | -------------------------------------------- | -------------------------------------------------------------- |
| Daemon | `/data/adb/auriya/daemon.log` (+ `.1`, `.2`) | Daemon stdout/stderr, tee’d by `service.sh`. |
| Companion | `/data/adb/auriya/companion.log` (+ `.1`) | `AuriyaSysMon` output. |
| Restart | `/data/adb/auriya/restart.log` | Output of `service.sh` when relaunched by `auriyactl restart`. |
| logcat | `logcat -s auriya` | The daemon and scripts also log under the `auriya` tag. |
Paths are from the [Filesystem reference](/reference/filesystem/#logs--dataadbauriya).
## Adjust log verbosity
[Section titled “Adjust log verbosity”](#adjust-log-verbosity)
The daemon’s level comes from `settings.daemon.log_level` at startup ([settings](/reference/settings/#daemon)). To change it **at runtime without a restart**, use IPC (this is the only settings value with a live runtime toggle):
```bash
auriyactl set-log debug # or info | warn | error
```
`log_level` is **not** re-read on config reload — `set-log` is the live path ([settings → reload behavior](/reference/settings/#reload-behavior)).
## Boundary-by-boundary
[Section titled “Boundary-by-boundary”](#boundary-by-boundary)
### Is the daemon even up?
[Section titled “Is the daemon even up?”](#is-the-daemon-even-up)
```bash
auriyactl ping # → "Daemon is alive (PONG)"
auriyactl status # → status block, or "Daemon: Not running"
```
`Not running` → the boot sequence failed. Check `daemon.log` and `companion.log`. Common causes are enumerated by `service.sh`’s own error messages (missing binary, missing companion APK, missing config, or the companion not producing `system_status` within 10 s).
### Socket / IPC
[Section titled “Socket / IPC”](#socket--ipc)
Talk to the socket directly to see the **raw** protocol (the CLI shows only a subset of `STATUS`):
```bash
printf 'STATUS\nQUIT\n' | nc -U /dev/socket/auriya.sock
```
Full command and response reference: [IPC protocol](/internals/ipc-protocol/). `ERR …` replies are documented there.
### Foreground / game detection
[Section titled “Foreground / game detection”](#foreground--game-detection)
If a game is not being picked up, inspect the companion snapshot and the daemon’s resolved state:
```bash
cat /data/adb/.config/auriya/system_status # focused_app/pid, screen, battery, zen
auriyactl get-pid # daemon's resolved PKG/PID
auriyactl inject com.your.game # force a package (debug); clear-inject to undo
```
See [Game detection](/internals/game-detection/). If `system_status` is stale or empty, the problem is the **companion**, not the daemon.
### Profile not applying / wrong profile
[Section titled “Profile not applying / wrong profile”](#profile-not-applying--wrong-profile)
* Confirm the package is whitelisted (`auriyactl list-games`).
* Remember the [decision order](/internals/profile-scheduler/#decision-order): screen-off/battery-saver wins over everything; a per-game `mode` typo silently resolves to Performance.
* The daemon only writes when the target profile differs from the current one ([idempotence guard](/internals/profile-scheduler/#the-idempotence-guard)) — so “nothing happened” can be correct.
* Check `/data/adb/.config/auriya/current_profile` (`1`/`2`/`3`) for the last-applied profile.
### Kernel node / tweak not taking effect
[Section titled “Kernel node / tweak not taking effect”](#kernel-node--tweak-not-taking-effect)
Tweaks are **best-effort**: a missing node is skipped silently ([System tweaks](/internals/system-tweaks/#guarded-best-effort-writes)). Raise the log level to `debug` to see which paths were found and written. If a vendor service is fighting Auriya, that is what [vendor lock](/internals/system-tweaks/#vendor-lock--stopping-vendor-services-from-fighting-back) addresses — verify the relevant `VENDOR_PATHS` node exists on your device.
## Restarting cleanly
[Section titled “Restarting cleanly”](#restarting-cleanly)
```bash
auriyactl restart # kills daemon+companion, clears socket/status/lock, re-runs service.sh
```
This is a **local** operation (not the IPC `RESTART`); it needs root and an installed device. See [Command reference](/reference/commands/#restart-is-local-not-an-ipc-command).
## Reporting an issue
[Section titled “Reporting an issue”](#reporting-an-issue)
Capture: the command and its response, the active package, root manager, ROM, kernel version, and the relevant `settings.toml` / `gamelist.toml` — **without** publishing private device identifiers. Root, `/proc`/`/sys` writes, the socket, and shell commands are trust boundaries; scrub sensitive identifiers and logs accordingly.
# Project Structure
```text
auriya/
├── src/ [Rust] daemon library and binaries
│ ├── main.rs [Rust] daemon entry point
│ ├── ctl.rs [Rust] auriyactl entry point
│ ├── cli/ [Rust] CLI parser/client/output
│ ├── daemon/ [Rust] tick loop, state, watchers, IPC
│ ├── core/ [Rust] config, telemetry, FPS/FAS, tweaks
│ └── common/ [Rust] constants and shared types
├── android/ [Android/Gradle]
│ ├── app/ [Android] Compose manager UI
│ ├── service/ [Android] headless companion service
│ └── shared/ [Android] models and TOML/status codecs
├── module/ [Module] bundled root-module payload source
│ ├── customize.sh [Module] install, verify, and copy payload
│ ├── service.sh [Module] boot startup
│ ├── uninstall.sh [Module] cleanup
│ ├── module.prop [Module] module metadata
│ └── META-INF/ [Module] recovery installer entry points
├── .github/
│ ├── workflows/ [CI/CD] build and release pipelines
│ └── actions/ [CI/CD] setup, package, notifications
├── Cargo.toml [Rust] dependencies and binary targets
├── settings.toml [Config] bundled default settings
├── gamelist.toml [Config] bundled default game profiles
└── update.json [Release] root-manager update metadata
```
Root files include `Cargo.toml`/`Cargo.lock`, `settings.toml`, `gamelist.toml`, `update.json`, `README.md`, and `CHANGELOG.md`. `.github/actions/` contains reusable setup, packaging, and Telegram notification actions; `.github/workflows/build.yml` is manual artifact CI and `release.yml` is tag/manual release CI. `module/` contains Magisk/KernelSU/APatch metadata and lifecycle scripts.
The Android tree has three Gradle modules: `app` (UI), `service` (background integration), and `shared` (models/parsers). Generated `android/shared/bin/` mirrors shared Kotlin declarations for tooling and is not the source of truth.
## Bundled ZIP preview
[Section titled “Bundled ZIP preview”](#bundled-zip-preview)
```text
auriya----.zip
├── customize.sh
├── service.sh
├── uninstall.sh
├── module.prop
├── settings.toml
├── gamelist.toml
├── libs/
│ ├── aarch64/
│ │ ├── auriya [Rust daemon]
│ │ ├── auriyactl [Rust CLI]
│ │ └── checksums.sha256
│ └── companion/
│ ├── service.apk [Android companion]
│ └── auriya-app.apk [Android Compose app]
└── META-INF/com/google/android/
├── update-binary
└── updater-script
```
During installation, `libs/` is only a staging area. `customize.sh` copies binaries/APKs to their runtime paths, moves the TOML defaults to `/data/adb/.config/auriya` when no user config exists, then removes `libs/`.
# Configuration
Tip
**You do not need to edit any files by hand.** The Auriya manager app is the intended way to change every setting global behavior and per-game overrides alike. Open the app, change what you want, and it writes the config for you; the daemon picks it up automatically. The file details below are for understanding and power-user fallback only a normal setup never touches a terminal or a text editor. For which values to choose, see [Performance tuning](/getting-started/performance-tuning/).
Under the hood Auriya reads two TOML files, both under `/data/adb/.config/auriya/` (the app writes these — you don’t have to):
| File | Scope | Full reference |
| --------------- | ---------------------------------------- | ----------------------------------------------- |
| `settings.toml` | **Global** daemon and scheduler defaults | [settings.toml reference](/reference/settings/) |
| `gamelist.toml` | **Per-app** whitelist and overrides | [gamelist.toml reference](/reference/gamelist/) |
This page is the orientation; the reference pages are the source of truth for every key (type, default, whether the daemon actually consumes it, and evidence).
## Dynamic Reload & Directory Watcher
[Section titled “Dynamic Reload & Directory Watcher”](#dynamic-reload--directory-watcher)
Auriya features a built-in, low-overhead background **Inotify Directory Watcher** running on a dedicated thread (`auriya-config-watcher`) that monitors `/data/adb/.config/auriya/`:
* **Atomic File Detection** — Captures both standard `Modify` events and atomic rename `Create` events (e.g. write-to-temp-then-rename workflows used by Android file writers and editors).
* **Settings Dynamic Reload** — Modifications to `settings.toml` immediately trigger `daemon.reload_settings()` and an instant scheduler tick. Runtime keys like `cpu.default_governor`, `daemon.default_mode`, and `daemon.check_interval_ms` apply on the fly without restarting the daemon.
* **Resilient Gamelist Reload** — When `gamelist.toml` changes, the watcher safely loads the new configuration into the daemon’s atomic shared memory (`Arc>>`) with a retry loop (up to 3 retries with backoff) to guard against partial in-flight writes, rebuilds the active process whitelist, and wakes the scheduling loop immediately.
## How edits reach the daemon
[Section titled “How edits reach the daemon”](#how-edits-reach-the-daemon)
* **From the manager app (recommended)** — The app writes both files, and for the game list the daemon also rewrites it in response to app commands. The directory watcher detects these updates instantly. This is the primary, supported path and covers every setting.
* **From the CLI** — `auriyactl` mutates the game list over IPC (`add-game`, `remove-game`, and raw `UPDATE_GAME`) and can trigger a settings reload with `auriyactl reload`. It has **no** command to edit individual `settings.toml` keys. See [Command reference](/reference/commands/).
* **By hand (fallback)** — You *can* edit the files directly with a root text editor. The directory watcher will automatically pick up your edits and reload them live; running `auriyactl reload` is also available as an explicit trigger.
## Two things to know before editing
[Section titled “Two things to know before editing”](#two-things-to-know-before-editing)
1. **Live configuration updates.** `cpu.default_governor`, `daemon.default_mode`, `daemon.check_interval_ms`, and the FAS block (`[fas]`, `[dynamic_governor]`, `[modes.*]`) are re-read live when `settings.toml` is modified and updated directly via `FasController::set_tuning`. The directory watcher safely captures atomic writes.
2. **`fas.default_mode` picks the active `[modes.*]`.** Only the mode it names drives FAS margin/thermal; the other `[modes.*]` blocks are inactive until selected. See [settings → `[modes.*]`](../reference/settings#modes).
## Invalid values
[Section titled “Invalid values”](#invalid-values)
There is no `deny_unknown_fields`, so unknown keys are **silently ignored** and some fields fall back to defaults rather than erroring (e.g. an unknown game `mode` resolves to Performance, an unparseable `ceiling` is dropped). A malformed `settings.toml` **aborts daemon startup**; a malformed `gamelist.toml` does too, but a *missing* game list is treated as empty. Details in the reference pages.
## Next
[Section titled “Next”](#next)
[Performance tuning](/getting-started/performance-tuning/) — which values to choose · [settings.toml reference](/reference/settings/) · [gamelist.toml reference](/reference/gamelist/) · [Profile scheduler](/internals/profile-scheduler/).
# First Run
After installing and rebooting, open **Auriya** from the launcher. This page covers what happens the first time — and what is verifiable from the daemon architecture versus the app UI.
Note
The manager-app onboarding (theme, navigation style, “setup complete” flag) is UI behavior in `android/app` and is not source-traced here. What *is* verifiable is the runtime contract below: the app is a client of the daemon and needs root to be useful.
## What the first run establishes
[Section titled “What the first run establishes”](#what-the-first-run-establishes)
1. **Root authorization.** The manager controls the daemon over the Unix socket `/dev/socket/auriya.sock` and reads/writes config under `/data/adb/.config/auriya` — all root-only paths. Without root, the app cannot query daemon status or change configuration, so onboarding cannot complete meaningfully.
2. **The daemon is already running.** Unlike many modules, Auriya’s daemon is **not** started by the app — it is launched at boot by `module/service.sh` (see [Installation → After reboot](/getting-started/installation/#after-reboot)). By the time you open the app, the daemon and companion should already be up.
3. **Appearance/onboarding preferences** are stored by the app for subsequent launches.
## Verifying it works
[Section titled “Verifying it works”](#verifying-it-works)
If you have the CLI installed, the fastest check is over IPC:
```console
$ auriyactl ping
Daemon is alive (PONG)
$ auriyactl status
Auriya Daemon Status
Daemon: Running
Enabled: true
Games: 3 configured
FPS: 59.8 SOURCE=ebpf
```
`Daemon: Not running` means the boot sequence failed — check `/data/adb/auriya/daemon.log` and `companion.log` ([Debugging](/development/debugging/)). See the full command set in [Command reference](/reference/commands/).
## Next
[Section titled “Next”](#next)
* [Configuration](/getting-started/configuration/) — tune global and per-app behavior.
* [Architecture overview](/architecture/overview/) — how the pieces fit.
# Installation
Auriya ships as a single flashable module ZIP. The ZIP contains **everything** — the daemon, the CLI, both APKs, and default config — so nothing is downloaded at boot. See [Module lifecycle](/architecture/module-lifecycle/) for the full packaging story.
Install behavior traced to [`module/customize.sh`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/module/customize.sh) at commit `10fe7c6`.
## Steps
[Section titled “Steps”](#steps)
1. **Download** the current `auriya-*.zip` from the [releases page](https://github.com/pavelc4/auriya/releases). You do **not** need a separate APK download — the manager app is bundled inside the ZIP.
2. **Flash** the ZIP through your root manager (Magisk / KernelSU / APatch).
3. **Watch the installer output.** `customize.sh` prints device info and a step-by-step log (see below). If it aborts, the message says why.
4. **Reboot** when prompted.
5. **Open Auriya** from the launcher and grant root when asked (see [First run](/getting-started/first-run/)).
## What the installer actually does
[Section titled “What the installer actually does”](#what-the-installer-actually-does)
`customize.sh` runs these checks and actions, in order:
1. **Architecture gate** — if `$ARCH` is not `arm64`, it prints “Unsupported architecture” and aborts. Only aarch64 is shipped.
2. **Extract** the ZIP into `/data/adb/modules/auriya`.
3. **Integrity check** — verifies the daemon binary’s SHA-256 against the bundled `checksums.sha256`; a mismatch **aborts** the install. The CLI checksum is checked too, but a CLI mismatch only downgrades to daemon-only mode.
4. **Install binaries** — copies the daemon to `/data/adb/modules/auriya/system/bin/auriya` (`0755`), and `auriyactl` if present.
5. **Install the companion** — copies `service.apk` to `system/etc/auriya/service.apk`. This is **required**; a missing companion APK aborts the install.
6. **Install the manager app** — `pm install -r -g` the bundled `auriya-app.apk` (`dev.auriya.app`). This is **best-effort**: if `pm install` fails or the APK is not bundled, it prints a manual `adb install` hint and continues (the daemon does not depend on the UI app).
7. **Seed config** — moves `settings.toml` / `gamelist.toml` into `/data/adb/.config/auriya/` **only if you have no existing config** there, so a reinstall never overwrites your settings.
8. **Root-manager symlinks** — links the binaries into `/data/adb/ksu/bin` (KernelSU) or `/data/adb/ap/bin` (APatch) when those directories exist, so `auriya`/`auriyactl` are on `PATH`. Magisk needs no symlink.
Exact runtime and staging paths are in the [Filesystem reference](/reference/filesystem/).
## After reboot
[Section titled “After reboot”](#after-reboot)
The module does **not** launch the daemon from the app. At boot, `module/service.sh` (via the root manager’s `service.d` hook) waits for `sys.boot_completed`, launches the companion with `app_process`, waits for its status file, then starts the daemon with explicit `--settings` / `--gamelist` paths. See [Architecture overview → Binary execution workflow](/architecture/overview/#binary-execution-workflow).
If something is wrong, the daemon and companion logs are under `/data/adb/auriya/` (`daemon.log`, `companion.log`) — see [Debugging](/development/debugging/).
## Next
[Section titled “Next”](#next)
[First run](/getting-started/first-run/) · [Configuration](/getting-started/configuration/).
# Performance Tuning
How to pick Auriya’s settings and what values to use — the “which knob, which value, and why” guide. For the exhaustive per-key spec (types, defaults, what the daemon consumes) see the [settings reference](/reference/settings/) and [gamelist reference](/reference/gamelist/); this page is the practical layer on top.
Tip
**Every setting on this page is configurable from the Auriya manager app.** You do **not** need to open a terminal or hand-edit `settings.toml` / `gamelist.toml`. The app writes both files for you and the daemon picks the changes up. Manual file editing exists (see [Configuration](/getting-started/configuration/)) but is a fallback for power users, not the intended workflow.
## The two levels of configuration
[Section titled “The two levels of configuration”](#the-two-levels-of-configuration)
| Level | Sets | Edit in the app under |
| ------------------------------ | ---------------------------------------------------------- | ------------------------------------ |
| **Global** (`settings.toml`) | daemon defaults, FAS behavior, thermal ceilings | Settings / Config screen |
| **Per-game** (`gamelist.toml`) | a game’s governor, target FPS, refresh rate, mode, ceiling | the game’s entry on the Games screen |
Per-game overrides win while that game is foreground; global values apply everywhere else.
## FAS: what it is and how to set it
[Section titled “FAS: what it is and how to set it”](#fas-what-it-is-and-how-to-set-it)
Frame-Aware Scheduling (FAS) watches real frame timing and nudges CPU/GPU up or down to hold your target FPS with the least power. It only runs for **whitelisted games** and only when the eBPF frame probe is available (see [FPS detection](/internals/fps-detection/) and [Kala eBPF frame probe](/internals/kala-research/)).
### Enabling FAS
[Section titled “Enabling FAS”](#enabling-fas)
`[fas] enabled = true` (default). If the device kernel can’t load the eBPF probe, the daemon automatically falls back to sysfs-only FPS and disables FAS — nothing to configure.
### FAS modes (the `margin` knob)
[Section titled “FAS modes (the margin knob)”](#fas-modes-the-margin-knob)
FAS behavior is chosen by **modes**. Each mode is a `margin` (FPS headroom) plus a `thermal_threshold`. `fas.default_mode` selects which mode is active. **Smaller margin = more aggressive** (pushes clocks harder to stay glued to the target); **larger margin = calmer** (tolerates dropping a little below target to save power/heat).
| Mode | `margin` | `thermal_threshold` | Feel |
| ------------- | -------- | ------------------- | -------------------------------------------------------- |
| `powersave` | 5.0 | 80 °C | Coolest/most battery; lets FPS sag furthest below target |
| `balance` | 2.0 | 90 °C | **Default** — good FPS, sensible heat |
| `performance` | 1.0 | 95 °C | Chases the target tightly |
| `fast` | 0.0 | 95 °C | Zero headroom — hugs the frame deadline hardest |
These are the shipped values and match the upstream [fas-rs](https://github.com/shadow3aaa/fas-rs) presets Auriya’s controller is adapted from. `fast` is a **FAS margin preset**, not a separate CPU profile — don’t confuse it with the three profile modes below.
### Recommended settings by goal
[Section titled “Recommended settings by goal”](#recommended-settings-by-goal)
| Your goal | `fas.default_mode` | Notes |
| ------------------------------------ | ------------------ | ------------------------------------------------------ |
| Balanced daily driver | `balance` | Leave everything default. |
| Max smoothness (comp games) | `performance` | Tighter frame pacing; more heat/battery. |
| Absolute lowest latency | `fast` | Only if your device stays cool enough (95 °C ceiling). |
| Long sessions / hot device / battery | `powersave` | Accepts minor FPS dips to run cool. |
`[dynamic_governor]` (defaults `cv_threshold = 0.15`, `debounce_frames = 3`) tunes how FAS decides CPU-bound vs GPU-bound. **Leave these at defaults** unless you are diagnosing a specific bottleneck-misclassification — they are advanced knobs, not everyday settings.
Note
Changes to `[fas]`, `[dynamic_governor]`, and `[modes.*]` are read when the daemon starts. After changing them, restart the daemon (the app does this for you; from a shell it’s `auriyactl restart`). `cpu.default_governor` and `daemon.default_mode` apply live. See [settings → reload behavior](/reference/settings/#reload-behavior).
## Per-game tuning
[Section titled “Per-game tuning”](#per-game-tuning)
On the Games screen, each whitelisted game can override:
| Field | What it does | Typical value |
| -------------- | ---------------------------------------------------------------- | ------------------------------------------- |
| `target_fps` | FAS target; single (`120`) or steps (`[60,90,120]`) | your game’s cap, e.g. `120` |
| `cpu_governor` | governor while this game runs | `performance` or `walt` |
| `mode` | static profile: `powersave` / `balance` / `performance` / `fast` | `performance` or `fast` for demanding games |
| `refresh_rate` | requested display Hz | match `target_fps` |
| `ceiling` | frequency-ceiling level | leave default unless throttling |
| `enable_dnd` | Do-Not-Disturb while playing | `true` for focus |
**Recommended per-game starting point for a demanding game:** `mode = performance` (or `fast`), `target_fps` = the game’s real cap, `refresh_rate` = same, `cpu_governor = performance`. Tune down toward `balance` if the device runs hot.
`target_fps` as an **array** (`[60, 90, 120]`) lets FAS match whichever rate the game actually renders at — useful for games with in-menu vs in-match rate changes.
## Profile modes and FAS tuning
[Section titled “Profile modes and FAS tuning”](#profile-modes-and-fas-tuning)
* **Profile modes** — `powersave` / `balance` / `performance` / `fast` (4). These set CPU governor, GPU mode, and tweaks. Chosen per-game via `mode`, or globally via `daemon.default_mode`. What each writes: [overview → static profiles](/architecture/overview/#what-each-static-profile-changes).
* **FAS tuning presets** — `powersave` / `balance` / `performance` / `fast` (4). These are `margin` + `thermal_threshold` presets for the FAS controller, chosen via `fas.default_mode` or per-profile tuning.
Profile modes decide the baseline CPU governor and kernel behavior; FAS presets decide how aggressively FAS chases the frame target on top of it.
## See also
[Section titled “See also”](#see-also)
* [settings.toml reference](/reference/settings/) — every global key.
* [gamelist.toml reference](/reference/gamelist/) — every per-game field.
* [Profile scheduler](/internals/profile-scheduler/) — how a profile is chosen each tick.
# Requirements
What a device needs before installing Auriya. Where a requirement comes from source, it is cited.
## Hardware and OS
[Section titled “Hardware and OS”](#hardware-and-os)
| Requirement | Detail | Source |
| ---------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Root manager | Magisk, KernelSU, or APatch | `module/customize.sh` detects `KSU`/APatch and links binaries accordingly |
| CPU architecture | **`arm64-v8a` (aarch64) only** | `customize.sh` aborts on any other `$ARCH`; the module ships only aarch64 binaries |
| Android version | **11 or newer** (`minSdk = 30`) | `android/app/build.gradle.kts:54`, `android/service/build.gradle.kts:54` |
| Root permission | The manager and daemon require root to read/write `/proc` and `/sys` and to bind the daemon socket | see [System tweaks](/internals/system-tweaks/), [IPC protocol](/internals/ipc-protocol/) |
## Kernel features (for Frame-Aware Scheduling)
[Section titled “Kernel features (for Frame-Aware Scheduling)”](#kernel-features-for-frame-aware-scheduling)
The base daemon runs on any supported device, but **FAS is optional and capability-gated**:
* FAS uses an eBPF uprobe (Kala) that needs a kernel with **uprobe + ring-buffer support (5.8+, tested on 5.10)**, root or `CAP_SYS_ADMIN` + `CAP_BPF`, and a real Android image containing `/system/lib64/libgui.so` ([Kala eBPF frame probe → Auriya integration](/internals/kala-research/#auriya-integration)).
* If any of that is missing, the daemon **continues with sysfs-only FPS and FAS disabled** — it does not fail to start ([FPS detection](/internals/fps-detection/)).
So: an older-kernel device still runs Auriya’s static profiles and tweaks; only the adaptive frame-aware layer is unavailable.
## Trust boundary
[Section titled “Trust boundary”](#trust-boundary)
Auriya runs as root and writes kernel nodes, binds a Unix socket, mounts over vendor nodes ([vendor lock](/internals/system-tweaks/#vendor-lock--stopping-vendor-services-from-fighting-back)), and installs packages. Review the module source and your [`settings.toml`](/reference/settings/) / [`gamelist.toml`](/reference/gamelist/) before installing. Device-specific `/proc` and `/sys` writes are best-effort and skipped when a node is absent, but they are still privileged operations.
## Next
[Section titled “Next”](#next)
[Installation](/getting-started/installation/).
# Uninstall
Uninstall Auriya through your root manager’s module list (Magisk / KernelSU / APatch) — flag the module for removal and reboot. The module’s `uninstall.sh` does the cleanup.
Traced to [`module/uninstall.sh`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/module/uninstall.sh) at commit `10fe7c6`.
## What `uninstall.sh` does, in order
[Section titled “What uninstall.sh does, in order”](#what-uninstallsh-does-in-order)
1. **Stops the daemon** — `SIGTERM` `auriya`, wait up to 5 s, then `SIGKILL`.
2. **Stops the companion** — the same TERM→KILL sequence for the `AuriyaSysMon` process.
3. **Force-stops and uninstalls the packages** — `am force-stop` then `pm uninstall` for `dev.auriya.app`, `dev.auriya.app.debug`, and `dev.auriya.service`. Each uninstall is retried up to 3 times with a 15 s timeout, because `pm uninstall` can hang.
4. **Deletes runtime data**:
* `/dev/socket/auriya.sock`
* `/data/adb/.config/auriya` — **all config**, including your `settings.toml` and `gamelist.toml`
* `/data/adb/auriya` — all logs
* the KernelSU/APatch symlinks (`/data/adb/ksu/bin/*`, `/data/adb/ap/bin/*`)
5. **Countdown** — a short “Do not reboot” countdown so Android can finish the package removals, then “Safe to reboot”.
The module directory itself (`/data/adb/modules/auriya`) is removed by the root manager after reboot.
Step 4 removes `/data/adb/.config/auriya` entirely. If you want to keep your `settings.toml` / `gamelist.toml`, back them up **before** uninstalling. A fresh install re-seeds the shipped defaults (see [Installation](/getting-started/installation/#what-the-installer-actually-does)).
## Do not reboot early
[Section titled “Do not reboot early”](#do-not-reboot-early)
`pm uninstall` runs asynchronously and is retried. Rebooting during the countdown can leave a package half-removed. Wait for **“Auriya uninstall complete. Safe to reboot.”** before rebooting.
## Uninstall triggered at boot
[Section titled “Uninstall triggered at boot”](#uninstall-triggered-at-boot)
`uninstall.sh` is also invoked automatically by `service.sh` if it finds a `remove` flag file for the module at boot (`module/service.sh`, `_cleanup_all`) — this is how a root-manager “remove on next boot” request is honored.
# Companion Service
The companion (`AuriyaSysMon`) is the Android half of Auriya. The root daemon cannot call Android framework APIs — it is a plain root binary, not an app — so a second process runs as an Android app with root uid to do two things the daemon can’t: **observe** Android state (foreground app, screen, battery-saver, Zen) and **actuate** framework settings (Do-Not-Disturb, refresh rate). It exchanges both with the daemon through files, never a socket.
`android/service/src/main/kotlin/dev/auriya/service/` — `Main.kt`, `sensor/`, `actuator/`, `io/`, `lock/`. Launched by `module/service.sh`.
## Why it exists
[Section titled “Why it exists”](#why-it-exists)
| Need | Daemon (root binary) | Companion (root-uid app) |
| ------------------------------------------------- | -------------------- | ------------------------ |
| Read/write `/proc`, `/sys` | Yes | — |
| Detect foreground app (TaskStack/ActivityManager) | No | Yes |
| Read screen / power-save / Zen state | No | Yes |
| Set Do-Not-Disturb, refresh rate | No | Yes |
So the daemon owns kernel-level work; the companion owns framework-level work. See [overview → runtime boundaries](/architecture/overview/#runtime-boundaries).
## Launch & single-instance lock
[Section titled “Launch & single-instance lock”](#launch--single-instance-lock)
`module/service.sh` starts it via `app_process` with `--nice-name=AuriyaSysMon`, entry point `dev.auriya.service.Main` (inherits the system uid from the root manager’s `service.d` hook). `Main.main` (`Main.kt`):
1. Acquires an exclusive `FileLock` on `companion.lock` (`lock/LockFile.kt`). If a companion is already running, it **exits** rather than fighting for the lock.
2. The lock is held for the JVM’s whole life; the OS releases it on exit/kill, so the daemon can detect a crashed companion in real time (`fcntl(F_GETLK)`), which drives the daemon’s `companion.lock` watcher and `settings put` fallback.
3. Starts sensors + the command reader, then parks on the main `Looper`.
## Architecture
[Section titled “Architecture”](#architecture)
```
flowchart LR
subgraph fw ["Android Framework"]
ts["TaskStackListener"]
am["ActivityManager"]
pm["PowerManager"]
zen["Settings.zen"]
nm["NotificationManager"]
disp["Display Refresh Rate"]
end
subgraph companion ["Companion Service (AuriyaSysMon)"]
tss["TaskStackSensor"]
ps["PowerSensor"]
zs["ZenSensor"]
sink["SensorSink
(merge + debounce 50ms)"]
agg["Aggregator"]
writer["StatusWriter
(atomic swap)"]
reader["CmdReader
(poll 500ms, seq dedup)"]
dnd["DnDActuator"]
disp_act["DisplayActuator"]
end
subgraph daemon_plane ["Rust Daemon Plane"]
sys_status[("system_status file")]
cmd_file[("auriya_cmd file")]
daemon["Rust Daemon"]
end
ts --> tss
am --> tss
pm --> ps
zen --> zs
tss --> sink
ps --> sink
zs --> sink
sink --> agg --> writer
writer --> sys_status --> daemon
daemon --> cmd_file --> reader
reader --> dnd --> nm
reader --> disp_act --> disp
```
Two independent directions: **sensors → `system_status`** (observe) and **`auriya_cmd` → actuators** (actuate). The daemon is on the other end of both files — see [Data flow](/architecture/data-flow/#the-four-channels-concretely).
## Sensors (observe → `system_status`)
[Section titled “Sensors (observe → system\_status)”](#sensors-observe--system_status)
Each sensor pushes a partial `SensorSnapshot` to a shared `SensorSink`; the `Aggregator` in `Main.kt` merges snapshots and, after a 50 ms debounce, hands the merged state to `StatusWriter`.
| Sensor | Observes | Mechanism | Cadence |
| ----------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `TaskStackSensor` | foreground `pkg`/`pid`/`uid` | Registers `ITaskStackListener` on `IActivityTaskManager` via binder reflection → **event-driven**; reads `getRunningAppProcesses` for the `IMPORTANCE_FOREGROUND` process | event + 1 s fallback poll |
| `PowerSensor` | `screen_awake`, `battery_saver` | `IPowerManager` reflection (`isInteractive`, `isPowerSaveMode`) | 1 s poll |
| `ZenSensor` | `zen_mode` | reads `Settings.Global zen_mode` | 1 s poll |
`TaskStackSensor` is the interesting one: it builds a `Proxy` implementing `ITaskStackListener` and registers it, so foreground changes arrive as callbacks (`onTaskMovedToFront`, `onTaskFocusChanged`, …) rather than polling. It requires **Android 11+** (`Build.VERSION_CODES.R`) and falls back to a 1 s poll if binder registration fails. It de-dupes by emitting only when the package changes.
The merged snapshot becomes `SystemStatus` (the exact fields are the [data model](/architecture/data-model/#systemstatus-companion--daemon)).
## Actuators (actuate ← `auriya_cmd`)
[Section titled “Actuators (actuate ← auriya\_cmd)”](#actuators-actuate--auriya_cmd)
`CmdReader` watches the daemon’s command file and dispatches each fresh `Cmd`:
| Actuator | Applies | Framework API |
| ----------------- | ----------------------------- | ----------------------------------------- |
| `DnDActuator` | `dnd` filter (All / Priority) | `NotificationManager` interruption filter |
| `DisplayActuator` | `refresh_rate` (0 = restore) | display / `Settings` via `SettingsHelper` |
## IO: crash-safe file exchange
[Section titled “IO: crash-safe file exchange”](#io-crash-safe-file-exchange)
### `StatusWriter` — atomic writes
[Section titled “StatusWriter — atomic writes”](#statuswriter--atomic-writes)
The daemon watches the parent dir for `IN_CLOSE_WRITE` and re-parses. To never expose a half-written file, `StatusWriter` writes a sibling tempfile → `fsync` → `Files.move(ATOMIC_MOVE, REPLACE_EXISTING)` (with a plain-replace fallback if the FS rejects atomic move). The daemon therefore always reads a complete snapshot. This mirrors the daemon’s own atomic-write pattern ([CmdWriter](/internals/system-tweaks/#actions-routed-through-android--cmdwriter), [gamelist save](/reference/gamelist/#persistence)).
### `CmdReader` — polling, not FileObserver
[Section titled “CmdReader — polling, not FileObserver”](#cmdreader--polling-not-fileobserver)
`CmdReader` polls `auriya_cmd` every **500 ms** and de-dupes on the command’s monotonic `seq` (a lower seq means the daemon restarted → still dispatched).
`CmdReader` **deliberately avoids** `android.os.FileObserver`: on Android 16 (API 36) the native FileObserver thread `SIGSEGV`s in `libandroid_runtime.so` when used from a headless `app_process`, taking the whole companion down. Polling at 500 ms is invisible here — the command file changes only a handful of times per day (`CmdReader.kt` doc comment).
## Liveness
[Section titled “Liveness”](#liveness)
`companion.lock` (exclusive `FileLock`, held for the JVM lifetime) is both the single-instance guard and the daemon’s liveness signal: the daemon watches it and, when the companion is considered dead, falls back to Android `settings put` for DnD / refresh rate ([overview → control paths](/architecture/overview/#control-and-status-paths)). `service.sh` restarts a dead companion; the daemon rate-limits restart attempts.
## Shared models
[Section titled “Shared models”](#shared-models)
The companion and the app share Kotlin models + codecs in `android/shared` (`SystemStatus`, `Cmd`, `StatusFormat`, `CmdFormat`) — the same `android/shared` that defines the app’s config models. Wire shapes must match the daemon’s Rust structs; see [data model → config entities](/architecture/data-model/#config-entities-rust--kotlin--must-stay-in-sync).
## See also
[Section titled “See also”](#see-also)
* [Data flow](/architecture/data-flow/) — how `system_status` / `auriya_cmd` move.
* [Game detection](/internals/game-detection/) — how the daemon consumes `focused_app`/`pid`.
* [System tweaks → CmdWriter](/internals/system-tweaks/#actions-routed-through-android--cmdwriter) — the daemon’s side of `auriya_cmd`.
# FPS Detection
Auriya reports a frames-per-second value in daemon status and, when Frame-Aware Scheduling is active, feeds frame data to the scheduler. FPS **observation** and FAS **control** are separate: this page covers observation (`src/core/fps_meter/mod.rs`); FAS is documented in [Kala eBPF frame probe](/internals/kala-research/) and [Profile scheduler](/internals/profile-scheduler/).
Traced to Auriya commit [`10fe7c6`](https://github.com/pavelc4/auriya/tree/10fe7c6b56474a00513fec34ebac1376b30e95e6), [`src/core/fps_meter/mod.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/core/fps_meter/mod.rs).
## Two sources, sysfs first
[Section titled “Two sources, sysfs first”](#two-sources-sysfs-first)
`FpsMeter::read` tries **sysfs**, and only falls back to **eBPF** frame deltas if sysfs yields nothing (`fps_meter/mod.rs`, `read()`). Sysfs is preferred because it reports the actual display-measured refresh, which is steadier than per-frame deltas under triple-buffering or vsync lock (module doc comment).
Every reading carries its origin so consumers can tell them apart:
```rust
pub enum FpsSource { Ebpf, Sysfs }
pub struct FpsReading { pub fps: f64, pub source: FpsSource }
```
```
flowchart TD
req([FPS Request]) --> check_sysfs{"Sysfs Node Available?"}
check_sysfs -->|yes| sysfs_cache{"Cache < 2s old?"}
sysfs_cache -->|yes| return_cached["Return cached sysfs reading"]
sysfs_cache -->|no| read_node["Read /sys node (f64)"]
read_node --> check_val{"0 < value ≤ 500?"}
check_val -->|yes| return_sysfs["Return FpsReading (Sysfs)"]
check_val -->|no / empty| fallback_ebpf
check_sysfs -->|no| fallback_ebpf{"eBPF Frame Stream Available?"}
fallback_ebpf -->|no| return_none["Return None"]
fallback_ebpf -->|yes| drain["Drain deltas < 500ms into 30-frame ring"]
drain --> check_ring{"Frames in last 3s?"}
check_ring -->|no| return_none
check_ring -->|yes| calc["Calculate 1.0 / mean(frametimes)"]
calc --> clamp{"0 < FPS ≤ 500?"}
clamp -->|yes| return_ebpf["Return FpsReading (Ebpf)"]
clamp -->|no| return_none
```
In `STATUS` this surfaces as `FPS= SOURCE=` (see [IPC protocol → STATUS](/internals/ipc-protocol/#status-response)).
## Sysfs source
[Section titled “Sysfs source”](#sysfs-source)
At construction, `detect_sysfs()` probes this ordered list and picks the **first** path that exists and is non-empty (`FPS_SYSFS_PATHS`, `fps_meter/mod.rs`):
```text
/sys/class/drm/sde-crtc-0/measured_fps
/sys/class/drm/card0/sde-crtc-0/measured_fps
/sys/class/drm/card0/sde_crtc_fps
/sys/class/drm/card0/fbc/fps
/sys/class/graphics/fb0/measured_fps
/sys/class/graphics/fb0/fps
/sys/kernel/debug/mali/fps
/sys/class/misc/mali0/device/fps
```
The first six are Qualcomm/DRM display-controller and framebuffer nodes; the last two are Mali (ARM GPU) nodes. If none qualify, there is no sysfs source and the meter relies on eBPF only.
Behavior (`read_sysfs`):
* Polled at most once every **2 seconds** (`SYSFS_POLL_INTERVAL`); between polls the last reading is returned from cache.
* The file is parsed as `f64`; values outside `(0, 500]` are rejected (returns `None`), guarding against garbage or a `0` reading when the panel is idle.
## eBPF fallback
[Section titled “eBPF fallback”](#ebpf-fallback)
Used only when sysfs produced nothing. Frame durations arrive over a `broadcast::Receiver` from the Kala frame stream (see [Kala eBPF frame probe → Auriya integration](/internals/kala-research/#auriya-integration)). `drain_ebpf` + `read` (`fps_meter/mod.rs`):
* Each incoming delta is kept only if **< 500 ms** (`Duration::from_millis(500)`); larger gaps (app not rendering) are dropped.
* Kept deltas fill a **30-frame** ring (`SHORT_WINDOW`, ≈ ½ s at 60 fps); FPS is `1.0 / mean(frametimes)`.
* If no frame has arrived for **3 seconds** (`ebpf_timeout`), or the ring is empty, `read` returns `None`.
* The computed FPS is subject to the same `(0, 500]` sanity clamp.
* A lagged broadcast (`TryRecvError::Lagged`) is logged and skipped; a closed channel disables the eBPF source for the rest of the meter’s life.
## Consequences
[Section titled “Consequences”](#consequences)
* **FAS availability does not gate status FPS.** Even when the eBPF program cannot attach (old kernel, missing symbols), sysfs FPS still populates status.
* The meter never blocks: sysfs is cache-throttled, eBPF is drained non-blocking. A tick that finds neither source simply reports no FPS.
* The eBPF value here is a **frame-submission** rate derived from `queueBuffer` deltas, not a display-present timestamp — see the limitations in [Kala eBPF frame probe → Scope and limitations](/internals/kala-research/#scope-and-limitations).
## Likely to drift first
[Section titled “Likely to drift first”](#likely-to-drift-first)
`FPS_SYSFS_PATHS` (device-specific), the 2 s / 3 s / 500 ms / 30-frame constants, and the `(0, 500]` clamp. Re-verify against `src/core/fps_meter/mod.rs`.
# Game Detection
“Game detection” is how the daemon decides **which package is in the foreground**, whether it is one Auriya manages, and whether its process is still alive. Auriya does **not** scan the system itself — the Android companion service supplies the focused package/PID, and the daemon validates and tracks it.
Traced to Auriya commit [`10fe7c6`](https://github.com/pavelc4/auriya/tree/10fe7c6b56474a00513fec34ebac1376b30e95e6). Files: [`src/core/dumpsys/activity.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/core/dumpsys/activity.rs) (PID validity/verification), [`src/core/pid_tracker.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/core/pid_tracker.rs) (liveness + exit events), [`src/daemon/tick.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/tick.rs) (the decision path).
## Where the foreground package comes from
[Section titled “Where the foreground package comes from”](#where-the-foreground-package-comes-from)
The companion service observes the Android task stack and writes the focused package and PID into `/data/adb/.config/auriya/system_status`. The daemon reads that snapshot each tick — it never calls `dumpsys` for foreground detection itself (`dumpsys/activity.rs` module comment: the old dumpsys-scanning path was removed). See [Data flow](/architecture/data-flow/) and [IPC protocol → data flow direction](/internals/ipc-protocol/#direction-of-data-flow).
An `INJECT ` IPC command overrides the companion’s focused package for debugging; `CLEAR_INJECT` removes the override (see [IPC protocol](/internals/ipc-protocol/#state-toggles)).
## The whitelist
[Section titled “The whitelist”](#the-whitelist)
At startup and on every `gamelist.toml` change, the daemon builds a `HashSet` of package names from the game list — the “whitelist” (`src/daemon/run.rs:212-217`, rebuilt by `rebuild_whitelist`, `run.rs:320-327`). A package is “a game” to Auriya iff it is in this set. Matching is **exact** (no wildcards); see [gamelist reference](/reference/gamelist/).
## PID validity vs. package verification
[Section titled “PID validity vs. package verification”](#pid-validity-vs-package-verification)
Two cheap checks in `dumpsys/activity.rs`:
| Function | Check | Use |
| ------------------------------ | ---------------------------------------------- | ------------------------------------------------------- |
| `is_pid_valid(pid)` | `pid > 0` **and** `/proc/` exists | Drop a stale PID reference cheaply. |
| `verify_pid_package(pid, pkg)` | reads `/proc//cmdline`, compares to `pkg` | Sanity-check that a PID really is the expected package. |
`verify_pid_package` matches the process name up to the first `\0` or `:` separator, then also accepts a substring match so **isolated processes** named `:` (a common Android pattern) still count as the package (`activity.rs`, `verify_pid_package`).
## Liveness tracking and instant exit
[Section titled “Liveness tracking and instant exit”](#liveness-tracking-and-instant-exit)
Once a whitelisted game’s PID is validated, the daemon spawns a `PidTracker` (`pid_tracker.rs`). It serves two roles:
1. **Cheap poll** — `PidTracker::is_alive()` is a non-blocking probe the tick loop uses on the fast path.
2. **Instant exit event** — a background thread blocks until the process actually dies and then pushes a `DaemonEvent::PidExited`, so the daemon re-evaluates *immediately* instead of waiting for the next timer tick.
Two kernel paths, chosen at runtime:
* **`pidfd_open` (Linux ≥ 5.3)** — the tracker opens a pidfd via raw syscall (number `434` on aarch64, `439` on x86-64) and blocks in `poll()` on it. Zero wakeups until the process exits. `is_alive` is a non-blocking `poll` on the same fd (`pid_tracker.rs`, `open_pidfd`, `pidfd_is_alive`).
* **`/proc` fallback** — on older kernels, the watcher polls `/proc/` every **150 ms** (`wait_proc_poll`, `POLL_INTERVAL_MS = 150`), and `is_alive` falls back to a `/proc/` existence check.
An `eventfd` lets `Drop` interrupt the blocked watcher the instant the daemon stops tracking (e.g. on game switch), so no thread lingers (`pid_tracker.rs`, `Drop`, `make_eventfd`). The exit event is sent with `try_send`, not `blocking_send`: if the channel is full the event is dropped and the next tick catches the exit via `is_alive` — deliberately chosen to avoid a deadlock where `Drop` runs on the same tokio worker that drains the channel (`track_loop` comment).
Note
`pid_tracker.rs` ships with tests that spawn a child, track it, kill it, and assert the `PidExited` event arrives (`detects_child_process_exit`) and measures exit→event latency (`pidfd_exit_latency`, target well under 100 ms). They exercise the actual `pidfd_open`/`poll` path on the target kernel.
## How detection drives the tick
[Section titled “How detection drives the tick”](#how-detection-drives-the-tick)
Each tick resolves the package/PID, then (`process_tick_logic`, `src/daemon/tick.rs`):
* **Same package, PID still alive** → fast path: the profile is *not* reapplied; only FAS may adjust within the session (see [Profile scheduler](/internals/profile-scheduler/)).
* **New package, or previous PID exited** → full re-evaluation.
* **Whitelisted package with a valid PID** → enter/refresh the game session.
* **Not whitelisted, or PID invalid/missing, or no foreground package** → clear game state and apply the default profile.
The exact branch order and what each branch writes are documented in [Profile scheduler](/internals/profile-scheduler/).
## Likely to drift first
[Section titled “Likely to drift first”](#likely-to-drift-first)
The `pidfd_open` syscall numbers, the 150 ms fallback interval, and the isolated-process (`pkg:suffix`) matching rule. Re-verify against `src/core/pid_tracker.rs` and `src/core/dumpsys/activity.rs`.
# IPC Protocol
The daemon exposes a local Unix socket. The manager app and `auriyactl` both use it to send commands and read status. This page documents the wire format and every command, request and response, exactly as implemented.
Traced to Auriya commit [`10fe7c6`](https://github.com/pavelc4/auriya/tree/10fe7c6b56474a00513fec34ebac1376b30e95e6). Command grammar: [`src/daemon/ipc/commands.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/ipc/commands.rs) (`Command::from_str`). Handlers and responses: [`src/daemon/ipc/handlers.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/ipc/handlers.rs).
## Transport
[Section titled “Transport”](#transport)
| Property | Value | Source |
| ----------- | ------------------------------------------------------------------- | ----------------------------------------------------- |
| Socket path | `/dev/socket/auriya.sock` | `SOCKET_PATH`, `src/common/constants.rs:1` |
| Type | `AF_UNIX` stream | `handle_client(stream: UnixStream, …)`, `handlers.rs` |
| Framing | Newline-delimited UTF-8 text, one command per line | `reader.read_line`, `handlers.rs` |
| Greeting | Server sends `OK AURIYA IPC\n` immediately on connect | `handlers.rs` (`write_all(b"OK AURIYA IPC\n")`) |
| Max input | **256 bytes per line**; longer → `ERR input too long`, line skipped | `handlers.rs` (`if s.len() > 256`) |
| Whitespace | Each line is trimmed; commands split on whitespace | `commands.rs` (`s.split_whitespace()`) |
| Session | Multiple commands per connection until `QUIT` or EOF | `while reader.read_line(...) > 0` |
A minimal exchange with `nc`:
```console
$ nc -U /dev/socket/auriya.sock
OK AURIYA IPC ← greeting (server → client)
PING ← you type this
PONG ← reply
QUIT
BYE
```
### Response conventions
[Section titled “Response conventions”](#response-conventions)
* Success replies start with `OK `(mutations) or return data directly (e.g. `PONG`, JSON, `STATUS` fields).
* Errors start with `ERR `. A parse failure yields `ERR `; the unknown-command reply is `ERR unknown command (try HELP)` (`commands.rs`).
* `QUIT` replies `BYE` and closes; empty computed responses are not written.
## Command grammar and aliases
[Section titled “Command grammar and aliases”](#command-grammar-and-aliases)
`Command::from_str` accepts a canonical token and, for many commands, a no-underscore alias (`commands.rs`). Command tokens are matched **case-sensitively** as shown (they are upper-case); their *arguments* may be normalized (profile/log tokens are upper-cased, `mode=` for games is lower-cased downstream).
| Canonical | Alias | Argument |
| --------------------- | -------------- | ------------------------------------------------------------ |
| `HELP` | `?` | — |
| `STATUS` | — | — |
| `ENABLE` / `DISABLE` | — | — |
| `RELOAD` | — | — |
| `RESTART` | — | — |
| `SETLOG` | `SET_LOG` | `` |
| `SET_FPS` | `SETFPS` | `` |
| `GET_FPS` | `GETFPS` | — |
| `GET_SUPPORTED_RATES` | `GETRATES` | — |
| `GET_STATS` | `GETSTATS` | — |
| `INJECT` | — | `` |
| `CLEAR_INJECT` | `CLEARINJECT` | — |
| `GETPID` | `GET_PID` | — |
| `PING` | — | — |
| `QUIT` | — | — |
| `SET_PROFILE` | `SETPROFILE` | `` |
| `ADD_GAME` | `ADDGAME` | `` |
| `REMOVE_GAME` | `REMOVEGAME` | `` |
| `UPDATE_GAME` | `UPDATEGAME` | ` [gov= dnd= fps= fps_array= rate= mode= ceiling=]` |
| `GET_GAMELIST` | `GETGAMELIST` | — |
| `LIST_PACKAGES` | `LISTPACKAGES` | — |
The built-in `HELP` text (`handlers.rs`, `const HELP`) lists only a subset — it omits `SET_FPS`, `GET_FPS`, `GET_SUPPORTED_RATES`, `SET_PROFILE`, `GET_GAMELIST`, `UPDATE_GAME`, `LIST_PACKAGES`, `RESTART`, and `QUIT`. Trust this page (derived from the parser), not the `HELP` output, for the full set.
## Command reference
[Section titled “Command reference”](#command-reference)
Each entry gives the exact response format string from `handlers.rs`. `{…}` marks interpolated values.
### Introspection
[Section titled “Introspection”](#introspection)
| Command | Success response | Errors |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------- |
| `PING` | `PONG` | — |
| `HELP` / `?` | Multi-line command list (partial — see warning) | — |
| `GETPID` / `GET_PID` | `PKG={pkg} PID={pid}`, or `PKG={pkg} PID=None`, or `PKG=None PID=None` | — |
| `GET_FPS` | `FPS={measured:.1} TARGET={target}` (measured `0` if none) | — |
| `GET_SUPPORTED_RATES` | JSON array of unique refresh rates, e.g. `[60,90,120]` (deduped/sorted from cached display modes) | `ERR JSON {e}` |
| `GET_STATS` | Single-line JSON perf snapshot (fps/thermal/battery/cpu/gpu/session) — full schema in [Stats API](/reference/stats-api/) | `ERR JSON {e}` |
| `STATUS` | See [STATUS format](#status-response) below | — |
#### STATUS response
[Section titled “STATUS response”](#status-response)
First line, always present:
```text
ENABLED={bool} PACKAGES={count} OVERRIDE={Option} LOG_LEVEL={level}
```
Then zero or more telemetry lines, emitted only when the corresponding data is present in `CurrentState` (`handlers.rs`, `Command::Status`):
```text
FPS={value:.1} SOURCE={ebpf|sysfs|?}
CPU_CORES={n} CPU_LOAD={pct}
CORE_{id}={id} online={bool} freq={khz} governor={name} cluster={Little|Big|Prime|…}
GPU_FREQ={mhz} GPU_LOAD={pct} GPU_VENDOR={vendor}
TEMP_CPU={c|N/A} TEMP_GPU={c|N/A}
```
One `CORE_{id}` line is emitted per online/known core. See [FPS detection](/internals/fps-detection/) for `SOURCE` semantics.
### State toggles
[Section titled “State toggles”](#state-toggles)
| Command | Success | Notes |
| ---------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------- |
| `ENABLE` | `OK ENABLED` | Sets the atomic enabled flag (`Ordering::Release`). |
| `DISABLE` | `OK DISABLED` | Clears it. |
| `SETLOG ` | `OK SET_LOG` | Live-reloads the `tracing` filter (`run.rs`). Bad level → `ERR usage: SETLOG `. |
| `INJECT ` | `OK INJECT` | Forces a foreground package for debugging (overrides the companion). |
| `CLEAR_INJECT` | `OK CLEAR_INJECT` | Clears the override. |
### Configuration lifecycle
[Section titled “Configuration lifecycle”](#configuration-lifecycle)
| Command | Success | Errors |
| --------- | --------------------------------------- | ------------------------------------------------ |
| `RELOAD` | `OK RELOADED {n}` (`n` = reload result) | `ERR RELOAD {e}` |
| `RESTART` | *(no response — daemon re-execs)* | `ERR RESTART_FAILED` if the relaunch spawn fails |
`RELOAD` re-reads config; `cpu.default_governor`, `daemon.default_mode`, `daemon.check_interval_ms`, FAS tuning parameters, and log filters take effect on reload (see [settings reference](/reference/settings/#reload-behavior)).
`RESTART` clears `/data/adb/auriya/daemon.log`, spawns `sh -c "sleep 2 && sh /data/adb/modules/auriya/service.sh"` in a new session (`setsid`), then exits the current process after 500 ms (`handlers.rs`, `Command::Restart`). Because it returns before replying, clients see the connection close rather than an `OK`. This is distinct from `auriyactl restart`, which does the kill/relaunch itself — see [Command reference](/reference/commands/#restart-is-local-not-an-ipc-command).
### Profile control
[Section titled “Profile control”](#profile-control)
| Command | Success | Errors |
| -------------------- | ----------------------- | -------------------------------------------------------------------------------- |
| `SET_PROFILE ` | `OK SET_PROFILE {Mode}` | `ERR SET_PROFILE {e}` on apply failure; `ERR usage: SETPROFILE <…>` on bad token |
| `SET_FPS ` | `OK SET_FPS {n}` | `ERR usage: SET_FPS ` on non-integer |
`SET_PROFILE` takes a **process-wide profile lock** before applying, so concurrent profile writes cannot interleave (`handlers.rs`, `profile_lock`). `MODE` ∈ `FAST`/`PERFORMANCE`/`BALANCE`/`POWERSAVE` (or numeric `4`/`1`/`2`/`3`). What each profile writes is documented once in [Architecture overview → What each static profile changes](/architecture/overview/#what-each-static-profile-changes).
### Game-list mutations
[Section titled “Game-list mutations”](#game-list-mutations)
All persist to `gamelist.toml` atomically on success (see [gamelist reference](/reference/gamelist/#persistence)). All can return `ERR lock poisoned` if the shared lock is poisoned, or `ERR SAVE_GAMELIST {e}` if the write fails after a successful in-memory change.
| Command | Success | Command-specific error |
| -------------------------- | ----------------------------- | ---------------------------------------- |
| `ADD_GAME ` | `OK ADD_GAME {pkg}` | `ERR ADD_GAME {e}` (e.g. already exists) |
| `REMOVE_GAME ` | `OK REMOVE_GAME {pkg}` | `ERR REMOVE_GAME {e}` (e.g. not found) |
| `UPDATE_GAME [k=v…]` | `OK UPDATE_GAME {pkg}` | `ERR UPDATE_GAME {e}` (e.g. not found) |
| `GET_GAMELIST` | JSON array of game profiles | `ERR GET_GAMELIST {e}` |
| `LIST_PACKAGES` | Raw `pm list packages` output | `ERR LIST_PACKAGES {e}` |
`ADD_GAME` inserts a **fixed default profile** (governor `performance`, DnD on, mode `performance`), not the shipped example values — see [gamelist reference → ADD\_GAME](/reference/gamelist/#add_game-package--injected-defaults). `UPDATE_GAME` token syntax (`gov=`, `dnd=`, `fps=`, `fps_array=`, `rate=`, `mode=`, `ceiling=`) is documented in the same page.
### Session
[Section titled “Session”](#session)
| Command | Response | Notes |
| -------------------- | -------------------------------- | ------------------------------------ |
| `QUIT` | `BYE` | Closes the connection. |
| *(unknown)* | `ERR unknown command (try HELP)` | Any unrecognized token. |
| *(line > 256 bytes)* | `ERR input too long` | Line skipped; connection stays open. |
## Direction of data flow
[Section titled “Direction of data flow”](#direction-of-data-flow)
Commands and status flow **into** the daemon over this socket. Companion-observed state (focused app, screen/battery/zen) flows the **other** way — the companion writes `/data/adb/.config/auriya/system_status`, which the daemon watches. See [Data flow](/architecture/data-flow/) and [Game detection](/internals/game-detection/).
## Likely to drift first
[Section titled “Likely to drift first”](#likely-to-drift-first)
The per-command response strings and the alias list — they are literal format strings in `handlers.rs`. Re-verify against `src/daemon/ipc/commands.rs` and `src/daemon/ipc/handlers.rs`. The stale `HELP` text is a known gap.
# Kala
This page describes the exact Kala revision consumed by Auriya: the git dependency recorded in `Cargo.lock` as `be1061bd032b4faf5e6ef1cf5eec19d924a5caf3` (`github.com/pavelc4/kala`, branch `main`). The source was read from the Cargo checkout at that revision.
Source revision: [`pavelc4/kala@be1061b`](https://github.com/pavelc4/kala/tree/be1061bd032b4faf5e6ef1cf5eec19d924a5caf3). Primary implementation files: [`kala/src/lib.rs`](https://github.com/pavelc4/kala/blob/be1061bd032b4faf5e6ef1cf5eec19d924a5caf3/kala/src/lib.rs), [`kala/src/uprobe.rs`](https://github.com/pavelc4/kala/blob/be1061bd032b4faf5e6ef1cf5eec19d924a5caf3/kala/src/uprobe.rs), [`kala/src/tracker.rs`](https://github.com/pavelc4/kala/blob/be1061bd032b4faf5e6ef1cf5eec19d924a5caf3/kala/src/tracker.rs), [`kala/src/wire.rs`](https://github.com/pavelc4/kala/blob/be1061bd032b4faf5e6ef1cf5eec19d924a5caf3/kala/src/wire.rs), and [`kala-ebpf/src/main.rs`](https://github.com/pavelc4/kala/blob/be1061bd032b4faf5e6ef1cf5eec19d924a5caf3/kala-ebpf/src/main.rs).
## What Kala measures
[Section titled “What Kala measures”](#what-kala-measures)
Kala is a Rust library plus a `no_std` eBPF program. The probe attaches a **uprobe** (a user-space function entry probe) to Android’s `/system/lib64/libgui.so`. Its target is one of several mangled `android::Surface::queueBuffer` symbols (`kala/src/uprobe.rs`, `QUEUE_BUFFER_SYMBOLS`). `queueBuffer` is called when an application hands a buffer to SurfaceFlinger, so Kala observes frame submissions rather than GPU work or display-scanout completion.
The eBPF entry point is `kala_frame_probe` in `kala-ebpf/src/main.rs`. It reads `ctx.arg(1)` as the buffer pointer and obtains a kernel monotonic timestamp with `bpf_ktime_get_ns()`. It writes this pair to the `RING_BUF` map:
```text
FrameRecord {
ktime_ns: u64, // monotonic nanoseconds
buffer: u64, // queueBuffer buffer pointer
}
```
The ring buffer is 256 KiB (`RingBuf::with_byte_size(256 * 1024, 0)`). If reservation or either argument lookup fails, `emit` returns `None` and the BPF function returns `1`; there is no user-visible error record. The wire layout is duplicated in `kala/src/wire.rs` and must remain byte-for-byte identical.
## Probe lifecycle
[Section titled “Probe lifecycle”](#probe-lifecycle)
`FrameProbe::new` (`kala/src/lib.rs`) only initializes an empty target map and does not load or attach an eBPF program. `FrameProbe::attach(pid)` is idempotent for an already-attached PID. For a new PID it calls `QueueBufferProbe::attach` (`kala/src/uprobe.rs`), which loads the embedded BPF object, loads the `kala_frame_probe` program, and tries each symbol in order:
1. `Surface::hook_queueBuffer(ANativeWindow*, ANativeWindowBuffer*, int)`
2. `Surface::queueBufferInternal(...)`
3. legacy `Surface::queueBuffer(ANativeWindowBuffer*, int)`
4. legacy overload with `SurfaceQueueBufferOutput*`
5. modern `sp` overloads
The first successful `program.attach` wins. If all attempts fail, the last Aya error is returned (or `SymbolNotFound` when no error is available). After a successful attach, Kala creates a `FrameTracker` and rebuilds a `mio::Poll` registry for all attached ring-buffer file descriptors.
`FrameProbe::detach(pid)` removes the PID, rebuilds the poll registry, and returns `true` only when that PID was present. Dropping `QueueBufferProbe` unloads the uprobe program (`Drop` in `kala/src/uprobe.rs`); dropping `FrameProbe` therefore cleans up all attached PIDs. Poll/rebuild errors during `recv_with_deadline` are ignored and retried on a later call.
## Receiving and frame-time reconstruction
[Section titled “Receiving and frame-time reconstruction”](#receiving-and-frame-time-reconstruction)
`FrameProbe::recv_with_deadline(timeout)` polls all registered ring buffers with `mio`. It returns the first available `(pid, Duration)`, or `None` when no event arrives, no PID is attached, or a ring item cannot be decoded. A ring item shorter than `size_of::()` is ignored. `FrameTracker::pump` (`kala/src/tracker.rs`) reads one record at a time.
Because Android commonly has multiple buffers in flight, `FrameTracker::record` keeps a separate timestamp history per buffer pointer. It computes a saturating delta from the previous timestamp for that same pointer; the first observation of a buffer produces no duration, and a zero delta is discarded. Each history stores up to 144 durations. Among buffers tied for the longest history, the buffer with the smallest cumulative duration is selected as the active buffer; only a delta from that buffer is returned. This is a heuristic to avoid triple-buffer interleaving and is not a direct display-present timestamp.
Errors while pumping a tracker are swallowed by `recv_with_deadline` (`Err(_) => continue`), so a broken ring read causes the current event to be skipped rather than terminating the probe.
## Auriya integration
[Section titled “Auriya integration”](#auriya-integration)
`src/core/ebpf.rs` wraps Kala in `EbpfFrameStream`. `EbpfFrameStream::new` calls `FrameProbe::new`, creates a worker thread, and broadcasts returned frame durations through a Tokio `broadcast` channel (capacity 4096). With no attached PID the worker blocks on its command channel; with at least one PID it drains commands and polls Kala on the `settings.fas.poll_interval_ms` deadline (clamped to `[1, 500]` ms; `recv_with_deadline`). The PID from Kala’s tuple is currently ignored; only the `Duration` is sent to subscribers.
`attach` and `detach` send commands to that worker and wait up to one second for the reply. They report explicit errors when the worker is gone, the timeout is exceeded, or the reply channel disconnects. The daemon’s tick path calls these methods for the validated foreground game PID (`src/daemon/tick.rs`, `ebpf_attach`/`ebpf_detach`). Attach failures are logged as warnings and do not abort the daemon.
At startup, failure to create the stream is handled in `src/daemon/run.rs`: Auriya continues with sysfs-only FPS telemetry and disables FAS. This covers old kernels, missing BPF capabilities, SELinux denial, missing symbols, and other Kala initialization errors. Kala’s documented runtime requirements are a Linux kernel with uprobe and ring-buffer support (5.8+, tested on 5.10), root or `CAP_SYS_ADMIN` plus `CAP_BPF`, and a real Android image containing `/system/lib64/libgui.so`. The embedded BPF object means downstream users do not need `bpf-linker` at runtime; building Kala itself requires `bpf-linker` in `PATH` when regenerating the object.
## Scope and limitations
[Section titled “Scope and limitations”](#scope-and-limitations)
Kala filters by PID through Aya’s uprobe attachment; the eBPF program itself has no package-name or surface-name filter. It records every matching `queueBuffer` call in the attached process. The measured interval is the time between queue calls for the selected buffer, not a guaranteed on-screen frame interval. Ring overflow, failed reservations, malformed records, and tracker read errors are silently dropped at the Kala layer. Auriya’s FPS meter may use the stream as a fallback, while FAS consumes the same broadcast independently; the surrounding selection and timeout policy is implemented in Auriya, not in Kala (`src/core/fps_meter/mod.rs`, `src/core/fas/source/mod.rs`).
# Profile Scheduler
The scheduler is the daemon’s decision core: once per tick it decides which performance profile the device should be in and applies it. This page documents the exact decision function; for the **system-level** view and the tables of what each profile writes, see [Architecture overview](/architecture/overview/#profile-decision-workflow) — this page is the source-precise companion to it, not a duplicate.
Traced to Auriya commit [`10fe7c6`](https://github.com/pavelc4/auriya/tree/10fe7c6b56474a00513fec34ebac1376b30e95e6), [`src/daemon/tick.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/tick.rs) and [`src/daemon/run.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/run.rs).
## Decision order
[Section titled “Decision order”](#decision-order)
`Daemon::process_tick_logic` (`tick.rs:91-192`) evaluates conditions in **strict priority order**; the first matching branch wins and no lower branch runs:
```
flowchart TD
tick([Tick Triggered]) --> branch1{"1. Screen OFF or
Battery Saver ON?"}
branch1 -->|yes| act1["POWERSAVE + Low ceiling
+ detach eBPF + disable game DnD"]
branch1 -->|no| branch2{"2. IPC foreground override
(INJECT) exists?"}
branch2 -->|yes| act2["Treat injected package as foreground"]
branch2 -->|no| branch3{"3. Companion reports
focused package?"}
branch3 -->|no| act3["Default mode
+ release game-owned state"]
branch3 -->|yes| branch4{"4. Same package &
tracked PID alive?"}
branch4 -->|yes| act4["Fast path: FAS adjusts if available;
Profile NOT reapplied"]
branch4 -->|no| branch5{"5. Package is whitelisted?"}
branch5 -->|yes| val_pid{"Validate PID against /proc"}
val_pid -->|valid| act5["Enter / update game session"]
val_pid -->|invalid| act3
branch5 -->|no| act6["6. Default mode
+ release game-owned state"]
```
Screen-off / battery-saver is checked first and unconditionally, so it wins even while a game is foregrounded. The injected override (2) exists for debugging via `INJECT` (see [Game detection](/internals/game-detection/#where-the-foreground-package-comes-from)).
## The idempotence guard
[Section titled “The idempotence guard”](#the-idempotence-guard)
Profiles are only (re)applied when the target differs from what is already active: `if self.last.profile_mode != Some(target_mode)` (`tick.rs:260`, and the mirror check on the clear path, `tick.rs:323`). Repeated ticks in a steady state therefore do **not** rewrite kernel nodes — a tick that changes nothing performs no writes.
## Entering a whitelisted game
[Section titled “Entering a whitelisted game”](#entering-a-whitelisted-game)
When branch 5 validates a live PID, `handle_whitelisted_app` (`tick.rs:194-307`) runs the game-session setup. The full ordered sequence (vendor lock, toast broadcast, mode resolution, ceiling, refresh rate, eBPF attach, DnD, PID tracker) is enumerated in [Architecture overview → Entering a whitelisted game](/architecture/overview/#entering-a-whitelisted-game). Source-level specifics worth pinning here:
* **Mode resolution is case-insensitive with a Performance default.** `powersave` → Powersave, `balance` → Balance, `fast` → Fast, **anything else or missing → Performance** (`tick.rs`). A typo silently resolves to Performance.
* **Governor fallback**: an empty per-game `cpu_governor` falls back to the global `balance_governor` (`tick.rs`).
* **Ceiling**: an unparseable per-game `ceiling` string is dropped to no-override, not an error (`tick.rs`).
* **Refresh rate** is only requested when it differs from the currently applied rate (`tick.rs`), and released (request `0`) when leaving (`tick.rs`).
What each profile actually writes to the kernel is the single-source-of-truth table in [Architecture overview → What each static profile changes](/architecture/overview/#what-each-static-profile-changes).
## FAS adjustments within a session
[Section titled “FAS adjustments within a session”](#fas-adjustments-within-a-session)
On the fast path (branch 4), if Frame-Aware Scheduling exists it consumes the Kala frame stream and picks one scaling action per tick. The action→effect mapping (`BoostGpu`, `BoostCpu`, `BoostBalanced`, `Maintain`, `Reduce`) is documented in [Architecture overview → FAS dynamic changes](/architecture/overview/#fas-dynamic-changes-inside-the-same-game). The frame-measurement mechanism itself is in [Kala eBPF frame probe](/internals/kala-research/).
## Leaving a game / no foreground
[Section titled “Leaving a game / no foreground”](#leaving-a-game--no-foreground)
The clear path (`apply_balance_and_clear`, `tick.rs`) applies `daemon.default_mode` only if it differs from the current mode, then restores default ceiling, detaches eBPF, requests normal notifications (DnD All), releases any refresh-rate override (request `0`), unlocks vendor controls, and clears the PID tracker.
## The `current_profile` file
[Section titled “The current\_profile file”](#the-current_profile-file)
On each applied profile change the daemon also writes `/data/adb/.config/auriya/current_profile` (`update_current_profile_file`, `run.rs`, called from `tick.rs`). It contains a single digit:
| Value | Profile |
| ----- | ----------- |
| `1` | Performance |
| `2` | Balance |
| `3` | Powersave |
| `4` | Fast |
This is a **legacy/compatibility** status output for external readers. It is best-effort (write failures are logged, not fatal) and is **not** the authoritative UI state — live daemon status over IPC is. See [Filesystem reference](/reference/filesystem/#configuration-and-runtime-state--dataadbconfigauriya).
## Error handling
[Section titled “Error handling”](#error-handling)
A failed profile application logs an error and leaves `last.profile_mode` unchanged, so the next tick retries (`tick.rs:274-279`, `330-335`). A tick that errors does not terminate the loop; identical errors are debounced for 30 s (see [Architecture overview → Event loop](/architecture/overview/#event-loop-and-execution-cadence)).
## Likely to drift first
[Section titled “Likely to drift first”](#likely-to-drift-first)
The branch order and the mode/ceiling parsing defaults. Re-verify against `Daemon::process_tick_logic` and `handle_whitelisted_app` in `src/daemon/tick.rs`.
# System Tweaks
The tweak layer is where the daemon actually touches the device: guarded writes to `/proc` and `/sys`, plus a few actions that must be routed through Android via the companion. Everything here is **best-effort and device-dependent** — a node that does not exist is skipped, never fatal.
Traced to Auriya commit [`10fe7c6`](https://github.com/pavelc4/auriya/tree/10fe7c6b56474a00513fec34ebac1376b30e95e6), [`src/core/tweaks/`](https://github.com/pavelc4/auriya/tree/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/core/tweaks) and [`src/core/cmd_writer/mod.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/core/cmd_writer/mod.rs).
## Module map
[Section titled “Module map”](#module-map)
`src/core/tweaks/` (`tweaks/mod.rs`) splits the surface by concern:
| Module | Responsibility |
| -------------------------------------------------- | ------------------------------------------------------------------------------- |
| `paths.rs` | Scans and **caches** sysfs paths once (CPU governors, online, Snapdragon KGSL). |
| `cpu.rs` | Governor, boost, core onlining, per-core affinity. |
| `gpu.rs` | GPU performance/balanced mode. |
| `sched.rs` | Scheduler tunables. |
| `memory.rs` | Memory / swappiness / cache drop. |
| `storage.rs` | Block-I/O tunables. |
| `touchpanel.rs` | Touch “game mode”. |
| `ceiling.rs` | Frequency-ceiling controller (Low/Balance). |
| `init.rs` | One-shot **general** tweaks applied with the Performance profile. |
| `vendor/` (`detect.rs`, `mtk.rs`, `snapdragon.rs`) | SoC detection + vendor-specific hooks. |
| `vendor_lock.rs` | Locks vendor perfmgr nodes so vendor services cannot fight Auriya. |
Which of these a given profile triggers is the single-source table in [Architecture overview → What each static profile changes](/architecture/overview/#what-each-static-profile-changes).
## Path detection and caching
[Section titled “Path detection and caching”](#path-detection-and-caching)
Sysfs layout varies per device, so paths are discovered **once** and memoized in a `OnceLock`. `CpuPaths::scan` probes `cpu0..15` and `policy0..7` governor nodes plus `cpu1..15/online`, keeping only those that exist (`paths.rs`, `cpu_paths()`). `set_governor_cached` then writes every cached governor node, ignoring individual write errors:
```rust
pub fn set_governor_cached(governor: &str) {
let paths = cpu_paths();
for path in &paths.governors_cpu { let _ = std::fs::write(path, governor); }
for path in &paths.governors_policy { let _ = std::fs::write(path, governor); }
}
```
The same pattern caches Snapdragon KGSL/memlat paths **and their original values** so they can be restored later (`SnapdragonPaths::scan`, `paths.rs`).
## Guarded, best-effort writes
[Section titled “Guarded, best-effort writes”](#guarded-best-effort-writes)
The prevailing idiom (e.g. `init.rs`) is: check existence, write, ignore failure. Missing nodes are silently skipped so a different kernel layout keeps running. A concrete slice of the general tweaks (`apply_general_tweaks`, `init.rs`):
```rust
// disable kernel panics (only nodes that exist)
for (path, value) in [
("/proc/sys/kernel/panic", "0"),
("/proc/sys/kernel/panic_on_oops", "0"),
("/proc/sys/kernel/panic_on_warn", "0"),
("/proc/sys/kernel/softlockup_panic", "0"),
] {
if Path::new(path).exists() { fs::write(path, value)?; }
}
```
`apply_general_tweaks` also: sets block-I/O tunables per `/sys/block/*/queue` (`iostats=0`, `add_random=0`, `read_ahead_kb=32`, `nr_requests=32`), picks the best available TCP congestion control from `bbr3 → bbr2 → bbrplus → bbr → westwood → cubic`, applies VM/scheduler tunables, and disables several OEM “assist”/bloat modules and the OEM battery-saver kernel module (`init.rs`). These are OEM-node-dependent and mostly no-ops on a device that lacks them.
## Actions routed through Android — `CmdWriter`
[Section titled “Actions routed through Android — CmdWriter”](#actions-routed-through-android--cmdwriter)
Two decisions cannot be done from the root daemon because they require Android framework APIs (`NotificationManager` for Do-Not-Disturb, the display manager for refresh rate): **DnD** and **refresh-rate** changes. The daemon serializes these to a small command file that the companion service watches and replays through the proper APIs (`cmd_writer/mod.rs` module comment).
* File: `/data/adb/.config/auriya/auriya_cmd` (`CMD_FILE`).
* Wire format (mirrors the companion’s `CmdFormat.kt`):
```text
seq 42
dnd 1 # 0 = All/off, 1 = Priority
refresh_rate 90 # Hz; 0 means "restore previous"
```
* **Stateful re-emit**: the companion reads the *whole* file each time (it is one command, not a queue). Two quick single-field writes — `dnd` then `refresh_rate` on a game switch — would otherwise clobber each other, so the writer remembers the last value of every field and re-emits the **full** state on each write (`CmdWriter::write`).
* **Atomic delivery**: written to `.auriya_cmd.tmp` then `rename`d, so the companion’s inotify watcher only sees a complete payload (`CmdWriter::write`). `seq` is a process-monotonic counter for dedup.
There is one process-wide writer (`shared()`); using more than one would reset the `seq` counter and break the companion’s dedup.
Note
When the companion is considered dead, refresh-rate and DnD requests fall back to Android `settings put` invocations from the daemon (see [Architecture overview → Control and status paths](/architecture/overview/#control-and-status-paths)).
## SoC detection
[Section titled “SoC detection”](#soc-detection)
Vendor hooks pick a SoC family once, cached in a `OnceLock` (`vendor/detect.rs`, `detect_soc`). Detection tries, in order: `ro.board.platform` prefixes (`mt`/`k6` → MediaTek; `sm`/`sdm`/`msm`/`apq` → Snapdragon; `exynos`; `ud710`/`ums` → Unisoc; `gs` → Tensor), then `ro.hardware` substrings, then filesystem probes (`/proc/ppm` → MediaTek, `/sys/class/kgsl/kgsl-3d0` → Snapdragon). Falls back to `Unknown`. Only MediaTek and Snapdragon have implemented hook modules at this revision.
## Vendor lock — stopping vendor services from fighting back
[Section titled “Vendor lock — stopping vendor services from fighting back”](#vendor-lock--stopping-vendor-services-from-fighting-back)
On many devices a vendor “perfmgr”/game service continuously rewrites the same CPU/GPU nodes Auriya sets, undoing Auriya’s changes within milliseconds. `VendorLock` neutralizes this by pinning a set of vendor toggles **read-only via a bind mount** (`vendor_lock.rs`, `lock_all`):
For each existing path in `VENDOR_PATHS` it: saves the current value, writes the desired value, `chmod`s the node to `0444`, then **bind-mounts** a Auriya-owned file over it (`MS_BIND | MS_REC`) so even privileged writes hit the overlay, not the real node. `unlock_all` unmounts, restores permissions, and writes the saved value back. The locked set (`VENDOR_PATHS`):
```text
/sys/module/mtk_fpsgo/parameters/perfmgr_enable → 0
/sys/module/perfmgr/parameters/perfmgr_enable → 0
/sys/module/perfmgr_policy/parameters/perfmgr_enable → 0
/sys/module/perfmgr_mtk/parameters/perfmgr_enable → 0
/sys/module/migt/parameters/glk_fbreak_enable → 0
/sys/module/migt/parameters/glk_disable → 1
/proc/game_opt/disable_cpufreq_limit → 1
```
Locking happens when entering a game session and unlocking when leaving it (see [Profile scheduler](/internals/profile-scheduler/#entering-a-whitelisted-game)). A failed mount-bind reverts the node’s permissions and logs a warning rather than aborting.
`VendorLock` calls `mount`/`umount2` and `chmod`s kernel nodes — a genuine trust boundary. It only touches the fixed `VENDOR_PATHS` list above, and only nodes that already exist.
## Likely to drift first
[Section titled “Likely to drift first”](#likely-to-drift-first)
`VENDOR_PATHS`, the general-tweak node lists and their values in `init.rs`, and the SoC-detection prefixes. These are hardware-specific and change most often. Re-verify against `src/core/tweaks/init.rs`, `vendor_lock.rs`, and `vendor/detect.rs`.
# Command Reference (auriyactl)
`auriyactl` is the command-line client for the Auriya daemon. It is a thin wrapper over the [IPC protocol](/internals/ipc-protocol/): almost every subcommand opens the daemon’s Unix socket, sends one text command, and prints the reply.
Traced to Auriya commit [`10fe7c6`](https://github.com/pavelc4/auriya/tree/10fe7c6b56474a00513fec34ebac1376b30e95e6). CLI definitions: [`src/cli/app.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/cli/app.rs) (clap subcommands), [`src/cli/executor.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/cli/executor.rs) (what each subcommand sends), [`src/cli/output.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/cli/output.rs) (formatting).
Note
The CLI is not the primary control surface — the manager app is. `auriyactl` wraps a **subset** of IPC commands and has no equivalent for `UPDATE_GAME`, `GET_FPS`-target editing, or per-field game edits. Use it for status checks, scripting, and debugging.
## Synopsis
[Section titled “Synopsis”](#synopsis)
```text
auriyactl [--socket ] [args]
```
* Running `auriyactl` with **no subcommand prints help and exits** non-zero (`arg_required_else_help = true`, `app.rs:5`).
* `--socket ` / `-s ` overrides the daemon socket. It is a **global** option — valid before or after the subcommand. Default: `/dev/socket/auriya.sock` (`SOCKET_PATH`, `src/common/constants.rs:1`; `executor.rs:14`).
### Liveness precondition
[Section titled “Liveness precondition”](#liveness-precondition)
Before running any subcommand **except `status` and `restart`**, the CLI checks that the socket is connectable; if not, it prints `Error: Daemon is not running` and exits (`executor.rs:17-19`). `status` degrades to a “Not running” banner instead of erroring; `restart` is a local operation that does not require a live daemon (see below).
## Subcommands
[Section titled “Subcommands”](#subcommands)
Each subcommand maps to a raw IPC command (or, for `restart`, to local shell actions). “Prints” describes stdout on success.
| Subcommand | Arguments | Sends (IPC) | Prints | Source |
| --------------- | ----------------------------------------- | ----------------------- | ---------------------------------------------------------- | --------------------------- |
| `status` | — | `STATUS` | Formatted status block (subset — see below) | `executor.rs:140-148` |
| `enable` | — | `ENABLE` | raw reply (`OK ENABLED`) | `executor.rs:34-37` |
| `disable` | — | `DISABLE` | raw reply (`OK DISABLED`) | `executor.rs:39-42` |
| `reload` | — | `RELOAD` | `Configuration reloaded: OK RELOADED ` | `executor.rs:44-47` |
| `restart` | — | *(none — local)* | `Restarting daemon + companion...` then a tail hint | `executor.rs:49`, `115-159` |
| `set-profile` | `` | `SET_PROFILE ` | `Profile set: OK SET_PROFILE ` | `executor.rs:51-56` |
| `set-fps` | `` (u32) | `SET_FPS ` | `FPS set: OK SET_FPS ` | `executor.rs:58-61` |
| `get-fps` | — | `GET_FPS` | `Current FPS: FPS= TARGET=` | `executor.rs:63-66` |
| `add-game` | `` | `ADD_GAME ` | `Game added: OK ADD_GAME ` | `executor.rs:68-71` |
| `remove-game` | `` | `REMOVE_GAME ` | `Game removed: OK REMOVE_GAME ` | `executor.rs:73-76` |
| `list-games` | — | `GET_GAMELIST` | `Configured games:` + JSON array | `executor.rs:78-81` |
| `list-packages` | — | `LIST_PACKAGES` | `Installed packages:` + `pm list packages` output | `executor.rs:83-86` |
| `get-rates` | — | `GET_SUPPORTED_RATES` | `Supported refresh rates:` + JSON array | `executor.rs:88-91` |
| `set-log` | `` | `SETLOG ` | `Log level set: OK SET_LOG` | `executor.rs:93-98` |
| `get-pid` | — | `GET_PID` | `Daemon PID: PKG= PID=` | `executor.rs:100-103` |
| `ping` | — | `PING` | `Daemon is alive (PONG)` or `Error: Daemon not responding` | `executor.rs:105-111` |
| `inject` | `` | `INJECT ` | `Injected: OK INJECT` | `executor.rs:113-116` |
| `clear-inject` | — | `CLEAR_INJECT` | `Inject cleared: OK CLEAR_INJECT` | `executor.rs:118-121` |
Subcommand names use kebab-case (clap derives them from the enum variants in `app.rs:14-52`); the raw socket protocol uses `SCREAMING_SNAKE_CASE`.
### `restart` is local, not an IPC command
[Section titled “restart is local, not an IPC command”](#restart-is-local-not-an-ipc-command)
`auriyactl restart` does **not** send the IPC `RESTART` command. It runs entirely in the CLI process (`handle_restart`, `executor.rs:115-159`):
1. `killall -TERM auriya AuriyaSysMon`, wait 3 s, then `killall -KILL` the same (`stop_processes`, `executor.rs:127-136`).
2. Remove `/data/adb/.config/auriya/system_status` and `companion.lock`, and truncate `/data/adb/auriya/daemon.log` (`clear_runtime_state`, `executor.rs:138-153`).
3. Spawn `sh /data/adb/modules/auriya/service.sh`, redirecting output to `/data/adb/auriya/restart.log` (`launch_service`, `executor.rs:155-159`).
Because it shells out to `killall` and the module’s `service.sh`, `restart` needs root and only works on an installed device — not in a bare build tree. The separate IPC `RESTART` command (used by the app) makes the *daemon* re-exec itself instead; see [IPC protocol](/internals/ipc-protocol/).
### `status` prints a subset of the daemon reply
[Section titled “status prints a subset of the daemon reply”](#status-prints-a-subset-of-the-daemon-reply)
The daemon’s raw `STATUS` reply contains `ENABLED`, `PACKAGES`, `OVERRIDE`, `LOG_LEVEL`, and multi-line telemetry (`FPS`, per-core CPU, GPU, thermal) — documented in [IPC protocol](/internals/ipc-protocol/). The CLI’s pretty-printer (`print_status`, `output.rs:1-40`) only renders four keys — `ENABLED`, `PROFILE`, `PACKAGES`, `FPS` — and silently drops the rest via its catch-all arm. Note `PROFILE` is matched by the printer but **not currently emitted** by the daemon’s `STATUS` response, so that line does not appear. To see everything the daemon returns, talk to the socket directly (see the raw example below).
## Examples
[Section titled “Examples”](#examples)
Real invocations and their output shape (values illustrative; format strings are exact, from `executor.rs`/`output.rs`):
```console
$ auriyactl ping
Daemon is alive (PONG)
$ auriyactl set-profile performance
Profile set: OK SET_PROFILE Performance
$ auriyactl get-fps
Current FPS: FPS=59.8 TARGET=60
$ auriyactl --socket /tmp/test.sock status
Auriya Daemon Status
Daemon: Running
Enabled: true
Games: 3 configured
FPS: 59.8 SOURCE=ebpf
```
Talking to the socket directly (bypasses the CLI’s subset view). The daemon sends a greeting line first, then the reply:
```console
$ printf 'STATUS\nQUIT\n' | nc -U /dev/socket/auriya.sock
OK AURIYA IPC
ENABLED=true PACKAGES=3 OVERRIDE=None LOG_LEVEL=Info
FPS=59.8 SOURCE=ebpf
CPU_CORES=8 CPU_LOAD=42
...
BYE
```
## Error output
[Section titled “Error output”](#error-output)
* Daemon not running (non-`status`/`restart` command): `Error: Daemon is not running`, non-zero exit (`executor.rs:18`).
* Any daemon-side failure is returned as an `ERR ...` line and printed verbatim inside the success wrapper (e.g. `Game added: ERR ADD_GAME "Game X already exists"`). The CLI does not currently translate `ERR` replies into a non-zero exit code for the send-and-print commands.
## Likely to drift first
[Section titled “Likely to drift first”](#likely-to-drift-first)
The subcommand-to-IPC mapping table and the `status` field subset. Re-verify against `src/cli/app.rs`, `src/cli/executor.rs`, and `src/cli/output.rs`.
# Filesystem Reference
Every path Auriya reads, writes, or installs, grouped by **when it exists**. This matters because the module archive’s layout is *not* the installed layout: `customize.sh` copies files to their runtime locations and then deletes the staging directory.
Traced to Auriya commit [`10fe7c6`](https://github.com/pavelc4/auriya/tree/10fe7c6b56474a00513fec34ebac1376b30e95e6). Path constants: [`src/common/constants.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/common/constants.rs) and [`src/core/config/path.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/core/config/path.rs). Install/boot layout: `module/customize.sh`, `module/service.sh`, `module/uninstall.sh`.
## Runtime paths (on an installed, running device)
[Section titled “Runtime paths (on an installed, running device)”](#runtime-paths-on-an-installed-running-device)
These exist after installation and boot.
### Configuration and runtime state — `/data/adb/.config/auriya/`
[Section titled “Configuration and runtime state — /data/adb/.config/auriya/”](#configuration-and-runtime-state--dataadbconfigauriya)
`CONFIG_DIR`, defined once in `src/common/constants.rs:3` and `src/core/config/path.rs:5`.
| Path | Written by | Read by | Purpose |
| ----------------- | ------------------------------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `settings.toml` | manager app | daemon (startup + watcher) | Global config. See [settings reference](/reference/settings/). `SETTINGS_FILE`, `constants.rs:4`. |
| `gamelist.toml` | manager app **and** daemon (IPC mutations) | daemon (startup + watcher) | Per-app whitelist/profiles. See [gamelist reference](/reference/gamelist/). `GAMELIST_FILE`, `constants.rs:5`. |
| `system_status` | companion service | daemon (`system_status` watcher) | Companion→daemon snapshot: focused app, screen/battery/zen state. Deleted at each boot by `service.sh` so the daemon only proceeds on fresh data. `STATUS_FILE`, `src/core/system_status`. |
| `companion.lock` | companion service (flock) | daemon (`companion_lock` watcher) | Liveness lock; the daemon watches its release to detect a dead companion. `src/daemon/companion_lock.rs:31-32`. |
| `current_profile` | daemon | external/legacy readers | Legacy status file holding `1`/`2`/`3`/`4` for Performance/Balance/Powersave/Fast. Best-effort compatibility output — **not** the authoritative UI state. `src/daemon/run.rs`. |
| `gpu_type` | `customize.sh` (install-time) | — | Detected GPU (`adreno`/`mali`/`unknown`), written once at install. `module/customize.sh` (`make_node`). |
| `arch` | `customize.sh` (install-time) | — | Detected device ABI, written once at install. |
### Daemon socket
[Section titled “Daemon socket”](#daemon-socket)
| Path | Purpose |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/dev/socket/auriya.sock` | Local Unix socket for all IPC (app + `auriyactl`). Created by the daemon on startup, removed by `service.sh` before restart and by `uninstall.sh`. `SOCKET_PATH`, `constants.rs:1`; bound at `src/daemon/run.rs:417`. See [IPC protocol](/internals/ipc-protocol/). |
### Logs — `/data/adb/auriya/`
[Section titled “Logs — /data/adb/auriya/”](#logs--dataadbauriya)
| Path | Purpose |
| ---------------------------------- | --------------------------------------------------------------------------------------------- |
| `daemon.log` | Daemon stdout/stderr, tee’d here by `service.sh`. `LOG_FILE`, `constants.rs:6`. |
| `daemon.log.1`, `daemon.log.2` | Rotated daemon logs (rotate when `daemon.log` exceeds 1 MB). `service.sh`. |
| `companion.log`, `companion.log.1` | Companion stdout/stderr and its single rotation. `service.sh`. |
| `restart.log` | Output of `service.sh` when relaunched by `auriyactl restart`. `src/cli/executor.rs:155-159`. |
| `daemon.log.old` | Previous boot’s `daemon.log`, renamed once at install. `customize.sh`. |
### Installed module tree — `/data/adb/modules/auriya/`
[Section titled “Installed module tree — /data/adb/modules/auriya/”](#installed-module-tree--dataadbmodulesauriya)
The module root after extraction. Runtime binaries live under `system/`, which the root manager mounts into the system image.
| Path | Purpose |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `system/bin/auriya` | **The daemon binary that actually runs.** Copied here from staging, `0755`. `customize.sh` (`cp "$DAEMON_BINARY" "$MODPATH/system/bin/auriya"`). |
| `system/bin/auriyactl` | Control CLI, if bundled (optional). `customize.sh`. |
| `system/etc/auriya/service.apk` | Installed companion APK; launched at boot via `app_process`. Required — the daemon refuses to start without the companion. `customize.sh`, `service.sh` (`COMPANION_APK`). |
| `service.sh` | Boot script: starts companion, waits for its status file, starts the daemon. Runs on every boot via the root manager’s `service.d` hook. |
| `uninstall.sh` | Cleanup script (also invoked mid-run when a `remove` flag appears). |
| `module.prop` | Module metadata (id, name, version, `updateJson` URL). `module/module.prop`. |
### Root-manager symlinks (conditional)
[Section titled “Root-manager symlinks (conditional)”](#root-manager-symlinks-conditional)
`customize.sh` symlinks the binaries into the active root manager’s `bin` directory **only if that directory exists**, so the binary is on `PATH`:
| Path | Condition |
| --------------------------------------------------------- | --------------------------------------------- |
| `/data/adb/ksu/bin/auriya`, `/data/adb/ksu/bin/auriyactl` | KernelSU present (`/data/adb/ksu/bin` exists) |
| `/data/adb/ap/bin/auriya`, `/data/adb/ap/bin/auriyactl` | APatch present (`/data/adb/ap/bin` exists) |
Magisk does not get symlinks; `system/bin` is already on `PATH` via the mounted overlay. All four are removed by `uninstall.sh`.
## Kernel interfaces (read/written by tweaks)
[Section titled “Kernel interfaces (read/written by tweaks)”](#kernel-interfaces-readwritten-by-tweaks)
Device-dependent; probed before use and skipped when absent. See [System tweaks](/internals/system-tweaks/).
| Path root | Purpose |
| --------- | ------------------------------------------------------------- |
| `/proc` | Kernel/process telemetry and a few control nodes. |
| `/sys` | CPU/GPU/scheduler/memory/thermal control and telemetry nodes. |
## ZIP-staging paths (inside the archive / during install only)
[Section titled “ZIP-staging paths (inside the archive / during install only)”](#zip-staging-paths-inside-the-archive--during-install-only)
These exist **only** inside the flashable ZIP and during `customize.sh`. The staging `libs/` directory is deleted after install (`rm -rf "$MODPATH/libs"`), and the root TOMLs are *moved* into `CONFIG_DIR` on first install.
| Path (relative to ZIP root) | Fate on install |
| ------------------------------- | ----------------------------------------------------------------------------------------------- |
| `libs/aarch64/auriya` | Verified (SHA256), copied to `system/bin/auriya`, then `libs/` removed. |
| `libs/aarch64/auriyactl` | Copied to `system/bin/auriyactl` if present, then removed. |
| `libs/aarch64/checksums.sha256` | Used for integrity check, then removed. |
| `libs/companion/service.apk` | Copied to `system/etc/auriya/service.apk`, then removed. |
| `libs/companion/auriya-app.apk` | `pm install`ed (manager app `dev.auriya.app`), then removed. Not required for the daemon. |
| `settings.toml` (ZIP root) | Moved to `CONFIG_DIR/settings.toml` **only if none exists**; otherwise the user’s copy is kept. |
| `gamelist.toml` (ZIP root) | Same move-if-absent behavior. |
A common mistake: the release ZIP and older docs show `libs/aarch64/auriya`, but that path **does not exist after installation**. The running daemon is `/data/adb/modules/auriya/system/bin/auriya` (or the root-manager symlink).
## Installed packages
[Section titled “Installed packages”](#installed-packages)
| Package | Role | Removed by |
| ---------------------- | -------------------------------------------------- | ------------------------------- |
| `dev.auriya.app` | Manager UI (Compose). `pm install`ed from staging. | `uninstall.sh` (`pm uninstall`) |
| `dev.auriya.service` | Companion service package identity. | `uninstall.sh` |
| `dev.auriya.app.debug` | Debug-variant manager, if installed. | `uninstall.sh` |
## What uninstall removes
[Section titled “What uninstall removes”](#what-uninstall-removes)
`uninstall.sh` stops `auriya` and the `AuriyaSysMon` companion, `pm uninstall`s the packages above, then deletes: `/dev/socket/auriya.sock`, `/data/adb/.config/auriya` (all config + runtime state), `/data/adb/auriya` (all logs), and the ksu/ap symlinks. The module directory itself is removed by the root manager.
## Likely to drift first
[Section titled “Likely to drift first”](#likely-to-drift-first)
Log rotation filenames, the conditional symlink paths, and the staging layout — they live in shell scripts (`module/*.sh`) that change independently of the Rust constants. Re-verify against `module/customize.sh`, `module/service.sh`, `module/uninstall.sh`, and `src/common/constants.rs`.
# gamelist.toml Reference
`gamelist.toml` is Auriya’s **per-app** configuration: the whitelist of Android packages that receive a managed performance profile, plus the per-package overrides applied while that app is in the foreground. Global defaults live in [`settings.toml`](/reference/settings/) instead.
Every claim is traced to Auriya commit [`10fe7c6`](https://github.com/pavelc4/auriya/tree/10fe7c6b56474a00513fec34ebac1376b30e95e6). Schema: [`src/core/config/gamelist.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/core/config/gamelist.rs). Runtime consumption: [`src/daemon/tick.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/daemon/tick.rs).
## Location and ownership
[Section titled “Location and ownership”](#location-and-ownership)
| Fact | Value | Source |
| ------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| Installed path | `/data/adb/.config/auriya/gamelist.toml` | `src/core/config/path.rs:11-13` (`gamelist_path()`) |
| Passed to daemon as | `auriya --gamelist ` | `module/service.sh` (daemon launch line) |
| Format | TOML array of tables (`[[game]]`) | `GameList` = `Vec`, `gamelist.rs:83-102` |
| Written by | manager app (`TomlParser.serializeGameList`) **and** the daemon itself on IPC mutation | `TomlParser.kt:198-219`; `gamelist.rs:118-130` |
| Read by | the daemon, cached as a package whitelist and consulted every tick | `src/daemon/run.rs:212-217`, `tick.rs:222` |
Unlike `settings.toml`, this file is **mutated at runtime by the daemon**. When a client sends `ADD_GAME`, `REMOVE_GAME`, or `UPDATE_GAME` over IPC, the daemon edits its in-memory list and writes the whole file back to disk (see [Mutations](#how-entries-are-added-and-changed)).
## The shipped default file
[Section titled “The shipped default file”](#the-shipped-default-file)
Exact `gamelist.toml` bundled in the module ZIP (copied to `/data/adb/.config/auriya/gamelist.toml` on first install when no user config exists):
```toml
[[game]]
package = "com.mobile.legends"
cpu_governor = "performance"
enable_dnd = true
target_fps = 120
[[game]]
package = "com.supercell.clashroyale"
cpu_governor = "schedutil"
enable_dnd = false
target_fps = 60
[[game]]
package = "com.tencent.ig"
cpu_governor = "performance"
enable_dnd = true
target_fps = 120
```
Each `[[game]]` block is one entry. `package`, `cpu_governor`, and `enable_dnd` are present on every shipped entry; `target_fps` is optional and the other override fields (`refresh_rate`, `mode`, `ceiling`) are simply omitted here.
## Loading behavior
[Section titled “Loading behavior”](#loading-behavior)
`GameList::load` (`gamelist.rs:104-117`) differs from settings loading in one important way:
* **A missing file is not fatal.** If `gamelist.toml` does not exist, the daemon logs `Gamelist file not found, using empty list` and starts with zero managed packages (`gamelist.rs:108-111`). Compare `settings.toml`, whose absence aborts startup.
* **A malformed file *is* fatal.** If the file exists but fails to parse, `toml::from_str` errors and startup fails (`gamelist.rs:113-116`).
* Like settings, there is no `deny_unknown_fields`, so unknown keys inside a `[[game]]` block are silently ignored.
At startup the daemon builds a `HashSet` of package names (the “whitelist”) from this list (`src/daemon/run.rs:212-217`). On a gamelist file change the whitelist is rebuilt and tracked package/PID state is cleared (`Daemon::rebuild_whitelist`, `run.rs:320-327`).
## Field reference
[Section titled “Field reference”](#field-reference)
Defined by `GameProfile`, `gamelist.rs:89-102`. The **Consumed** column uses the same legend as the [settings reference](/reference/settings/#key-by-key-reference): **Yes** (read and effective), **No** (parsed but unused).
| Key | Type | Required | Default when omitted | Consumed | Meaning & evidence |
| -------------- | ---------------------------- | -------- | ------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `package` | string | **Yes** | — | Yes | Exact Android package name, e.g. `com.tencent.ig`. This is the whitelist key matched against the companion’s focused package (`tick.rs:222`, `gamelist.find(pkg)`). No wildcards, no partial match. |
| `cpu_governor` | string | **Yes** | — | Yes | CPU governor applied for this game. Passed straight to the profile writer (`tick.rs:223-225`). An empty string falls back to the global `balance_governor` (`tick.rs:266-269`). Raw kernel governor name; not validated against the device. |
| `enable_dnd` | bool | **Yes** | — | Yes | `true` → request Priority Do-Not-Disturb while foreground; `false` → All/normal notifications (`tick.rs:296-300`). If a client omits it during mutation, the daemon treats it as `true` (`tick.rs:226`). |
| `target_fps` | integer **or** integer array | No | `None` (FAS keeps its current target) | Yes | FAS frame-rate target. Accepts **two shapes** — see [The `target_fps` field](#the-target_fps-field-single-value-or-array) below. Applied to the FAS controller when set (`tick.rs:159-170`). |
| `refresh_rate` | integer (Hz) | No | `None` (no display override) | Yes | Requested display refresh rate while foreground. Applied only when it differs from the currently applied rate (`tick.rs:287-293`); released back to automatic on exit by requesting `0` (`tick.rs:315-320`). |
| `mode` | string | No | `None` → **Performance** | Yes | Profile for this game. Parsed **case-insensitively**: `powersave` → Powersave, `balance` → Balance, `fast` → Fast, **any other value *or* missing → Performance** (`tick.rs`). So a typo like `mode = "perf"` silently resolves to Performance, not an error. |
| `ceiling` | string | No | `None` (no ceiling override) | Yes | Frequency-ceiling level for this game. Parsed to `CeilingLevel`; an **unparseable value is dropped to no-override**, not an error (`tick.rs:282-285`). |
Note
`mode` values map to `ProfileMode` (`src/common/types.rs:14-16`). `ceiling` values map to `CeilingLevel` (`src/core/tweaks/ceiling.rs:20-24`). For what each profile actually writes to the kernel, see [Architecture overview → What each static profile changes](/architecture/overview/#what-each-static-profile-changes).
### The `target_fps` field: single value or array
[Section titled “The target\_fps field: single value or array”](#the-target_fps-field-single-value-or-array)
`target_fps` has a **custom deserializer** (`TargetFpsConfig`, `gamelist.rs:4-60`) that accepts either form:
```toml
# Single fixed target
target_fps = 120
# Array of candidate targets (adaptive)
target_fps = [60, 90, 120]
```
* A bare integer deserializes to `TargetFpsConfig::Single` (`gamelist.rs:32-44`).
* A TOML array deserializes to `TargetFpsConfig::Array` (`gamelist.rs:46-55`).
* If the key is absent the profile stores `None`; the default value of the type itself is `Single(60)` (`gamelist.rs:10-14`).
Both forms are passed to the FAS buffer via `to_buffer_config()` (`gamelist.rs:74-81`). The array form is how a game exposes multiple acceptable frame-rate steps to Frame-Aware Scheduling; the single form pins one target.
## How entries are added and changed
[Section titled “How entries are added and changed”](#how-entries-are-added-and-changed)
There is **no direct-edit CLI** for individual fields; entries are mutated over the IPC socket (by the manager app, or by `auriyactl` for the subset it wraps — see [Command reference](/reference/commands/) and [IPC protocol](/internals/ipc-protocol/)). Each mutation rewrites the whole file.
### `ADD_GAME ` — injected defaults
[Section titled “ADD\_GAME \ — injected defaults”](#add_game-package--injected-defaults)
Adding a package via IPC does **not** copy the shipped example values. It inserts a fixed default profile (`src/daemon/ipc/handlers.rs:208-217`):
| Field | Value injected by `ADD_GAME` |
| --------------------------------------- | ---------------------------- |
| `cpu_governor` | `"performance"` |
| `enable_dnd` | `true` |
| `mode` | `"performance"` |
| `target_fps`, `refresh_rate`, `ceiling` | unset (`None`) |
Adding a package that already exists returns an error (`ADD_GAME` → `add()` bails, `gamelist.rs:136-143`).
### `UPDATE_GAME [key=value ...]` — partial edit
[Section titled “UPDATE\_GAME \ \[key=value ...\] — partial edit”](#update_game-package-keyvalue---partial-edit)
`UPDATE_GAME` changes only the fields you name; unspecified fields are left as-is (`GameList::update`, `gamelist.rs:155-182`). Recognized tokens (`src/daemon/ipc/commands.rs`, `UpdateGame` parsing):
| Token | Sets field | Notes |
| ------------------- | ----------------------------- | ------------------------------------------------ |
| `gov=` | `cpu_governor` | — |
| `dnd=` | `enable_dnd` | unparseable value falls back to `true` |
| `fps=` | `target_fps` = `Single(n)` | ignored if `fps_array` is also given |
| `fps_array=` | `target_fps` = `Array([...])` | takes precedence over `fps=`; empty list ignored |
| `rate=` | `refresh_rate` | — |
| `mode=` | `mode` | — |
| `ceiling=` | `ceiling` | — |
Updating a package that is not in the list returns an error (`gamelist.rs:178`).
### Persistence
[Section titled “Persistence”](#persistence)
Every successful `ADD_GAME` / `REMOVE_GAME` / `UPDATE_GAME` calls `GameList::save`, which writes **atomically**: serialize to `gamelist.toml.tmp`, then `rename` over the real file (`gamelist.rs:118-130`). A crash mid-write cannot leave a half-written `gamelist.toml`. Note that a save re-serializes the entire list, so any hand-added comments or unknown keys are lost on the next mutation.
## Ordering and duplicates
[Section titled “Ordering and duplicates”](#ordering-and-duplicates)
* Entries are a `Vec`, searched linearly by `find()` (`gamelist.rs:132-134`), so file **order is preserved** on save.
* `ADD_GAME` refuses to insert a package that already exists.
* If you **hand-edit** the file to contain the same `package` twice, parsing succeeds and the daemon uses the **first** match; `REMOVE_GAME` then deletes **all** entries with that name (`retain`, `gamelist.rs:147`).
## Likely to drift first
[Section titled “Likely to drift first”](#likely-to-drift-first)
* The `ADD_GAME` default profile (`handlers.rs:208-217`).
* The `UPDATE_GAME` token list (`commands.rs`).
* `mode` / `ceiling` accepted values, if new profiles or ceiling levels are added.
Re-verify against `src/core/config/gamelist.rs`, `src/daemon/tick.rs`, and `src/daemon/ipc/`.
# LLM Context & llms.txt
> Machine-readable documentation formats for AI models, agents, and IDE extensions.
Auriya provides standardized machine-readable documentation compliant with the [llmstxt.org](https://llmstxt.org) specification. These endpoints allow Large Language Models (LLMs), AI coding assistants, and autonomous agents to index and reference Auriya’s technical documentation directly.
## Available Endpoints
[Section titled “Available Endpoints”](#available-endpoints)
[llms.txt](/llms.txt)Standardized root index and documentation catalog for AI discovery.
[llms-small.txt](/llms-small.txt)Abridged context containing essential architecture, APIs, and tuning guides.
[llms-full.txt](/llms-full.txt)Comprehensive complete documentation concatenated into a single Markdown file.
***
## Integration Guide
[Section titled “Integration Guide”](#integration-guide)
### 1. In AI IDEs & Agents
[Section titled “1. In AI IDEs & Agents”](#1-in-ai-ides--agents)
Add Auriya’s documentation as a custom doc source in your AI editor (Cursor, Windsurf, Claude Code, Antigravity, GitHub Copilot):
* Index URL (Recommended)
```text
https://auriya.pages.dev/llms.txt
```
* Full Context File
```text
https://auriya.pages.dev/llms-full.txt
```
### 2. Prompt Ingestion via cURL / Terminal
[Section titled “2. Prompt Ingestion via cURL / Terminal”](#2-prompt-ingestion-via-curl--terminal)
Fetch documentation context directly inside terminal sessions or pipe into local AI CLI tools:
* cURL to File
```bash
# Download complete documentation for offline LLM prompting
curl -sSL https://auriya.pages.dev/llms-full.txt -o auriya_docs.md
```
* Direct Pipe to LLM CLI
```bash
# Pipe abridged context directly to your CLI runner
curl -sSL https://auriya.pages.dev/llms-small.txt | llm "Explain Auriya FAS dynamic scheduling"
```
***
## Automatic Generation
[Section titled “Automatic Generation”](#automatic-generation)
These files are built automatically during every deployment using `starlight-llms-txt`. Any updates to guides, configuration keys, or kernel tweaks are immediately synchronized across all LLM endpoints.
# settings.toml Reference
`settings.toml` is Auriya’s **global** configuration: daemon-wide defaults that apply regardless of which app is in the foreground. Per-app behavior lives in [`gamelist.toml`](/reference/gamelist/) instead.
Every claim on this page is traced to Auriya commit [`10fe7c6`](https://github.com/pavelc4/auriya/tree/10fe7c6b56474a00513fec34ebac1376b30e95e6). The Rust type that defines the schema is [`src/core/config/settings.rs`](https://github.com/pavelc4/auriya/blob/10fe7c6b56474a00513fec34ebac1376b30e95e6/src/core/config/settings.rs). Re-verify this page if that file, `settings.toml`, or `android/shared/src/main/kotlin/dev/auriya/shared/config/TomlParser.kt` changes.
## Location and ownership
[Section titled “Location and ownership”](#location-and-ownership)
| Fact | Value | Source |
| ------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| Installed path | `/data/adb/.config/auriya/settings.toml` | `src/core/config/path.rs:5-9` (`CONFIG_DIR` + `settings_path()`) |
| Passed to daemon as | `auriya --settings ` | `module/service.sh` (daemon launch line) |
| Format | TOML | parsed by `toml::from_str` in `Settings::load`, `settings.rs:96-103` |
| Written by | the **manager app** (Kotlin `TomlParser.serializeSettings`), `TomlParser.kt:109-134` | — |
| Read by | the **Rust daemon** at startup and on file change | `main.rs:11`, `src/daemon/run.rs:288-317` |
Note
As of this revision the CLI (`auriyactl`) has **no** command that edits `settings.toml`. The file is written by the manager app and re-read by the daemon (some keys live, most at startup — see [Reload behavior](#reload-behavior)). The Rust and Kotlin schemas must stay in sync — see [Schema sync](#schema-sync-rust--app).
## The shipped default file
[Section titled “The shipped default file”](#the-shipped-default-file)
This is the exact `settings.toml` bundled in the module ZIP (repository root, copied to `/data/adb/.config/auriya/settings.toml` on first install by `module/customize.sh` only when no user config exists):
```toml
[daemon]
log_level = "info"
check_interval_ms = 2000
default_mode = "balance"
[cpu]
default_governor = "schedutil"
[dnd]
default_enable = true
[fas]
enabled = true
default_mode = "balance"
thermal_threshold = 90.0
poll_interval_ms = 300
target_fps = 60
[dynamic_governor]
enabled = true
cv_threshold = 0.15
debounce_frames = 3
[modes.powersave]
margin = 5.0
thermal_threshold = 80.0
[modes.balance]
margin = 2.0
thermal_threshold = 90.0
[modes.performance]
margin = 1.0
thermal_threshold = 95.0
[modes.fast]
margin = 0.0
thermal_threshold = 95.0
```
## How the file is loaded
[Section titled “How the file is loaded”](#how-the-file-is-loaded)
`Settings::load` reads the file and calls `toml::from_str` with **no** `#[serde(deny_unknown_fields)]` (`settings.rs:6`, `96-103`). Two consequences, both verified:
1. **Unknown keys are silently discarded.** A key the `Settings` struct does not declare parses without error and is dropped. You get no warning.
2. **Sections without a serde default are mandatory.** If a required section is missing, `toml::from_str` returns an error, `main` returns before the daemon starts, and startup fails.
### Which sections are required to start
[Section titled “Which sections are required to start”](#which-sections-are-required-to-start)
| Section | Required at startup? | Why | Source |
| -------------------- | ----------------------- | -------------------------------------------- | ---------------------------- |
| `[daemon]` | Optional | field-level `#[serde(default)]` on every key | `settings.rs:8-9`, `20-30` |
| `[cpu]` | **Required** | no serde default on the field or struct | `settings.rs:11`, `33-35` |
| `[dnd]` | **Required** | no serde default | `settings.rs:11`, `38-40` |
| `[fas]` | **Required** | no serde default | `settings.rs:12`, `43-49` |
| `[dynamic_governor]` | Optional | `#[serde(default)]` + `impl Default` | `settings.rs:13-14`, `67-75` |
| `[ceiling]` | Optional | `#[serde(default)]` + `impl Default` | `settings.rs:15-16`, `85-93` |
| `[modes.*]` | **Required (≥1 table)** | `modes: HashMap` has no serde default | `settings.rs:17` |
Note
`Settings.modes` has no `#[serde(default)]`, so **at least one** `[modes.X]` table must exist or the daemon refuses to start. The mode named by `fas.default_mode` is the one whose `margin`/`thermal_threshold` drive FAS; the others are inactive until selected.
## Key-by-key reference
[Section titled “Key-by-key reference”](#key-by-key-reference)
Legend for the **Consumed** column:
* **Yes** — the daemon reads this value and it affects behavior.
* **No** — parsed into memory but never read by the daemon (no effect if changed).
### `[daemon]`
[Section titled “\[daemon\]”](#daemon)
Defined by `DaemonConfig`, `settings.rs:20-30`.
| Key | Type | Default | Consumed | Meaning & evidence |
| ------------------- | ------------ | ----------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `log_level` | string | `"info"` | Yes | `tracing` env-filter directive applied **at startup** (`main.rs:13-14`, `EnvFilter::new(level)`). Accepts anything `EnvFilter` accepts (`error`/`warn`/`info`/`debug`/`trace`, or per-target like `auriya::daemon=debug`). **Not** re-read on file reload — change the running level with the IPC `SETLOG` command instead (`src/daemon/run.rs:378-392`). |
| `check_interval_ms` | integer (ms) | `2000` | Yes | Idle/foreground tick cadence. Feeds `Daemon::normal_interval_ms` (clamped ≥100 ms), used in the event-loop sleep selection (`src/daemon/run.rs`). Re-read on reload. The in-game (500 ms) and screen-off (10 s) cadences stay fixed. |
| `default_mode` | string | `"balance"` | Yes | The profile applied when no whitelisted game is foreground. Parsed via `ProfileMode::from_str`; unrecognized values fall back to `Balance` (`src/daemon/run.rs:164-170`). Re-read on reload (`run.rs`). Valid: `fast`, `performance`, `balance`, `powersave` (`src/common/types.rs`). |
### `[cpu]`
[Section titled “\[cpu\]”](#cpu)
Defined by `CpuConfig`, `settings.rs:33-35`.
| Key | Type | Default | Consumed | Meaning & evidence |
| ------------------ | ------ | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `default_governor` | string | none (**required**) | Yes | The CPU governor written when the Balance profile is applied (the daemon’s `balance_governor`, `src/daemon/run.rs:163`). On reload, if it changed **and** the current profile is Balance, it is re-applied immediately (`run.rs:290-300`). Value is a raw governor name written to the kernel (e.g. `schedutil`, `walt`); Auriya does not validate it against the device’s available governors. |
### `[dnd]`
[Section titled “\[dnd\]”](#dnd)
Defined by `DndConfig`, `settings.rs:38-40`.
| Key | Type | Default | Consumed | Meaning & evidence |
| ---------------- | ---- | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `default_enable` | bool | none (**required**) | Yes | Default `enable_dnd` for a game created via IPC `ADD_GAME` (`src/daemon/ipc/handlers.rs`, snapshotted into `IpcHandles.dnd_default`). Per-game DnD in `gamelist.toml` overrides it once a game has an explicit value. |
### `[fas]`
[Section titled “\[fas\]”](#fas)
Frame-Aware Scheduling. Defined by `FasConfig`, `settings.rs:43-49`.
| Key | Type | Default | Consumed | Meaning & evidence |
| ------------------- | ------------ | ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | bool | none (**required**) | Yes | Master switch for FAS. When `true` **and** the eBPF frame stream initialized, the daemon runs the `FasController`; when `false`, FAS scaling is bypassed (`src/daemon/tick.rs`). |
| `default_mode` | string | none (**required**) | Yes | Selects which `[modes.*]` entry is active, supplying the FAS `margin` and (preferentially) thermal ceiling (`FasTuning::from_settings`, `src/daemon/fas.rs`). Unknown name → default margin + `fas.thermal_threshold` fallback (logged). |
| `thermal_threshold` | float (°C) | none (**required**) | Yes | Fallback skin-temp ceiling for FAS `Reduce`, used when the active `[modes.*]` entry omits its own `thermal_threshold` (`FasTuning::from_settings`). |
| `poll_interval_ms` | integer (ms) | `100` | Yes | eBPF frame-poll deadline, clamped to `[1, 500]` ms (`EbpfFrameStream::new`, `src/core/ebpf.rs`). |
| `target_fps` | integer | `60` | Yes | Global FAS target when a game has no per-game `target_fps` (`FasConfig.target_fps` → `FasController` construction, `src/daemon/run.rs`). Per-game `target_fps` in `gamelist.toml` still overrides it at runtime. |
### `[dynamic_governor]`
[Section titled “\[dynamic\_governor\]”](#dynamic_governor)
Defined by `DynamicGovernorConfig`, `settings.rs:58-65`.
| Key | Type | Default | Consumed | Meaning & evidence |
| ----------------- | ------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `enabled` | bool | `true` | Yes | When `false`, FAS skips bottleneck classification and treats every boost as `BoostBalanced` (full profile) instead of CPU/GPU-targeted (`FasController::tick`, `src/daemon/fas.rs`). |
| `cv_threshold` | float | `0.15` | Yes | Coefficient-of-variation split between GPU- and CPU-bound classification. Threaded into `BottleneckDetector::new` via `FasTuning` (`src/daemon/fas.rs`). |
| `debounce_frames` | integer | `3` | Yes | Frames a new bottleneck class must persist before it is accepted (`BottleneckDetector::new` via `FasTuning`). |
### `[ceiling]`
[Section titled “\[ceiling\]”](#ceiling)
Frequency-ceiling override applied outside game sessions / in power-save. Defined by `CeilingConfig`, `settings.rs:78-83`. **Absent from the shipped file**, so it currently runs entirely on defaults.
| Key | Type | Default | Consumed | Meaning & evidence |
| --------------------- | ----------------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
| `default` | string | `"balance"` | Yes | Ceiling level parsed to `CeilingLevel`; unrecognized → `Balance` (`src/daemon/run.rs:220-225`). |
| `low_freq_little_khz` | integer (kHz) or absent | `None` | Yes | Little-cluster max frequency used by the Low ceiling (`run.rs:226`, consumed in `src/core/tweaks/ceiling.rs:283`). |
| `low_freq_big_khz` | integer (kHz) or absent | `None` | Yes | Big-cluster equivalent (`run.rs:227`, `ceiling.rs:286`). |
### `[modes.*]`
[Section titled “\[modes.\*\]”](#modes)
A TOML table per mode name, deserialized into `HashMap` (`FasMode`, `settings.rs:52-55`).
| Key | Type | Default | Consumed | Meaning & evidence |
| ------------------- | ----------- | ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `margin` | float (fps) | none (required per table) | Yes | FPS headroom subtracted from the target for the **active** mode (the one named by `fas.default_mode`). Higher margin biases FAS toward boosting. Fed to `FasController` via `FasTuning` (`src/daemon/fas.rs`). |
| `thermal_threshold` | float (°C) | none (required per table) | Yes | Skin-temp ceiling for the active mode; above it FAS forces `Reduce`. Overrides `fas.thermal_threshold` when the active mode defines it. |
The shipped file defines four modes (`powersave`, `balance`, `performance`, `fast`). `margin`/`thermal_threshold` drive FAS tuning.
## Reload behavior
[Section titled “Reload behavior”](#reload-behavior)
The settings watcher reacts to runtime edits of `settings.toml`. Verified in `Daemon::reload_settings` (`src/daemon/run.rs`):
| Key | Re-read on file change? | Effect |
| -------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------ |
| `cpu.default_governor` | Yes | Updates `balance_governor`; re-applies immediately only if the current profile is Balance. |
| `daemon.default_mode` | Yes | Updates the fallback profile for the next tick. |
| `daemon.check_interval_ms` | Yes | Updates the idle/foreground tick cadence for the next loop iteration. |
| `[fas]` / `[dynamic_governor]` / `[modes.*]` | Yes | Re-tunes `FasController` live via `FasController::set_tuning`. |
| everything else | No | Applied at startup (including `log_level` — use `SETLOG` over IPC). |
## Schema sync (Rust ↔ app)
[Section titled “Schema sync (Rust ↔ app)”](#schema-sync-rust--app)
The manager app’s `TomlParser.kt` parses **and re-serializes every key above** (`TomlParser.kt` parse + serialize), so a settings save from the app rewrites the full key set. The Rust `Settings` struct and the Kotlin model/parser must stay in sync: adding or removing a key means editing **both** sides (plus the shipped `settings.toml`), or the app will silently re-add what only Rust dropped. There is no `#[serde(deny_unknown_fields)]`, so a key present in one schema but not the other is ignored rather than erroring.
Note
`[fas]`, `[dynamic_governor]`, and the active `[modes.*]` entry are resolved into the `FasController` once at construction (`FasTuning::from_settings`). Editing them at runtime has no effect until `auriyactl restart`.
## Likely to drift first
[Section titled “Likely to drift first”](#likely-to-drift-first)
* Per-key **evidence** references — they point to functions (`FasTuning::from_settings`, `EbpfFrameStream::new`, `Daemon::reload_settings`) rather than line numbers, but re-verify if those move.
* The `[modes.*]` semantics and the `fast` preset, if FAS gains real per-mode profiles.
Re-verify against `src/core/config/settings.rs`, `src/daemon/run.rs`, `src/daemon/fas.rs`, and `TomlParser.kt`.
# Telemetry Protocol & FPS Recorder (GET_STATS)
Auriya exposes an internal performance snapshot that the manager app polls to render its telemetry cards (FPS, temps, battery, CPU/GPU clocks) and to drive per-game FPS recording.
The `GET_STATS` payload is an **internal IPC communication format** designed specifically between the Auriya Rust daemon and the Auriya Android manager app. It is not intended as a generic plug-and-play public API for external projects without adaptation.
## Transport
[Section titled “Transport”](#transport)
| Fact | Value |
| -------- | ------------------------------------------------------------------------------------ |
| Command | `GET_STATS` (alias `GETSTATS`) |
| Channel | Unix socket `/dev/socket/auriya.sock` (see [IPC protocol](/internals/ipc-protocol/)) |
| Response | one line of JSON, then the connection closes on `QUIT` |
| Cost | computed **on request** — the daemon accumulates nothing between polls |
From a root shell:
```console
$ printf 'GET_STATS\nQUIT\n' | nc -U /dev/socket/auriya.sock
OK AURIYA IPC
{"fps":{"avg":118.0,...},"thermal":{...},...}
BYE
```
## JSON schema (one group = one UI card)
[Section titled “JSON schema (one group = one UI card)”](#json-schema-one-group--one-ui-card)
```json
{
"fps": { "avg": 118.0, "peak": 258.9, "low_1pct": 78.5, "jank": 2, "frames": 600 },
"thermal": { "cpu_c": 64.7, "gpu_c": null, "battery_c": 41.5 },
"battery": { "pct": 100, "current_ma": 573, "voltage_v": 4.23, "status": "Charging", "health": "Good" },
"cpu": { "load_pct": 60.0, "cores": [ { "id": 0, "khz": 1804800, "gov": "walt", "cluster": "Little", "online": true } ] },
"gpu": { "mhz": 580, "load_pct": null, "vendor": "kgsl" },
"session": { "pkg": "com.mobile.legends", "profile": "performance", "active": true }
}
```
### Field reference
[Section titled “Field reference”](#field-reference)
| Group | Field | Meaning |
| --------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `fps` | `avg` | Mean FPS over the window (`1 / mean frametime`). |
| | `peak` | Fastest single frame (`1 / min frametime`). |
| | `low_1pct` | Mean FPS of the worst 1% of frames — the stutter metric. |
| | `jank` | Frames slower than `target × 1.5`. |
| | `frames` | Sample count the window was computed from. |
| `thermal` | `cpu_c` / `gpu_c` / `battery_c` | °C. `battery_c` lives here (it is a temperature), not in `battery`. |
| `battery` | `pct` | Charge 0–100 %. |
| | `current_ma` | Instantaneous current. **Sign is device-specific** — use `status`, not the sign, for direction. |
| | `voltage_v` | Terminal voltage. |
| | `status` / `health` | e.g. `Charging` / `Good`. |
| `cpu` | `load_pct` | Overall CPU load. |
| | `cores[]` | Per-core `id`, `khz` (clock), `gov` (governor), `cluster` (`Little`/`Big`/`Prime`), `online`. |
| `gpu` | `mhz` / `load_pct` / `vendor` | GPU clock, busy %, driver. |
| `session` | `pkg` | Foreground package. |
| | `profile` | Active profile (`performance`/`balance`/`powersave`). |
| | `active` | **`true` only when a whitelisted game with a live PID is running.** When `true`, `pkg` is that game. This is the record trigger. |
### Null rules (must handle)
[Section titled “Null rules (must handle)”](#null-rules-must-handle)
* **`fps` is `null`** when no game is running (idle) — render the FPS card as “inactive”, not “0”.
* **Any field can be `null`** when the device does not expose that node (e.g. `gpu_c`, `gpu.load_pct` on some SoCs). Each card renders independently and skips null fields — never crash on a null.
* **`session.active == false`** means no managed game (could be another app in the foreground). Only `active == true` is a game session.
## Recommended access method (for the UI)
[Section titled “Recommended access method (for the UI)”](#recommended-access-method-for-the-ui)
The socket is root-only (app-uid cannot open it under SELinux), so poll it through the app’s existing libsu root shell — the same pattern `OverlayService` already uses. **Do not write a raw `LocalSocket` client.**
```kotlin
// Persistent root shell (libsu). Poll on a coroutine loop.
fun fetchStats(): Stats? {
val raw = RootShell.run("printf 'GET_STATS\\nQUIT\\n' | timeout 2 nc -U /dev/socket/auriya.sock")
val json = raw?.lineSequence()?.firstOrNull { it.startsWith("{") } ?: return null
return Json { ignoreUnknownKeys = true }.decodeFromString(json) // kotlinx.serialization
}
```
* Poll cadence: reuse the existing `update_interval_ms` pref (default 1000 ms). \~1 Hz is the design point; higher is unnecessary and the daemon computes on-request anyway.
* Parse with `ignoreUnknownKeys = true` so future daemon fields never break the UI.
## FPS auto-record (app-side)
[Section titled “FPS auto-record (app-side)”](#fps-auto-record-app-side)
Auto-record is orchestrated **in the manager app**, not the daemon — `GET_STATS` already provides everything needed:
* **Enable/disable per whitelisted game** — store as an app preference keyed by package. Do **not** add a field to `gamelist.toml`; the daemon does not consume it and it would be dead config. The whitelist itself *is* `gamelist.toml`.
* **Trigger** — watch `session.active`. On `false → true` (and the game has auto-record enabled), start a recording buffer for `session.pkg`; while `true`, append each poll’s `fps` (plus any telemetry you want) with a timestamp; on `true → false`, finalize a session summary (avg, min `low_1pct`, max `cpu_c`, total `jank`, duration).
* **Run it in a foreground service** (like `OverlayService`) so recording continues while the game — not the app UI — is in the foreground.
* **Store recordings in the app’s own sandbox** (`filesDir` / Room / DataStore). Never write under `/data/adb`.
### Resolution ceiling
[Section titled “Resolution ceiling”](#resolution-ceiling)
Poll-based recording is \~1 sample/second (coarse) — right for a session FPS graph and summary. **Per-frame traces are not available this way**; that would require a new daemon streaming API (out of scope today). Flag it if the product needs frame-level detail.
## See also
[Section titled “See also”](#see-also)
* [IPC protocol](/internals/ipc-protocol/) — the socket and every command.
* [FPS detection](/internals/fps-detection/) — where the FPS numbers come from.
* [Performance tuning](/getting-started/performance-tuning/) — FAS modes & values.