├── module ├── NOTICES ├── README_CN.md ├── .gitignore ├── README_EN.md ├── META-INF │ └── com │ │ └── google │ │ └── android │ │ ├── updater-script │ │ └── update-binary ├── system.prop ├── games.toml ├── vtools │ ├── init_vtools.sh │ ├── powercfg.sh │ └── gen_json.sh ├── uninstall.sh ├── service.sh └── customize.sh ├── update ├── update_ebpf.json ├── update_zygisk.json ├── update_ebpf_en.json ├── update_zygisk_en.json ├── zh-CN │ └── changelog.md ├── en-US │ └── changelog.md ├── update.json └── update_en.json ├── rust-toolchain.toml ├── .vim └── coc-settings.json ├── webui ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── layout.tsx │ │ ├── client-layout.tsx │ │ └── page.tsx │ ├── lib │ │ ├── utils.ts │ │ ├── api.ts │ │ ├── kernelsu.d.ts │ │ └── kernelsu.js │ ├── i18n.d.ts │ ├── components │ │ ├── theme-provider.tsx │ │ ├── config │ │ │ ├── PowerModes.tsx │ │ │ ├── DeleteGameDialog.tsx │ │ │ ├── GeneralConfig.tsx │ │ │ ├── ModeSwitch.tsx │ │ │ ├── PowerModeSettings.tsx │ │ │ └── GameItem.tsx │ │ ├── ui │ │ │ ├── input.tsx │ │ │ ├── switch.tsx │ │ │ ├── popover.tsx │ │ │ ├── tabs.tsx │ │ │ ├── slider.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── combobox.tsx │ │ │ ├── dialog.tsx │ │ │ ├── alert-dialog.tsx │ │ │ └── command.tsx │ │ ├── LanguageSwitcher.tsx │ │ └── Navbar.tsx │ ├── i18n.ts │ ├── types │ │ └── config.ts │ └── locales │ │ ├── zh │ │ └── common.json │ │ └── en │ │ └── common.json ├── pnpm-workspace.yaml ├── postcss.config.mjs ├── public │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ ├── next.svg │ └── icon.svg ├── next.config.ts ├── components.json ├── README.md ├── .gitignore ├── tsconfig.json ├── eslint.config.mjs └── package.json ├── .gitignore ├── .vscode └── settings.json ├── .zed └── settings.json ├── NOTICES ├── LICENSE_HEADER ├── .cargo └── config.toml ├── src ├── framework │ ├── prelude.rs │ ├── config │ │ ├── data │ │ │ ├── default.rs │ │ │ └── mod.rs │ │ ├── inner.rs │ │ ├── merge.rs │ │ ├── read.rs │ │ └── mod.rs │ ├── scheduler │ │ ├── looper │ │ │ ├── policy │ │ │ │ ├── mod.rs │ │ │ │ └── controll.rs │ │ │ ├── clean.rs │ │ │ └── buffer │ │ │ │ ├── calculate.rs │ │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── thermal.rs │ ├── pid_utils.rs │ ├── mod.rs │ ├── node │ │ ├── power_mode.rs │ │ └── mod.rs │ ├── extension │ │ ├── api │ │ │ ├── misc.rs │ │ │ ├── v0.rs │ │ │ ├── v1.rs │ │ │ ├── v2.rs │ │ │ ├── v3.rs │ │ │ ├── v4.rs │ │ │ ├── helper_funs.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── core.rs │ └── error.rs ├── misc.rs ├── cpu_common │ ├── extra_policy.rs │ └── process_monitor.rs ├── file_handler.rs └── main.rs ├── .github └── dependabot.yml ├── xtask ├── Cargo.toml └── src │ └── zip_ext.rs ├── licenserc.toml ├── Cargo.toml ├── assets └── icon.svg └── README.md /module/NOTICES: -------------------------------------------------------------------------------- 1 | ../NOTICES -------------------------------------------------------------------------------- /module/README_CN.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /module/.gitignore: -------------------------------------------------------------------------------- 1 | /module.prop 2 | -------------------------------------------------------------------------------- /module/README_EN.md: -------------------------------------------------------------------------------- 1 | ../README_EN.md -------------------------------------------------------------------------------- /update/update_ebpf.json: -------------------------------------------------------------------------------- 1 | update.json -------------------------------------------------------------------------------- /update/update_zygisk.json: -------------------------------------------------------------------------------- 1 | update.json -------------------------------------------------------------------------------- /update/update_ebpf_en.json: -------------------------------------------------------------------------------- 1 | update_en.json -------------------------------------------------------------------------------- /update/update_zygisk_en.json: -------------------------------------------------------------------------------- 1 | update_en.json -------------------------------------------------------------------------------- /module/META-INF/com/google/android/updater-script: -------------------------------------------------------------------------------- 1 | #MAGISK 2 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /.vim/coc-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": ["Cargo.toml"] 3 | } 4 | -------------------------------------------------------------------------------- /update/zh-CN/changelog.md: -------------------------------------------------------------------------------- 1 | # v4.9.1 2 | 3 | - 适配了安卓16 4 | - 修改了版本号计算方式 5 | - 优化webui 6 | - 更新依赖 7 | -------------------------------------------------------------------------------- /webui/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shadow3aaa/fas-rs/HEAD/webui/src/app/favicon.ico -------------------------------------------------------------------------------- /webui/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - core-js 3 | - sharp 4 | - unrs-resolver 5 | -------------------------------------------------------------------------------- /webui/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /module/system.prop: -------------------------------------------------------------------------------- 1 | vendor.perf.framepacing.enable=false 2 | debug.sf.latch_unsignaled=true 3 | persist.sys.framepredict.enable=false 4 | -------------------------------------------------------------------------------- /webui/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /update/en-US/changelog.md: -------------------------------------------------------------------------------- 1 | # v4.9.1 2 | 3 | - Added support for Android 16 4 | - Changed the version number calculation method 5 | - Improved the web UI 6 | - Updated dependencies 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # target 2 | /target 3 | 4 | # build 5 | /output 6 | 7 | # py 8 | /__pycache__ 9 | 10 | # idea 11 | /.idea 12 | 13 | # Darwin 14 | /.DS_Store 15 | 16 | # pnpm 17 | .pnpm* -------------------------------------------------------------------------------- /webui/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /webui/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | output: "export", 5 | distDir: "webroot", 6 | trailingSlash: true, 7 | }; 8 | 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /update/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "versionCode": 409001, 3 | "version": "v4.9.1", 4 | "zipUrl": "https://github.com/shadow3aaa/fas-rs/releases/download/v4.9.1/fas-rs.zip", 5 | "changelog": "https://github.com/shadow3aaa/fas-rs/raw/master/update/zh-CN/changelog.md" 6 | } -------------------------------------------------------------------------------- /update/update_en.json: -------------------------------------------------------------------------------- 1 | { 2 | "versionCode": 409001, 3 | "version": "v4.9.1", 4 | "zipUrl": "https://github.com/shadow3aaa/fas-rs/releases/download/v4.9.1/fas-rs.zip", 5 | "changelog": "https://github.com/shadow3aaa/fas-rs/raw/master/update/en-US/changelog.md" 6 | } -------------------------------------------------------------------------------- /webui/src/i18n.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@/i18n" { 2 | import { i18n } from "i18next"; 3 | const instance: i18n; 4 | export default instance; 5 | } 6 | 7 | declare module "*.json" { 8 | const value: Record; 9 | export default value; 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.buildScripts.overrideCommand": [ 3 | "cargo", 4 | "ndk", 5 | "-t", 6 | "arm64-v8a", 7 | "check", 8 | "--quiet", 9 | "--workspace", 10 | "--message-format=json" 11 | ], 12 | "rust-analyzer.linkedProjects": ["Cargo.toml"] 13 | } 14 | -------------------------------------------------------------------------------- /webui/src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 3 | import type { ThemeProviderProps } from "next-themes"; 4 | 5 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 6 | return {children}; 7 | } 8 | -------------------------------------------------------------------------------- /webui/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webui/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "lsp": { 3 | "rust-analyzer": { 4 | "initialization_options": { 5 | "check": { 6 | "overrideCommand": [ 7 | "cargo", 8 | "ndk", 9 | "-t", 10 | "arm64-v8a", 11 | "check", 12 | "--quiet", 13 | "--workspace", 14 | "--message-format=json" 15 | ] 16 | }, 17 | "linkedProjects": ["Cargo.toml"] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /webui/README.md: -------------------------------------------------------------------------------- 1 | # fas-rs WebUI 2 | 3 | Web-based visual editor for fas-rs system configurations. Provides GUI management for performance profiles and system settings. 4 | 5 | ## Features 6 | 7 | - Visual configuration editing 8 | - Multi-language support (English/中文) 9 | 10 | ## Development Setup 11 | 12 | Install dependencies: 13 | 14 | ```bash 15 | npm install 16 | ``` 17 | 18 | Then run the development server: 19 | 20 | ```bash 21 | npm run dev 22 | ``` 23 | 24 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 25 | -------------------------------------------------------------------------------- /webui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | /package-lock.json 13 | 14 | # testing 15 | /coverage 16 | 17 | # next.js 18 | /.next/ 19 | /out/ 20 | 21 | # production 22 | /build 23 | /webroot 24 | 25 | # misc 26 | .DS_Store 27 | *.pem 28 | 29 | # debug 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | .pnpm-debug.log* 34 | 35 | # env files (can opt-in for committing if needed) 36 | .env* 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | next-env.d.ts 44 | -------------------------------------------------------------------------------- /NOTICES: -------------------------------------------------------------------------------- 1 | Copyright 2023-2025, shadow3 (@shadow3aaa) 2 | 3 | This file is part of fas-rs. 4 | 5 | fas-rs is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free 7 | Software Foundation, either version 3 of the License, or (at your option) 8 | any later version. 9 | 10 | fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with fas-rs. If not, see . -------------------------------------------------------------------------------- /webui/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import LanguageDetector from "i18next-browser-languagedetector"; 4 | import enTranslations from "./locales/en/common.json"; 5 | import zhTranslations from "./locales/zh/common.json"; 6 | 7 | i18n 8 | .use(LanguageDetector) 9 | .use(initReactI18next) 10 | .init({ 11 | resources: { 12 | en: { common: enTranslations }, 13 | zh: { common: zhTranslations }, 14 | }, 15 | fallbackLng: "en", 16 | debug: process.env.NODE_ENV !== "production", 17 | interpolation: { 18 | escapeValue: false, 19 | }, 20 | ns: ["common"], 21 | defaultNS: "common", 22 | }); 23 | 24 | export default i18n; 25 | -------------------------------------------------------------------------------- /webui/src/types/config.ts: -------------------------------------------------------------------------------- 1 | export type ConfigOptions = { 2 | keep_std: boolean; 3 | scene_game_list: boolean; 4 | language: "en" | "zh"; 5 | }; 6 | 7 | export type PowerSettings = { 8 | margin_fps: number; 9 | core_temp_thresh: number | "disabled"; 10 | }; 11 | 12 | export type UpdatePowerModeFn = ( 13 | mode: keyof PowerModes, 14 | setting: keyof PowerSettings, 15 | value: number | number[] | "disabled", 16 | ) => void; 17 | 18 | export type PowerModes = { 19 | powersave: PowerSettings; 20 | balance: PowerSettings; 21 | performance: PowerSettings; 22 | fast: PowerSettings; 23 | }; 24 | 25 | export type FpsValue = number | number[]; 26 | 27 | export type GameList = { 28 | [packageName: string]: FpsValue; 29 | }; 30 | -------------------------------------------------------------------------------- /module/META-INF/com/google/android/update-binary: -------------------------------------------------------------------------------- 1 | #!/sbin/sh 2 | 3 | ################# 4 | # Initialization 5 | ################# 6 | 7 | umask 022 8 | 9 | # echo before loading util_functions 10 | ui_print() { echo "$1"; } 11 | 12 | require_new_magisk() { 13 | ui_print "*******************************" 14 | ui_print " Please install Magisk v20.4+! " 15 | ui_print "*******************************" 16 | exit 1 17 | } 18 | 19 | ######################### 20 | # Load util_functions.sh 21 | ######################### 22 | 23 | OUTFD=$2 24 | ZIPFILE=$3 25 | 26 | mount /data 2>/dev/null 27 | 28 | [ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk 29 | . /data/adb/magisk/util_functions.sh 30 | [ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk 31 | 32 | install_module 33 | exit 0 34 | -------------------------------------------------------------------------------- /LICENSE_HEADER: -------------------------------------------------------------------------------- 1 | Copyright {{ attrs.git_file_created_year }}-{{ attrs.git_file_modified_year }}, {{ attrs.git_authors | join(", ") }} 2 | 3 | This file is part of fas-rs. 4 | 5 | fas-rs is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free 7 | Software Foundation, either version 3 of the License, or (at your option) 8 | any later version. 9 | 10 | fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with fas-rs. If not, see . -------------------------------------------------------------------------------- /webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": [ 26 | "**/*.ts", 27 | "**/*.tsx", 28 | ".next/types/**/*.ts", 29 | "next-env.d.ts", 30 | "webroot/types/**/*.ts" 31 | ], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025-2025, shadow3aaa 2 | # 3 | # This file is part of fas-rs. 4 | # 5 | # fas-rs is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free 7 | # Software Foundation, either version 3 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with fas-rs. If not, see . 17 | 18 | [alias] 19 | xtask = "run --package xtask --" 20 | -------------------------------------------------------------------------------- /module/games.toml: -------------------------------------------------------------------------------- 1 | [config] 2 | keep_std = true 3 | scene_game_list = true 4 | 5 | [game_list] 6 | "com.hypergryph.arknights" = [30, 60] 7 | "com.miHoYo.Yuanshen" = [30, 60] 8 | "com.miHoYo.enterprise.NGHSoD" = [30, 60, 90] 9 | "com.miHoYo.hkrpg" = [30, 60] 10 | "com.kurogame.mingchao" = [24, 30, 45, 60] 11 | "com.pwrd.hotta.laohu" = [25, 30, 45, 60, 90] 12 | "com.mojang.minecraftpe" = [60, 90, 120] 13 | "com.netease.party" = [30, 60] 14 | "com.shangyoo.neon" = 60 15 | "com.tencent.tmgp.pubgmhd" = [60, 90, 120] 16 | "com.tencent.tmgp.sgame" = [30, 60, 90, 120] 17 | 18 | [powersave] 19 | margin_fps = 3.0 20 | core_temp_thresh = 80000 21 | 22 | [balance] 23 | margin_fps = 1.0 24 | core_temp_thresh = 90000 25 | 26 | [performance] 27 | margin_fps = 0.3 28 | core_temp_thresh = 95000 29 | 30 | [fast] 31 | margin_fps = 0 32 | core_temp_thresh = 95000 33 | -------------------------------------------------------------------------------- /src/framework/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | #![allow(unused_imports)] 19 | pub use super::{Api, Extension, Scheduler, api, config::Config, node::Mode}; 20 | -------------------------------------------------------------------------------- /webui/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.config({ 14 | extends: ["next/core-web-vitals", "next/typescript"], 15 | rules: { 16 | "@typescript-eslint/no-unused-vars": [ 17 | "error", 18 | { 19 | args: "all", 20 | argsIgnorePattern: "^_", 21 | caughtErrors: "all", 22 | caughtErrorsIgnorePattern: "^_", 23 | destructuredArrayIgnorePattern: "^_", 24 | varsIgnorePattern: "^_", 25 | ignoreRestSiblings: true, 26 | }, 27 | ], 28 | }, 29 | }), 30 | ]; 31 | 32 | export default eslintConfig; 33 | -------------------------------------------------------------------------------- /webui/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import "./globals.css"; 5 | import { ThemeProvider } from "@/components/theme-provider"; 6 | import ClientLayout from "./client-layout"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Game Performance Config", 12 | description: "Configure game performance settings", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | 24 | {children} 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /webui/src/components/config/PowerModes.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { 4 | PowerModes as PowerModesType, 5 | PowerSettings, 6 | } from "@/types/config"; 7 | import { PowerModeSettings } from "./PowerModeSettings"; 8 | 9 | interface PowerModesProps { 10 | powerModes: PowerModesType; 11 | updatePowerMode: ( 12 | mode: keyof PowerModesType, 13 | setting: keyof PowerSettings, 14 | value: number | number[] | "disabled", 15 | ) => void; 16 | } 17 | 18 | export function PowerModes({ powerModes, updatePowerMode }: PowerModesProps) { 19 | return ( 20 |
21 | {Object.entries(powerModes).map(([mode, settings], index) => ( 22 | 29 | ))} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024-2025, dependabot[bot], shadow3, shadow3aaa 2 | # 3 | # This file is part of fas-rs. 4 | # 5 | # fas-rs is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free 7 | # Software Foundation, either version 3 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with fas-rs. If not, see . 17 | 18 | version: 2 19 | updates: 20 | - package-ecosystem: "cargo" # See documentation for possible values 21 | directory: "/" # Location of package manifests 22 | schedule: 23 | interval: "daily" 24 | -------------------------------------------------------------------------------- /module/vtools/init_vtools.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | # Copyright 2023-2025, shadow3aaa 3 | # 4 | # This file is part of fas-rs. 5 | # 6 | # fas-rs is free software: you can redistribute it and/or modify it under 7 | # the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 14 | # details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with fas-rs. If not, see . 18 | 19 | BASEDIR="$(dirname $(readlink -f "$0"))" 20 | 21 | source $BASEDIR/gen_json.sh $1 22 | echo "$json" >/data/powercfg.json 23 | 24 | cp -af $BASEDIR/powercfg.sh /data/powercfg.sh 25 | chmod 755 /data/powercfg.sh 26 | -------------------------------------------------------------------------------- /module/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | # Copyright 2023-2025, shadow3aaa 3 | # 4 | # This file is part of fas-rs. 5 | # 6 | # fas-rs is free software: you can redistribute it and/or modify it under 7 | # the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 14 | # details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with fas-rs. If not, see . 18 | 19 | DIR=/sdcard/Android/fas-rs 20 | 21 | { 22 | until [ -d $DIR ] && [ -d /data ]; do 23 | sleep 1 24 | done 25 | 26 | rm -rf $DIR 27 | rm -f /data/powercfg.json 28 | rm -f /data/powercfg.sh 29 | } & # do not block boot 30 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025-2025, dependabot[bot], shadow3, shadow3aaa 2 | # 3 | # This file is part of fas-rs. 4 | # 5 | # fas-rs is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free 7 | # Software Foundation, either version 3 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with fas-rs. If not, see . 17 | 18 | [package] 19 | name = "xtask" 20 | version = "0.1.0" 21 | edition = "2024" 22 | 23 | [dependencies] 24 | anyhow = "1.0.100" 25 | clap = { version = "4.5.53", features = ["derive"] } 26 | fs_extra = "1.3.0" 27 | zip = "7.0.0" 28 | -------------------------------------------------------------------------------- /src/misc.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::process::Command; 19 | 20 | pub fn setprop(k: S, v: S) 21 | where 22 | S: AsRef, 23 | { 24 | let key = k.as_ref(); 25 | let value = v.as_ref(); 26 | let _ = Command::new("setprop").args([key, value]).spawn(); 27 | } 28 | -------------------------------------------------------------------------------- /webui/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/framework/config/data/default.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use super::Config; 19 | 20 | impl Config { 21 | pub const fn default_value_keep_std() -> bool { 22 | true 23 | } 24 | 25 | pub const fn default_value_scene_game_list() -> bool { 26 | true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /module/vtools/powercfg.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | # Copyright 2023-2025, shadow3aaa 3 | # 4 | # This file is part of fas-rs. 5 | # 6 | # fas-rs is free software: you can redistribute it and/or modify it under 7 | # the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 14 | # details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with fas-rs. If not, see . 18 | 19 | mode=/dev/fas_rs/mode 20 | 21 | case "$1" in 22 | "init" | "fast" | "pedestal") echo fast >$mode ;; 23 | "powersave" | "standby") echo powersave >$mode ;; 24 | "balance") echo balance >$mode ;; 25 | "performance") echo performance >$mode ;; 26 | esac 27 | -------------------------------------------------------------------------------- /src/framework/scheduler/looper/policy/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | pub mod controll; 19 | 20 | #[derive(Debug, Copy, Clone)] 21 | pub struct ControllerParams { 22 | pub kp: f64, 23 | } 24 | 25 | impl Default for ControllerParams { 26 | fn default() -> Self { 27 | Self { kp: 0.000_3 } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webui/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ); 19 | } 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /webui/src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "@/lib/kernelsu"; 2 | 3 | export interface App { 4 | package_name: string; 5 | } 6 | 7 | export const parseNumberedList = (text: string): App[] => { 8 | const lineRegex = /^package:(\S+)/gm; 9 | const matches = [...text.matchAll(lineRegex)]; 10 | return matches.map((m) => ({ package_name: m[1] })); 11 | }; 12 | 13 | export const fetchApps = async (): Promise => { 14 | try { 15 | const { errno, stdout } = await exec("pm list packages -3"); 16 | if (errno) { 17 | throw new Error(`Failed to fetch apps: ${stdout}`); 18 | } 19 | const data = parseNumberedList(stdout); 20 | 21 | return Array.isArray(data) 22 | ? data.map((item) => { 23 | if (item && typeof item.package_name === "object") { 24 | const firstKey = Object.keys(item.package_name)[0]; 25 | return { package_name: firstKey || "unknown" }; 26 | } 27 | return item; 28 | }) 29 | : []; 30 | } catch (error) { 31 | console.error("Error fetching apps:", error); 32 | return []; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /licenserc.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024-2025, shadow3aaa 2 | # 3 | # This file is part of fas-rs. 4 | # 5 | # fas-rs is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free 7 | # Software Foundation, either version 3 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with fas-rs. If not, see . 17 | 18 | headerPath = "LICENSE_HEADER" 19 | includes = [ 20 | "licenserc.toml", 21 | "src/**", 22 | "Cargo.toml", 23 | "build.rs", 24 | "maketools/**", 25 | "module/**", 26 | ".github/**", 27 | ".cargo/**", 28 | "xtask/src/**", 29 | "xtask/Cargo.toml", 30 | ] 31 | excludes = ["module/games.toml", "module/META-INF/**", "module/*.prop"] 32 | 33 | [git] 34 | attrs = 'auto' 35 | ignore = 'auto' -------------------------------------------------------------------------------- /src/framework/pid_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::{fs, path::Path}; 19 | 20 | use crate::framework::Result; 21 | 22 | pub fn get_process_name(pid: i32) -> Result { 23 | let cmdline = Path::new("/proc").join(pid.to_string()).join("cmdline"); 24 | let cmdline = fs::read_to_string(cmdline)?; 25 | let cmdline = cmdline.split(':').next().unwrap_or_default(); 26 | Ok(cmdline.trim_matches(['\0']).trim().to_string()) 27 | } 28 | -------------------------------------------------------------------------------- /webui/src/components/LanguageSwitcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslation } from "react-i18next"; 3 | import { Button } from "./ui/button"; 4 | import { useState, useEffect } from "react"; 5 | 6 | export default function LanguageSwitcher() { 7 | const { i18n } = useTranslation(); 8 | const [currentLang, setCurrentLang] = useState(i18n.language); 9 | useEffect(() => { 10 | setCurrentLang(i18n.language); 11 | }, [i18n.language]); 12 | 13 | const changeLanguage = (lng: string) => { 14 | i18n.changeLanguage(lng); 15 | setCurrentLang(lng); 16 | }; 17 | 18 | return ( 19 |
20 | 27 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/framework/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | mod config; 19 | mod error; 20 | mod extension; 21 | mod node; 22 | mod pid_utils; 23 | pub mod prelude; 24 | mod scheduler; 25 | 26 | #[allow(unused_imports)] 27 | pub use config::Config; 28 | #[allow(unused_imports)] 29 | pub use error::Result; 30 | #[allow(unused_imports)] 31 | pub use extension::{Api, Extension, api}; 32 | #[allow(unused_imports)] 33 | pub use node::Mode; 34 | #[allow(unused_imports)] 35 | pub use scheduler::Scheduler; 36 | -------------------------------------------------------------------------------- /src/cpu_common/extra_policy.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | #[derive(Debug, PartialEq, Eq)] 19 | pub enum ExtraPolicy { 20 | AbsRangeBound(AbsRangeBound), 21 | RelRangeBound(RelRangeBound), 22 | None, 23 | } 24 | 25 | #[derive(Debug, PartialEq, Eq)] 26 | pub struct AbsRangeBound { 27 | pub min: Option, 28 | pub max: Option, 29 | } 30 | 31 | #[derive(Debug, PartialEq, Eq)] 32 | pub struct RelRangeBound { 33 | pub rel_to: i32, 34 | pub min: Option, 35 | pub max: Option, 36 | } 37 | -------------------------------------------------------------------------------- /webui/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitive from "@radix-ui/react-switch"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Switch({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | 27 | 28 | ); 29 | } 30 | 31 | export { Switch }; 32 | -------------------------------------------------------------------------------- /src/framework/node/power_mode.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{self, Display, Formatter}, 3 | str::FromStr, 4 | }; 5 | 6 | use super::Node; 7 | use crate::framework::error::{Error, Result}; 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 10 | pub enum Mode { 11 | Powersave, 12 | Balance, 13 | Performance, 14 | Fast, 15 | } 16 | 17 | impl FromStr for Mode { 18 | type Err = Error; 19 | 20 | fn from_str(s: &str) -> Result { 21 | Ok(match s { 22 | "powersave" => Self::Powersave, 23 | "balance" => Self::Balance, 24 | "performance" => Self::Performance, 25 | "fast" => Self::Fast, 26 | _ => return Err(Error::ParseNode), 27 | }) 28 | } 29 | } 30 | 31 | impl Display for Mode { 32 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 33 | let mode = match self { 34 | Self::Powersave => "powersave", 35 | Self::Balance => "balance", 36 | Self::Performance => "performance", 37 | Self::Fast => "fast", 38 | }; 39 | 40 | write!(f, "{mode}") 41 | } 42 | } 43 | 44 | impl Node { 45 | pub fn get_mode(&mut self) -> Result { 46 | let mode = self.get_node("mode").or(Err(Error::NodeNotFound))?; 47 | 48 | Mode::from_str(mode.trim()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /webui/src/locales/zh/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "game_list": "游戏列表", 3 | "configure_fps": "配置游戏帧率设置", 4 | "add_game": "添加游戏", 5 | "package_name": "包名", 6 | "fps_values": "帧率值", 7 | "comma_separated": "逗号分隔", 8 | "cancel": "取消", 9 | "add": "添加", 10 | "language": "语言", 11 | "english": "英文", 12 | "chinese": "中文", 13 | "save_configuration": "保存配置", 14 | "fas_margin": "帧率余量", 15 | "margin": "容忍掉帧量", 16 | "thermal": "温控阈值", 17 | "thermal_desc": "超过时将触发温控", 18 | "powersave_mode": "省电模式", 19 | "powersave_mode_desc": "环保", 20 | "balance_mode": "均衡模式", 21 | "balance_mode_desc": "适中", 22 | "performance_mode": "性能模式", 23 | "performance_mode_desc": "比均衡更激进", 24 | "fast_mode": "极速模式", 25 | "fast_mode_desc": "帧率表现至上", 26 | "config": "基础配置", 27 | "basic_settings": "fas-rs基础设置", 28 | "keep_std": "保留标准配置", 29 | "keep_std_desc": "每次更新时新的设置会覆盖(除了游戏列表)", 30 | "scene_game_list": "Scene 游戏列表", 31 | "scene_game_list_desc": "使用 Scene 中的游戏列表(优先级低于配置里面的游戏列表)", 32 | "games_config": "配置", 33 | "manage_settings": "管理游戏性能设置", 34 | "tab_general": "全局", 35 | "tab_games": "游戏列表", 36 | "tab_power": "模式", 37 | "delete_game": "删除游戏", 38 | "delete_game_confirm": "您确定要删除 {{game}} 吗?此操作无法撤消。", 39 | "delete": "删除", 40 | "search_app": "搜索应用...", 41 | "no_apps_found": "未找到应用", 42 | "loading_apps": "正在加载应用..." 43 | } 44 | -------------------------------------------------------------------------------- /src/framework/config/inner.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::sync::mpsc::Receiver; 19 | 20 | use super::data::ConfigData; 21 | 22 | #[derive(Debug)] 23 | pub struct Inner { 24 | rx: Receiver, 25 | config: ConfigData, 26 | } 27 | 28 | impl Inner { 29 | pub const fn new(config: ConfigData, rx: Receiver) -> Self { 30 | Self { rx, config } 31 | } 32 | 33 | pub fn config(&mut self) -> &mut ConfigData { 34 | if let Some(config) = self.rx.try_iter().last() { 35 | self.config = config; 36 | } 37 | 38 | &mut self.config 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webui/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webui/src/lib/kernelsu.d.ts: -------------------------------------------------------------------------------- 1 | interface ExecOptions { 2 | cwd?: string; 3 | env?: { [key: string]: string }; 4 | } 5 | 6 | interface ExecResults { 7 | errno: number; 8 | stdout: string; 9 | stderr: string; 10 | } 11 | 12 | declare function exec(command: string): Promise; 13 | declare function exec( 14 | command: string, 15 | options: ExecOptions, 16 | ): Promise; 17 | 18 | interface SpawnOptions { 19 | cwd?: string; 20 | env?: { [key: string]: string }; 21 | } 22 | 23 | interface Stdio { 24 | on(event: "data", callback: (data: string) => void); 25 | } 26 | 27 | interface ChildProcess { 28 | stdout: Stdio; 29 | stderr: Stdio; 30 | on(event: "exit", callback: (code: number) => void); 31 | on(event: "error", callback: (err: Error) => void); 32 | } 33 | 34 | declare function spawn(command: string): ChildProcess; 35 | declare function spawn(command: string, args: string[]): ChildProcess; 36 | declare function spawn(command: string, options: SpawnOptions): ChildProcess; 37 | declare function spawn( 38 | command: string, 39 | args: string[], 40 | options: SpawnOptions, 41 | ): ChildProcess; 42 | 43 | declare function fullScreen(isFullScreen: boolean); 44 | 45 | declare function toast(message: string); 46 | 47 | declare function moduleInfo(): string; 48 | 49 | export { exec, spawn, fullScreen, toast, moduleInfo }; 50 | -------------------------------------------------------------------------------- /module/service.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | # Copyright 2023-2025, dependabot[bot], shadow3, shadow3aaa 3 | # 4 | # This file is part of fas-rs. 5 | # 6 | # fas-rs is free software: you can redistribute it and/or modify it under 7 | # the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 14 | # details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with fas-rs. If not, see . 18 | 19 | MODDIR=${0%/*} 20 | DIR=/sdcard/Android/fas-rs 21 | MERGE_FLAG=$DIR/.need_merge 22 | LOG=$DIR/fas_log.txt 23 | 24 | sh $MODDIR/vtools/init_vtools.sh $(realpath $MODDIR/module.prop) 25 | 26 | resetprop fas-rs-installed true 27 | 28 | until [ -d $DIR ]; do 29 | sleep 1 30 | done 31 | 32 | if [ -f $MERGE_FLAG ]; then 33 | $MODDIR/fas-rs merge $MODDIR/games.toml >$DIR/.update_games.toml 34 | rm $MERGE_FLAG 35 | mv $DIR/.update_games.toml $DIR/games.toml 36 | fi 37 | 38 | killall fas-rs 39 | RUST_BACKTRACE=1 nohup $MODDIR/fas-rs run $MODDIR/games.toml >$LOG 2>&1 & 40 | -------------------------------------------------------------------------------- /module/vtools/gen_json.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | # Copyright 2023-2025, shadow3aaa 3 | # 4 | # This file is part of fas-rs. 5 | # 6 | # fas-rs is free software: you can redistribute it and/or modify it under 7 | # the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 14 | # details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with fas-rs. If not, see . 18 | 19 | propPath=$1 20 | version=$(cat $propPath | grep "version=" | cut -d "=" -f2) 21 | versionCode=$(cat $propPath | grep "versionCode=" | cut -d "=" -f2) 22 | 23 | json=$( 24 | cat < { 20 | if (typeof window !== "undefined") { 21 | return i18n.language; 22 | } 23 | return "en"; 24 | }); 25 | 26 | useEffect(() => { 27 | const handleLanguageChange = () => { 28 | setCurrentLang(i18n.language); 29 | document.documentElement.lang = i18n.language; 30 | }; 31 | 32 | i18n.on("languageChanged", handleLanguageChange); 33 | return () => i18n.off("languageChanged", handleLanguageChange); 34 | }, []); 35 | 36 | return ( 37 | 38 | 39 |
40 | 41 |
{children}
42 |
43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /webui/src/components/config/DeleteGameDialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTranslation } from "react-i18next"; 4 | import { 5 | AlertDialog, 6 | AlertDialogAction, 7 | AlertDialogCancel, 8 | AlertDialogContent, 9 | AlertDialogDescription, 10 | AlertDialogFooter, 11 | AlertDialogHeader, 12 | AlertDialogTitle, 13 | } from "@/components/ui/alert-dialog"; 14 | 15 | interface DeleteGameDialogProps { 16 | isOpen: boolean; 17 | onClose: () => void; 18 | onConfirm: () => void; 19 | gameName: string; 20 | } 21 | 22 | export function DeleteGameDialog({ 23 | isOpen, 24 | onClose, 25 | onConfirm, 26 | gameName, 27 | }: DeleteGameDialogProps) { 28 | const { t } = useTranslation(); 29 | 30 | return ( 31 | 32 | 33 | 34 | {t("common:delete_game")} 35 | 36 | {t("common:delete_game_confirm", { game: gameName })} 37 | 38 | 39 | 40 | {t("common:cancel")} 41 | 45 | {t("common:delete")} 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/framework/extension/api/misc.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::path::Path; 19 | 20 | use likely_stable::LikelyResult; 21 | use log::error; 22 | use mlua::{Function, IntoLuaMulti, Lua}; 23 | 24 | pub fn get_api_version(lua: &Lua) -> u8 { 25 | lua.globals().get("API_VERSION").unwrap_or(0) 26 | } 27 | 28 | pub fn do_callback(extension: P, lua: &Lua, function: S, args: A) 29 | where 30 | P: AsRef, 31 | S: AsRef, 32 | A: IntoLuaMulti, 33 | { 34 | let function = function.as_ref(); 35 | let extension = extension.as_ref(); 36 | 37 | if let Ok(func) = lua.globals().get::(function) { 38 | func.call(args).unwrap_or_else_likely(|e| { 39 | error!( 40 | "Got an error when executing extension '{}', reason: {e:#?}", 41 | extension.display() 42 | ); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/framework/extension/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, dependabot[bot], shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | pub mod api; 19 | mod core; 20 | 21 | use std::{ 22 | fs, 23 | sync::mpsc::{self, SyncSender}, 24 | thread, 25 | }; 26 | 27 | use crate::framework::error::Result; 28 | pub use api::Api; 29 | 30 | const EXTENSIONS_PATH: &str = "/dev/fas_rs/extensions"; 31 | 32 | pub struct Extension { 33 | sx: SyncSender>, 34 | } 35 | 36 | impl Extension { 37 | pub fn init() -> Result { 38 | let _ = fs::create_dir_all(EXTENSIONS_PATH); 39 | let (sx, rx) = mpsc::sync_channel(16); 40 | 41 | thread::Builder::new() 42 | .name("ExtensionThread".into()) 43 | .spawn(move || core::thread(&rx))?; 44 | 45 | Ok(Self { sx }) 46 | } 47 | 48 | pub fn trigger_extentions(&self, trigger: impl Api + 'static) { 49 | let _ = self.sx.try_send(trigger.into_box()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /webui/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Popover({ 9 | ...props 10 | }: React.ComponentProps) { 11 | return ; 12 | } 13 | 14 | function PopoverTrigger({ 15 | ...props 16 | }: React.ComponentProps) { 17 | return ; 18 | } 19 | 20 | function PopoverContent({ 21 | className, 22 | align = "center", 23 | sideOffset = 4, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 28 | 38 | 39 | ); 40 | } 41 | 42 | function PopoverAnchor({ 43 | ...props 44 | }: React.ComponentProps) { 45 | return ; 46 | } 47 | 48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 49 | -------------------------------------------------------------------------------- /webui/src/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "game_list": "Game List", 3 | "configure_fps": "Configure FPS settings for games", 4 | "add_game": "Add Game", 5 | "package_name": "Package Name", 6 | "fps_values": "FPS Values", 7 | "comma_seperated": "comma-separated", 8 | "cancel": "Cancel", 9 | "add": "Add", 10 | "language": "Language", 11 | "english": "English", 12 | "chinese": "Chinese", 13 | "save_configuration": "Save Configuration", 14 | "fas_margin": "Fps Margin", 15 | "margin": "Allowable frame drop amount", 16 | "thermal": "Thermal Threshold", 17 | "thermal_desc": "Enable thermal throttling when exceeded", 18 | "powersave_mode": "Powersave", 19 | "powersave_mode_desc": "ECO-Friendly", 20 | "balance_mode": "Balance", 21 | "balance_mode_desc": "Balanced performance", 22 | "performance_mode": "Performance", 23 | "performance_mode_desc": "More aggressive than balance", 24 | "fast_mode": "Fast", 25 | "fast_mode_desc": "Fastest", 26 | "keep_std": "Keep Standard Configuration", 27 | "keep_std_desc": "New settings will overwrite on each update (except the game list)", 28 | "scene_game_list": "Scene Game List", 29 | "scene_game_list_desc": "Use the game list from Scene (lower priority than the one in the configuration)", 30 | "config": "Basic Configuration", 31 | "basic_settings": "Basic settings for fas-rs", 32 | "games_config": "Game Configuration", 33 | "manage_settings": "Manage performance settings for your games", 34 | "tab_general": "General", 35 | "tab_games": "Game List", 36 | "tab_power": "Modes", 37 | "delete_game": "Delete Game", 38 | "delete_game_confirm": "Are you sure you want to delete {{game}}? This action cannot be undone.", 39 | "delete": "Delete", 40 | "search_app": "Search application...", 41 | "no_apps_found": "No applications found", 42 | "loading_apps": "Loading applications..." 43 | } 44 | -------------------------------------------------------------------------------- /webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "prettier": "prettier --write ." 11 | }, 12 | "dependencies": { 13 | "@iarna/toml": "^2.2.5", 14 | "@radix-ui/react-alert-dialog": "^1.1.10", 15 | "@radix-ui/react-dialog": "^1.1.10", 16 | "@radix-ui/react-dropdown-menu": "^2.1.11", 17 | "@radix-ui/react-popover": "^1.1.10", 18 | "@radix-ui/react-separator": "^1.1.4", 19 | "@radix-ui/react-slider": "^1.3.2", 20 | "@radix-ui/react-slot": "^1.2.0", 21 | "@radix-ui/react-switch": "^1.2.2", 22 | "@radix-ui/react-tabs": "^1.1.8", 23 | "@tailwindcss/postcss": "^4.1.4", 24 | "@tanstack/react-query": "^5.74.4", 25 | "axios": "^1.8.4", 26 | "class-variance-authority": "^0.7.1", 27 | "clsx": "^2.1.1", 28 | "cmdk": "^1.1.1", 29 | "framer-motion": "^12.7.4", 30 | "i18next": "^25.0.0", 31 | "i18next-browser-languagedetector": "^8.0.5", 32 | "kernelsu": "^1.0.6", 33 | "lucide-react": "^0.501.0", 34 | "next": "15.3.1", 35 | "next-i18next": "^15.4.2", 36 | "next-themes": "^0.4.6", 37 | "react": "^19.1.0", 38 | "react-dom": "^19.1.0", 39 | "react-i18next": "^15.4.1", 40 | "sonner": "^2.0.3", 41 | "tailwind-merge": "^3.2.0", 42 | "tailwindcss": "^4.1.4", 43 | "tw-animate-css": "^1.2.5", 44 | "use-debounce": "^10.0.4" 45 | }, 46 | "devDependencies": { 47 | "@eslint/eslintrc": "^3.3.1", 48 | "@types/i18next": "^13.0.0", 49 | "@types/node": "^22.14.1", 50 | "@types/react": "^19.1.2", 51 | "@types/react-dom": "^19.1.2", 52 | "@types/react-i18next": "^8.1.0", 53 | "autoprefixer": "^10.4.21", 54 | "eslint": "^9.25.0", 55 | "eslint-config-next": "15.3.1", 56 | "postcss": "^8.5.3", 57 | "prettier": "^3.5.3", 58 | "typescript": "^5.8.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/framework/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::{ffi::NulError, io}; 19 | 20 | use frame_analyzer::AnalyzerError; 21 | use thiserror::Error as ThisError; 22 | 23 | pub type Result = std::result::Result; 24 | 25 | #[derive(ThisError, Debug)] 26 | pub enum Error { 27 | #[error(transparent)] 28 | Anyhow(#[from] anyhow::Error), 29 | #[error(transparent)] 30 | FrameAnalyzer(#[from] AnalyzerError), 31 | #[error("Got an error when parsing config")] 32 | ParseConfig, 33 | #[error("Got an error when parsing node")] 34 | ParseNode, 35 | #[error("No such a node")] 36 | NodeNotFound, 37 | #[error(transparent)] 38 | SerToml(#[from] toml::ser::Error), 39 | #[error(transparent)] 40 | DeToml(#[from] toml::de::Error), 41 | #[error(transparent)] 42 | SerXml(#[from] quick_xml::DeError), 43 | #[error("Missing {0} when building Scheduler")] 44 | SchedulerMissing(&'static str), 45 | #[error(transparent)] 46 | Io(#[from] io::Error), 47 | #[error(transparent)] 48 | Lua { 49 | #[from] 50 | source: mlua::Error, 51 | }, 52 | #[error(transparent)] 53 | Null { 54 | #[from] 55 | source: NulError, 56 | }, 57 | #[error("Got an error: {0}")] 58 | #[allow(dead_code)] 59 | Other(&'static str), 60 | } 61 | -------------------------------------------------------------------------------- /src/framework/extension/api/v0.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use libc::pid_t; 19 | 20 | use super::{ 21 | super::core::ExtensionMap, 22 | Api, 23 | misc::{do_callback, get_api_version}, 24 | }; 25 | 26 | #[derive(Debug, Clone)] 27 | pub enum ApiV0 { 28 | LoadFas(pid_t, String), 29 | UnloadFas(pid_t, String), 30 | StartFas, 31 | StopFas, 32 | InitCpuFreq, 33 | ResetCpuFreq, 34 | } 35 | 36 | impl Api for ApiV0 { 37 | fn handle_api(&self, ext: &ExtensionMap) { 38 | for (extension, lua) in ext.iter().filter(|(_, lua)| get_api_version(lua) == 0) { 39 | match self.clone() { 40 | Self::LoadFas(pid, pkg) => { 41 | do_callback(extension, lua, "load_fas", (pid, pkg)); 42 | } 43 | Self::UnloadFas(pid, pkg) => { 44 | do_callback(extension, lua, "unload_fas", (pid, pkg)); 45 | } 46 | Self::StartFas => { 47 | do_callback(extension, lua, "start_fas", ()); 48 | } 49 | Self::StopFas => { 50 | do_callback(extension, lua, "stop_fas", ()); 51 | } 52 | Self::InitCpuFreq => { 53 | do_callback(extension, lua, "init_cpu_freq", ()); 54 | } 55 | Self::ResetCpuFreq => { 56 | do_callback(extension, lua, "reset_cpu_freq", ()); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/framework/extension/api/v1.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use libc::pid_t; 19 | 20 | use super::{ 21 | super::core::ExtensionMap, 22 | Api, 23 | misc::{do_callback, get_api_version}, 24 | }; 25 | 26 | #[derive(Debug, Clone)] 27 | pub enum ApiV1 { 28 | LoadFas(pid_t, String), 29 | UnloadFas(pid_t, String), 30 | StartFas, 31 | StopFas, 32 | InitCpuFreq, 33 | ResetCpuFreq, 34 | } 35 | 36 | impl Api for ApiV1 { 37 | fn handle_api(&self, ext: &ExtensionMap) { 38 | for (extension, lua) in ext.iter().filter(|(_, lua)| get_api_version(lua) == 1) { 39 | match self.clone() { 40 | Self::LoadFas(pid, pkg) => { 41 | do_callback(extension, lua, "load_fas", (pid, pkg)); 42 | } 43 | Self::UnloadFas(pid, pkg) => { 44 | do_callback(extension, lua, "unload_fas", (pid, pkg)); 45 | } 46 | Self::StartFas => { 47 | do_callback(extension, lua, "start_fas", ()); 48 | } 49 | Self::StopFas => { 50 | do_callback(extension, lua, "stop_fas", ()); 51 | } 52 | Self::InitCpuFreq => { 53 | do_callback(extension, lua, "init_cpu_freq", ()); 54 | } 55 | Self::ResetCpuFreq => { 56 | do_callback(extension, lua, "reset_cpu_freq", ()); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /webui/src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Tabs({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ); 19 | } 20 | 21 | function TabsList({ 22 | className, 23 | ...props 24 | }: React.ComponentProps) { 25 | return ( 26 | 34 | ); 35 | } 36 | 37 | function TabsTrigger({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ); 51 | } 52 | 53 | function TabsContent({ 54 | className, 55 | ...props 56 | }: React.ComponentProps) { 57 | return ( 58 | 63 | ); 64 | } 65 | 66 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 67 | -------------------------------------------------------------------------------- /webui/src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SliderPrimitive from "@radix-ui/react-slider"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Slider({ 9 | className, 10 | defaultValue, 11 | value, 12 | min = 0, 13 | max = 100, 14 | ...props 15 | }: React.ComponentProps) { 16 | const _values = React.useMemo( 17 | () => 18 | Array.isArray(value) 19 | ? value 20 | : Array.isArray(defaultValue) 21 | ? defaultValue 22 | : [min, max], 23 | [value, defaultValue, min, max], 24 | ); 25 | 26 | return ( 27 | 39 | 45 | 51 | 52 | {Array.from({ length: _values.length }, (_, index) => ( 53 | 58 | ))} 59 | 60 | ); 61 | } 62 | 63 | export { Slider }; 64 | -------------------------------------------------------------------------------- /module/customize.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | # Copyright 2023-2025, shadow3, shadow3aaa 3 | # 4 | # This file is part of fas-rs. 5 | # 6 | # fas-rs is free software: you can redistribute it and/or modify it under 7 | # the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 14 | # details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with fas-rs. If not, see . 18 | 19 | DIR=/sdcard/Android/fas-rs 20 | CONF=$DIR/games.toml 21 | MERGE_FLAG=$DIR/.need_merge 22 | LOCALE=$(getprop persist.sys.locale) 23 | 24 | local_print() { 25 | if [ $LOCALE = zh-CN ]; then 26 | ui_print "$1" 27 | else 28 | ui_print "$2" 29 | fi 30 | } 31 | 32 | local_echo() { 33 | if [ $LOCALE = zh-CN ]; then 34 | echo "$1" 35 | else 36 | echo "$2" 37 | fi 38 | } 39 | 40 | if [ $ARCH != arm64 ]; then 41 | local_print "设备不支持, 非arm64设备" "Only for arm64 device !" 42 | abort 43 | elif [ $API -le 30 ]; then 44 | local_print "系统版本过低, 需要安卓12及以上的系统版本版本" "Required A12+ !" 45 | abort 46 | elif uname -r | awk -F. '{if ($1 < 5 || ($1 == 5 && $2 < 8)) exit 0; else exit 1}'; then 47 | local_print "内核版本过低,需要5.8或以上 !" "The kernel version is too low. Requires 5.8+ !" 48 | abort 49 | fi 50 | 51 | if [ -f $CONF ]; then 52 | touch $MERGE_FLAG 53 | else 54 | mkdir -p $DIR 55 | cp $MODPATH/games.toml $CONF 56 | fi 57 | 58 | cp -f $MODPATH/README_CN.md $DIR/doc_cn.md 59 | cp -f $MODPATH/README_EN.md $DIR/doc_en.md 60 | 61 | sh $MODPATH/vtools/init_vtools.sh $(realpath $MODPATH/module.prop) 62 | 63 | set_perm_recursive $MODPATH 0 0 0755 0644 64 | set_perm $MODPATH/fas-rs 0 0 0755 65 | 66 | local_print "配置文件夹:/sdcard/Android/fas-rs" "Configuration folder: /sdcard/Android/fas-rs" 67 | local_echo "updateJson=https://github.com/shadow3aaa/fas-rs/raw/master/update/update.json" "updateJson=https://github.com/shadow3aaa/fas-rs/raw/master/update/update_en.json" >>$MODPATH/module.prop 68 | 69 | resetprop fas-rs-installed true 70 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2023-2025, YuSaki丶Kanade, dependabot[bot], shadow3, shadow3aaa 2 | # 3 | # This file is part of fas-rs. 4 | # 5 | # fas-rs is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free 7 | # Software Foundation, either version 3 of the License, or (at your option) 8 | # any later version. 9 | # 10 | # fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with fas-rs. If not, see . 17 | 18 | [workspace] 19 | members = ["xtask"] 20 | 21 | [package] 22 | name = "fas-rs" 23 | version = "4.9.1" 24 | edition = "2024" 25 | description = "Frame aware scheduling for android. Requires kernel ebpf support." 26 | authors = ["shadow3"] 27 | license = "GPL-3.0" 28 | readme = "README.md" 29 | repository = "https://github.com/shadow3aaa/fas-rs" 30 | 31 | [dependencies] 32 | likely_stable = "0.1.3" 33 | parking_lot = "0.12.5" 34 | thiserror = "2.0.17" 35 | log = "0.4.29" 36 | anyhow = { version = "1.0.100" } 37 | inotify = { version = "0.11.0", default-features = false } 38 | flexi_logger = "0.31.7" 39 | libc = "0.2.178" 40 | toml = "0.9.10" 41 | serde = { version = "1.0.228", features = ["derive"] } 42 | sys-mount = { version = "3.0.1", default-features = false } 43 | quick-xml = { version = "0.38.4", features = ["serialize"] } 44 | mlua = { version = "0.11.4", features = ["luajit", "vendored", "error-send"] } 45 | frame-analyzer = "0.3.4" 46 | dumpsys-rs = { git = "https://github.com/shadow3aaa/dumpsys-rs" } 47 | mimalloc = "0.1.48" 48 | num_cpus = "1.17.0" 49 | nix = { version = "0.30.1", features = ["sched"] } 50 | 51 | [build-dependencies] 52 | anyhow = "1.0.100" 53 | toml = "0.9.10" 54 | serde_json = "1.0.147" 55 | serde = { version = "1.0.228", features = ["derive"] } 56 | rocket = { version = "0.5.0", features = ["json"] } 57 | 58 | [profile.dev] 59 | overflow-checks = false 60 | opt-level = 3 61 | strip = true 62 | 63 | [profile.release] 64 | overflow-checks = false 65 | codegen-units = 1 66 | lto = "fat" 67 | opt-level = 3 68 | strip = true 69 | -------------------------------------------------------------------------------- /webui/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | }, 36 | ); 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean; 47 | }) { 48 | const Comp = asChild ? Slot : "button"; 49 | 50 | return ( 51 | 56 | ); 57 | } 58 | 59 | export { Button, buttonVariants }; 60 | -------------------------------------------------------------------------------- /webui/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ); 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ); 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ); 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ); 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ); 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ); 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ); 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | }; 93 | -------------------------------------------------------------------------------- /src/framework/scheduler/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | mod looper; 19 | mod thermal; 20 | mod topapp; 21 | 22 | use std::time::Duration; 23 | 24 | use super::{ 25 | Extension, 26 | config::Config, 27 | error::{Error, Result}, 28 | node::Node, 29 | }; 30 | use crate::Controller; 31 | 32 | use frame_analyzer::Analyzer; 33 | use looper::Looper; 34 | 35 | #[derive(Debug, Clone, Copy)] 36 | pub struct FasData { 37 | pub pid: i32, 38 | pub frametime: Duration, 39 | } 40 | 41 | pub struct Scheduler { 42 | controller: Option, 43 | config: Option, 44 | } 45 | 46 | impl Scheduler { 47 | #[must_use] 48 | pub const fn new() -> Self { 49 | Self { 50 | controller: None, 51 | config: None, 52 | } 53 | } 54 | 55 | #[must_use] 56 | #[allow(clippy::missing_const_for_fn)] 57 | pub fn config(mut self, c: Config) -> Self { 58 | self.config = Some(c); 59 | self 60 | } 61 | 62 | #[must_use] 63 | #[allow(clippy::missing_const_for_fn)] 64 | pub fn controller(mut self, c: Controller) -> Self { 65 | self.controller = Some(c); 66 | self 67 | } 68 | 69 | pub fn start_run(self) -> Result<()> { 70 | let extension = Extension::init()?; 71 | let config = self.config.ok_or(Error::SchedulerMissing("Config"))?; 72 | 73 | let controller = self 74 | .controller 75 | .ok_or(Error::SchedulerMissing("Controller"))?; 76 | 77 | let node = Node::init()?; 78 | let analyzer = Analyzer::new()?; 79 | 80 | Looper::new(analyzer, config, node, extension, controller).enter_loop() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/framework/node/mod.rs: -------------------------------------------------------------------------------- 1 | mod power_mode; 2 | 3 | use std::{ 4 | collections::HashMap, 5 | fs, 6 | path::Path, 7 | time::{Duration, Instant}, 8 | }; 9 | 10 | use crate::framework::error::{Error, Result}; 11 | use likely_stable::unlikely; 12 | pub use power_mode::Mode; 13 | 14 | const NODE_PATH: &str = "/dev/fas_rs"; 15 | const REFRESH_TIME: Duration = Duration::from_secs(1); 16 | 17 | pub struct Node { 18 | map: HashMap, 19 | timer: Instant, 20 | } 21 | 22 | impl Node { 23 | pub fn init() -> Result { 24 | let _ = fs::create_dir(NODE_PATH); 25 | 26 | let mut result = Self { 27 | map: HashMap::new(), 28 | timer: Instant::now(), 29 | }; 30 | 31 | let _ = result.remove_node("mode"); 32 | result.create_node("mode", "balance")?; 33 | 34 | Ok(result) 35 | } 36 | 37 | pub fn create_node(&mut self, i: S, d: S) -> Result<()> 38 | where 39 | S: AsRef, 40 | { 41 | let id = i.as_ref(); 42 | let default = d.as_ref(); 43 | 44 | let path = Path::new(NODE_PATH).join(id); 45 | fs::write(path, default)?; 46 | self.refresh() 47 | } 48 | 49 | pub fn remove_node(&mut self, i: S) -> Result<()> 50 | where 51 | S: AsRef, 52 | { 53 | let id = i.as_ref(); 54 | 55 | let path = Path::new(NODE_PATH).join(id); 56 | fs::remove_file(path)?; 57 | 58 | self.refresh() 59 | } 60 | 61 | pub fn get_node(&mut self, id: S) -> Result 62 | where 63 | S: AsRef, 64 | { 65 | let id = id.as_ref(); 66 | 67 | if unlikely(self.timer.elapsed() > REFRESH_TIME) { 68 | self.refresh()?; 69 | } 70 | 71 | self.map 72 | .get_mut(id) 73 | .map_or_else(|| Err(Error::NodeNotFound), |value| Ok(value.clone())) 74 | } 75 | 76 | fn refresh(&mut self) -> Result<()> { 77 | for entry in fs::read_dir(NODE_PATH)? { 78 | let Ok(entry) = entry else { 79 | continue; 80 | }; 81 | 82 | if entry.file_type()?.is_file() { 83 | let id = entry.file_name().into_string().unwrap(); 84 | let value = fs::read_to_string(entry.path())?; 85 | self.map.insert(id, value); 86 | } 87 | } 88 | 89 | Ok(()) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/framework/extension/api/v2.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use libc::pid_t; 19 | 20 | use super::{ 21 | super::core::ExtensionMap, 22 | Api, 23 | misc::{do_callback, get_api_version}, 24 | }; 25 | 26 | #[derive(Debug, Clone)] 27 | pub enum ApiV2 { 28 | LoadFas(pid_t, String), 29 | UnloadFas(pid_t, String), 30 | StartFas, 31 | StopFas, 32 | InitCpuFreq, 33 | ResetCpuFreq, 34 | TargetFpsChange(u32, String), 35 | } 36 | 37 | impl Api for ApiV2 { 38 | fn handle_api(&self, ext: &ExtensionMap) { 39 | for (extension, lua) in ext.iter().filter(|(_, lua)| get_api_version(lua) == 2) { 40 | match self.clone() { 41 | Self::LoadFas(pid, pkg) => { 42 | do_callback(extension, lua, "load_fas", (pid, pkg)); 43 | } 44 | Self::UnloadFas(pid, pkg) => { 45 | do_callback(extension, lua, "unload_fas", (pid, pkg)); 46 | } 47 | Self::StartFas => { 48 | do_callback(extension, lua, "start_fas", ()); 49 | } 50 | Self::StopFas => { 51 | do_callback(extension, lua, "stop_fas", ()); 52 | } 53 | Self::InitCpuFreq => { 54 | do_callback(extension, lua, "init_cpu_freq", ()); 55 | } 56 | Self::ResetCpuFreq => { 57 | do_callback(extension, lua, "reset_cpu_freq", ()); 58 | } 59 | Self::TargetFpsChange(target_fps, pkg) => { 60 | do_callback(extension, lua, "target_fps_change", (target_fps, pkg)); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/framework/extension/api/v3.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use libc::pid_t; 19 | 20 | use super::{ 21 | super::core::ExtensionMap, 22 | Api, 23 | misc::{do_callback, get_api_version}, 24 | }; 25 | 26 | #[derive(Debug, Clone)] 27 | pub enum ApiV3 { 28 | LoadFas(pid_t, String), 29 | UnloadFas(pid_t, String), 30 | StartFas, 31 | StopFas, 32 | InitCpuFreq, 33 | ResetCpuFreq, 34 | TargetFpsChange(u32, String), 35 | } 36 | 37 | impl Api for ApiV3 { 38 | fn handle_api(&self, ext: &ExtensionMap) { 39 | for (extension, lua) in ext.iter().filter(|(_, lua)| get_api_version(lua) == 3) { 40 | match self.clone() { 41 | Self::LoadFas(pid, pkg) => { 42 | do_callback(extension, lua, "load_fas", (pid, pkg)); 43 | } 44 | Self::UnloadFas(pid, pkg) => { 45 | do_callback(extension, lua, "unload_fas", (pid, pkg)); 46 | } 47 | Self::StartFas => { 48 | do_callback(extension, lua, "start_fas", ()); 49 | } 50 | Self::StopFas => { 51 | do_callback(extension, lua, "stop_fas", ()); 52 | } 53 | Self::InitCpuFreq => { 54 | do_callback(extension, lua, "init_cpu_freq", ()); 55 | } 56 | Self::ResetCpuFreq => { 57 | do_callback(extension, lua, "reset_cpu_freq", ()); 58 | } 59 | Self::TargetFpsChange(target_fps, pkg) => { 60 | do_callback(extension, lua, "target_fps_change", (target_fps, pkg)); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/framework/extension/api/v4.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use libc::pid_t; 19 | 20 | use super::{ 21 | super::core::ExtensionMap, 22 | Api, 23 | misc::{do_callback, get_api_version}, 24 | }; 25 | 26 | #[derive(Debug, Clone)] 27 | pub enum ApiV4 { 28 | LoadFas(pid_t, String), 29 | UnloadFas(pid_t, String), 30 | StartFas, 31 | StopFas, 32 | InitCpuFreq, 33 | ResetCpuFreq, 34 | TargetFpsChange(u32, String), 35 | } 36 | 37 | impl Api for ApiV4 { 38 | fn handle_api(&self, ext: &ExtensionMap) { 39 | for (extension, lua) in ext.iter().filter(|(_, lua)| get_api_version(lua) == 4) { 40 | match self.clone() { 41 | Self::LoadFas(pid, pkg) => { 42 | do_callback(extension, lua, "load_fas", (pid, pkg)); 43 | } 44 | Self::UnloadFas(pid, pkg) => { 45 | do_callback(extension, lua, "unload_fas", (pid, pkg)); 46 | } 47 | Self::StartFas => { 48 | do_callback(extension, lua, "start_fas", ()); 49 | } 50 | Self::StopFas => { 51 | do_callback(extension, lua, "stop_fas", ()); 52 | } 53 | Self::InitCpuFreq => { 54 | do_callback(extension, lua, "init_cpu_freq", ()); 55 | } 56 | Self::ResetCpuFreq => { 57 | do_callback(extension, lua, "reset_cpu_freq", ()); 58 | } 59 | Self::TargetFpsChange(target_fps, pkg) => { 60 | do_callback(extension, lua, "target_fps_change", (target_fps, pkg)); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /webui/src/components/config/GeneralConfig.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ConfigOptions } from "@/types/config"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardHeader, 9 | CardTitle, 10 | } from "@/components/ui/card"; 11 | import { Switch } from "@/components/ui/switch"; 12 | import { useTranslation } from "react-i18next"; 13 | 14 | interface GeneralConfigProps { 15 | configOptions: ConfigOptions; 16 | toggleConfigOption: (option: keyof ConfigOptions) => void; 17 | } 18 | 19 | export function GeneralConfig({ 20 | configOptions, 21 | toggleConfigOption, 22 | }: GeneralConfigProps) { 23 | const { t } = useTranslation(); 24 | 25 | return ( 26 | 27 | 28 | 29 | {t("common:config")} 30 | 31 | {t("common:basic_settings")} 32 | 33 | 34 |
35 |
36 |
37 | 40 | 41 | {t("common:keep_std_desc")} 42 | 43 |
44 | toggleConfigOption("keep_std")} 47 | className="data-[state=checked]:bg-primary" 48 | /> 49 |
50 |
51 | 52 |
53 |
54 |
55 | 58 | 59 | {t("common:scene_game_list_desc")} 60 | 61 |
62 | toggleConfigOption("scene_game_list")} 65 | className="data-[state=checked]:bg-primary" 66 | /> 67 |
68 |
69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/file_handler.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::{ 19 | collections::{HashMap, hash_map::Entry}, 20 | fs::{File, set_permissions}, 21 | io::{self, ErrorKind, prelude::*}, 22 | os::unix::fs::PermissionsExt, 23 | path::{Path, PathBuf}, 24 | }; 25 | 26 | use anyhow::Result; 27 | use sys_mount::{UnmountFlags, unmount}; 28 | 29 | #[derive(Debug)] 30 | pub struct FileHandler { 31 | files: HashMap, 32 | } 33 | 34 | impl FileHandler { 35 | pub fn new() -> Self { 36 | Self { 37 | files: HashMap::new(), 38 | } 39 | } 40 | 41 | pub fn write_with_workround(&mut self, path: P, content: S) -> Result<()> 42 | where 43 | P: AsRef, 44 | S: AsRef<[u8]>, 45 | { 46 | if let Err(e) = self.write(path.as_ref(), content.as_ref()) { 47 | match e.kind() { 48 | ErrorKind::PermissionDenied => { 49 | set_permissions(path.as_ref(), PermissionsExt::from_mode(0o644))?; 50 | self.write(path, content)?; 51 | Ok(()) 52 | } 53 | ErrorKind::InvalidInput => Ok(()), 54 | _ => Err(e.into()), 55 | } 56 | } else { 57 | Ok(()) 58 | } 59 | } 60 | 61 | pub fn write(&mut self, path: P, content: S) -> io::Result<()> 62 | where 63 | P: AsRef, 64 | S: AsRef<[u8]>, 65 | { 66 | match self.files.entry(path.as_ref().to_path_buf()) { 67 | Entry::Occupied(mut entry) => { 68 | entry.get_mut().write_all(content.as_ref())?; 69 | } 70 | Entry::Vacant(entry) => { 71 | let _ = unmount(path.as_ref(), UnmountFlags::DETACH); 72 | set_permissions(path.as_ref(), PermissionsExt::from_mode(0o644))?; 73 | let mut file = File::create(path)?; 74 | file.write_all(content.as_ref())?; 75 | entry.insert(file); 76 | } 77 | } 78 | 79 | Ok(()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/framework/scheduler/thermal.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::{fs, path::PathBuf}; 19 | 20 | use anyhow::Result; 21 | #[cfg(debug_assertions)] 22 | use log::debug; 23 | 24 | use crate::{Config, Mode, framework::config::TemperatureThreshold}; 25 | 26 | pub struct Thermal { 27 | target_fps_offset: f64, 28 | core_temperature: u64, 29 | nodes: Vec, 30 | } 31 | 32 | impl Thermal { 33 | pub fn new() -> Result { 34 | let mut nodes = Vec::new(); 35 | for device in fs::read_dir("/sys/devices/virtual/thermal")? { 36 | let device = device?; 37 | let device_type = device.path().join("type"); 38 | let Ok(device_type) = fs::read_to_string(device_type) else { 39 | continue; 40 | }; 41 | if device_type.contains("cpu-") 42 | || device_type.contains("soc_max") 43 | || device_type.contains("mtktscpu") 44 | { 45 | nodes.push(device.path().join("temp")); 46 | } 47 | } 48 | 49 | Ok(Self { 50 | target_fps_offset: 0.0, 51 | core_temperature: 0, 52 | nodes, 53 | }) 54 | } 55 | 56 | pub fn target_fps_offset(&mut self, config: &mut Config, mode: Mode) -> f64 { 57 | let target_core_temperature = match config.mode_config(mode).core_temp_thresh { 58 | TemperatureThreshold::Disabled => u64::MAX, 59 | TemperatureThreshold::Temp(t) => t, 60 | }; 61 | 62 | self.temperature_update(); 63 | 64 | #[cfg(debug_assertions)] 65 | { 66 | debug!("target_core_temperature: {target_core_temperature}"); 67 | debug!("core_temperature: {}", self.core_temperature); 68 | } 69 | 70 | if self.core_temperature > target_core_temperature { 71 | self.target_fps_offset -= 0.1; 72 | } else { 73 | self.target_fps_offset += 0.1; 74 | } 75 | 76 | self.target_fps_offset 77 | } 78 | 79 | fn temperature_update(&mut self) { 80 | self.core_temperature = self 81 | .nodes 82 | .iter() 83 | .filter_map(|path| fs::read_to_string(path).ok()) 84 | .map(|temp| temp.trim().parse::().unwrap_or_default()) 85 | .max() 86 | .unwrap_or_default(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/framework/config/merge.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use likely_stable::LikelyOption; 19 | use serde::{Deserialize, Serialize}; 20 | use toml::{Table, Value}; 21 | 22 | use super::Config; 23 | use crate::framework::error::{Error, Result}; 24 | 25 | #[derive(Deserialize, Serialize)] 26 | struct ConfigData { 27 | pub config: Table, 28 | pub game_list: Table, 29 | pub powersave: Table, 30 | pub balance: Table, 31 | pub performance: Table, 32 | pub fast: Table, 33 | } 34 | 35 | impl Config { 36 | pub fn merge(l: S, s: S) -> Result 37 | where 38 | S: AsRef, 39 | { 40 | let local_conf = l.as_ref(); 41 | let std_conf = s.as_ref(); 42 | 43 | let std_conf: ConfigData = toml::from_str(std_conf)?; 44 | let local_conf: ConfigData = toml::from_str(local_conf)?; 45 | 46 | if local_conf 47 | .config 48 | .get("keep_std") 49 | .and_then_likely(Value::as_bool) 50 | .ok_or(Error::ParseConfig)? 51 | { 52 | let new_conf = ConfigData { 53 | config: std_conf.config, 54 | game_list: local_conf.game_list, 55 | powersave: std_conf.powersave, 56 | balance: std_conf.balance, 57 | performance: std_conf.performance, 58 | fast: std_conf.fast, 59 | }; 60 | return Ok(toml::to_string(&new_conf)?); 61 | } 62 | 63 | let config = Self::table_merge(std_conf.config, local_conf.config); 64 | let powersave = Self::table_merge(std_conf.powersave, local_conf.powersave); 65 | let balance = Self::table_merge(std_conf.balance, local_conf.balance); 66 | let performance = Self::table_merge(std_conf.performance, local_conf.performance); 67 | let fast = Self::table_merge(std_conf.fast, local_conf.fast); 68 | 69 | let new_conf = ConfigData { 70 | config, 71 | game_list: local_conf.game_list, 72 | powersave, 73 | balance, 74 | performance, 75 | fast, 76 | }; 77 | 78 | Ok(toml::to_string(&new_conf)?) 79 | } 80 | 81 | fn table_merge(mut s: Table, l: Table) -> Table { 82 | let old: Table = l.into_iter().filter(|(k, _)| s.contains_key(k)).collect(); 83 | s.extend(old); 84 | s 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /webui/src/lib/kernelsu.js: -------------------------------------------------------------------------------- 1 | let callbackCounter = 0; 2 | function getUniqueCallbackName(prefix) { 3 | return `${prefix}_callback_${Date.now()}_${callbackCounter++}`; 4 | } 5 | 6 | export function exec(command, options) { 7 | if (typeof options === "undefined") { 8 | options = {}; 9 | } 10 | 11 | return new Promise((resolve, reject) => { 12 | // Generate a unique callback function name 13 | const callbackFuncName = getUniqueCallbackName("exec"); 14 | 15 | // Define the success callback function 16 | window[callbackFuncName] = (errno, stdout, stderr) => { 17 | resolve({ errno, stdout, stderr }); 18 | cleanup(callbackFuncName); 19 | }; 20 | 21 | function cleanup(successName) { 22 | delete window[successName]; 23 | } 24 | 25 | try { 26 | ksu.exec(command, JSON.stringify(options), callbackFuncName); 27 | } catch (_error) { 28 | reject(error); 29 | cleanup(callbackFuncName); 30 | } 31 | }); 32 | } 33 | 34 | function Stdio() { 35 | this.listeners = {}; 36 | } 37 | 38 | Stdio.prototype.on = function (event, listener) { 39 | if (!this.listeners[event]) { 40 | this.listeners[event] = []; 41 | } 42 | this.listeners[event].push(listener); 43 | }; 44 | 45 | Stdio.prototype.emit = function (event, ...args) { 46 | if (this.listeners[event]) { 47 | this.listeners[event].forEach((listener) => listener(...args)); 48 | } 49 | }; 50 | 51 | function ChildProcess() { 52 | this.listeners = {}; 53 | this.stdin = new Stdio(); 54 | this.stdout = new Stdio(); 55 | this.stderr = new Stdio(); 56 | } 57 | 58 | ChildProcess.prototype.on = function (event, listener) { 59 | if (!this.listeners[event]) { 60 | this.listeners[event] = []; 61 | } 62 | this.listeners[event].push(listener); 63 | }; 64 | 65 | ChildProcess.prototype.emit = function (event, ...args) { 66 | if (this.listeners[event]) { 67 | this.listeners[event].forEach((listener) => listener(...args)); 68 | } 69 | }; 70 | 71 | export function spawn(command, args, options) { 72 | if (typeof args === "undefined") { 73 | args = []; 74 | } else if (!(args instanceof Array)) { 75 | // allow for (command, options) signature 76 | options = args; 77 | } 78 | 79 | if (typeof options === "undefined") { 80 | options = {}; 81 | } 82 | 83 | const child = new ChildProcess(); 84 | const childCallbackName = getUniqueCallbackName("spawn"); 85 | window[childCallbackName] = child; 86 | 87 | function cleanup(name) { 88 | delete window[name]; 89 | } 90 | 91 | child.on("exit", (_) => { 92 | cleanup(childCallbackName); 93 | }); 94 | 95 | try { 96 | ksu.spawn( 97 | command, 98 | JSON.stringify(args), 99 | JSON.stringify(options), 100 | childCallbackName, 101 | ); 102 | } catch (error) { 103 | child.emit("error", error); 104 | cleanup(childCallbackName); 105 | } 106 | return child; 107 | } 108 | 109 | export function fullScreen(isFullScreen) { 110 | ksu.fullScreen(isFullScreen); 111 | } 112 | 113 | export function toast(message) { 114 | ksu.toast(message); 115 | } 116 | 117 | export function moduleInfo() { 118 | return ksu.moduleInfo(); 119 | } 120 | -------------------------------------------------------------------------------- /src/framework/config/data/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | mod default; 19 | 20 | use std::collections::{HashMap, HashSet}; 21 | 22 | use serde::{Deserialize, Serialize}; 23 | use toml::Table; 24 | 25 | #[derive(Debug, Serialize, Deserialize, Clone)] 26 | pub struct ConfigData { 27 | pub config: Config, 28 | pub game_list: Table, 29 | #[serde(skip)] 30 | pub scene_game_list: HashSet, 31 | pub powersave: ModeConfig, 32 | pub balance: ModeConfig, 33 | pub performance: ModeConfig, 34 | pub fast: ModeConfig, 35 | } 36 | 37 | #[allow(clippy::struct_excessive_bools)] 38 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 39 | pub struct Config { 40 | #[serde(default = "Config::default_value_keep_std")] 41 | pub keep_std: bool, 42 | #[serde(default = "Config::default_value_scene_game_list")] 43 | pub scene_game_list: bool, 44 | } 45 | 46 | #[derive(Debug, Serialize, Deserialize, Clone)] 47 | pub struct ModeConfig { 48 | pub margin_fps: MarginFps, 49 | pub core_temp_thresh: TemperatureThreshold, 50 | } 51 | 52 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 53 | pub enum TemperatureThreshold { 54 | #[serde(rename = "disabled")] 55 | Disabled, 56 | #[serde(untagged)] 57 | Temp(u64), 58 | } 59 | 60 | #[derive(Debug, Serialize, Deserialize, Clone)] 61 | pub enum MarginFps { 62 | #[serde(untagged)] 63 | BaseOnly(MarginFpsValue), 64 | #[serde(untagged)] 65 | Advanced { 66 | base: MarginFpsValue, 67 | #[serde(flatten)] 68 | overrides: HashMap, 69 | }, 70 | } 71 | 72 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 73 | pub enum MarginFpsValue { 74 | #[serde(untagged)] 75 | Float(f64), 76 | #[serde(untagged)] 77 | Int(u64), 78 | } 79 | 80 | impl From for f64 { 81 | fn from(value: MarginFpsValue) -> Self { 82 | match value { 83 | MarginFpsValue::Float(f) => f, 84 | MarginFpsValue::Int(i) => i as Self, 85 | } 86 | } 87 | } 88 | 89 | #[derive(Debug, Serialize, Deserialize, Clone)] 90 | #[serde(rename = "map")] 91 | pub struct SceneAppList { 92 | #[serde(rename = "boolean")] 93 | pub apps: Vec, 94 | } 95 | 96 | #[derive(Debug, Serialize, Deserialize, Clone)] 97 | pub struct SceneApp { 98 | #[serde(rename = "@name")] 99 | pub pkg: String, 100 | #[serde(rename = "@value")] 101 | pub is_game: bool, 102 | } 103 | -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /webui/public/icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /xtask/src/zip_ext.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{Read, Write}, 4 | path::{Component, Path, PathBuf}, 5 | }; 6 | 7 | use zip::{ 8 | ZipWriter, 9 | result::ZipResult, 10 | write::{FileOptionExtension, FileOptions}, 11 | }; 12 | 13 | /// Creates a zip archive that contains the files and directories from the specified directory, uses the specified compression level. 14 | pub fn zip_create_from_directory_with_options( 15 | archive_file: &PathBuf, 16 | directory: &Path, 17 | cb_file_options: F, 18 | ) -> ZipResult<()> 19 | where 20 | T: FileOptionExtension, 21 | F: Fn(&PathBuf) -> FileOptions, 22 | { 23 | let file = File::create(archive_file)?; 24 | let zip_writer = ZipWriter::new(file); 25 | create_from_directory_with_options(zip_writer, directory, cb_file_options) 26 | } 27 | 28 | fn create_from_directory_with_options( 29 | mut zip_writer: ZipWriter, 30 | directory: &Path, 31 | cb_file_options: F, 32 | ) -> ZipResult<()> 33 | where 34 | T: FileOptionExtension, 35 | F: Fn(&PathBuf) -> FileOptions, 36 | { 37 | let mut paths_queue: Vec = vec![]; 38 | paths_queue.push(directory.to_path_buf()); 39 | 40 | let mut buffer = Vec::new(); 41 | 42 | while let Some(next) = paths_queue.pop() { 43 | let directory_entry_iterator = std::fs::read_dir(next)?; 44 | 45 | for entry in directory_entry_iterator { 46 | let entry_path = entry?.path(); 47 | let file_options = cb_file_options(&entry_path); 48 | let entry_metadata = std::fs::metadata(entry_path.clone())?; 49 | if entry_metadata.is_file() { 50 | let mut f = File::open(&entry_path)?; 51 | f.read_to_end(&mut buffer)?; 52 | let relative_path = make_relative_path(directory, &entry_path); 53 | zip_writer.start_file(path_as_string(&relative_path), file_options)?; 54 | zip_writer.write_all(buffer.as_ref())?; 55 | buffer.clear(); 56 | } else if entry_metadata.is_dir() { 57 | let relative_path = make_relative_path(directory, &entry_path); 58 | zip_writer.add_directory(path_as_string(&relative_path), file_options)?; 59 | paths_queue.push(entry_path.clone()); 60 | } 61 | } 62 | } 63 | 64 | zip_writer.finish()?; 65 | Ok(()) 66 | } 67 | 68 | fn make_relative_path(root: &Path, current: &Path) -> PathBuf { 69 | let mut result = PathBuf::new(); 70 | let root_components = root.components().collect::>(); 71 | let current_components = current.components().collect::>(); 72 | for i in 0..current_components.len() { 73 | let current_path_component: Component = current_components[i]; 74 | if i < root_components.len() { 75 | let other: Component = root_components[i]; 76 | if other != current_path_component { 77 | break; 78 | } 79 | } else { 80 | result.push(current_path_component) 81 | } 82 | } 83 | result 84 | } 85 | 86 | fn path_as_string(path: &std::path::Path) -> String { 87 | let mut path_str = String::new(); 88 | for component in path.components() { 89 | if let Component::Normal(os_str) = component { 90 | if !path_str.is_empty() { 91 | path_str.push('/'); 92 | } 93 | path_str.push_str(&os_str.to_string_lossy()); 94 | } 95 | } 96 | path_str 97 | } 98 | -------------------------------------------------------------------------------- /webui/src/components/ui/combobox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Check, ChevronsUpDown } from "lucide-react"; 5 | import { cn } from "@/lib/utils"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Command, 9 | CommandEmpty, 10 | CommandGroup, 11 | CommandInput, 12 | CommandItem, 13 | CommandList, 14 | } from "@/components/ui/command"; 15 | import { 16 | Popover, 17 | PopoverContent, 18 | PopoverTrigger, 19 | } from "@/components/ui/popover"; 20 | 21 | interface ComboboxProps { 22 | value: string; 23 | onValueChange: (value: string) => void; 24 | options: Array<{ label: string; value: string; disabled?: boolean }>; 25 | placeholder?: string; 26 | emptyText?: string; 27 | searchText?: string; 28 | className?: string; 29 | } 30 | 31 | export function Combobox({ 32 | value, 33 | onValueChange, 34 | options, 35 | placeholder, 36 | emptyText, 37 | searchText, 38 | className, 39 | }: ComboboxProps) { 40 | const [open, setOpen] = React.useState(false); 41 | 42 | return ( 43 | 44 | 45 | 61 | 62 | 67 | 68 | 69 | 70 | 71 | {emptyText} 72 | 73 | 74 | {options.map((option) => ( 75 | { 79 | if (!option.disabled) { 80 | onValueChange(currentValue === value ? "" : currentValue); 81 | setOpen(false); 82 | } 83 | }} 84 | disabled={option.disabled} 85 | className={cn( 86 | "py-1.5", 87 | option.disabled && "opacity-50 cursor-not-allowed" 88 | )} 89 | > 90 | {option.label} 91 | 97 | 98 | ))} 99 | 100 | 101 | 102 | 103 | 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025, shadow3, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | #![deny(clippy::all, clippy::pedantic)] 19 | #![warn(clippy::nursery)] 20 | #![allow( 21 | clippy::module_name_repetitions, 22 | clippy::cast_possible_truncation, 23 | clippy::cast_sign_loss, 24 | clippy::cast_precision_loss, 25 | clippy::cast_possible_wrap 26 | )] 27 | 28 | mod cpu_common; 29 | mod file_handler; 30 | mod framework; 31 | mod misc; 32 | 33 | use std::{ 34 | env, fs, 35 | io::{self, prelude::*}, 36 | process, 37 | }; 38 | 39 | use framework::prelude::*; 40 | 41 | use anyhow::Result; 42 | use flexi_logger::{DeferredNow, LogSpecification, Logger, Record}; 43 | use log::{error, warn}; 44 | use mimalloc::MiMalloc; 45 | 46 | #[cfg(debug_assertions)] 47 | use log::debug; 48 | 49 | use cpu_common::Controller; 50 | use misc::setprop; 51 | 52 | #[global_allocator] 53 | static GLOBAL: MiMalloc = MiMalloc; 54 | 55 | const USER_CONFIG: &str = "/sdcard/Android/fas-rs/games.toml"; 56 | 57 | fn main() -> Result<()> { 58 | let args: Vec<_> = env::args().collect(); 59 | 60 | if args[1] == "merge" { 61 | let local = fs::read_to_string(USER_CONFIG)?; 62 | let std = fs::read_to_string(&args[2])?; 63 | 64 | let new = Config::merge(&local, &std).unwrap_or(std); 65 | println!("{new}"); 66 | 67 | return Ok(()); 68 | } else if args[1] == "run" { 69 | setprop("fas-rs-server-started", "true"); 70 | run(&args[2]).unwrap_or_else(|e| { 71 | for cause in e.chain() { 72 | error!("{cause:#?}"); 73 | } 74 | error!("{:#?}", e.backtrace()); 75 | }); 76 | } 77 | 78 | Ok(()) 79 | } 80 | 81 | fn run(std_path: S) -> Result<()> 82 | where 83 | S: AsRef, 84 | { 85 | #[cfg(not(debug_assertions))] 86 | let logger_spec = LogSpecification::info(); 87 | 88 | #[cfg(debug_assertions)] 89 | let logger_spec = LogSpecification::debug(); 90 | 91 | Logger::with(logger_spec) 92 | .log_to_stdout() 93 | .format(log_format) 94 | .start()?; 95 | 96 | let std_path = std_path.as_ref(); 97 | 98 | let self_pid = process::id(); 99 | let _ = fs::write("/dev/cpuset/background/cgroup.procs", self_pid.to_string()); 100 | 101 | let config = Config::new(USER_CONFIG, std_path)?; 102 | let cpu = Controller::new()?; 103 | 104 | #[cfg(debug_assertions)] 105 | debug!("{cpu:#?}"); 106 | 107 | Scheduler::new() 108 | .config(config) 109 | .controller(cpu) 110 | .start_run()?; 111 | 112 | Ok(()) 113 | } 114 | 115 | fn log_format( 116 | write: &mut dyn Write, 117 | now: &mut DeferredNow, 118 | record: &Record<'_>, 119 | ) -> Result<(), io::Error> { 120 | let time = now.format("%Y-%m-%d %H:%M:%S"); 121 | write!(write, "[{time}] {}: {}", record.level(), record.args()) 122 | } 123 | -------------------------------------------------------------------------------- /src/framework/extension/api/helper_funs.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::sync::atomic::{AtomicBool, Ordering}; 19 | 20 | use anyhow::Context; 21 | #[cfg(debug_assertions)] 22 | use log::debug; 23 | use log::warn; 24 | 25 | use crate::cpu_common::{ 26 | EXTRA_POLICY_MAP, IGNORE_MAP, 27 | extra_policy::{AbsRangeBound, ExtraPolicy, RelRangeBound}, 28 | }; 29 | 30 | static WARNING_FLAG: AtomicBool = AtomicBool::new(false); 31 | 32 | pub fn remove_extra_policy(policy: i32) { 33 | *EXTRA_POLICY_MAP 34 | .get() 35 | .context("EXTRA_POLICY_MAP not initialized") 36 | .unwrap() 37 | .get(&policy) 38 | .context("CPU Policy not found") 39 | .unwrap() 40 | .lock() = ExtraPolicy::None; 41 | } 42 | 43 | pub fn set_extra_policy_abs(policy: i32, min: Option, max: Option) { 44 | let extra_policy = if min.is_none() && max.is_none() { 45 | ExtraPolicy::None 46 | } else { 47 | ExtraPolicy::AbsRangeBound(AbsRangeBound { min, max }) 48 | }; 49 | 50 | *EXTRA_POLICY_MAP 51 | .get() 52 | .context("EXTRA_POLICY_MAP not initialized") 53 | .unwrap() 54 | .get(&policy) 55 | .context("CPU Policy not found") 56 | .unwrap() 57 | .lock() = extra_policy; 58 | 59 | #[cfg(debug_assertions)] 60 | debug!("EXTRA_POLICY_MAP: {:?}", EXTRA_POLICY_MAP.get().unwrap()); 61 | } 62 | 63 | pub fn set_extra_policy_rel( 64 | policy: i32, 65 | target_policy: i32, 66 | min: Option, 67 | max: Option, 68 | ) { 69 | let extra_policy = if min.is_none() && max.is_none() { 70 | ExtraPolicy::None 71 | } else { 72 | ExtraPolicy::RelRangeBound(RelRangeBound { 73 | min, 74 | max, 75 | rel_to: target_policy, 76 | }) 77 | }; 78 | 79 | *EXTRA_POLICY_MAP 80 | .get() 81 | .context("EXTRA_POLICY_MAP not initialized") 82 | .unwrap() 83 | .get(&policy) 84 | .context("CPU Policy not found") 85 | .unwrap() 86 | .lock() = extra_policy; 87 | 88 | #[cfg(debug_assertions)] 89 | debug!("EXTRA_POLICY_MAP: {:?}", EXTRA_POLICY_MAP.get().unwrap()); 90 | } 91 | 92 | pub fn set_policy_freq_offset(_: i32, _: isize) { 93 | if !WARNING_FLAG.load(Ordering::Acquire) { 94 | warn!( 95 | "The API set_policy_freq_offset was removed in v4.2.0. If you see this warning, it means an outdated plugin is trying to use it. The warning will only appear once." 96 | ); 97 | WARNING_FLAG.store(true, Ordering::Release); 98 | } 99 | } 100 | 101 | pub fn set_ignore_policy(policy: i32, val: bool) { 102 | IGNORE_MAP 103 | .get() 104 | .unwrap() 105 | .get(&policy) 106 | .ok_or_else(|| mlua::Error::runtime("Policy Not Found!")) 107 | .unwrap() 108 | .store(val, Ordering::Release); 109 | } 110 | -------------------------------------------------------------------------------- /src/framework/extension/api/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | pub mod helper_funs; 19 | pub mod misc; 20 | pub mod v0; 21 | pub mod v1; 22 | pub mod v2; 23 | pub mod v3; 24 | pub mod v4; 25 | 26 | use super::{Extension, core::ExtensionMap}; 27 | pub use v0::ApiV0; 28 | use v1::ApiV1; 29 | use v2::ApiV2; 30 | use v3::ApiV3; 31 | use v4::ApiV4; 32 | 33 | pub trait Api: Send { 34 | fn handle_api(&self, ext: &ExtensionMap); 35 | 36 | fn into_box(self) -> Box 37 | where 38 | Self: Sized + 'static, 39 | { 40 | Box::new(self) 41 | } 42 | } 43 | 44 | pub fn trigger_init_cpu_freq(extension: &Extension) { 45 | extension.trigger_extentions(ApiV0::InitCpuFreq); 46 | extension.trigger_extentions(ApiV1::InitCpuFreq); 47 | extension.trigger_extentions(ApiV2::InitCpuFreq); 48 | extension.trigger_extentions(ApiV3::InitCpuFreq); 49 | extension.trigger_extentions(ApiV4::InitCpuFreq); 50 | } 51 | 52 | pub fn trigger_reset_cpu_freq(extension: &Extension) { 53 | extension.trigger_extentions(ApiV0::ResetCpuFreq); 54 | extension.trigger_extentions(ApiV1::ResetCpuFreq); 55 | extension.trigger_extentions(ApiV2::ResetCpuFreq); 56 | extension.trigger_extentions(ApiV3::ResetCpuFreq); 57 | extension.trigger_extentions(ApiV4::ResetCpuFreq); 58 | } 59 | 60 | pub fn trigger_load_fas(extension: &Extension, pid: i32, pkg: String) { 61 | extension.trigger_extentions(ApiV0::LoadFas(pid, pkg.clone())); 62 | extension.trigger_extentions(ApiV1::LoadFas(pid, pkg.clone())); 63 | extension.trigger_extentions(ApiV2::LoadFas(pid, pkg.clone())); 64 | extension.trigger_extentions(ApiV3::LoadFas(pid, pkg.clone())); 65 | extension.trigger_extentions(ApiV4::LoadFas(pid, pkg)); 66 | } 67 | 68 | pub fn trigger_unload_fas(extension: &Extension, pid: i32, pkg: String) { 69 | extension.trigger_extentions(ApiV0::UnloadFas(pid, pkg.clone())); 70 | extension.trigger_extentions(ApiV1::UnloadFas(pid, pkg.clone())); 71 | extension.trigger_extentions(ApiV2::UnloadFas(pid, pkg.clone())); 72 | extension.trigger_extentions(ApiV3::UnloadFas(pid, pkg.clone())); 73 | extension.trigger_extentions(ApiV4::UnloadFas(pid, pkg)); 74 | } 75 | 76 | pub fn trigger_start_fas(extension: &Extension) { 77 | extension.trigger_extentions(ApiV0::StartFas); 78 | extension.trigger_extentions(ApiV1::StartFas); 79 | extension.trigger_extentions(ApiV2::StartFas); 80 | extension.trigger_extentions(ApiV3::StartFas); 81 | extension.trigger_extentions(ApiV4::StartFas); 82 | } 83 | 84 | pub fn trigger_stop_fas(extension: &Extension) { 85 | extension.trigger_extentions(ApiV0::StopFas); 86 | extension.trigger_extentions(ApiV1::StopFas); 87 | extension.trigger_extentions(ApiV2::StopFas); 88 | extension.trigger_extentions(ApiV3::StopFas); 89 | extension.trigger_extentions(ApiV4::StopFas); 90 | } 91 | 92 | pub fn trigger_target_fps_change(extension: &Extension, target_fps: u32, pkg: String) { 93 | extension.trigger_extentions(ApiV2::TargetFpsChange(target_fps, pkg.clone())); 94 | extension.trigger_extentions(ApiV3::TargetFpsChange(target_fps, pkg.clone())); 95 | extension.trigger_extentions(ApiV4::TargetFpsChange(target_fps, pkg)); 96 | } 97 | -------------------------------------------------------------------------------- /src/framework/config/read.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::{fs, path::Path, sync::mpsc::Sender, time::Duration}; 19 | 20 | use inotify::{Inotify, WatchMask}; 21 | use log::{debug, error}; 22 | 23 | use super::data::{ConfigData, SceneAppList}; 24 | use crate::framework::error::Result; 25 | 26 | const SCENE_PROFILE: &str = "/data/data/com.omarea.vtools/shared_prefs/games.xml"; 27 | const MAX_RETRY_COUNT: u8 = 10; 28 | 29 | pub(super) fn wait_and_read(path: &Path, std_path: &Path, sx: &Sender) -> Result<()> { 30 | let std_config = read_config(std_path)?; 31 | 32 | loop { 33 | match read_config_with_retry(path) { 34 | Ok(mut config) => { 35 | if config.config.scene_game_list { 36 | if let Err(e) = read_scene_games(&mut config) { 37 | error!("Failed to read scene games: {e}"); 38 | } 39 | } 40 | sx.send(config).unwrap(); 41 | } 42 | Err(e) => { 43 | error!("Too many retries reading config: {e}"); 44 | error!("Using standard profile until user config is available."); 45 | sx.send(std_config.clone()).unwrap(); 46 | } 47 | } 48 | 49 | wait_until_update(path)?; 50 | } 51 | } 52 | 53 | fn read_config(path: &Path) -> Result { 54 | let content = fs::read_to_string(path)?; 55 | let config = toml::from_str(&content)?; 56 | Ok(config) 57 | } 58 | 59 | fn read_config_with_retry(path: &Path) -> Result { 60 | let mut retry_count = 0; 61 | 62 | loop { 63 | match read_config(path) { 64 | Ok(config) => return Ok(config), 65 | Err(e) => { 66 | debug!("Failed to read config at {}: {e}", path.display()); 67 | retry_count += 1; 68 | if retry_count >= MAX_RETRY_COUNT { 69 | return Err(e); 70 | } 71 | std::thread::sleep(Duration::from_secs(1)); 72 | } 73 | } 74 | } 75 | } 76 | 77 | fn read_scene_games(config: &mut ConfigData) -> Result<()> { 78 | if Path::new(SCENE_PROFILE).exists() { 79 | let scene_apps = fs::read_to_string(SCENE_PROFILE)?; 80 | let scene_apps: SceneAppList = quick_xml::de::from_str(&scene_apps)?; 81 | let game_list = scene_apps 82 | .apps 83 | .into_iter() 84 | .filter(|app| app.is_game) 85 | .map(|game| game.pkg) 86 | .collect(); 87 | 88 | config.scene_game_list = game_list; 89 | } 90 | 91 | Ok(()) 92 | } 93 | 94 | fn wait_until_update(path: &Path) -> Result<()> { 95 | let mut inotify = Inotify::init()?; 96 | 97 | if Path::new(SCENE_PROFILE).exists() { 98 | inotify 99 | .watches() 100 | .add(SCENE_PROFILE, WatchMask::MODIFY | WatchMask::CLOSE_WRITE)?; 101 | } 102 | 103 | inotify 104 | .watches() 105 | .add(path, WatchMask::MODIFY | WatchMask::CLOSE_WRITE)?; 106 | 107 | let mut buffer = [0; 1024]; 108 | inotify.read_events_blocking(&mut buffer)?; 109 | 110 | Ok(()) 111 | } 112 | -------------------------------------------------------------------------------- /webui/src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTranslation } from "react-i18next"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Moon, Sun, Gamepad2 } from "lucide-react"; 6 | import { useTheme } from "next-themes"; 7 | import { 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuTrigger, 12 | } from "@/components/ui/dropdown-menu"; 13 | import { useState, useEffect } from "react"; 14 | import { cn } from "@/lib/utils"; 15 | 16 | export default function Navbar() { 17 | const { i18n } = useTranslation(); 18 | const [currentLang, setCurrentLang] = useState(i18n.language); 19 | const { setTheme } = useTheme(); 20 | const [mounted, setMounted] = useState(false); 21 | const [visible, setVisible] = useState(true); 22 | const [lastScrollY, setLastScrollY] = useState(0); 23 | 24 | // Ensure theme component doesn't render until mounted on client 25 | useEffect(() => { 26 | setMounted(true); 27 | }, []); 28 | 29 | useEffect(() => { 30 | setCurrentLang(i18n.language); 31 | }, [i18n.language]); 32 | 33 | useEffect(() => { 34 | const controlNavbar = () => { 35 | if (typeof window !== "undefined") { 36 | if (window.scrollY > 100) { 37 | // If scroll down hide the navbar 38 | if (window.scrollY > lastScrollY) { 39 | setVisible(false); 40 | } else { 41 | setVisible(true); 42 | } 43 | setLastScrollY(window.scrollY); 44 | } else { 45 | setVisible(true); 46 | } 47 | } 48 | }; 49 | 50 | if (typeof window !== "undefined") { 51 | window.addEventListener("scroll", controlNavbar); 52 | 53 | // Cleanup function 54 | return () => { 55 | window.removeEventListener("scroll", controlNavbar); 56 | }; 57 | } 58 | }, [lastScrollY]); 59 | 60 | const changeLanguage = (lng: string) => { 61 | i18n.changeLanguage(lng); 62 | setCurrentLang(lng); 63 | }; 64 | 65 | if (!mounted) { 66 | return null; 67 | } 68 | 69 | return ( 70 |
76 |
77 |
78 | 79 | Game Performance 80 |
81 | 82 |
83 | 91 | 92 | 93 | 94 | 99 | 100 | 101 | setTheme("light")}> 102 | Light 103 | 104 | setTheme("dark")}> 105 | Dark 106 | 107 | setTheme("system")}> 108 | System 109 | 110 | 111 | 112 |
113 |
114 |
115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/framework/scheduler/looper/clean.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::{ 19 | collections::HashMap, 20 | ffi::CString, 21 | fs::{self, set_permissions}, 22 | os::unix::fs::PermissionsExt, 23 | path::Path, 24 | ptr, 25 | }; 26 | 27 | use libc::{MS_BIND, MS_REC, mount, umount, umount2}; 28 | 29 | use crate::framework::error::Result; 30 | 31 | fn lock_value(path: P, value: S) 32 | where 33 | P: AsRef, 34 | S: AsRef, 35 | { 36 | let value = value.as_ref(); 37 | let path = path.as_ref(); 38 | 39 | let path_str = path.display().to_string(); 40 | let mount_path = format!("/cache/mount_mask_{value}"); 41 | 42 | let _ = unmount(&path_str); 43 | let _ = set_permissions(path, PermissionsExt::from_mode(0o644)); 44 | let _ = fs::write(&path_str, value); 45 | let _ = set_permissions(path, PermissionsExt::from_mode(0o444)); 46 | let _ = fs::write(&mount_path, value); 47 | let _ = mount_bind(&mount_path, &path_str); 48 | } 49 | 50 | fn mount_bind(src_path: &str, dest_path: &str) -> Result<()> { 51 | let src_path = CString::new(src_path)?; 52 | let dest_path = CString::new(dest_path)?; 53 | 54 | unsafe { 55 | umount2(dest_path.as_ptr(), libc::MNT_DETACH); 56 | 57 | if mount( 58 | src_path.as_ptr().cast(), 59 | dest_path.as_ptr().cast(), 60 | ptr::null(), 61 | MS_BIND | MS_REC, 62 | ptr::null(), 63 | ) != 0 64 | { 65 | return Err(std::io::Error::last_os_error().into()); 66 | } 67 | } 68 | 69 | Ok(()) 70 | } 71 | 72 | fn unmount(file_system: &str) -> Result<()> { 73 | let path = CString::new(file_system)?; 74 | if unsafe { umount(path.as_ptr()) } != 0 { 75 | return Err(std::io::Error::last_os_error().into()); 76 | } 77 | Ok(()) 78 | } 79 | 80 | macro_rules! lock_values { 81 | ($map: expr, ($($path: literal),*), $value: literal) => { 82 | $( 83 | if let Ok(last_value) = fs::read_to_string($path) { 84 | $map.insert($path, last_value); 85 | } 86 | 87 | lock_value($path, $value); 88 | )* 89 | } 90 | } 91 | 92 | pub struct Cleaner { 93 | map: HashMap<&'static str, String>, 94 | } 95 | 96 | impl Cleaner { 97 | pub fn new() -> Self { 98 | Self { 99 | map: HashMap::new(), 100 | } 101 | } 102 | 103 | pub fn cleanup(&mut self) { 104 | lock_values!( 105 | self.map, 106 | ( 107 | "/sys/module/mtk_fpsgo/parameters/perfmgr_enable", 108 | "/sys/module/perfmgr/parameters/perfmgr_enable", 109 | "/sys/module/perfmgr_policy/parameters/perfmgr_enable", 110 | "/sys/module/perfmgr_mtk/parameters/perfmgr_enable", 111 | "/sys/module/migt/parameters/glk_fbreak_enable" 112 | ), 113 | "0" 114 | ); 115 | 116 | lock_values!( 117 | self.map, 118 | ( 119 | "/sys/module/migt/parameters/glk_disable", 120 | "/proc/game_opt/disable_cpufreq_limit" 121 | ), 122 | "1" 123 | ); 124 | } 125 | 126 | pub fn undo_cleanup(&self) { 127 | for (path, value) in &self.map { 128 | let _ = unmount(path); 129 | let _ = fs::write(path, value); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /webui/src/components/config/ModeSwitch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardHeader, 9 | CardTitle, 10 | } from "@/components/ui/card"; 11 | import { toast } from "sonner"; 12 | import { useTranslation } from "react-i18next"; 13 | import { 14 | Zap, 15 | BatteryCharging, 16 | SlidersHorizontal, 17 | Gauge, 18 | } from "lucide-react"; 19 | import { exec } from "@/lib/kernelsu"; 20 | import { useEffect, useState } from "react"; 21 | 22 | const MODES = ["powersave", "balance", "performance", "fast"] as const; 23 | 24 | const MODE_ICONS = { 25 | powersave: BatteryCharging, 26 | balance: SlidersHorizontal, 27 | performance: Gauge, 28 | fast: Zap, 29 | } as const; 30 | 31 | type Mode = (typeof MODES)[number]; 32 | 33 | export function ModeSwitch() { 34 | const { t } = useTranslation(); 35 | const [currentMode, setCurrentMode] = useState(null); 36 | 37 | useEffect(() => { 38 | const fetchMode = async () => { 39 | // if (process.env.NODE_ENV === "development") { 40 | // setCurrentMode("balance"); 41 | // return; 42 | // } 43 | 44 | try { 45 | const { errno, stdout } = await exec(`cat /dev/fas_rs/mode`, { cwd: "/" }); 46 | if (errno === 0) { 47 | const mode = stdout.trim() as Mode; 48 | if (MODES.includes(mode)) { 49 | setCurrentMode(mode); 50 | } 51 | } 52 | } catch (_e) { 53 | /* ignore */ 54 | } 55 | }; 56 | 57 | fetchMode(); 58 | }, []); 59 | 60 | const getModeColor = (mode: Mode) => { 61 | switch (mode) { 62 | case "powersave": 63 | return "bg-green-500 hover:bg-green-600 text-white"; 64 | case "balance": 65 | return "bg-blue-500 hover:bg-blue-600 text-white"; 66 | case "performance": 67 | return "bg-orange-500 hover:bg-orange-600 text-white"; 68 | case "fast": 69 | return "bg-red-500 hover:bg-red-600 text-white"; 70 | default: 71 | return ""; 72 | } 73 | }; 74 | 75 | const switchMode = async (mode: Mode) => { 76 | // if (process.env.NODE_ENV === "development") { 77 | // setCurrentMode(mode); 78 | // toast.info(`Switch mode to ${mode} (skipped in dev)`); 79 | // console.log(`Switch mode to ${mode} (skipped in dev)`); 80 | // return; 81 | // } 82 | 83 | try { 84 | const { errno, stderr } = await exec(`echo -n ${mode} > /dev/fas_rs/mode`, { 85 | cwd: "/", 86 | }); 87 | 88 | if (errno === 0) { 89 | setCurrentMode(mode); 90 | toast.success(`Switched to ${t(`common:${mode}_mode`)}`); 91 | } else { 92 | toast.error(`Failed to switch mode: ${stderr}`); 93 | } 94 | } catch (error) { 95 | toast.error(`Failed to switch mode: ${error}`); 96 | } 97 | }; 98 | 99 | return ( 100 | 101 | 102 |
103 | 104 | {t("common:tab_power")} 105 | 106 | {currentMode && ( 107 | 108 | {t(`common:${currentMode}_mode`)} 109 | 110 | )} 111 |
112 |
113 | 114 |
115 | {MODES.map((mode) => { 116 | const Icon = MODE_ICONS[mode]; 117 | return ( 118 | 127 | ); 128 | })} 129 |
130 |
131 |
132 | ); 133 | } -------------------------------------------------------------------------------- /webui/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { XIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Dialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return ; 13 | } 14 | 15 | function DialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return ; 19 | } 20 | 21 | function DialogPortal({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return ; 25 | } 26 | 27 | function DialogClose({ 28 | ...props 29 | }: React.ComponentProps) { 30 | return ; 31 | } 32 | 33 | function DialogOverlay({ 34 | className, 35 | ...props 36 | }: React.ComponentProps) { 37 | return ( 38 | 46 | ); 47 | } 48 | 49 | function DialogContent({ 50 | className, 51 | children, 52 | ...props 53 | }: React.ComponentProps) { 54 | return ( 55 | 56 | 57 | 65 | {children} 66 | 67 | 68 | Close 69 | 70 | 71 | 72 | ); 73 | } 74 | 75 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 76 | return ( 77 |
82 | ); 83 | } 84 | 85 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 86 | return ( 87 |
95 | ); 96 | } 97 | 98 | function DialogTitle({ 99 | className, 100 | ...props 101 | }: React.ComponentProps) { 102 | return ( 103 | 108 | ); 109 | } 110 | 111 | function DialogDescription({ 112 | className, 113 | ...props 114 | }: React.ComponentProps) { 115 | return ( 116 | 121 | ); 122 | } 123 | 124 | export { 125 | Dialog, 126 | DialogClose, 127 | DialogContent, 128 | DialogDescription, 129 | DialogFooter, 130 | DialogHeader, 131 | DialogOverlay, 132 | DialogPortal, 133 | DialogTitle, 134 | DialogTrigger, 135 | }; 136 | -------------------------------------------------------------------------------- /webui/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTranslation } from "react-i18next"; 4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 5 | import { useConfig } from "@/hooks/useConfig"; 6 | import { GeneralConfig } from "@/components/config/GeneralConfig"; 7 | import { GameList } from "@/components/config/GameList"; 8 | import { PowerModes } from "@/components/config/PowerModes"; 9 | import { Settings, Gamepad, Zap } from "lucide-react"; 10 | import { ModeSwitch } from "@/components/config/ModeSwitch"; 11 | 12 | export default function Home() { 13 | const { t } = useTranslation(); 14 | const { 15 | configOptions, 16 | gameList, 17 | powerModes, 18 | newGamePackage, 19 | setNewGamePackage, 20 | newGameFps, 21 | setNewGameFps, 22 | isAddingGame, 23 | setIsAddingGame, 24 | editingGame, 25 | editingGameFps, 26 | setEditingGameFps, 27 | toggleConfigOption, 28 | updatePowerMode, 29 | addNewGame, 30 | removeGame, 31 | startEditGame, 32 | saveEditedGame, 33 | } = useConfig(); 34 | 35 | return ( 36 |
37 | {/* Header */} 38 |
39 |

40 | {t("common:games_config")} 41 |

42 |

43 | {t("common:manage_settings")} 44 |

45 |
46 | 47 |
48 | 49 | 50 | 54 | 55 | {t("common:tab_general")} 56 | 57 | 61 | 62 | {t("common:tab_games")} 63 | 64 | 68 | 69 | {t("common:tab_power")} 70 | 71 | 72 | 73 | {/* General Config Tab */} 74 | 75 | {/* Mode Switch Card */} 76 | 77 | 78 | {/* Basic Configuration Card */} 79 | 83 | 84 | 85 | {/* Game List Tab */} 86 | 87 | 103 | 104 | 105 | {/* Power Modes Tab */} 106 | 107 | 111 | 112 | 113 |
114 |
115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/framework/scheduler/looper/policy/controll.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::time::{Duration, Instant}; 19 | 20 | use likely_stable::unlikely; 21 | #[cfg(debug_assertions)] 22 | use log::debug; 23 | 24 | use super::super::buffer::Buffer; 25 | use crate::framework::{config::MarginFps, prelude::*, scheduler::looper::ControllerState}; 26 | 27 | pub fn calculate_control( 28 | buffer: &Buffer, 29 | config: &mut Config, 30 | mode: Mode, 31 | controller_state: &mut ControllerState, 32 | target_fps_offset_thermal: f64, 33 | ) -> Option<(isize, bool)> // control, is_janked 34 | { 35 | if unlikely(buffer.frametime_state.frametimes.len() < 60) { 36 | return None; 37 | } 38 | 39 | let target_fps = f64::from(buffer.target_fps_state.target_fps?); 40 | let margin_fps: f64 = match &config.mode_config(mode).margin_fps { 41 | MarginFps::BaseOnly(base) => target_fps / 60.0 * f64::from(*base), 42 | MarginFps::Advanced { base, overrides } => overrides 43 | .get(&target_fps.to_string()) 44 | .copied() 45 | .map_or_else(|| target_fps / 60.0 * f64::from(*base), f64::from), 46 | }; 47 | 48 | assert!(margin_fps.is_sign_positive(), "margin_fps must be positive"); 49 | 50 | let target_fps = (target_fps + target_fps_offset_thermal).clamp(0.0, target_fps); 51 | let adjusted_target_fps = adjust_target_fps(target_fps, controller_state) - margin_fps; 52 | let adjusted_last_frame = get_normalized_last_frame(buffer, adjusted_target_fps); 53 | let target_frametime = Duration::from_secs(1); 54 | 55 | #[cfg(debug_assertions)] 56 | { 57 | debug!("adjusted_target_fps: {adjusted_target_fps}"); 58 | debug!("adjusted_last_frame: {adjusted_last_frame:?}"); 59 | debug!("target_frametime: {target_frametime:?}"); 60 | } 61 | 62 | Some(( 63 | calculate_control_inner(controller_state, adjusted_last_frame, target_frametime), 64 | buffer.frametime_state.current_fps_long < target_fps - 2.0, 65 | )) 66 | } 67 | 68 | fn get_normalized_last_frame(buffer: &Buffer, target_fps: f64) -> Duration { 69 | let last_frame = buffer 70 | .frametime_state 71 | .frametimes 72 | .front() 73 | .copied() 74 | .unwrap_or_default(); 75 | 76 | if buffer.frametime_state.additional_frametime == Duration::ZERO { 77 | last_frame 78 | } else { 79 | buffer.frametime_state.additional_frametime.max(last_frame) 80 | } 81 | .mul_f64(target_fps) 82 | } 83 | 84 | fn adjust_target_fps(target_fps: f64, controller_state: &mut ControllerState) -> f64 { 85 | if controller_state.usage_sample_timer.elapsed() >= Duration::from_secs(1) { 86 | controller_state.usage_sample_timer = Instant::now(); 87 | let util = controller_state.controller.util_max(); 88 | 89 | if util <= 0.1 { 90 | controller_state.target_fps_offset = 0.0; 91 | } else if util <= 0.55 { 92 | controller_state.target_fps_offset -= 0.1; 93 | } else if util >= 0.65 { 94 | controller_state.target_fps_offset += 0.1; 95 | } 96 | } 97 | 98 | controller_state.target_fps_offset = controller_state.target_fps_offset.clamp(-3.0, 0.0); 99 | target_fps + controller_state.target_fps_offset 100 | } 101 | 102 | fn calculate_control_inner( 103 | controller_state: &ControllerState, 104 | current_frametime: Duration, 105 | target_frametime: Duration, 106 | ) -> isize { 107 | let error_p = (current_frametime.as_nanos() as f64 - target_frametime.as_nanos() as f64) 108 | * controller_state.params.kp; 109 | 110 | #[cfg(debug_assertions)] 111 | debug!("error_p {error_p}"); 112 | 113 | error_p as isize 114 | } 115 | -------------------------------------------------------------------------------- /webui/src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { buttonVariants } from "@/components/ui/button"; 8 | 9 | function AlertDialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return ; 13 | } 14 | 15 | function AlertDialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return ( 19 | 20 | ); 21 | } 22 | 23 | function AlertDialogPortal({ 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 28 | ); 29 | } 30 | 31 | function AlertDialogOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 44 | ); 45 | } 46 | 47 | function AlertDialogContent({ 48 | className, 49 | ...props 50 | }: React.ComponentProps) { 51 | return ( 52 | 53 | 54 | 62 | 63 | ); 64 | } 65 | 66 | function AlertDialogHeader({ 67 | className, 68 | ...props 69 | }: React.ComponentProps<"div">) { 70 | return ( 71 |
76 | ); 77 | } 78 | 79 | function AlertDialogFooter({ 80 | className, 81 | ...props 82 | }: React.ComponentProps<"div">) { 83 | return ( 84 |
92 | ); 93 | } 94 | 95 | function AlertDialogTitle({ 96 | className, 97 | ...props 98 | }: React.ComponentProps) { 99 | return ( 100 | 105 | ); 106 | } 107 | 108 | function AlertDialogDescription({ 109 | className, 110 | ...props 111 | }: React.ComponentProps) { 112 | return ( 113 | 118 | ); 119 | } 120 | 121 | function AlertDialogAction({ 122 | className, 123 | ...props 124 | }: React.ComponentProps) { 125 | return ( 126 | 130 | ); 131 | } 132 | 133 | function AlertDialogCancel({ 134 | className, 135 | ...props 136 | }: React.ComponentProps) { 137 | return ( 138 | 142 | ); 143 | } 144 | 145 | export { 146 | AlertDialog, 147 | AlertDialogPortal, 148 | AlertDialogOverlay, 149 | AlertDialogTrigger, 150 | AlertDialogContent, 151 | AlertDialogHeader, 152 | AlertDialogFooter, 153 | AlertDialogTitle, 154 | AlertDialogDescription, 155 | AlertDialogAction, 156 | AlertDialogCancel, 157 | }; 158 | -------------------------------------------------------------------------------- /src/framework/scheduler/looper/buffer/calculate.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::time::Duration; 19 | 20 | use likely_stable::unlikely; 21 | #[cfg(debug_assertions)] 22 | use log::debug; 23 | 24 | use super::Buffer; 25 | use crate::{Extension, api::trigger_target_fps_change, framework::config::TargetFps}; 26 | 27 | impl Buffer { 28 | pub fn calculate_current_fps(&mut self) { 29 | let avg_time_long = self.calculate_average_frametime(None); 30 | #[cfg(debug_assertions)] 31 | debug!("avg_time_long: {avg_time_long:?}"); 32 | 33 | self.frametime_state.avg_time_long = avg_time_long; 34 | 35 | let current_fps_long = 1.0 / avg_time_long.as_secs_f64(); 36 | #[cfg(debug_assertions)] 37 | debug!("current_fps_long: {current_fps_long:.2}"); 38 | 39 | self.frametime_state.current_fps_long = current_fps_long; 40 | 41 | let avg_time_short = self 42 | .calculate_average_frametime(self.target_fps().map(|target_fps| target_fps as usize)); 43 | #[cfg(debug_assertions)] 44 | debug!("avg_time_short: {avg_time_short:?}"); 45 | 46 | self.frametime_state.avg_time_short = avg_time_short; 47 | 48 | let current_fps_short = 1.0 / avg_time_short.as_secs_f64(); 49 | #[cfg(debug_assertions)] 50 | debug!("current_fps_short: {current_fps_short:.2}"); 51 | 52 | self.frametime_state.current_fps_short = current_fps_short; 53 | } 54 | 55 | fn calculate_average_frametime(&self, it_takes: Option) -> Duration { 56 | let total_time: Duration = self 57 | .frametime_state 58 | .frametimes 59 | .iter() 60 | .take(it_takes.unwrap_or(self.frametime_state.frametimes.len())) 61 | .sum::() 62 | .saturating_add(self.frametime_state.additional_frametime); 63 | 64 | total_time 65 | .checked_div( 66 | it_takes 67 | .unwrap_or(self.frametime_state.frametimes.len()) 68 | .min(self.frametime_state.frametimes.len()) 69 | .try_into() 70 | .unwrap(), 71 | ) 72 | .unwrap_or_default() 73 | } 74 | 75 | pub fn calculate_target_fps(&mut self, extension: &Extension) { 76 | let new_target_fps = self.target_fps(); 77 | if self.target_fps_state.target_fps != new_target_fps || new_target_fps.is_none() { 78 | self.reset_frametime_state(); 79 | if let Some(target_fps) = new_target_fps { 80 | self.trigger_target_fps_change(extension, target_fps); 81 | } 82 | self.target_fps_state.target_fps = new_target_fps; 83 | self.unusable(); 84 | } 85 | } 86 | 87 | fn reset_frametime_state(&mut self) { 88 | self.frametime_state.frametimes.clear(); 89 | } 90 | 91 | fn trigger_target_fps_change(&self, extension: &Extension, target_fps: u32) { 92 | trigger_target_fps_change(extension, target_fps, self.package_info.pkg.clone()); 93 | } 94 | 95 | fn target_fps(&self) -> Option { 96 | let target_fpses = match &self.target_fps_state.target_fps_config { 97 | TargetFps::Value(t) => vec![*t], 98 | TargetFps::Array(arr) => arr.clone(), 99 | }; 100 | 101 | let current_fps = self.frametime_state.current_fps_long; 102 | 103 | if unlikely(current_fps < (target_fpses.first()?.saturating_sub(10).max(10)).into()) { 104 | return None; 105 | } 106 | 107 | for &target_fps in &target_fpses { 108 | if current_fps <= f64::from(target_fps) + 3.0 { 109 | #[cfg(debug_assertions)] 110 | debug!("Matched target_fps: current: {current_fps:.2} target_fps: {target_fps}"); 111 | return Some(target_fps); 112 | } 113 | } 114 | 115 | target_fpses.last().copied() 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/framework/config/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | mod data; 19 | mod inner; 20 | mod merge; 21 | mod read; 22 | 23 | use std::{fs, path::Path, sync::mpsc, thread}; 24 | 25 | use inner::Inner; 26 | use log::{error, info}; 27 | use toml::Value; 28 | 29 | use crate::framework::{error::Result, node::Mode}; 30 | pub use data::{ConfigData, MarginFps, ModeConfig, TemperatureThreshold}; 31 | use read::wait_and_read; 32 | 33 | #[derive(Debug, Clone, PartialEq, Eq)] 34 | pub enum TargetFps { 35 | Value(u32), 36 | Array(Vec), 37 | } 38 | 39 | #[derive(Debug)] 40 | pub struct Config { 41 | inner: Inner, 42 | } 43 | 44 | impl Config { 45 | pub fn new

(p: P, sp: P) -> Result 46 | where 47 | P: AsRef, 48 | { 49 | let path = p.as_ref(); 50 | let std_path = sp.as_ref(); 51 | let toml_raw = fs::read_to_string(path)?; 52 | let toml: ConfigData = toml::from_str(&toml_raw)?; 53 | 54 | let (sx, rx) = mpsc::channel(); 55 | let inner = Inner::new(toml, rx); 56 | 57 | { 58 | let path = path.to_owned(); 59 | let std_path = std_path.to_owned(); 60 | 61 | thread::Builder::new() 62 | .name("ConfigThread".into()) 63 | .spawn(move || { 64 | wait_and_read(&path, &std_path, &sx).unwrap_or_else(|e| error!("{e:#?}")); 65 | panic!("An unrecoverable error occurred!"); 66 | })?; 67 | } 68 | 69 | info!("Config watcher started"); 70 | 71 | Ok(Self { inner }) 72 | } 73 | 74 | pub fn need_fas(&mut self, pkg: S) -> bool 75 | where 76 | S: AsRef, 77 | { 78 | let pkg = pkg.as_ref(); 79 | 80 | self.inner.config().game_list.contains_key(pkg) 81 | || self.inner.config().scene_game_list.contains(pkg) 82 | } 83 | 84 | pub fn target_fps(&mut self, pkg: S) -> Option 85 | where 86 | S: AsRef, 87 | { 88 | let pkg = pkg.as_ref(); 89 | let pkg = pkg.split(':').next()?; 90 | 91 | self.inner.config().game_list.get(pkg).cloned().map_or_else( 92 | || { 93 | if self.inner.config().scene_game_list.contains(pkg) { 94 | Some(TargetFps::Array(vec![30, 45, 60, 90, 120, 144])) 95 | } else { 96 | None 97 | } 98 | }, 99 | |value| match value { 100 | Value::Array(arr) => { 101 | let mut arr: Vec<_> = arr 102 | .iter() 103 | .filter_map(toml::Value::as_integer) 104 | .map(|i| i as u32) 105 | .collect(); 106 | arr.sort_unstable(); 107 | Some(TargetFps::Array(arr)) 108 | } 109 | Value::Integer(i) => Some(TargetFps::Value(i as u32)), 110 | Value::String(s) => { 111 | if s == "auto" { 112 | Some(TargetFps::Array(vec![30, 45, 60, 90, 120, 144])) 113 | } else { 114 | error!("Find target game {pkg} in config, but meet illegal data type"); 115 | error!("Sugg: try \'{pkg} = \"auto\"\'"); 116 | None 117 | } 118 | } 119 | _ => { 120 | error!("Find target game {pkg} in config, but meet illegal data type"); 121 | error!("Sugg: try \'{pkg} = \"auto\"\'"); 122 | None 123 | } 124 | }, 125 | ) 126 | } 127 | 128 | #[must_use] 129 | pub fn mode_config(&mut self, m: Mode) -> &ModeConfig { 130 | match m { 131 | Mode::Powersave => &self.inner.config().powersave, 132 | Mode::Balance => &self.inner.config().balance, 133 | Mode::Performance => &self.inner.config().performance, 134 | Mode::Fast => &self.inner.config().fast, 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/framework/extension/core.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | use std::{collections::HashMap, fs, path::PathBuf, sync::mpsc::Receiver, time::Duration}; 19 | 20 | use inotify::{Inotify, WatchMask}; 21 | use log::{debug, error, info}; 22 | use mlua::Lua; 23 | 24 | use super::{ 25 | EXTENSIONS_PATH, 26 | api::{Api, helper_funs}, 27 | }; 28 | use crate::framework::error::Result; 29 | 30 | pub type ExtensionMap = HashMap; 31 | 32 | pub fn thread(rx: &Receiver>) { 33 | let mut extensions = load_extensions().unwrap_or_default(); 34 | let mut inotify = Inotify::init().unwrap(); 35 | 36 | inotify 37 | .watches() 38 | .add( 39 | EXTENSIONS_PATH, 40 | WatchMask::CLOSE_WRITE | WatchMask::CREATE | WatchMask::DELETE, 41 | ) 42 | .unwrap(); 43 | 44 | loop { 45 | if need_update(&mut inotify) { 46 | extensions = load_extensions().unwrap_or_default(); 47 | } 48 | 49 | if let Ok(trigger) = rx.recv_timeout(Duration::from_secs(1)) { 50 | trigger.handle_api(&extensions); 51 | } 52 | } 53 | } 54 | 55 | fn need_update(inotify: &mut Inotify) -> bool { 56 | inotify.read_events(&mut [0; 1024]).is_ok() 57 | } 58 | 59 | fn load_extensions() -> Result { 60 | let mut map: ExtensionMap = HashMap::new(); 61 | 62 | for file in fs::read_dir(EXTENSIONS_PATH)? 63 | .map(std::result::Result::unwrap) 64 | .filter(|f| f.file_type().unwrap().is_file() && f.path().extension().unwrap() == "lua") 65 | { 66 | let lua = Lua::new(); 67 | let path = file.path(); 68 | let file = fs::read_to_string(&path)?; 69 | 70 | lua.globals().set( 71 | "log_info", 72 | lua.create_function(|_, message: String| { 73 | info!("extension: {message}"); 74 | Ok(()) 75 | })?, 76 | )?; 77 | 78 | lua.globals().set( 79 | "log_debug", 80 | lua.create_function(|_, message: String| { 81 | debug!("extension: {message}"); 82 | Ok(()) 83 | })?, 84 | )?; 85 | 86 | lua.globals().set( 87 | "log_error", 88 | lua.create_function(|_, message: String| { 89 | error!("extension: {message}"); 90 | Ok(()) 91 | })?, 92 | )?; 93 | 94 | // Add in api v1 95 | // Removed in v4.2.0(apiv4) 96 | lua.globals().set( 97 | "set_policy_freq_offset", 98 | lua.create_function(|_, (policy, offset)| { 99 | helper_funs::set_policy_freq_offset(policy, offset); 100 | Ok(()) 101 | })?, 102 | )?; 103 | 104 | // Add in api v3 105 | lua.globals().set( 106 | "set_ignore_policy", 107 | lua.create_function(|_, (policy, val)| { 108 | helper_funs::set_ignore_policy(policy, val); 109 | Ok(()) 110 | })?, 111 | )?; 112 | 113 | // Add in api v4 114 | lua.globals().set( 115 | "set_extra_policy_abs", 116 | lua.create_function(|_, (policy, min, max)| { 117 | helper_funs::set_extra_policy_abs(policy, min, max); 118 | Ok(()) 119 | })?, 120 | )?; 121 | 122 | // Add in api v4 123 | lua.globals().set( 124 | "set_extra_policy_rel", 125 | lua.create_function(|_, (policy, target_policy, min, max)| { 126 | helper_funs::set_extra_policy_rel(policy, target_policy, min, max); 127 | Ok(()) 128 | })?, 129 | )?; 130 | 131 | // Add in api v4 132 | lua.globals().set( 133 | "remove_extra_policy", 134 | lua.create_function(|_, policy| { 135 | helper_funs::remove_extra_policy(policy); 136 | Ok(()) 137 | })?, 138 | )?; 139 | 140 | match lua.load(&file).exec() { 141 | Ok(()) => { 142 | info!("Extension loaded successfully: {}", path.display()); 143 | map.insert(path, lua); 144 | } 145 | Err(e) => { 146 | error!("Extension loading failed, reason: {e:#?}"); 147 | } 148 | } 149 | } 150 | 151 | Ok(map) 152 | } 153 | -------------------------------------------------------------------------------- /src/framework/scheduler/looper/buffer/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025, dependabot[bot], shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | 18 | pub mod calculate; 19 | 20 | use std::{ 21 | collections::VecDeque, 22 | time::{Duration, Instant}, 23 | }; 24 | 25 | use libc::pid_t; 26 | use likely_stable::unlikely; 27 | 28 | use crate::{Extension, framework::config::TargetFps}; 29 | 30 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 31 | pub enum BufferWorkingState { 32 | Unusable, 33 | Usable, 34 | } 35 | 36 | #[derive(Debug)] 37 | pub struct PackageInfo { 38 | pub pid: pid_t, 39 | pub pkg: String, 40 | } 41 | 42 | #[derive(Debug)] 43 | pub struct FrameTimeState { 44 | pub current_fps_long: f64, 45 | pub avg_time_long: Duration, 46 | pub current_fps_short: f64, 47 | pub avg_time_short: Duration, 48 | pub frametimes: VecDeque, 49 | pub additional_frametime: Duration, 50 | } 51 | 52 | impl FrameTimeState { 53 | fn new() -> Self { 54 | Self { 55 | current_fps_long: 0.0, 56 | avg_time_long: Duration::ZERO, 57 | current_fps_short: 0.0, 58 | avg_time_short: Duration::ZERO, 59 | frametimes: VecDeque::with_capacity(1440), 60 | additional_frametime: Duration::ZERO, 61 | } 62 | } 63 | } 64 | 65 | #[derive(Debug)] 66 | pub struct TargetFpsState { 67 | pub target_fps: Option, 68 | target_fps_config: TargetFps, 69 | } 70 | 71 | impl TargetFpsState { 72 | const fn new(target_fps_config: TargetFps) -> Self { 73 | Self { 74 | target_fps: None, 75 | target_fps_config, 76 | } 77 | } 78 | } 79 | 80 | #[derive(Debug)] 81 | pub struct BufferState { 82 | pub last_update: Instant, 83 | pub working_state: BufferWorkingState, 84 | calculate_timer: Instant, 85 | working_state_timer: Instant, 86 | } 87 | 88 | impl BufferState { 89 | fn new() -> Self { 90 | let now = Instant::now(); 91 | Self { 92 | last_update: now, 93 | working_state: BufferWorkingState::Unusable, 94 | calculate_timer: now, 95 | working_state_timer: now, 96 | } 97 | } 98 | } 99 | 100 | #[derive(Debug)] 101 | pub struct Buffer { 102 | pub package_info: PackageInfo, 103 | pub frametime_state: FrameTimeState, 104 | pub target_fps_state: TargetFpsState, 105 | pub state: BufferState, 106 | } 107 | 108 | impl Buffer { 109 | pub fn new(target_fps_config: TargetFps, pid: pid_t, pkg: String) -> Self { 110 | Self { 111 | package_info: PackageInfo { pid, pkg }, 112 | frametime_state: FrameTimeState::new(), 113 | target_fps_state: TargetFpsState::new(target_fps_config), 114 | state: BufferState::new(), 115 | } 116 | } 117 | 118 | pub fn push_frametime(&mut self, d: Duration, extension: &Extension) { 119 | self.frametime_state.additional_frametime = Duration::ZERO; 120 | self.state.last_update = Instant::now(); 121 | 122 | while self.frametime_state.frametimes.len() 123 | >= self.target_fps_state.target_fps.unwrap_or(144) as usize * 5 124 | { 125 | self.frametime_state.frametimes.pop_back(); 126 | self.try_usable(); 127 | } 128 | 129 | self.frametime_state.frametimes.push_front(d); 130 | self.try_calculate(extension); 131 | } 132 | 133 | fn try_calculate(&mut self, extension: &Extension) { 134 | self.calculate_current_fps(); 135 | if unlikely(self.state.calculate_timer.elapsed() >= Duration::from_millis(100)) { 136 | self.state.calculate_timer = Instant::now(); 137 | self.calculate_target_fps(extension); 138 | } 139 | } 140 | 141 | pub fn try_usable(&mut self) { 142 | if self.state.working_state == BufferWorkingState::Unusable 143 | && self.state.working_state_timer.elapsed() >= Duration::from_secs(1) 144 | { 145 | self.state.working_state = BufferWorkingState::Usable; 146 | } 147 | } 148 | 149 | pub fn unusable(&mut self) { 150 | self.state.working_state = BufferWorkingState::Unusable; 151 | self.state.working_state_timer = Instant::now(); 152 | } 153 | 154 | pub fn additional_frametime(&mut self, extension: &Extension) { 155 | self.frametime_state.additional_frametime = self.state.last_update.elapsed(); 156 | self.try_calculate(extension); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /webui/src/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Command as CommandPrimitive } from "cmdk"; 5 | import { SearchIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogDescription, 12 | DialogHeader, 13 | DialogTitle, 14 | } from "@/components/ui/dialog"; 15 | 16 | function Command({ 17 | className, 18 | ...props 19 | }: React.ComponentProps) { 20 | return ( 21 | 29 | ); 30 | } 31 | 32 | function CommandDialog({ 33 | title = "Command Palette", 34 | description = "Search for a command to run...", 35 | children, 36 | ...props 37 | }: React.ComponentProps & { 38 | title?: string; 39 | description?: string; 40 | }) { 41 | return ( 42 |

