From 9c7dc06bad142e04d8a14c9d37cdb321f54ed879 Mon Sep 17 00:00:00 2001 From: toorux Date: Wed, 7 Oct 2026 18:07:23 +0800 Subject: [PATCH] Add opt-in per-app framebuffer compatibility fix --- docs/user-guide/apk-manager.md | 4 + docs/zh-CN/user-guide/apk-manager.md | 4 + native/android/fbo-compat.sh | 23 +++ native/fbo_compat.c | 32 +++++ src/apk.rs | 62 +++++++- src/apk/graphics.rs | 207 +++++++++++++++++++++++++++ src/apk/native.rs | 1 + tests/fbo-layer.c | 31 ++++ tools/build-native.sh | 3 + tools/package-release.sh | 2 +- ui/locales/en-US.json | 6 +- ui/locales/zh-CN.json | 6 +- ui/src/apk-manager.tsx | 4 +- 13 files changed, 378 insertions(+), 7 deletions(-) create mode 100644 native/android/fbo-compat.sh create mode 100644 native/fbo_compat.c create mode 100644 src/apk/graphics.rs create mode 100644 tests/fbo-layer.c diff --git a/docs/user-guide/apk-manager.md b/docs/user-guide/apk-manager.md index 470dfdb..59c87de 100644 --- a/docs/user-guide/apk-manager.md +++ b/docs/user-guide/apk-manager.md @@ -176,3 +176,7 @@ Container list headings show installed application names; the data path identifi Keeping application data uses Android PackageManager readiness instead of requiring user unlock or external storage mounting. Deleting data still waits for those conditions. Framely boots a stopped container without showing its desktop and repairs only the selected APK registration if needed. It reports startup, PackageManager, unlock, storage and container-exit failures separately. Steam entries are removed after Android confirms successful uninstall; Android success is saved first so a Steam cleanup retry does not uninstall again. Failed startup or uninstall preserves the application record, Steam binding and application data. The error dialog offers Retry and Export logs. A container that cannot boot still requires diagnosis; Framely does not silently delete it or edit Android package databases. L2/R2 digital button activation is adjustable in **APK Manager → Settings**, from 1% to 100%, with an 80% default. Both triggers use this threshold with either Steam Input or Frame direct input. Analog trigger travel is unaffected. Saving applies to running bridges immediately; release occurs five percentage points below the selected threshold (at 0% for thresholds below 5%). + +## Per-app graphics compatibility + +Under **Manage app → Advanced settings**, **Framebuffer compatibility fix** is off by default. Enable it only for games that play sound but show a black screen (tested with Rock Kingdom on the current Mesa/Zink driver). Restart the app after changing it. The GLES layer validates framebuffer status after texture attachment, only in the selected application; other apps, system drivers, game files and saved data remain unchanged. Turning it off restores the normal rendering path on the next app launch. It is a targeted workaround, not a general graphics or performance enhancement. diff --git a/docs/zh-CN/user-guide/apk-manager.md b/docs/zh-CN/user-guide/apk-manager.md index 734df91..c2f3160 100644 --- a/docs/zh-CN/user-guide/apk-manager.md +++ b/docs/zh-CN/user-guide/apk-manager.md @@ -150,3 +150,7 @@ Steam 入口显示 APK 的应用名称和图标,并根据图标生成对应的 保留应用数据的卸载使用 Android 包管理器就绪检查,不再强制等待用户解锁或外部存储挂载。删除数据仍等待这些条件。Framely 在后台启动已停止的容器,不显示容器桌面;需要时只修复目标 APK 的登记。启动、包管理器、解锁、存储和容器退出分别报错。Android 确认卸载成功后才移除 Steam 入口,先保存 Android 卸载结果,重试 Steam 清理不会重复卸载。启动或卸载失败保留应用记录、Steam 绑定和应用数据。错误弹窗提供重试与导出日志。无法启动的容器仍需诊断,不会自动删除容器或直接修改 Android 包数据库。 在“APK 管理 → 设置”中可调节 L2/R2 按键触发行程,范围 1%~100%,默认 80%。两个扳机共用该设置,Steam Input 和 Frame 直连均适用,不改变模拟轴的完整行程。保存后立即应用到正在运行的转发进程;松开阈值比按下低 5 个百分点,设置低于 5% 时在归零后松开。 + +## 单应用图形兼容 + +在**管理应用 → 高级设置**中,“帧缓冲兼容修复”默认关闭。仅为有声音但黑屏的游戏开启(已在当前 Mesa/Zink 驱动下验证洛克王国),修改后需重启应用。GLES 修复层只在所选应用中,于绑定纹理后检查帧缓冲状态,不改变其他应用、系统驱动、游戏文件或存档。关闭后,下次启动恢复正常渲染路径。这是针对特定问题的兼容措施,并非通用画质或性能优化。 diff --git a/native/android/fbo-compat.sh b/native/android/fbo-compat.sh new file mode 100644 index 0000000..618c79f --- /dev/null +++ b/native/android/fbo-compat.sh @@ -0,0 +1,23 @@ +# Always mount the shipped layer if available, but activate it only for the +# explicitly selected package. No Android/system/Lepton files are replaced. +if [[ -n "${FRAMELY_FBO_LIBRARY:-}" ]]; then + declare -F setup_podman_mounts >/dev/null || { echo 'Unsupported Lepton graphics hooks' >&2; exit 64; } + eval "$(declare -f setup_podman_mounts | sed '1s/setup_podman_mounts/framely_original_fbo_mounts/')" + function setup_podman_mounts() { + framely_original_fbo_mounts "$@" || return + podman_mount_entry "$FRAMELY_FBO_LIBRARY" /data/local/debug/gles/libFramelyFBOCompat.so ro + } +fi +if [[ -n "${FRAMELY_FBO_LIBRARY:-}" ]]; then + [[ -z "${FRAMELY_FBO_PACKAGE:-}" || "$FRAMELY_FBO_PACKAGE" =~ ^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$ && -n "${FRAMELY_FBO_LIBRARY:-}" ]] || { echo 'Invalid framebuffer compatibility configuration' >&2; exit 64; } + declare -F generate_vulkan_settings_script >/dev/null || { echo 'Unsupported Lepton graphics settings hook' >&2; exit 64; } + eval "$(declare -f generate_vulkan_settings_script | sed '1s/generate_vulkan_settings_script/framely_original_fbo_settings/')" + function generate_vulkan_settings_script() { + # Lepton does not clear stale GLES settings when its GLES list is empty. + # Remove our previous selection before its own settings are generated. + printf '%s\n' 'if [ "$(settings get global gpu_debug_layers_gles)" = libFramelyFBOCompat.so ]; then settings delete global gpu_debug_layers_gles; settings delete global gpu_debug_layer_app; fi;' + framely_original_fbo_settings "$@" || return + [[ -n "${FRAMELY_FBO_PACKAGE:-}" ]] || return 0 + printf '%s\n' 'settings put global enable_gpu_debug_layers 1;' "settings put global gpu_debug_app $FRAMELY_FBO_PACKAGE;" 'settings put global gpu_debug_layers_gles libFramelyFBOCompat.so;' "settings put global gpu_debug_layer_app $FRAMELY_FBO_PACKAGE;" + } +fi diff --git a/native/fbo_compat.c b/native/fbo_compat.c new file mode 100644 index 0000000..26ae558 --- /dev/null +++ b/native/fbo_compat.c @@ -0,0 +1,32 @@ +// Android GLES layer: opt-in workaround for missing framebuffer attachments. +// No libc/GL dependency: entry points come from Android's layer loader. +typedef void (*Attach)(unsigned, unsigned, unsigned, unsigned, int); +typedef unsigned (*Check)(unsigned); +static Attach attach; +static Check check; +static int same(const char *a, const char *b) { + while (*a && *a == *b) { ++a; ++b; } + return *a == *b; +} +static void validate_attachment(unsigned target, unsigned attachment, + unsigned texture_target, unsigned texture, int level) { + Attach next = __atomic_load_n(&attach, __ATOMIC_RELAXED); + Check validate = __atomic_load_n(&check, __ATOMIC_RELAXED); + next(target, attachment, texture_target, texture, level); + // Force driver attachment validation before the next draw. Keep all GL + // arguments, shader code and render commands unchanged. + if (validate) validate(target); +} +__attribute__((visibility("default"))) +void AndroidGLESLayer_Initialize(void *id, void *(*lookup)(void *, const char *)) { + __atomic_store_n(&attach, (Attach)lookup(id, "glFramebufferTexture2D"), __ATOMIC_RELAXED); + __atomic_store_n(&check, (Check)lookup(id, "glCheckFramebufferStatus"), __ATOMIC_RELAXED); +} +__attribute__((visibility("default"))) +void *AndroidGLESLayer_GetProcAddress(const char *name, void *next) { + if (next && __atomic_load_n(&check, __ATOMIC_RELAXED) && same(name, "glFramebufferTexture2D")) { + __atomic_store_n(&attach, (Attach)next, __ATOMIC_RELAXED); + return (void *)validate_attachment; + } + return next; +} diff --git a/src/apk.rs b/src/apk.rs index b6ca7f7..8c4aa48 100644 --- a/src/apk.rs +++ b/src/apk.rs @@ -21,6 +21,7 @@ use std::{ time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; mod cleanup; +mod graphics; mod lifecycle; mod native; mod steam_bridge; @@ -75,6 +76,8 @@ struct Record { show_window: Option, #[serde(default)] orientation: Option, + #[serde(default)] + framebuffer_compatibility: bool, } #[derive(Serialize, Deserialize)] struct Database { @@ -144,6 +147,8 @@ struct App { activity: Option, show_window: Option, orientation: Option, + #[serde(default)] + framebuffer_compatibility: bool, } fn gamepad_trigger_threshold_default() -> u8 { 80 @@ -785,6 +790,7 @@ fn apps(home: &Path, db: &Database, cs: &[Container], measure: bool) -> (Vec (Vec (Vec, log: &Path) -> Result<()> { start_container(home, c, show, true, log) @@ -1579,6 +1594,7 @@ fn start_oriented_container_for( let (window_dex, window_script) = window_runtime(home)?; cmd.env("FRAMELY_WINDOW_DEX", window_dex) .env("FRAMELY_WINDOW_SCRIPT", window_script); + graphics::configure_command(&mut cmd, false, "")?; cmd.env_remove("SteamAppId"); storage::configure( &mut cmd, @@ -1734,6 +1750,7 @@ fn record_app(db: &mut Database, a: &App) { activity: a.activity.clone(), show_window: a.show_window, orientation: a.orientation.clone(), + framebuffer_compatibility: a.framebuffer_compatibility, ..Default::default() }); } @@ -2064,6 +2081,7 @@ fn launch(c: &Container, a: &App, log: &Path) -> Result<()> { } fn launch_with_started(c: &Container, a: &App, log: &Path, started: impl FnOnce()) -> Result<()> { let component = launch_component(a)?; + graphics::apply(c, a, log)?; podman( &[ "exec", @@ -2821,6 +2839,11 @@ progress(json!({"phase":"starting"}));start(home,&c,Some(false),&log)?; ensure!(v.is_boolean() || v.is_null(), "Invalid window setting"); rec.show_window = v.as_bool(); } + if let Some(v) = p.get("framebufferCompatibility") { + rec.framebuffer_compatibility = v + .as_bool() + .context("Invalid framebuffer compatibility setting")?; + } if let Some(v) = p.get("orientation") { ensure!( v.as_str() @@ -2900,6 +2923,12 @@ progress(json!({"phase":"starting"}));start(home,&c,Some(false),&log)?; launch_component(&a)?; progress(json!({"phase":"starting"})); let flat = a.show_window.unwrap_or(!a.metadata.vr); + if a.framebuffer_compatibility { + graphics::library(true)?; + if running(&c.name) && !graphics::mounted(&c, &log)? { + stop(&c, &log)?; + } + } if flat && running(&c.name) { let name = format!("lepton-{}", c.name); let headless = podman( @@ -4476,7 +4505,8 @@ setup_props ); assert!(!fs::read_to_string(f.dir.path().join("commands")) .unwrap() - .contains("stop ")); + .lines() + .any(|line| line.starts_with("stop "))); // Unknown lifecycle output refuses a destructive stop. fs::write(f.dir.path().join("bad-probe"), "").unwrap(); assert!(steam_bridge::test_request(&f.home, id, "test-token", "stop").is_err()); @@ -5426,6 +5456,34 @@ esac assert!(inspect(&f.home, &f.dir.path().join("upload.apk"), &c).is_err()); } #[test] + fn framebuffer_preference_defaults_off_validates_and_survives_refresh() { + let f = Fixture::new(); + list(&f.home).unwrap(); + let id = "test/com.example.app"; + assert!(!load(&f.home).unwrap().records[id].framebuffer_compatibility); + assert!(f + .operation( + "settings", + json!({"app":id,"framebufferCompatibility":"yes"}) + ) + .is_err()); + f.operation( + "settings", + json!({"app":id,"framebufferCompatibility":true}), + ) + .unwrap(); + list(&f.home).unwrap(); + let db = load(&f.home).unwrap(); + assert!(db.records[id].framebuffer_compatibility); + assert!(app(&f.home, &db, id).unwrap().0.framebuffer_compatibility); + f.operation( + "settings", + json!({"app":id,"framebufferCompatibility":false}), + ) + .unwrap(); + assert!(!load(&f.home).unwrap().records[id].framebuffer_compatibility); + } + #[test] fn window_orientation_is_validated_persisted_and_applied() { let f = Fixture::new(); list(&f.home).unwrap(); diff --git a/src/apk/graphics.rs b/src/apk/graphics.rs new file mode 100644 index 0000000..e0bbfbf --- /dev/null +++ b/src/apk/graphics.rs @@ -0,0 +1,207 @@ +//! Per-application GLES workaround. The library is inert unless Android's +//! debug-layer loader selects this exact package at process startup. +use super::*; + +pub(super) const HOOKS: &str = include_str!("../../native/android/fbo-compat.sh"); +const LAYER: &str = "libFramelyFBOCompat.so"; + +pub(super) fn library(required: bool) -> Result> { + let exe = std::env::current_exe()?; + let path = exe + .parent() + .and_then(Path::parent) + .context("Missing Framely runtime directory")? + .join("lib/openvr") + .join(LAYER); + ensure!( + !required || path.is_file(), + "Framebuffer compatibility runtime is missing; install a complete Framely build" + ); + Ok(path.is_file().then_some(path)) +} +pub(super) fn configure_command(cmd: &mut Command, enabled: bool, package: &str) -> Result<()> { + // Do not inherit a launch configuration from another application/process. + cmd.env_remove("FRAMELY_FBO_LIBRARY") + .env_remove("FRAMELY_FBO_PACKAGE"); + if let Some(path) = library(enabled)? { + cmd.env("FRAMELY_FBO_LIBRARY", path); + } + if enabled { + ensure!(apk_metadata::valid_package(package), "Invalid APK package"); + cmd.env("FRAMELY_FBO_PACKAGE", package); + } + Ok(()) +} +pub(super) fn configure_environment(env: &mut Value, enabled: bool, package: &str) -> Result<()> { + // Empty values also prevent inherited Steam environment settings from + // enabling a layer for an application whose preference is off. + env["FRAMELY_FBO_LIBRARY"] = json!(library(enabled)?); + if env["FRAMELY_FBO_LIBRARY"].is_null() { + env["FRAMELY_FBO_LIBRARY"] = json!(""); + } + ensure!( + !enabled || apk_metadata::valid_package(package), + "Invalid APK package" + ); + env["FRAMELY_FBO_PACKAGE"] = json!(if enabled { package } else { "" }); + Ok(()) +} +pub(super) fn mounted(c: &Container, log: &Path) -> Result { + Ok(podman( + &[ + "exec", + &format!("lepton-{}", c.name), + "sh", + "-c", + "if [ -r /data/local/debug/gles/libFramelyFBOCompat.so ]; then echo ready; fi", + ], + Some(log), + )? + .trim() + == "ready") +} +pub(super) fn apply(c: &Container, a: &App, log: &Path) -> Result<()> { + ensure!( + apk_metadata::valid_package(&a.metadata.package), + "Invalid APK package" + ); + // Reusing a container is safe: restart only the selected app if its loader + // configuration changed, and clear only our own layer when disabling it. + let script = r#" +package=$1 +enabled=$2 +layer=$(settings get global gpu_debug_layers_gles) +target=$(settings get global gpu_debug_app) +if [ "$enabled" = true ]; then + [ -r /data/local/debug/gles/libFramelyFBOCompat.so ] || exit 64 + active=$(settings get global enable_gpu_debug_layers) + if [ "$layer" != libFramelyFBOCompat.so ] || [ "$target" != "$package" ] || [ "$active" != 1 ]; then + am force-stop --user 0 "$package" || exit + fi + settings put global enable_gpu_debug_layers 1 || exit + settings put global gpu_debug_app "$package" || exit + settings put global gpu_debug_layers_gles libFramelyFBOCompat.so || exit + settings put global gpu_debug_layer_app "$package" || exit +elif [ "$layer" = libFramelyFBOCompat.so ]; then + if [ "$target" = "$package" ]; then am force-stop --user 0 "$package" || exit; fi + settings delete global gpu_debug_layers_gles || exit + settings delete global gpu_debug_layer_app || exit +fi +"#; + podman( + &[ + "exec", + &format!("lepton-{}", c.name), + "sh", + "-c", + script, + "framely-fbo", + &a.metadata.package, + if a.framebuffer_compatibility { + "true" + } else { + "false" + }, + ], + Some(log), + )?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn compiled_layer_preserves_calls_and_handles_missing_symbols() { + let dir = tempfile::tempdir().unwrap(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")); + let library = dir.path().join("libFramelyFBOCompat.so"); + let tester = dir.path().join("test-layer"); + assert!(Command::new("gcc") + .args([ + "-shared", + "-fPIC", + "-nostdlib", + "-fno-stack-protector", + "-fvisibility=hidden", + "-Wl,-z,defs", + "-Wall", + "-Wextra", + "-Werror" + ]) + .arg(base.join("native/fbo_compat.c")) + .arg("-o") + .arg(&library) + .status() + .unwrap() + .success()); + assert!(Command::new("gcc") + .arg(base.join("tests/fbo-layer.c")) + .args(["-ldl", "-o"]) + .arg(&tester) + .status() + .unwrap() + .success()); + assert!(Command::new(tester) + .arg(library) + .status() + .unwrap() + .success()); + } + #[test] + fn boot_hooks_scope_layer_and_clear_stale_configuration() { + let dir = tempfile::tempdir().unwrap(); + let script = dir.path().join("test.sh"); + fs::write( + &script, + format!( + r#"set -euo pipefail +function setup_podman_mounts() {{ echo original-mounts; }} +function podman_mount_entry() {{ printf '%s|%s|%s\n' "$1" "$2" "$3"; }} +function generate_vulkan_settings_script() {{ echo original-vulkan-settings; }} +{HOOKS} +setup_podman_mounts +generate_vulkan_settings_script +"# + ), + ) + .unwrap(); + for enabled in [false, true] { + let result = Command::new("bash") + .arg(&script) + .env("FRAMELY_FBO_LIBRARY", "/runtime/layer.so") + .env( + "FRAMELY_FBO_PACKAGE", + if enabled { "com.example.game" } else { "" }, + ) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + let output = String::from_utf8(result.stdout).unwrap(); + assert!(output.contains("original-mounts")); + assert!(output + .contains("/runtime/layer.so|/data/local/debug/gles/libFramelyFBOCompat.so|ro")); + assert!(output.contains("settings delete global gpu_debug_layers_gles")); + assert!(output.contains("original-vulkan-settings")); + assert_eq!( + output.contains("settings put global gpu_debug_app com.example.game"), + enabled + ); + assert_eq!( + output.contains("settings put global gpu_debug_layers_gles libFramelyFBOCompat.so"), + enabled + ); + } + let result = Command::new("bash") + .arg(&script) + .env("FRAMELY_FBO_LIBRARY", "/runtime/layer.so") + .env("FRAMELY_FBO_PACKAGE", "bad;command") + .output() + .unwrap(); + assert!(!result.status.success()); + } +} diff --git a/src/apk/native.rs b/src/apk/native.rs index fb4d1b1..7a8b121 100644 --- a/src/apk/native.rs +++ b/src/apk/native.rs @@ -176,6 +176,7 @@ pub(super) fn prepare( "APP_WANTS_FLATSCREEN":if flat {"true"}else{"false"}, "LEPTON_NO_CLEANUP":"true", "TERM":"dumb" }); + graphics::configure_environment(&mut env, a.framebuffer_compatibility, &a.metadata.package)?; let installed_dir = c .baked .join("data_overlay") diff --git a/tests/fbo-layer.c b/tests/fbo-layer.c new file mode 100644 index 0000000..65a5a30 --- /dev/null +++ b/tests/fbo-layer.c @@ -0,0 +1,31 @@ +// Verify actual compiled GLES entry points without needing a graphics context. +#include +#include +#include +static int calls, queries; +static void attachment(unsigned t, unsigned a, unsigned tt, unsigned id, int l) { + assert(t==1 && a==2 && tt==3 && id==4 && l==5); ++calls; +} +static unsigned status(unsigned t) { assert(t==1 && calls==queries+1); ++queries; return 0x8cd5; } +static void *lookup(void *id, const char *name) { + assert(id==(void*)42); + if (!strcmp(name,"glFramebufferTexture2D")) return attachment; + if (!strcmp(name,"glCheckFramebufferStatus")) return status; + return 0; +} +static void *missing(void *id, const char *name) { (void)id; (void)name; return 0; } +int main(int argc, char **argv) { + assert(argc==2); + void *lib=dlopen(argv[1],RTLD_NOW); assert(lib); + void (*init)(void*,void*(*)(void*,const char*))=dlsym(lib,"AndroidGLESLayer_Initialize"); + void *(*get)(const char*,void*)=dlsym(lib,"AndroidGLESLayer_GetProcAddress"); + assert(init && get); + init((void*)42,lookup); + assert(get("glDrawArrays",(void*)123)==(void*)123); + assert(get("glFramebufferTexture2D",0)==0); + void (*hook)(unsigned,unsigned,unsigned,unsigned,int)=get("glFramebufferTexture2D",attachment); + hook(1,2,3,4,5); assert(calls==1 && queries==1); + init(0,missing); + assert(get("glFramebufferTexture2D",attachment)==attachment); + dlclose(lib); +} diff --git a/tools/build-native.sh b/tools/build-native.sh index 091d7af..14aa580 100755 --- a/tools/build-native.sh +++ b/tools/build-native.sh @@ -11,3 +11,6 @@ g++ -std=c++17 -O2 -I"$cef" -I"$openvr" -I"$base/native" -I"$out" "$base/native/ # Independent of CEF; both binaries ship in the core/update archive. g++ -std=c++17 -O2 -I"$openvr" "$base/native/gamepad.cpp" -L"$openvr/lib/linuxarm64" -lopenvr_api -ldl -pthread -Wl,-rpath,'$ORIGIN' -o "$out/framely-gamepad" gcc -shared -fPIC -nostdlib -fno-stack-protector -Wl,--hash-style=both "$base/native/gamepad_grab.c" -o "$out/libframely-gamepad-grab.so" + +# Android GLES layer uses loader-supplied symbols; no host glibc dependency. +gcc -shared -fPIC -nostdlib -fno-stack-protector -fvisibility=hidden -Wl,-z,defs,--hash-style=both,-soname,libFramelyFBOCompat.so "$base/native/fbo_compat.c" -o "$out/libFramelyFBOCompat.so" diff --git a/tools/package-release.sh b/tools/package-release.sh index 27b99f9..c9321be 100755 --- a/tools/package-release.sh +++ b/tools/package-release.sh @@ -23,7 +23,7 @@ rm -f "$stage/lib/cef/framely-probe" "$stage/lib/cef/framely-browser-probe" # Remove distribution debug information; browser functionality is unchanged. strip --strip-debug "$stage/lib/cef/libcef.so" cp native/vendor/openvr/lib/linuxarm64/libopenvr_api.so "$stage/lib/openvr/" -cp target/native/framely-gamepad target/native/libframely-gamepad-grab.so "$stage/lib/openvr/" +cp target/native/framely-gamepad target/native/libframely-gamepad-grab.so target/native/libFramelyFBOCompat.so "$stage/lib/openvr/" cp -a ui/dist/. "$stage/share/ui/" cp -a assets/search "$stage/share/" cp assets/search/COPYING "$stage/share/licenses/ipadic.txt" diff --git a/ui/locales/en-US.json b/ui/locales/en-US.json index 776523c..1a22411 100644 --- a/ui/locales/en-US.json +++ b/ui/locales/en-US.json @@ -979,6 +979,10 @@ "Android readiness probe did not respond; export logs and retry.": "Android readiness probe did not respond; export logs and retry.", "在此 Frame 上运行": "Run on this Frame", "远程设备 {0}": "Remote device {0}", - "架构": "Architecture" + "架构": "Architecture", + "帧缓冲兼容修复": "Framebuffer compatibility fix", + "仅用于部分游戏有声音但黑屏的问题,默认关闭。修改后需重启应用生效,不改变系统驱动或游戏文件。": "For specific games with sound but a black screen. Off by default. Restart the app after changing this option; system drivers and game files are unchanged.", + "Invalid framebuffer compatibility setting": "Invalid framebuffer compatibility setting", + "Framebuffer compatibility runtime is missing; install a complete Framely build": "Framebuffer compatibility runtime is missing; install a complete Framely build" } } diff --git a/ui/locales/zh-CN.json b/ui/locales/zh-CN.json index 96c34c1..eccbe18 100644 --- a/ui/locales/zh-CN.json +++ b/ui/locales/zh-CN.json @@ -979,6 +979,10 @@ "默认开启,在 Steam 库中保留已安装应用。关闭后只在启动时注册 Steam 入口,退出后删除入口,应用数据保留。": "默认开启,在 Steam 库中保留已安装应用。关闭后只在启动时注册 Steam 入口,退出后删除入口,应用数据保留。", "在此 Frame 上运行": "在此 Frame 上运行", "远程设备 {0}": "远程设备 {0}", - "架构": "架构" + "架构": "架构", + "帧缓冲兼容修复": "帧缓冲兼容修复", + "仅用于部分游戏有声音但黑屏的问题,默认关闭。修改后需重启应用生效,不改变系统驱动或游戏文件。": "仅用于部分游戏有声音但黑屏的问题,默认关闭。修改后需重启应用生效,不改变系统驱动或游戏文件。", + "Invalid framebuffer compatibility setting": "无效的帧缓冲兼容设置", + "Framebuffer compatibility runtime is missing; install a complete Framely build": "缺少帧缓冲兼容修复组件,请安装完整的 Framely 更新包。" } } diff --git a/ui/src/apk-manager.tsx b/ui/src/apk-manager.tsx index 2239958..ec8ffd8 100644 --- a/ui/src/apk-manager.tsx +++ b/ui/src/apk-manager.tsx @@ -13,7 +13,7 @@ import {ApkUninstall} from './apk-uninstall'; import {ApkInstallReview} from './apk-install-review'; import {GamepadTriggerSetting} from './gamepad-trigger-setting'; type Metadata={package:string;name:string;version:string;versionCode:number;minSdk:number;activities:string[];declaredActivities:string[];icon?:string;abis:string[];vr:boolean}; -type App={steamLaunch?:boolean;steamRegistered?:boolean;steamRegistrationError?:string;stateKnown?:boolean;id:string;context:string;metadata:Metadata;installed:boolean;running:boolean;managed:boolean;steam:boolean;size:number;pending?:string;activity?:string;showWindow?:boolean|null;orientation?:string|null}; +type App={steamLaunch?:boolean;steamRegistered?:boolean;steamRegistrationError?:string;stateKnown?:boolean;id:string;context:string;metadata:Metadata;installed:boolean;running:boolean;managed:boolean;steam:boolean;size:number;pending?:string;activity?:string;showWindow?:boolean|null;orientation?:string|null;framebufferCompatibility?:boolean}; type Container={acceptsAdditionalApps?:boolean;id:string;name:string;baked:string;running:boolean;managed:boolean;steam:boolean}; type Data={apps:App[];containers:Container[];warnings:string[];roots:string[];available:boolean;retainSteamEntries?:boolean;gamepadEnabled?:boolean;gamepadSource?:string;gamepadRumble?:boolean;gamepadTriggerThreshold?:number;autoStopContainer?:boolean;stopContainerOnClose?:boolean}; type Cleanup={id:string;kind:string;name:string;bytes:number;sensitive:boolean;app?:string;context?:string;restorable?:boolean;unknown?:boolean}; @@ -62,7 +62,7 @@ export function ApkManager({view:externalView,onViewChange}:{view?:string;onView {view==='containers'&&<>
{data.containers.map(c=>

{containerTitle(c)}

{c.baked}

{t(c.steam?'Steam 管理':c.managed?'Framely 管理':'已有侧载容器')} · {t(c.running?'运行中':'已停止')}

{!data.apps.some(a=>a.context===c.id&&a.installed)&&

{t('未发现已安装的应用,启动容器不会安装 APK。')}

}
{!c.steam&&<>}
)}

{t('其他数据位置')}

{t('添加其他工具使用的 baked 数据目录,不移动原有应用。')}

setPath(e.target.value)}/>setContextName(e.target.value)}/>
{data.roots.map(p=>
{p}
)}
} {view==='cleanup'&&<>

{t('数据清理')}

{t('存档、设置与恢复备份默认不选中。未知归属的数据不会自动删除。')}

{cleanup.map(i=>
setSelected(s=>checked?[...s,i.id]:s.filter(v=>v!==i.id))}/>
{i.name}

{t(i.kind==='unknown'?'未知归属的数据':i.kind==='data'?'保留的应用数据':i.kind==='backups'?'恢复备份':(i.kind==='reviews'||i.kind==='uploads')?'过期上传文件':'旧 APK')} · {bytes(i.bytes)}

{i.unknown?{t('无法确认归属,不提供自动删除。')}:i.sensitive&&{t('删除后不可恢复')}}
{i.restorable&&}
)}{!cleanup.length&&

{t('没有可清理的数据。')}

}} {!!data.warnings.length&&
{t('部分容器无法读取')}{data.warnings.map((w,i)=>

{w}

)}
} - {details&&!confirm&&!review&&!log&&!job&&dialog(<>

{details.metadata.name}

{details.metadata.package}
{t('版本')}
{details.metadata.version} ({details.metadata.versionCode})
{t('容器')}
{contextFor(details.context)?.name??details.context}
{t('空间占用')}
{bytes(details.size)}
{details.pending&&<>

{t(details.pending)}

}
{details.installed&&details.stateKnown!==false&&!details.steam&&<>}{!details.installed&&details.stateKnown!==false&&!details.steam&&}
{details.installed&&details.stateKnown!==false&&!details.steam&&
{t('高级设置')}void perform('steam.settings',{app:details.id,enabled})}/>{data.retainSteamEntries!==false&&(details.steamLaunch??true)&&!details.steamRegistered&&

{t('等待 Steam 注册,Steam 就绪后会自动重试。')}

}{details.steamRegistrationError&&

{details.steamRegistrationError}

}

{t('默认自动处理窗口显示。此开关不改变应用的二维或 VR 类型。')}

void perform('settings',{app:details.id,showWindow:checked?null:true})}/>{details.showWindow!=null&&void perform('settings',{app:details.id,showWindow:checked})}/>}

{t('窗口设置将在下次打开应用时应用;必要时自动重启容器。')}

}
,t('管理应用'))} + {details&&!confirm&&!review&&!log&&!job&&dialog(<>

{details.metadata.name}

{details.metadata.package}
{t('版本')}
{details.metadata.version} ({details.metadata.versionCode})
{t('容器')}
{contextFor(details.context)?.name??details.context}
{t('空间占用')}
{bytes(details.size)}
{details.pending&&<>

{t(details.pending)}

}
{details.installed&&details.stateKnown!==false&&!details.steam&&<>}{!details.installed&&details.stateKnown!==false&&!details.steam&&}
{details.installed&&details.stateKnown!==false&&!details.steam&&
{t('高级设置')}void perform('steam.settings',{app:details.id,enabled})}/>{data.retainSteamEntries!==false&&(details.steamLaunch??true)&&!details.steamRegistered&&

{t('等待 Steam 注册,Steam 就绪后会自动重试。')}

}{details.steamRegistrationError&&

{details.steamRegistrationError}

}

{t('默认自动处理窗口显示。此开关不改变应用的二维或 VR 类型。')}

void perform('settings',{app:details.id,showWindow:checked?null:true})}/>{details.showWindow!=null&&void perform('settings',{app:details.id,showWindow:checked})}/>}

