Skip to content

Add support for aarch64 devices (armada OS) - #184

Open
vinegh4 wants to merge 5 commits into
FrogTheFrog:mainfrom
vinegh4:upstream-arm64
Open

vinegh4 wants to merge 5 commits into
FrogTheFrog:mainfrom
vinegh4:upstream-arm64

Conversation

@vinegh4

@vinegh4 vinegh4 commented Oct 1, 2026

Copy link
Copy Markdown

Add ARM64 (aarch64) support

Most of the ~23k-line diff is vendored psutil. The real code change is about 160 lines.

This makes the same moondeck.zip work on aarch64 handhelds running Decky Loader. I tested it on an AYN Odin 2 Portal with Armada OS. There's no second build. The plugin picks the right dependencies when it starts, and on x86_64 the only behavior change is the resolution rounding below.

What changed

The runner now puts externals-<arch> on the path ahead of externals (in main.py, moondeckrun.py and cli.py). psutil is the only package that needs this. Its C extension is named .abi3.so, so an aarch64 Python tries to load the x86_64 copy and the import fails. Every other bundled package ships cpython-313-x86_64 extensions, which aarch64 skips, and falls back to pure Python.

externals-aarch64 holds the psutil 7.0.0 aarch64 wheel. Its .py files are identical to the copy already in externals. Only the two .so files differ. On x86_64 the directory doesn't exist, so the extra path entry does nothing.

moonlightproxy.py runs the Moonlight flatpak for the host architecture instead of hardcoding --arch=x86_64. On x86_64 the arguments are the same as before.

settingsparser.py rounds the automatic resolution down to even dimensions. This one applies on every platform, and I think it's a fix everywhere. Fractional display scaling can report odd sizes (the Portal reports 1919×1078), and host video encoders refuse them. Even sizes like the Deck's 1280×800 don't change, and custom resolutions are left alone.

A new check-externals CI job runs on ubuntu-latest and ubuntu-24.04-arm. It fails if requirements-aarch64.txt drifts from requirements.txt, and it imports every bundled package using the runner's path order. The nightly release now waits for it.

scripts/update-arch-externals.sh regenerates the aarch64 copy. The README gets a short ARM64 section and a note on keeping dependencies in sync, and .gitattributes collapses the vendored directory in diffs.

Keeping it in sync

When psutil is bumped in requirements.txt, bump requirements-aarch64.txt to the same version and run scripts/update-arch-externals.sh. CI fails if you forget. No other dependency needs this.

Known limitations on ARM

  • Moonlight's flatpak has no hardware video decoding on Adreno GPUs, so the Portal decodes on the CPU. Lowering the bitrate or switching to H.264 helps if the stream stutters.
  • Linked displays don't work, because the ARM Steam client has no SteamClient.System.DisplayManager. The existing try/catch handles it, so it only logs errors.

Testing

  • Played games through MoonDeck on an Odin 2 Portal running Armada OS.
  • check-externals passes on x86_64 and arm64:
  • The vendored psutil binaries match PyPI's psutil-7.0.0-cp36-abi3-manylinux2014_aarch64 wheel, regenerated with the script.

The bundled externals are built for x86_64 only. On aarch64 hosts (e.g. Armada
on the AYN Odin 2 Portal) the runner runs under native Python, and psutil fails
to import because its extension has no pure-Python fallback. Every other
native dependency falls back to pure Python.

Add externals-aarch64 with psutil's abi3 aarch64 wheel and put
externals-<machine> on sys.path ahead of externals. On x86_64 that directory
doesn't exist, so behavior there is unchanged.
The flatpak was always started with --arch=x86_64, which fails immediately on
aarch64 hosts where only the aarch64 build of Moonlight is installed. Use the
host arch instead, and omit --arch for arches Moonlight is not published for.
x86_64 hosts get exactly the same arguments as before.
The automatic resolution is screen size times devicePixelRatio, which can be
odd with fractional scaling (1919x1078 on the AYN Odin 2 Portal). Host video
encoders reject odd sizes, so Moonlight gets audio but never any video and
ends the stream with "No video received from host". Even sizes are unchanged.
scripts/check-externals.py fails when requirements-<arch>.txt drifts from
requirements.txt or externals-<arch> lacks the pinned version, and with
--smoke imports every bundled package using moondeckrun.py's path order.
CI runs both on ubuntu-latest and ubuntu-24.04-arm, and the nightly
release waits for them.

Also run pip via python3 -m in update-arch-externals.sh and mark the
vendored aarch64 externals as generated so GitHub collapses them.
@@ -116,7 +116,12 @@ def update_dimensions(dimension: Dimension):

logger.info(f"Using auto resolution from MoonDeck: {auto_resolution}")
if auto_resolution:
dimensions["size"] = { "width": auto_resolution["width"], "height": auto_resolution["height"] }
# Fractional display scaling can yield odd sizes (e.g. 1919x1078), which host video encoders reject

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we go the other way? If the resolution is 1919x1078, due to fractional scaling it should have lost 1 pixel, so it should be 1920x1078, no?

Could you please play around with the manual resolution and see which variant works the best or fractional displays? Adding 1px or subtracting 1px.

def __get_exec_with_args(self):
if self.exec_path is None:
exec = self.__get_flatpak_exec()
args = ["run", "--arch=x86_64", "--command=moonlight", self.flatpak_moonlight]
args = ["run", *self.__get_flatpak_arch_args(), "--command=moonlight", self.flatpak_moonlight]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
args = ["run", *self.__get_flatpak_arch_args(), "--command=moonlight", self.flatpak_moonlight]
args = ["run", self.flatpak_moonlight]

I think we should get rid o the arch and command args altogether and just let flatpak use defaults or user overrides.

Comment thread defaults/python/cli.py
script_dir = Path(__file__).parent.resolve()
directories = [["lib"], ["externals"]]
# Arch-specific externals (e.g. aarch64 psutil) must shadow the default x86_64 ones; the dir only exists where needed
directories = [["lib"], [f"externals-{platform.machine()}"], ["externals"]]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of this at all :/

Since it's only needed for psutils, I want to play around a little and see what other options are. I'll get back to you on this point later.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed psutil, please rebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants