Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install host test dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libsdl2-dev pkg-config
- run: tools/bootstrap.sh
- run: tools/zig.sh build fmt-check
- run: tools/zig.sh build test
Expand Down
60 changes: 37 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

<img width="640" height="480" alt="GreenOvercast game library" src="packaging/portmaster/greenovercast/screenshot.png" />

GreenOvercast is a native Xbox Cloud Gaming client for small ARM64 Linux
handhelds. It requests a stream matching the device display and uses hardware
video decoding on supported H700 and Rockchip firmware.
GreenOvercast is a native Xbox Cloud Gaming and GeForce NOW client for small
ARM64 Linux handhelds, with hardware video decoding on supported H700 and
Rockchip firmware.

This independent project is not affiliated with or endorsed by Microsoft.
This independent project is not affiliated with Microsoft or NVIDIA.

Requires an [Xbox Game Pass plan](https://www.xbox.com/cloud-gaming) that includes cloud gaming and internet access.
You need internet access and an account for your chosen service:

- Xbox: an [Xbox Game Pass plan](https://www.xbox.com/cloud-gaming) that includes cloud gaming.
- [GeForce NOW](https://www.nvidia.com/en-us/geforce-now/): a membership and access to the games you want to play.

## Install

Expand All @@ -19,9 +22,9 @@ the [PortMaster FAQ](https://portmaster.games/faq.html#do-i-have-to-use-portmast
When the installation finishes, return to the device frontend and launch
GreenOvercast from Ports.

The first launch shows a Microsoft device code. Open
[microsoft.com/link](https://www.microsoft.com/link) on another device and enter
the code to sign in.
Choose Xbox or GeForce NOW, then open the sign-in link shown on screen on
another device and enter the code. Your login is saved. You can switch services
from Settings without closing the app.

GreenOvercast is experimental.

Expand All @@ -44,28 +47,39 @@ GreenOvercast is experimental.
Settings include Xbox/Nintendo face-button layouts, game artwork, and Sign
out. Games that return a 16:9 stream remain letterboxed on 4:3 displays.

GeForce NOW uses the same gamepad controls. For store dialogs and game
launchers, press Select + Y to toggle mouse mode: D-pad or left stick moves the
pointer, A clicks, B right-clicks, and L1 / R1 scroll. Store sign-in may still be
required inside the stream.

## Supported devices

| Device | OS | Status |
| ---------- | ------------------------------- | ------ |
| RG35XX-H | muOS 2508.4 | Tested |
| RG40XX-H | Knulli (Batocera 42) | Tested |
| RG40XX-H | ROCKNIX 20260801 | Tested |
| Miyoo Flip | SpruceOS 4.2.0 | Tested |
| R36S | AmberELEC prerelease-20250515 | Tested |

Hardware decoding is verified on the H700 systems above. muOS and Knulli use
CedarX; ROCKNIX 20260801 uses the bundled Cedrus modules. Tested Rockchip builds
use Rockchip MPP on RK3566 with SpruceOS and RK3326 with AmberELEC. Other
| Device | SoC | OS | Status |
| ---------- | --------------- | ----------------------------- | ------ |
| RG35XX-H | Allwinner H700 | muOS 2508.4 | Tested |
| RG40XX-H | Allwinner H700 | Knulli (Batocera 42) | Tested |
| RG40XX-H | Allwinner H700 | ROCKNIX 20260801 | Tested |
| Miyoo Flip | Rockchip RK3566 | SpruceOS 4.3.3 | Tested |
| R36S | Rockchip RK3326 | AmberELEC prerelease-20250515 | Tested |

Hardware decoding is verified on the configurations above. H700 uses CedarX on
muOS and Knulli, and Cedrus on ROCKNIX. RK3566 and RK3326 use Rockchip MPP. Other
devices fall back to software decoding, which is too slow for normal gameplay.

The release requires glibc 2.38 or newer. ArkOS ships glibc 2.30 and is not
supported.
GeForce NOW playback has been tested on muOS, Knulli, SpruceOS, and the R36S
running dArkOS 02012026. R36S frame pacing still needs work. The ROCKNIX and
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
AmberELEC rows above currently cover Xbox playback.

The release requires glibc 2.38 or newer. ArkOS (glibc 2.30) is not supported;
the tested dArkOS fork uses glibc 2.41.

## Build

You need a Linux or macOS host with `cmake`, `curl`, `git`, `make`, `patch`,
`perl`, and `python3`.
You need a Linux or macOS host with CMake 3.24+, `curl`, `git`, `make`, `patch`,
`perl`, `pkg-config`, `python3`, and `tar`.

Host tests also need SDL2 development files: install
`libsdl2-dev pkg-config` on Debian/Ubuntu or `sdl2 pkg-config` with Homebrew.

```sh
tools/bootstrap.sh
Expand Down
5 changes: 4 additions & 1 deletion THIRDPARTY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@ release. Full license texts for the bundled libraries ship alongside the port in
| [libudev-zero](https://github.com/illiliti/libudev-zero) | ISC | V4L2 media-device discovery (static) |
| [SDL2](https://www.libsdl.org/) | Zlib | Window, renderer, audio, and controller input (device) |
| [OpenSSL](https://www.openssl.org/) | Apache-2.0 | TLS, WebRTC DTLS, and credential encryption (static) |
| [libcurl](https://curl.se/) | curl (MIT-style) | HTTP for Xbox services (static) |
| [libcurl](https://curl.se/) | curl (MIT-style) | HTTP and WebSocket transport (static) |
| [Opus](https://opus-codec.org/) | BSD-3-Clause | Audio decode (static) |

Xbox and NVIDIA icons are from [Bootstrap Icons](https://github.com/twbs/icons)
(MIT).
180 changes: 154 additions & 26 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const aarch64_linux_query: std.Target.Query = .{
const c_test_flags = &.{ "-std=c11", "-Wall", "-Wextra", "-Werror" };

const project_include_paths = [_][]const u8{
".tools/deps/aarch64-linux-gnu/include",
"vendor/libdatachannel/include",
"src/media/audio",
"src/media/video",
Expand Down Expand Up @@ -51,6 +50,7 @@ const release_zig_roots = [_]struct {
.imports = &.{
.{ .name = "json_reader", .path = "src/net/json_reader.zig" },
.{ .name = "json_writer", .path = "src/net/json_writer.zig" },
.{ .name = "uuid", .path = "src/util/uuid.zig" },
},
},
.{ .name = "greenovercast-controller", .path = "src/input/controller.zig" },
Expand Down Expand Up @@ -81,6 +81,7 @@ const release_c_sources = [_][]const u8{
"src/media/video/video_decoder_v4l2_request.c",
"src/auth/token_store_adapter.c",
"src/net/http_client.c",
"src/net/websocket_client.c",
"src/ui/artwork_decoder.c",
};

Expand Down Expand Up @@ -122,6 +123,7 @@ fn addReleaseZigObject(
.link_libc = true,
});
addProjectIncludes(b, module);
module.addIncludePath(b.path(".tools/deps/aarch64-linux-gnu/include"));
for (imports) |item|
addZigImport(b, module, target, optimize, item.name, item.path);
return b.addObject(.{ .name = name, .root_module = module });
Expand All @@ -146,6 +148,28 @@ fn addStaticArchive(module: *std.Build.Module, b: *std.Build, path: []const u8)
module.addObjectFile(b.path(path));
}

fn addHostUnitTest(
b: *std.Build,
test_step: *std.Build.Step,
source: []const u8,
project_includes: bool,
sdl: bool,
imports: []const ZigImport,
) void {
const module = b.createModule(.{
.root_source_file = b.path(source),
.target = b.graph.host,
.optimize = .Debug,
.link_libc = project_includes,
});
if (project_includes) addProjectIncludes(b, module);
if (sdl) module.linkSystemLibrary("SDL2", .{ .use_pkg_config = .force });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The host tests request the wrong pkg-config module name, so CI cannot resolve SDL2 and build test fails before running. Use the lowercase Debian/Homebrew module name sdl2.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At build.zig, line 166:

<comment>The host tests request the wrong pkg-config module name, so CI cannot resolve SDL2 and `build test` fails before running. Use the lowercase Debian/Homebrew module name `sdl2`.</comment>

<file context>
@@ -161,6 +163,7 @@ fn addHostUnitTest(
         .link_libc = project_includes,
     });
     if (project_includes) addProjectIncludes(b, module);
+    if (sdl) module.linkSystemLibrary("SDL2", .{ .use_pkg_config = .force });
     for (imports) |item|
         addZigImport(b, module, b.graph.host, .Debug, item.name, item.path);
</file context>
Suggested change
if (sdl) module.linkSystemLibrary("SDL2", .{ .use_pkg_config = .force });
if (sdl) module.linkSystemLibrary("sdl2", .{ .use_pkg_config = .force });

for (imports) |item|
addZigImport(b, module, b.graph.host, .Debug, item.name, item.path);
const unit_tests = b.addTest(.{ .root_module = module });
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}

fn addReleaseArtifacts(
b: *std.Build,
target: std.Build.ResolvedTarget,
Expand All @@ -166,6 +190,31 @@ fn addReleaseArtifacts(
.strip = true,
});
addProjectIncludes(b, main_module);
main_module.addIncludePath(b.path(".tools/deps/aarch64-linux-gnu/include"));
addZigImport(
b,
main_module,
target,
optimize,
"form_writer",
"src/net/form_writer.zig",
);
addZigImport(
b,
main_module,
target,
optimize,
"catalog_parser",
"src/catalog/catalog_parser.zig",
);
addZigImport(
b,
main_module,
target,
optimize,
"uuid",
"src/util/uuid.zig",
);
main_module.addLibraryPath(b.path(".tools/deps/aarch64-linux-gnu/lib"));
main_module.addRPathSpecial("$ORIGIN");
main_module.addCSourceFiles(.{
Expand Down Expand Up @@ -456,32 +505,97 @@ pub fn build(b: *std.Build) void {
});
rocknix_build_regression_test.setCwd(b.path("."));
test_step.dependOn(&rocknix_build_regression_test.step);

const test_roots = [_][]const u8{
"src/app/state.zig",
"src/catalog/catalog_parser.zig",
"src/catalog/catalog_search.zig",
"src/input/wire_encoder.zig",
"src/input/guide_chord.zig",
"src/session/message_protocol.zig",
"src/ui/keyboard.zig",
"src/ui/control_icons.zig",
"src/ui/navigation_repeat.zig",
"src/ui/persistent_settings.zig",
"src/ui/stream_dimensions.zig",
"src/media/rtp/h264_depacketizer.zig",
"src/net/json_reader.zig",
"src/net/json_writer.zig",
"src/net/form_writer.zig",
const dependency_toolchain_test = b.addSystemCommand(&.{
"sh",
b.pathFromRoot("tests/dependency_toolchain_test.sh"),
});
dependency_toolchain_test.setCwd(b.path("."));
test_step.dependOn(&dependency_toolchain_test.step);

const tests = [_]struct {
source: []const u8,
project_includes: bool = false,
sdl: bool = false,
imports: []const ZigImport = &.{},
}{
.{ .source = "src/app/state.zig" },
.{ .source = "src/catalog/catalog_parser.zig" },
.{ .source = "src/catalog/catalog_search.zig" },
.{ .source = "src/input/wire_encoder.zig" },
.{ .source = "src/input/guide_chord.zig" },
.{ .source = "src/session/message_protocol.zig" },
.{ .source = "src/ui/keyboard.zig" },
.{ .source = "src/ui/control_icons.zig" },
.{ .source = "src/ui/navigation_repeat.zig" },
.{ .source = "src/ui/persistent_settings.zig" },
.{ .source = "src/ui/stream_dimensions.zig" },
.{ .source = "src/media/rtp/h264_depacketizer.zig" },
.{ .source = "src/net/json_reader.zig" },
.{ .source = "src/net/json_writer.zig" },
.{ .source = "src/net/form_writer.zig" },
.{ .source = "src/util/uuid.zig" },
.{ .source = "src/provider/geforce_now/input_protocol.zig" },
.{ .source = "src/provider/geforce_now/pointer_input.zig" },
.{ .source = "src/provider/geforce_now/endpoint.zig" },
.{ .source = "src/provider/geforce_now/cloudmatch_protocol.zig" },
.{ .source = "src/provider/geforce_now/subscription_protocol.zig" },
.{ .source = "src/provider/geforce_now/provider_protocol.zig" },
.{ .source = "src/provider/geforce_now/signaling_protocol.zig" },
.{ .source = "src/provider/geforce_now/sdp_protocol.zig" },
.{
.source = "src/provider/geforce_now/auth_protocol.zig",
.imports = &.{.{ .name = "form_writer", .path = "src/net/form_writer.zig" }},
},
.{
.source = "src/provider/geforce_now/auth_client.zig",
.project_includes = true,
.sdl = true,
.imports = &.{
.{ .name = "gfn_http_fake", .path = "tests/gfn_http_fake.zig" },
.{ .name = "form_writer", .path = "src/net/form_writer.zig" },
.{ .name = "uuid", .path = "src/util/uuid.zig" },
},
},
.{
.source = "src/provider/geforce_now/catalog_protocol.zig",
.imports = &.{.{ .name = "catalog_parser", .path = "src/catalog/catalog_parser.zig" }},
},
.{
.source = "src/provider/geforce_now/catalog_service.zig",
.project_includes = true,
.sdl = true,
.imports = &.{
.{ .name = "gfn_http_fake", .path = "tests/gfn_http_fake.zig" },
.{ .name = "form_writer", .path = "src/net/form_writer.zig" },
.{ .name = "catalog_parser", .path = "src/catalog/catalog_parser.zig" },
.{ .name = "uuid", .path = "src/util/uuid.zig" },
},
},
.{
.source = "src/provider/geforce_now/session_client.zig",
.project_includes = true,
.sdl = true,
.imports = &.{
.{ .name = "gfn_http_fake", .path = "tests/gfn_http_fake.zig" },
.{ .name = "form_writer", .path = "src/net/form_writer.zig" },
.{ .name = "uuid", .path = "src/util/uuid.zig" },
},
},
.{
.source = "src/provider/geforce_now/webrtc_session.zig",
.project_includes = true,
.sdl = true,
},
};
for (test_roots) |root| {
const unit_tests = b.addTest(.{
.root_source_file = b.path(root),
.target = b.graph.host,
.optimize = .Debug,
});
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}
for (tests) |test_config|
addHostUnitTest(
b,
test_step,
test_config.source,
test_config.project_includes,
test_config.sdl,
test_config.imports,
);

const video_decoder_object =
addHostZigObject(b, "video-decoder", "src/media/video/video_decoder.zig");
Expand Down Expand Up @@ -536,6 +650,20 @@ pub fn build(b: *std.Build) void {

const fake_cedar_valid =
addHostCFakeLibrary(b, "fake-cedar-valid", "tests/cedar_fake_valid.c");
const cedar_list_test = b.addExecutable(.{
.name = "cedar-list-test",
.root_source_file = null,
.target = b.graph.host,
.optimize = .ReleaseSafe,
});
cedar_list_test.addIncludePath(b.path("vendor/cedarx/base/include"));
cedar_list_test.addCSourceFile(.{
.file = b.path("tests/cedar_list_test.c"),
.flags = &.{ "-std=gnu11", "-Wall", "-Wextra", "-Werror" },
});
cedar_list_test.linkLibC();
test_step.dependOn(&b.addRunArtifact(cedar_list_test).step);

const cedar_decoder_test = addHostCExecutable(
b,
"video-decoder-cedar-test",
Expand Down
14 changes: 13 additions & 1 deletion packaging/portmaster/greenovercast/GreenOvercast.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ chmod 700 "$credential_dir" || fail "Unable to protect GreenOvercast's private s

credential_file="$credential_dir/tokens.bin"
credential_key_file="$credential_dir/tokens.key"
gfn_credential_file="$credential_dir/geforce-now-tokens.bin"
gfn_credential_key_file="$credential_dir/geforce-now-tokens.key"
gfn_device_id_file="$credential_dir/geforce-now-device-id"
video_bootstrap_file="$credential_dir/h264-parameter-sets.bin"
catalog_file="$credential_dir/catalog.tsv"
settings_file="$config_dir/settings.tsv"
Expand All @@ -66,7 +69,9 @@ log_file="$credential_dir/greenovercast.log"
chmod 600 "$log_file"
exec > >(tee "$log_file") 2>&1

for private_file in "$credential_file" "$credential_key_file" "$catalog_file"; do
for private_file in "$credential_file" "$credential_key_file" \
"$gfn_credential_file" "$gfn_credential_key_file" "$gfn_device_id_file" \
"$catalog_file"; do
[ ! -e "$private_file" ] || chmod 600 "$private_file"
done

Expand All @@ -78,6 +83,13 @@ fi

export GREENOVERCAST_TOKEN_FILE="$credential_file"
export GREENOVERCAST_TOKEN_KEY_FILE="$credential_key_file"
export GREENOVERCAST_GFN_TOKEN_FILE="$gfn_credential_file"
export GREENOVERCAST_GFN_TOKEN_KEY_FILE="$gfn_credential_key_file"
export GREENOVERCAST_GFN_DEVICE_ID_FILE="$gfn_device_id_file"
[ ! -f "$config_dir/geforce-now-oauth-client-id" ] ||
export GREENOVERCAST_GFN_CLIENT_ID_FILE="$config_dir/geforce-now-oauth-client-id"
[ ! -f "$config_dir/geforce-now-protocol-client-id" ] ||
export GREENOVERCAST_GFN_PROTOCOL_CLIENT_ID_FILE="$config_dir/geforce-now-protocol-client-id"
export GREENOVERCAST_H264_BOOTSTRAP_FILE="$video_bootstrap_file"
export GREENOVERCAST_CATALOG_FILE="$catalog_file"
export GREENOVERCAST_SETTINGS_FILE="$settings_file"
Expand Down
Loading
Loading