43 | 44 | {title} 45 | {description} 46 | 47 | 48 | 49 | {children} 50 | 51 | 52 | 53 | ); 54 | } 55 | 56 | function CommandInput({ 57 | className, 58 | ...props 59 | }: React.ComponentProps) { 60 | return ( 61 |
65 | 66 | 74 |
75 | ); 76 | } 77 | 78 | function CommandList({ 79 | className, 80 | ...props 81 | }: React.ComponentProps) { 82 | return ( 83 | 91 | ); 92 | } 93 | 94 | function CommandEmpty({ 95 | ...props 96 | }: React.ComponentProps) { 97 | return ( 98 | 103 | ); 104 | } 105 | 106 | function CommandGroup({ 107 | className, 108 | ...props 109 | }: React.ComponentProps) { 110 | return ( 111 | 119 | ); 120 | } 121 | 122 | function CommandSeparator({ 123 | className, 124 | ...props 125 | }: React.ComponentProps) { 126 | return ( 127 | 132 | ); 133 | } 134 | 135 | function CommandItem({ 136 | className, 137 | ...props 138 | }: React.ComponentProps) { 139 | return ( 140 | 148 | ); 149 | } 150 | 151 | function CommandShortcut({ 152 | className, 153 | ...props 154 | }: React.ComponentProps<"span">) { 155 | return ( 156 | 164 | ); 165 | } 166 | 167 | export { 168 | Command, 169 | CommandDialog, 170 | CommandInput, 171 | CommandList, 172 | CommandEmpty, 173 | CommandGroup, 174 | CommandItem, 175 | CommandShortcut, 176 | CommandSeparator, 177 | }; 178 | -------------------------------------------------------------------------------- /webui/src/components/config/PowerModeSettings.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { PowerModes, PowerSettings } from "@/types/config"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardHeader, 9 | CardTitle, 10 | } from "@/components/ui/card"; 11 | import { Slider } from "@/components/ui/slider"; 12 | import { Switch } from "@/components/ui/switch"; 13 | import { useTranslation } from "react-i18next"; 14 | import { Zap, Thermometer } from "lucide-react"; 15 | 16 | interface PowerModeSettingsProps { 17 | mode: keyof PowerModes; 18 | settings: PowerSettings; 19 | updatePowerMode: ( 20 | mode: keyof PowerModes, 21 | setting: keyof PowerSettings, 22 | value: number | number[] | "disabled", 23 | ) => void; 24 | index: number; 25 | } 26 | 27 | export function PowerModeSettings({ 28 | mode, 29 | settings, 30 | updatePowerMode, 31 | index, // eslint-disable-line @typescript-eslint/no-unused-vars 32 | }: PowerModeSettingsProps) { 33 | const { t } = useTranslation(); 34 | 35 | // Get appropriate icon color based on power mode 36 | const getModeColor = () => { 37 | switch (mode) { 38 | case "powersave": 39 | return "text-green-500"; 40 | case "balance": 41 | return "text-blue-500"; 42 | case "performance": 43 | return "text-orange-500"; 44 | case "fast": 45 | return "text-red-500"; 46 | default: 47 | return "text-primary"; 48 | } 49 | }; 50 | 51 | return ( 52 | 56 | 57 | 58 | 59 |
60 | 61 | {t(`common:${mode}_mode`)} 62 | 63 | {t(`common:${mode}_mode_desc`)} 64 |
65 |
66 | 67 |
68 |
69 |
70 | 71 | {t("common:fas_margin")} 72 | 73 |
74 | 75 | {settings.margin_fps.toFixed(1)} fps 76 | 77 |
78 | 84 | updatePowerMode(mode, "margin_fps", value) 85 | } 86 | className="py-3 [&_[data-part=thumb]]:h-6 [&_[data-part=thumb]]:w-6 [&_[data-part=track]]:h-3 [&_[data-part=thumb]]:bg-primary [&_[data-part=track]]:bg-muted" 87 | /> 88 |

89 | {t("common:margin")} 90 |

91 |
92 | 93 |
94 |
95 |
96 | 97 | 98 | {t("common:thermal")} 99 | 100 | 103 | updatePowerMode( 104 | mode, 105 | "core_temp_thresh", 106 | checked 107 | ? Number( 108 | localStorage.getItem(`fasrs-${mode}-temp`) || 80000, 109 | ) 110 | : "disabled", 111 | ) 112 | } 113 | className="data-[state=checked]:bg-primary" 114 | /> 115 |
116 | 117 | {settings.core_temp_thresh === "disabled" 118 | ? t("common:disabled") 119 | : `${Math.round(settings.core_temp_thresh / 1000)}°C`} 120 | 121 |
122 | { 133 | localStorage.setItem(`fasrs-${mode}-temp`, value[0].toString()); 134 | updatePowerMode(mode, "core_temp_thresh", value[0]); 135 | }} 136 | className="py-3 [&_[data-part=thumb]]:h-6 [&_[data-part=thumb]]:w-6 [&_[data-part=track]]:h-3 data-[disabled]:opacity-50 [&_[data-part=thumb]]:bg-primary [&_[data-part=track]]:bg-muted" 137 | /> 138 |

139 | {t("common:thermal_desc")} 140 |

141 |
142 |
143 |
144 | ); 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | SVG Image 4 | 5 | # **fas-rs** 6 | 7 | ### Frame aware scheduling for android 8 | 9 | [![English][readme-en-badge]][readme-en-url] 10 | [![Stars][stars-badge]][stars-url] 11 | [![CI Build][ci-badge]][ci-url] 12 | [![Release][release-badge]][release-url] 13 | [![Download][download-badge]][download-url] 14 | [![Telegram][telegram-badge]][telegram-url] 15 | 16 |
17 | 18 | [readme-en-badge]: https://img.shields.io/badge/README-English-blue.svg?style=for-the-badge&logo=readme 19 | [readme-en-url]: README_EN.md 20 | [stars-badge]: https://img.shields.io/github/stars/shadow3aaa/fas-rs?style=for-the-badge&logo=github 21 | [stars-url]: https://github.com/shadow3aaa/fas-rs 22 | [ci-badge]: https://img.shields.io/github/actions/workflow/status/shadow3aaa/fas-rs/ci.yml?style=for-the-badge&label=CI%20Build&logo=githubactions 23 | [ci-url]: https://github.com/shadow3aaa/fas-rs/actions/workflows/ci.yml 24 | [release-badge]: https://img.shields.io/github/v/release/shadow3aaa/fas-rs?style=for-the-badge&logo=rust 25 | [release-url]: https://github.com/shadow3aaa/fas-rs/releases/latest 26 | [download-badge]: https://img.shields.io/github/downloads/shadow3aaa/fas-rs/total?style=for-the-badge 27 | [download-url]: https://github.com/shadow3aaa/fas-rs/releases/latest 28 | [telegram-badge]: https://img.shields.io/badge/Group-blue?style=for-the-badge&logo=telegram&label=Telegram 29 | [telegram-url]: https://t.me/fas_rs_official 30 | 31 | ## **简介** 32 | 33 | > 假如肉眼看到的画面能直接反映在调度上,也就是说以把调度器放在观看者的角度来决定性能,是否就能实现完美的性能控制和最大化体验? `FAS (Frame Aware Scheduling)`就是这种调度概念,通过监视画面渲染来尽量控制性能以在保证渲染时间的同时实现最小化开销 34 | 35 | - ### **什么是`fas-rs`?** 36 | 37 | - `fas-rs`是运行在用户态的`FAS(Frame Aware Scheduling)`实现,对比核心思路一致但是在内核态的`MI FEAS`有着近乎在任何设备通用的兼容性和灵活性方面的优势 38 | 39 | ## **插件系统** 40 | 41 | - 为了最大化用户态的灵活性,`fas-rs`有自己的一套插件系统,开发说明详见[插件的模板仓库](https://github.com/shadow3aaa/fas-rs-extension-module-template) 42 | 43 | ## **自定义(配置)** 44 | 45 | - ### **配置路径: `/sdcard/Android/fas-rs/games.toml`** 46 | 47 | - ### **参数(`config`)说明:** 48 | 49 | - **keep_std** 50 | 51 | - 类型: `bool` 52 | - `true`: 永远在配置合并时保持标准配置的 profile,保留本地配置的应用列表,其它地方和 false 相同 \* 53 | - `false`: 见[配置合并的默认行为](#配置合并) 54 | 55 | - **scene_game_list** 56 | 57 | - 类型: `bool` 58 | - `true`: 使用 scene 游戏列表 \* 59 | - `false`: 不使用 scene 游戏列表 60 | 61 | - `*`: 默认配置 62 | 63 | - ### **游戏列表(`game_list`)说明:** 64 | 65 | - **`"package"` = `target_fps`** 66 | 67 | - `package`: 字符串,应用包名 68 | - `target_fps`: 一个数组(如`[30,60,120,144]`)或者单个整数,表示游戏会渲染到的目标帧率,`fas-rs`会在运行时动态匹配 69 | 70 | - ### **模式(`powersave` / `balance` / `performance` / `fast`)说明:** 71 | 72 | - #### **模式切换:** 73 | 74 | - 目前`fas-rs`还没有官方的切换模式的管理器,而是接入了[`scene`](http://vtools.omarea.com)的配置接口,如果你不用 scene 则默认使用`balance`的配置 75 | - 如果你有在 linux 上编程的一些了解,向`/dev/fas_rs/mode`节点写入 4 模式中的任意一个即可切换到对应模式,同时读取它也可以知道现在`fas-rs`所处的模式 76 | 77 | - #### **模式参数说明:** 78 | 79 | - **margin_fps:** 80 | - 支持两种格式: 81 | 1. 完整格式:`margin_fps = { base = , = (可多项) }` 82 | 2. 简写:`margin_fps = `,等效`margin_fps = { base = }` 83 | - 解释: 以 fps 为单位的额外允许掉帧量,除非用`target_fps margin override`强制指定`margin_fps`值,否则会根据公式(`target_fps / 60 * base`)缩放 84 | 85 | - **core_temp_thresh:** 86 | 87 | - 类型: `整数`或者`"disabled"` 88 | - `整数`: 让`fas-rs`触发温控的核心温度(单位0.001℃) 89 | - `"disabled"`: 关闭`fas-rs`内置温控 90 | 91 | ### **`games.toml`配置标准例:** 92 | 93 | ```toml 94 | [config] 95 | keep_std = true 96 | scene_game_list = true 97 | 98 | [game_list] 99 | "com.hypergryph.arknights" = [30, 60] 100 | "com.miHoYo.Yuanshen" = [30, 60] 101 | "com.miHoYo.enterprise.NGHSoD" = [30, 60, 90] 102 | "com.miHoYo.hkrpg" = [30, 60] 103 | "com.kurogame.mingchao" = [24, 30, 45, 60] 104 | "com.pwrd.hotta.laohu" = [25, 30, 45, 60, 90] 105 | "com.mojang.minecraftpe" = [60, 90, 120] 106 | "com.netease.party" = [30, 60] 107 | "com.shangyoo.neon" = 60 108 | "com.tencent.tmgp.pubgmhd" = [60, 90, 120] 109 | "com.tencent.tmgp.sgame" = [30, 60, 90, 120] 110 | 111 | [powersave] 112 | margin_fps = 3 113 | core_temp_thresh = 80000 114 | 115 | [balance] 116 | margin_fps = 1 117 | core_temp_thresh = 90000 118 | 119 | [performance] 120 | margin_fps = 0 121 | core_temp_thresh = 95000 122 | 123 | [fast] 124 | margin_fps = 0 125 | core_temp_thresh = 95000 126 | ``` 127 | 128 | ## **配置合并** 129 | 130 | - ### `fas-rs`内置配置合并系统,来解决未来的配置功能变动问题。它的行为如下 131 | 132 | - 删除本地配置中,标准配置不存在的配置 133 | - 插入本地配置缺少,标准配置存在的配置 134 | - 保留标准配置和本地配置都存在的配置 135 | 136 | - ### 注意 137 | 138 | - 使用自动序列化和反序列化实现,无法保存注释等非序列化必须信息 139 | - 安装时的自动合并配置不会马上应用,不然可能会影响现版本运行,而是会在下一次重启时用合并后的新配置替换掉本地的 140 | 141 | - ### 手动合并 142 | 143 | - 模块每次安装都会自动调用一次 144 | - 手动例 145 | 146 | ```bash 147 | fas-rs merge /path/to/std/profile 148 | ``` 149 | 150 | ## **编译** 151 | 152 | ```bash 153 | # Ubuntu (NDK is required) 154 | apt install gcc-multilib git-lfs 155 | 156 | # Rust (Nightly version is required) 157 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 158 | rustup default nightly 159 | rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android 160 | rustup component add rust-src 161 | 162 | # Cargo-ndk 163 | cargo install cargo-ndk 164 | 165 | # Clone 166 | git clone https://github.com/shadow3aaa/fas-rs 167 | cd fas-rs 168 | 169 | # Compile 170 | cargo xtask build -r 171 | ``` 172 | 173 | ## **捐赠** 174 | 175 | [🐷🐷的爱发电](https://afdian.com/a/shadow3qaq),你的捐赠可以增加🐷🐷维护开发此项目的动力。 176 | -------------------------------------------------------------------------------- /webui/src/components/config/GameItem.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useRef, useEffect } from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Card, CardContent } from "@/components/ui/card"; 6 | import { Input } from "@/components/ui/input"; 7 | import type { FpsValue } from "@/types/config"; 8 | import { Pencil, Save, Trash2, X, Gamepad } from "lucide-react"; 9 | import { DeleteGameDialog } from "./DeleteGameDialog"; 10 | 11 | interface GameItemProps { 12 | game: string; 13 | fps: FpsValue; 14 | editingGame: string | null; 15 | editingGameFps: string; 16 | setEditingGameFps: (value: string) => void; 17 | startEditGame: (game: string, fps: FpsValue) => void; 18 | saveEditedGame: () => void; 19 | removeGame: (game: string) => void; 20 | index: number; 21 | } 22 | 23 | export function GameItem({ 24 | game, 25 | fps, 26 | editingGame, 27 | editingGameFps, 28 | setEditingGameFps, 29 | startEditGame, 30 | saveEditedGame, 31 | removeGame, 32 | index, // eslint-disable-line @typescript-eslint/no-unused-vars 33 | }: GameItemProps) { 34 | const isEditing = editingGame === game; 35 | const [showDeleteDialog, setShowDeleteDialog] = useState(false); 36 | const [isPopupVisible, setIsPopupVisible] = useState(false); 37 | const inputRef = useRef(null); 38 | 39 | useEffect(() => { 40 | if (isEditing) { 41 | setTimeout(() => { 42 | setIsPopupVisible(true); 43 | }, 50); 44 | 45 | setTimeout(() => { 46 | if (inputRef.current) { 47 | inputRef.current.focus(); 48 | } 49 | }, 350); 50 | } else { 51 | setIsPopupVisible(false); 52 | } 53 | }, [isEditing]); 54 | 55 | const handleDelete = () => { 56 | setShowDeleteDialog(true); 57 | }; 58 | 59 | const confirmDelete = () => { 60 | removeGame(game); 61 | setShowDeleteDialog(false); 62 | }; 63 | 64 | return ( 65 | <> 66 | 67 | 68 |
69 |
70 | 71 | 72 | {game} 73 | 74 |
75 |
76 | FPS: 77 | 78 | {Array.isArray(fps) ? fps.join(", ") : fps} 79 | 80 |
81 |
82 | 90 | 98 |
99 |
100 |
101 |
102 | 103 | {isEditing && ( 104 |
109 |
startEditGame("", fps)} 113 | /> 114 | 115 | 116 | 117 |
118 | 119 | 120 | {game} 121 | 122 |
123 | setEditingGameFps(e.target.value)} 128 | className="w-full text-sm sm:text-base mb-3 focus-visible:ring-primary" 129 | placeholder="Enter FPS" 130 | /> 131 |
132 | 140 | 147 |
148 |
149 |
150 |
151 | )} 152 | 153 | setShowDeleteDialog(false)} 156 | onConfirm={confirmDelete} 157 | gameName={game} 158 | /> 159 | 160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /src/cpu_common/process_monitor.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025-2025, shadow3, shadow3aaa 2 | // 3 | // This file is part of fas-rs. 4 | // 5 | // fas-rs is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free 7 | // Software Foundation, either version 3 of the License, or (at your option) 8 | // any later version. 9 | // 10 | // fas-rs is distributed in the hope that it will be useful, but WITHOUT ANY 11 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | // details. 14 | // 15 | // You should have received a copy of the GNU General Public License along 16 | // with fas-rs. If not, see . 17 | use std::{ 18 | cmp, 19 | collections::{HashMap, hash_map::Entry}, 20 | fs, 21 | time::{Duration, Instant}, 22 | }; 23 | 24 | use anyhow::Result; 25 | use libc::{_SC_CLK_TCK, sysconf}; 26 | 27 | #[derive(Debug, Clone, Copy)] 28 | struct UsageTracker { 29 | pid: i32, 30 | tid: i32, 31 | last_cputime: u64, 32 | read_timer: Instant, 33 | current_usage: f64, 34 | } 35 | 36 | impl UsageTracker { 37 | fn new(pid: i32, tid: i32) -> Result { 38 | Ok(Self { 39 | pid, 40 | tid, 41 | last_cputime: get_thread_cpu_time(pid, tid)?, 42 | read_timer: Instant::now(), 43 | current_usage: 0.0, 44 | }) 45 | } 46 | 47 | fn try_calculate(&mut self) -> Result { 48 | let tick_per_sec = unsafe { sysconf(_SC_CLK_TCK) }; 49 | let new_cputime = get_thread_cpu_time(self.pid, self.tid)?; 50 | let elapsed_ticks = self.read_timer.elapsed().as_secs_f64() * tick_per_sec as f64; 51 | self.read_timer = Instant::now(); 52 | let cputime_slice = new_cputime - self.last_cputime; 53 | self.last_cputime = new_cputime; 54 | self.current_usage = cputime_slice as f64 / elapsed_ticks; 55 | Ok(self.current_usage) 56 | } 57 | } 58 | 59 | #[derive(Debug)] 60 | pub struct ProcessMonitor { 61 | current_pid: Option, 62 | all_trackers: HashMap, 63 | top_trackers: HashMap, 64 | last_full_update: Instant, 65 | last_update: Instant, 66 | } 67 | 68 | impl ProcessMonitor { 69 | pub fn new() -> Self { 70 | Self { 71 | current_pid: None, 72 | all_trackers: HashMap::new(), 73 | top_trackers: HashMap::new(), 74 | last_full_update: Instant::now(), 75 | last_update: Instant::now(), 76 | } 77 | } 78 | 79 | pub fn set_pid(&mut self, pid: Option) { 80 | if self.current_pid != pid { 81 | self.current_pid = pid; 82 | self.all_trackers.clear(); 83 | self.top_trackers.clear(); 84 | self.last_full_update = Instant::now(); 85 | self.last_update = Instant::now(); 86 | } 87 | } 88 | 89 | pub fn update(&mut self) -> Option { 90 | if self.last_update.elapsed() < Duration::from_millis(300) { 91 | return None; 92 | } 93 | 94 | self.last_update = Instant::now(); 95 | let pid = self.current_pid?; 96 | 97 | if self.last_full_update.elapsed() >= Duration::from_secs(1) { 98 | self.update_thread_list(pid); 99 | self.last_full_update = Instant::now(); 100 | } 101 | 102 | let mut util_max: f64 = 0.0; 103 | for tracker in self.top_trackers.values_mut() { 104 | if let Ok(usage) = tracker.try_calculate() { 105 | util_max = util_max.max(usage); 106 | } 107 | } 108 | 109 | Some(util_max) 110 | } 111 | 112 | fn update_thread_list(&mut self, pid: i32) { 113 | if let Ok(threads) = get_thread_ids(pid) { 114 | self.all_trackers = threads 115 | .iter() 116 | .copied() 117 | .filter_map(|tid| { 118 | Some(( 119 | tid, 120 | match self.all_trackers.entry(tid) { 121 | Entry::Occupied(o) => o.remove(), 122 | Entry::Vacant(_) => UsageTracker::new(pid, tid).ok()?, 123 | }, 124 | )) 125 | }) 126 | .collect(); 127 | 128 | let mut top_threads: Vec<_> = self 129 | .all_trackers 130 | .iter() 131 | .filter_map(|(tid, tracker)| Some((*tid, tracker.clone().try_calculate().ok()?))) 132 | .collect(); 133 | 134 | top_threads.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(cmp::Ordering::Equal)); 135 | top_threads.truncate(8); 136 | 137 | self.top_trackers = top_threads 138 | .into_iter() 139 | .filter_map(|(tid, _)| match self.top_trackers.entry(tid) { 140 | Entry::Occupied(o) => Some((tid, o.remove())), 141 | Entry::Vacant(_) => Some((tid, UsageTracker::new(pid, tid).ok()?)), 142 | }) 143 | .collect(); 144 | } 145 | } 146 | 147 | pub fn top_threads(&self) -> impl Iterator { 148 | self.top_trackers.keys().copied() 149 | } 150 | } 151 | 152 | fn get_thread_ids(pid: i32) -> Result> { 153 | let proc_path = format!("/proc/{pid}/task"); 154 | Ok(fs::read_dir(proc_path)? 155 | .filter_map(|entry| { 156 | entry 157 | .ok() 158 | .and_then(|e| e.file_name().to_string_lossy().parse::().ok()) 159 | }) 160 | .collect()) 161 | } 162 | 163 | fn get_thread_cpu_time(pid: i32, tid: i32) -> Result { 164 | let stat_path = format!("/proc/{pid}/task/{tid}/stat"); 165 | let stat_content = fs::read_to_string(stat_path)?; 166 | let parts: Vec<&str> = stat_content.split_whitespace().collect(); 167 | let utime = parts[13].parse::().unwrap_or(0); 168 | let stime = parts[14].parse::().unwrap_or(0); 169 | Ok(utime + stime) 170 | } 171 | --------------------------------------------------------------------------------