diff --git a/README.md b/README.md index 73f9dfe..8e0a61d 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ It keeps only the parts that were actually used: - `run_redroid.py` - `bootstrap_redroid.py` - `verify_offline_bundle.py` +- `tools/compat.py` (host / GPU / runtime checks) - `offline/README.md` - `offline/manifest.json` - `stuff/mindthegapps.py` @@ -63,6 +64,28 @@ python verify_offline_bundle.py python -m pip install -r requirements.txt ``` +## Compatibility checks + +The project probes the **host** before critical steps (no separate manual checklist required): + +| Step | What it checks | Opt-out | +|------|----------------|---------| +| `redroid.py` | Container runtime (`docker`/`podman`) responds; OS is plausible for redroid | `--no-compat-check` | +| `run_redroid.py` | Same + Linux **`/dev/dri`** for GPU; **`binder_linux`** module hint on Linux | `--no-compat-check` | + +**GPU mode (`run_redroid.py`):** + +- **`--gpu-mode auto` (default):** uses **`host`** when DRI devices exist and automatically adds `docker run --device …` for each `/dev/dri/card*` / `renderD*` node; otherwise **`guest`** (software GLES). +- **`--gpu-mode host`:** pass-through when DRI exists; otherwise **falls back to `guest`** with a warning. +- **`--gpu-mode guest`:** force software rendering (common black-screen case on headless servers). + +Quick probe only: + +```bash +python tools/compat.py +python tools/compat.py -c podman --gpu-mode auto +``` + ## Build the 5570 image ```bash @@ -112,6 +135,33 @@ If you still see `unauthorized` after an upgrade, stop the container, remove per rm -rf ~/data-redroid13/misc/adb ``` +## Black screen or flickering + +With **`--gpu-mode guest`** (or **`auto`** when no `/dev/dri` exists), the display uses **software GLES** (SwiftShader). On many **headless servers**, SurfaceFlinger may show a **black screen**, **flicker**, or very low FPS. That is a **display stack** issue, not ADB. The default **`auto`** mode uses **host** GPU when DRI nodes are visible and injects **`--device`** flags automatically. + +**What to try** + +1. **GPU on the host:** ensure **`--gpu-mode auto`** (default) so DRI is detected, or set **`--gpu-mode host`** explicitly. If you must pass devices manually (unusual when `auto` works), for example: + ```bash + docker run ... --device /dev/dri ... + ``` + The exact device path varies (Intel/AMD/NVIDIA, `nvidia-docker`, etc.); see [redroid-doc](https://github.com/remote-android/redroid-doc) GPU notes. + +2. **Stay on software rendering** but tune the virtual panel (sometimes reduces glitches): + ```bash + python run_redroid.py ... --adb-insecure \ + --extra-prop androidboot.redroid_width=720 \ + --extra-prop androidboot.redroid_height=1280 \ + --extra-prop androidboot.redroid_fps=15 + ``` + +3. **First boot / SetupWizard** can sit on a dark screen for a while. Run **`bootstrap_redroid.py`** once `adb` shows `device` so provisioning and wizard are skipped as intended. + +4. **Debug with logcat** (on the host, after `adb connect`): + ```bash + adb -s 127.0.0.1:5570 logcat -d | grep -iE 'SurfaceFlinger|EGL|GLES|redroid' + ``` + ## Apply the 5570 hardening ```bash diff --git a/redroid.py b/redroid.py index ce1c301..3efe343 100644 --- a/redroid.py +++ b/redroid.py @@ -3,11 +3,13 @@ import argparse import os import subprocess +import sys from stuff.houdini import Houdini from stuff.houdini_hack import Houdini_Hack from stuff.magisk import Magisk from stuff.mindthegapps import MindTheGapps +import tools.compat as compat import tools.helper as helper @@ -163,10 +165,21 @@ def main(): action="store_true", help="Do not pull images or download archives; require offline/downloads and offline/images", ) + parser.add_argument( + "--no-compat-check", + dest="no_compat_check", + action="store_true", + help="Skip container-runtime / OS compatibility checks before build", + ) args = parser.parse_args() if args.offline: os.environ["REDROID_OFFLINE"] = "1" + if not args.no_compat_check: + build_report = compat.analyze_for_build(args.container) + compat.emit_compat_report(build_report) + if build_report.errors: + sys.exit(1) base_image_source = ensure_base_image(args.container, args.android) dockerfile, tags = build_layers(args) diff --git a/run_redroid.py b/run_redroid.py index afe2655..45edda0 100644 --- a/run_redroid.py +++ b/run_redroid.py @@ -5,7 +5,9 @@ import os import re import shlex import subprocess +import sys +import tools.compat as compat from tools.helper import bcolors, print_color @@ -224,8 +226,11 @@ def main(): ) parser.add_argument( "--gpu-mode", - default="guest", - help="Value for androidboot.redroid_gpu_mode", + default="auto", + help=( + "androidboot.redroid_gpu_mode: auto=host if /dev/dri exists else guest (default); " + "guest=software GLES; host=GPU (adds --device for each DRI node when present)" + ), ) parser.add_argument( "--device-profile", @@ -266,6 +271,11 @@ def main(): "(before device profile; see redroid-doc README / #579). Overrides user-build secure props." ), ) + parser.add_argument( + "--no-compat-check", + action="store_true", + help="Skip compatibility messages and docker/podman probe (still resolves --gpu-mode auto from DRI)", + ) args = parser.parse_args() image = args.image @@ -278,6 +288,16 @@ def main(): os.makedirs(data_dir, exist_ok=True) + run_report, effective_gpu_mode, dri_device_args = compat.analyze_for_run( + args.container, + args.gpu_mode, + skip_check=args.no_compat_check, + ) + if not args.no_compat_check: + compat.emit_compat_report(run_report) + if run_report.errors: + sys.exit(1) + cmd = [ args.container, "run", @@ -294,6 +314,7 @@ def main(): cmd.extend(["--memory", memory]) if not args.keep_container: cmd.append("--rm") + cmd.extend(dri_device_args) cmd.append(image) # Init applies read-only props in order; later ro.adb.secure=0 is ignored if adb.secure # was already set (redroid-doc #579). Put these immediately after the image name. @@ -308,7 +329,7 @@ def main(): "ro.debuggable=1", ] ) - cmd.append("androidboot.redroid_gpu_mode={}".format(args.gpu_mode)) + cmd.append("androidboot.redroid_gpu_mode={}".format(effective_gpu_mode)) cmd.extend(bridge_props(bridge)) cmd.extend(preset_props(preset)) cmd.extend(device_profile_props(device_profile)) @@ -322,6 +343,10 @@ def main(): check=False, ) + print_color( + "Selected GPU mode: {} (androidboot.redroid_gpu_mode)".format(effective_gpu_mode), + bcolors.GREEN, + ) print_color("Selected native bridge: {}".format(bridge), bcolors.GREEN) if preset != "none": print_color("Selected launch preset: {}".format(preset), bcolors.GREEN) diff --git a/tools/compat.py b/tools/compat.py new file mode 100644 index 0000000..ef72208 --- /dev/null +++ b/tools/compat.py @@ -0,0 +1,167 @@ +""" +Host and container-runtime compatibility checks for redroid-android13. +""" + +from __future__ import annotations + +import glob +import os +import platform +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import List, Tuple + +_REPO_ROOT = Path(__file__).resolve().parents[1] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from tools.helper import bcolors, print_color + + +@dataclass +class CompatReport: + errors: List[str] = field(default_factory=list) + warnings: List[str] = field(default_factory=list) + notes: List[str] = field(default_factory=list) + + +def _runtime_available(runtime: str) -> bool: + try: + result = subprocess.run( + [runtime, "version"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=30, + check=False, + ) + return result.returncode == 0 + except OSError: + return False + + +def detect_dri_devices() -> List[str]: + """Paths suitable for docker/podman --device (Linux DRM).""" + found: List[str] = [] + for pattern in ("/dev/dri/card*", "/dev/dri/renderD*"): + found.extend(glob.glob(pattern)) + return sorted(set(found)) + + +def _linux_binder_notes() -> List[str]: + if platform.system() != "Linux": + return [] + notes: List[str] = [] + try: + with open("/proc/modules", "r", encoding="utf-8", errors="ignore") as handle: + loaded = handle.read() + if "binder_linux" not in loaded: + notes.append( + "binder_linux not loaded; redroid usually needs: " + "modprobe binder_linux devices=binder,hwbinder,vndbinder" + ) + except OSError: + pass + return notes + + +def analyze_for_build(container_runtime: str) -> CompatReport: + report = CompatReport() + if not _runtime_available(container_runtime): + report.errors.append( + "Container runtime {!r} not found or not working (install Docker/Podman and check PATH).".format( + container_runtime + ) + ) + system = platform.system() + if system == "Windows": + report.warnings.append( + "Running on Windows: redroid containers are meant for Linux hosts (binder/ashmem); " + "building files here is fine, running the container typically happens on Linux." + ) + elif system != "Linux": + report.warnings.append( + "Host OS is not Linux; upstream redroid expects Linux with binder and related kernel pieces." + ) + return report + + +def resolve_gpu_mode(requested: str, dri_devices: List[str]) -> Tuple[str, List[str]]: + """ + Map user --gpu-mode to an effective redroid mode and docker --device arguments. + auto -> host if DRI nodes exist, else guest. + """ + req = (requested or "auto").strip().lower() + if req == "auto": + effective = "host" if dri_devices else "guest" + elif req == "host": + effective = "host" if dri_devices else "guest" + elif req == "guest": + effective = "guest" + else: + effective = "guest" + + device_args: List[str] = [] + if effective == "host" and dri_devices: + for path in dri_devices: + device_args.extend(["--device", path]) + return effective, device_args + + +def analyze_for_run( + container_runtime: str, + gpu_mode: str, + skip_check: bool, +) -> Tuple[CompatReport, str, List[str]]: + """ + Returns (report, effective_gpu_mode, flat docker --device args). + """ + report = CompatReport() + dri = detect_dri_devices() + + if not skip_check: + build = analyze_for_build(container_runtime) + report.errors.extend(build.errors) + report.warnings.extend(build.warnings) + report.notes.extend(build.notes) + if dri: + report.notes.append("DRI present: {} — GPU passthrough can be used.".format(", ".join(dri))) + elif platform.system() == "Linux": + report.warnings.append( + "No /dev/dri render nodes: software GLES (guest) often black-screens or flickers on headless servers." + ) + report.notes.extend(_linux_binder_notes()) + + effective, device_args = resolve_gpu_mode(gpu_mode, dri) + req = (gpu_mode or "auto").strip().lower() + if req == "host" and effective == "guest": + report.warnings.append( + "--gpu-mode host requested but no DRI devices found; falling back to guest." + ) + if req == "auto" and not skip_check: + report.notes.append("Resolved --gpu-mode auto -> {}".format(effective)) + + return report, effective, device_args + + +def emit_compat_report(report: CompatReport, prefix: str = "[compat] ") -> None: + for msg in report.errors: + print_color(prefix + msg, bcolors.RED) + for msg in report.warnings: + print_color(prefix + msg, bcolors.YELLOW) + for msg in report.notes: + print_color(prefix + msg, bcolors.GREEN) + + +if __name__ == "__main__": + import argparse + + ap = argparse.ArgumentParser(description="Host/runtime compatibility probe (same logic as run_redroid.py)") + ap.add_argument("-c", "--container", default="docker", help="docker or podman") + ap.add_argument("--gpu-mode", default="auto", help="guest, host, or auto") + ns = ap.parse_args() + rep, gpu_eff, dev_args = analyze_for_run(ns.container, ns.gpu_mode, skip_check=False) + emit_compat_report(rep) + print("effective_gpu_mode={}".format(gpu_eff)) + print("flat_device_args={}".format(dev_args))