{t('窗口设置将在下次打开应用时应用;必要时自动重启容器。')}

void perform('settings',{app:details.id,framebufferCompatibility:enabled})}/>
}
,t('管理应用'))} {review&&!confirm&&!job&&dialog(installContainer&&containerAsk(installContainer,'container.stop')} onCancel={()=>void discard()} onConfirm={()=>void perform('install',{ticket:review.ticket,context:target,app:updating?.id,showWindow:displayMode==='flat',approve:true})}/>,t('安装 APK'))} {confirm?.app&&!job&&['uninstall','purge'].includes(confirm.operation)&&dialog(setConfirm(c=>c?.app?{...c,purge,deleteContainer:purge&&canDeleteContainer(c.app)}:c)} onDeleteContainer={deleteContainer=>setConfirm(c=>c?{...c,deleteContainer}:c)} onCancel={()=>setConfirm(null)} onConfirm={()=>void perform(confirm.operation,{app:confirm.app!.id,purge:confirm.purge,deleteContainer:!!confirm.deleteContainer&&!!confirm.purge&&canDeleteContainer(confirm.app!),approve:true})}/>,confirm.title)} {confirm&&!job&&!(confirm.app&&['uninstall','purge'].includes(confirm.operation))&&dialog(<>

{confirm.title}

{confirm.description}

{confirm.app&&

{confirm.app.metadata.name}
{confirm.app.metadata.package}

}{deletesData&&

{t('永久删除的数据无法撤销。')}

}{error&&

{error}

},confirm.title)}