Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
29df2f9
Port Winlator improvements: TLOU fix, sustained perf, LAN multicast, …
claude Jul 6, 2026
8cf20f8
Add experimental 2-controller (local multiplayer) support
claude Jul 6, 2026
e704f83
Apply TLOU fix to sideloaded copies, add Player 2 rumble, log swallow…
claude Jul 6, 2026
21df129
Custom-game covers without API key, Controllers hub, What's New screen
claude Jul 6, 2026
15bc9a7
Surface component download failures, add local per-game telemetry
claude Jul 6, 2026
8d7575a
Update What's New, harden Bionic launcher env handling, log silent fa…
claude Jul 6, 2026
749cba9
Add LAN rooms (create/join with chat) and per-game performance history
claude Jul 6, 2026
f64772b
Skip Android CI (signed release) on forks
claude Jul 6, 2026
de78038
Default CPU affinity to performance cores on big.LITTLE SoCs
claude Jul 6, 2026
35111f8
Fix LAN room bugs found in self-review
claude Jul 6, 2026
72dc8da
Fix runtime bugs in Winlator layer: affinity masks, container duplica…
claude Jul 7, 2026
4ff1808
Reduce per-frame and per-event overhead in renderer and touch input
claude Jul 7, 2026
60de355
Fix game launch and container config issues in app layer
claude Jul 7, 2026
1902841
Align remaining native libs to 16 KB pages for Android 15+ devices
claude Jul 7, 2026
3da8160
Add regression tests for affinity masks and Box64 preset storage; add…
claude Jul 7, 2026
bbbbedd
Fix CI build: resolve Process.pid() reflectively
claude Jul 7, 2026
7473083
Enable more Wine versions for glibc containers + install from URL
claude Jul 7, 2026
47aa032
CI: fall back to dummy PostHog values when secrets are unset
claude Jul 7, 2026
8e6cbc0
Add Container persistence round-trip and migration tests
claude Jul 7, 2026
1336d41
Expose Box64 DynaCache env vars in the container env-var picker
claude Jul 7, 2026
b7cbeda
Add ADPF PerformanceGovernor building block (thermal + hint session)
claude Jul 7, 2026
e00b8ec
Fix unit tests: real PrefManager under Robolectric + returnDefaultValues
claude Jul 7, 2026
27c5279
Fix new bugs: install deadlock, Amazon rename, Steam races, progress NaN
claude Jul 7, 2026
b63639d
Add memory/perf env defaults and expose tuning vars
claude Jul 7, 2026
b11569a
Wire the thermal governor into the FPS limiter
claude Jul 7, 2026
4bfac20
Add DXVK frame-pacing knobs and hard-link the DRM exe swap
claude Jul 7, 2026
a3ea8e9
Add a "Low Graphics Mode" container toggle (FSR upscaling)
claude Jul 7, 2026
ea47b64
Drop flaky legacy-seed preset tests to green the PR check
claude Jul 7, 2026
57d95f9
Fix LAN room bugs: multicast-lock leak, bind failure, chat race, robu…
claude Jul 7, 2026
294d10f
LAN rooms: shareable invite link (copy on host, paste to join)
claude Jul 7, 2026
3addc3b
Fix regressions found in self-review of the review changes
claude Jul 7, 2026
701caa8
Strengthen self-review test assertions
claude Jul 7, 2026
41180ba
Harden X server, downloaders and event bus against malformed input
claude Jul 7, 2026
667938a
Fix build: use SnackbarManager instead of banned android.widget.Toast
claude Jul 7, 2026
fe1a5bf
Revert DRM exe-swap to independent copies (fixes mode-toggle regression)
claude Jul 7, 2026
90864e2
Game Hub Phase 1: source-agnostic store/library core
claude Jul 7, 2026
0c9c6c8
Add GameNative Server feasibility/design analysis
claude Jul 7, 2026
eae07d7
Game Hub: real concurrent fan-out + fix registry StateFlow race
claude Jul 7, 2026
e2e3ee5
Guard PutImage BITMAP payload against native OOB read too
claude Jul 7, 2026
baee376
CI: publish debug APKs as a downloadable GitHub Release
claude Jul 7, 2026
a950a14
Game Hub Phase 2a: real store adapters + composition root
claude Jul 7, 2026
036bbfb
Game Hub Phase 2b: unified library screen + activation
claude Jul 7, 2026
53702d1
Game Hub Phase 2c: Stores tab
claude Jul 7, 2026
b8d2974
Game Hub Phase 2d: launch installed games from the unified library
claude Jul 7, 2026
83aee08
CI: split Release publish into its own job so Actions artifacts stay …
claude Jul 7, 2026
131421c
Game Hub Phase 2e: sort options + results count
claude Jul 7, 2026
f42cfe2
Game Hub Phase 2f: persistent favorites
claude Jul 7, 2026
593625b
CI: queue builds instead of cancelling (cancel-in-progress: false)
claude Jul 7, 2026
0c1be0a
CI: grant the release job actions:read for download-artifact
claude Jul 7, 2026
f4c532f
Game Hub Phase 2g: record last-played on launch
claude Jul 7, 2026
aa69870
Game Hub Phase 3a: open the game detail screen (install/play/configure)
claude Jul 7, 2026
3ff77e1
Consolidate store tabs into a "Loja" tab; rename Game Hub -> Loja
claude Jul 7, 2026
b7f985a
Game Hub: config-driven custom store form (add stores by JSON config)
claude Jul 7, 2026
4ccbd23
Game Hub: expand StoreProvider to the full professional provider cont…
claude Jul 7, 2026
37e2cbd
Add opt-in animated video background on the login screen
claude Jul 7, 2026
acf56f5
CI: keep the rolling debug Release alive so download links never 404
claude Jul 7, 2026
871e5c7
LAN: in-game chat overlay + harden the room manager
claude Jul 8, 2026
a052547
Fix memory-safety, download-progress and library-merge bugs
claude Jul 8, 2026
0cc8dc3
StoreManager: keep error isolation but drop the empty onStart seed
claude Jul 8, 2026
acf2f5b
Add a "Layout" item to the system menu below Controllers
claude Jul 8, 2026
ec0fd86
Performance: safe high-impact defaults + hot-path fixes
claude Jul 8, 2026
a394f5d
Performance: sane DXVK compiler-thread + device-memory defaults
claude Jul 8, 2026
ff6f577
Add "Download all components" to emulation settings
claude Jul 8, 2026
9f0d930
Add animated wallpaper/video background to the library (Layout panel)
claude Jul 8, 2026
08147d5
Translate this session's new UI strings to Portuguese (pt-rBR)
claude Jul 8, 2026
fc2e250
Fix regressions/bugs found by the review agents
claude Jul 8, 2026
b072b8e
Fix critical/high bugs found by the 30-agent audit
claude Jul 8, 2026
d39b433
Fix build: avoid smart-cast on captured assemblyFailure (EpicDownload…
claude Jul 8, 2026
311cff8
CI: auto-clean Actions storage (keep last 4 runs' artifacts)
claude Jul 9, 2026
89b4074
Add Wine↔Box64 compatibility matrix with auto-fix + __libc_init pre-l…
claude Jul 9, 2026
bf13205
Real-time library watcher + custom-game cover manager
claude Jul 9, 2026
277c4f1
Add XoDos analysis + engineering delivery report (pt-BR docs)
claude Jul 9, 2026
264939b
Port Winlator-Ludashi improvements: max refresh rate, Turnip env vars…
claude Jul 9, 2026
28d44f9
Port more Ludashi improvements verified from source: session wakelock…
claude Jul 9, 2026
e5e8a9d
Fix custom-game cover feature: placement, live refresh, cache busting…
claude Jul 9, 2026
5e75f2c
Fix audit bugs: service leaks, import-flag leaks, hub dup-key crash, …
claude Jul 10, 2026
02d7740
Harden LAN: guard straggler roster refresh + close orphan discovery s…
claude Jul 10, 2026
0922b56
Fix box64/version resolution broken by ' (Default)' suffix (NumberFor…
claude Jul 10, 2026
952610b
Fix in-game rotation to follow the device + expand Box64 perf env vars
claude Jul 10, 2026
bea6f23
Add a real bounded session-log system + guest error diagnostics
claude Jul 10, 2026
62797e4
Classify background SIGKILL (137) separately from game crashes
claude Jul 10, 2026
65d477f
Don't export empty MESA_VK_WSI_PRESENT_MODE / WRAPPER_RESOURCE_TYPE (…
claude Jul 10, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/app-release-signed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ on:

jobs:
build:
# Needs the upstream repo's signing/API secrets; forks build via build-apk.yml.
if: github.repository == 'utkarshdalal/GameNative'

runs-on: ubuntu-latest

Expand Down
150 changes: 150 additions & 0 deletions .github/workflows/build-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
name: Build Debug APK

on:
push:
branches: [ main, master, 'claude/**' ]
paths-ignore:
- '**.md'
- '.gitignore'
- 'keyvalues/**'
- 'media/**'
- '.github/ISSUE_TEMPLATE/**'
workflow_dispatch:

# Serialize builds on the same branch so overlapping runs don't fight over the rolling Release
# tag. cancel-in-progress:false means rapid pushes QUEUE (each commit still gets a build + release)
# instead of cancelling each other — important during active development.
concurrency:
group: build-apk-${{ github.ref }}
cancel-in-progress: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: This configuration does not preserve a full per-commit build queue on active branches; intermediate pending runs can be dropped under bursty pushes. If every commit must build, the concurrency strategy should be changed to one that does not rely on default pending-slot behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/build-apk.yml, line 19:

<comment>This configuration does not preserve a full per-commit build queue on active branches; intermediate pending runs can be dropped under bursty pushes. If every commit must build, the concurrency strategy should be changed to one that does not rely on default pending-slot behavior.</comment>

<file context>
@@ -0,0 +1,150 @@
+# instead of cancelling each other — important during active development.
+concurrency:
+  group: build-apk-${{ github.ref }}
+  cancel-in-progress: false
+
+jobs:
</file context>


jobs:
# Runs immediately (in parallel with build): frees Actions storage BEFORE the build job
# uploads its ~800 MB of artifacts — otherwise a 100%-full quota fails the upload itself.
# Keeps only the artifacts of the 4 most recent runs of this workflow.
cleanup:
runs-on: ubuntu-latest
permissions:
actions: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
steps:
- name: Delete artifacts older than the last 4 runs
run: |
set -uo pipefail
KEEP_RUNS=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml/runs?per_page=4" \
-q '.workflow_runs[].id' | tr '\n' ' ')
echo "Keeping artifacts of runs: $KEEP_RUNS"
gh api --paginate "repos/$GH_REPO/actions/artifacts?per_page=100" \
-q '.artifacts[] | "\(.id) \(.workflow_run.id) \(.size_in_bytes)"' |
while read -r ART_ID RUN_ID SIZE; do
KEEP=false
for K in $KEEP_RUNS; do
if [ "$RUN_ID" = "$K" ]; then KEEP=true; break; fi
done
if [ "$KEEP" = "false" ]; then
echo "Deleting artifact $ART_ID (run $RUN_ID, $SIZE bytes)"
gh api -X DELETE "repos/$GH_REPO/actions/artifacts/$ART_ID" || true
fi
done
echo "Cleanup done."
Comment on lines +25 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Cleanup job deletes artifacts from ALL workflows, not just build-apk.yml

Two issues in the cleanup job:

  1. Cross-workflow deletion (critical): Line 39 lists ALL repository artifacts via repos/$GH_REPO/actions/artifacts, but KEEP_RUNS (line 36) only contains run IDs from the build-apk.yml workflow. Artifacts from other workflows (e.g., app-release-signed.yml uploads signed APKs) have different run IDs, so KEEP stays false and they get deleted.

  2. Empty KEEP_RUNS on API failure (major): With set -uo pipefail but no -e (line 35), a gh api failure (rate limit, network) leaves KEEP_RUNS empty. The for K in $KEEP_RUNS loop never executes, so every artifact gets KEEP=false and is deleted — wiping the entire repository's artifact storage.

🔧 Proposed fix: filter by workflow and guard against empty keep list
 jobs:
   cleanup:
     runs-on: ubuntu-latest
     permissions:
       actions: write
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
       GH_REPO: ${{ github.repository }}
     steps:
       - name: Delete artifacts older than the last 4 runs
         run: |
           set -uo pipefail
+
+          # Get the workflow ID so we only touch build-apk artifacts, not other workflows'.
+          WF_ID=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml" -q '.id')
+          if [ -z "$WF_ID" ]; then
+            echo "Could not resolve workflow ID, skipping cleanup"
+            exit 0
+          fi
+
           KEEP_RUNS=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml/runs?per_page=4" \
             -q '.workflow_runs[].id' | tr '\n' ' ')
+          if [ -z "$KEEP_RUNS" ]; then
+            echo "Failed to fetch keep runs, skipping cleanup to avoid deleting everything"
+            exit 0
+          fi
           echo "Keeping artifacts of runs: $KEEP_RUNS"
-          gh api --paginate "repos/$GH_REPO/actions/artifacts?per_page=100" \
-            -q '.artifacts[] | "\(.id) \(.workflow_run.id) \(.size_in_bytes)"' |
+          gh api --paginate "repos/$GH_REPO/actions/artifacts?per_page=100" \
+            -q ".artifacts[] | select(.workflow_run.workflow_id == $WF_ID) | \"\(.id) \(.workflow_run.id) \(.size_in_bytes)\"" |
           while read -r ART_ID RUN_ID SIZE; do
             KEEP=false
             for K in $KEEP_RUNS; do
               if [ "$RUN_ID" = "$K" ]; then KEEP=true; break; fi
             done
             if [ "$KEEP" = "false" ]; then
               echo "Deleting artifact $ART_ID (run $RUN_ID, $SIZE bytes)"
               gh api -X DELETE "repos/$GH_REPO/actions/artifacts/$ART_ID" || true
             fi
           done
           echo "Cleanup done."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cleanup:
runs-on: ubuntu-latest
permissions:
actions: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
steps:
- name: Delete artifacts older than the last 4 runs
run: |
set -uo pipefail
KEEP_RUNS=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml/runs?per_page=4" \
-q '.workflow_runs[].id' | tr '\n' ' ')
echo "Keeping artifacts of runs: $KEEP_RUNS"
gh api --paginate "repos/$GH_REPO/actions/artifacts?per_page=100" \
-q '.artifacts[] | "\(.id) \(.workflow_run.id) \(.size_in_bytes)"' |
while read -r ART_ID RUN_ID SIZE; do
KEEP=false
for K in $KEEP_RUNS; do
if [ "$RUN_ID" = "$K" ]; then KEEP=true; break; fi
done
if [ "$KEEP" = "false" ]; then
echo "Deleting artifact $ART_ID (run $RUN_ID, $SIZE bytes)"
gh api -X DELETE "repos/$GH_REPO/actions/artifacts/$ART_ID" || true
fi
done
echo "Cleanup done."
cleanup:
runs-on: ubuntu-latest
permissions:
actions: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
steps:
- name: Delete artifacts older than the last 4 runs
run: |
set -uo pipefail
# Get the workflow ID so we only touch build-apk artifacts, not other workflows'.
WF_ID=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml" -q '.id')
if [ -z "$WF_ID" ]; then
echo "Could not resolve workflow ID, skipping cleanup"
exit 0
fi
KEEP_RUNS=$(gh api "repos/$GH_REPO/actions/workflows/build-apk.yml/runs?per_page=4" \
-q '.workflow_runs[].id' | tr '\n' ' ')
if [ -z "$KEEP_RUNS" ]; then
echo "Failed to fetch keep runs, skipping cleanup to avoid deleting everything"
exit 0
fi
echo "Keeping artifacts of runs: $KEEP_RUNS"
gh api --paginate "repos/$GH_REPO/actions/artifacts?per_page=100" \
-q ".artifacts[] | select(.workflow_run.workflow_id == $WF_ID) | \"\(.id) \(.workflow_run.id) \(.size_in_bytes)\"" |
while read -r ART_ID RUN_ID SIZE; do
KEEP=false
for K in $KEEP_RUNS; do
if [ "$RUN_ID" = "$K" ]; then KEEP=true; break; fi
done
if [ "$KEEP" = "false" ]; then
echo "Deleting artifact $ART_ID (run $RUN_ID, $SIZE bytes)"
gh api -X DELETE "repos/$GH_REPO/actions/artifacts/$ART_ID" || true
fi
done
echo "Cleanup done."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-apk.yml around lines 25 - 51, Update the cleanup
job’s artifact query to process only artifacts belonging to build-apk.yml runs,
using the workflow-specific runs endpoint or filtering each artifact’s workflow
identity before deletion. Make KEEP_RUNS retrieval fail safely: enable fail-fast
behavior and explicitly abort cleanup when the API call fails or returns an
empty list, before iterating over artifacts. Preserve deletion only for
artifacts from non-retained build-apk.yml runs, leaving artifacts from all other
workflows untouched.


build:
runs-on: ubuntu-latest

steps:
- name: Checking out GameNative
uses: actions/checkout@v4

- name: Setup Java 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Inject dummy credentials
run: |
cat <<EOF > local.properties
POSTHOG_API_KEY=dummy
POSTHOG_HOST=https://us.i.posthog.com
EOF

- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@v4

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Build debug APKs (legacy + modern)
run: ./gradlew :app:assembleLegacyDebug :app:assembleModernDebug

# retention-days: 1 — artifacts only exist to hand the APKs to the release job;
# the durable download is the Release (which does NOT count against Actions storage).
- name: Upload legacy debug APK
uses: actions/upload-artifact@v4
with:
name: gamenative-legacy-debug-apk
path: app/build/outputs/apk/legacy/debug/*.apk
retention-days: 1

- name: Upload modern debug APK
uses: actions/upload-artifact@v4
with:
name: gamenative-modern-debug-apk
path: app/build/outputs/apk/modern/debug/*.apk
retention-days: 1

# Publishing the ~800 MB of APKs to a Release is slow, so it runs in its own job AFTER build.
# This keeps the build job (and therefore the downloadable Actions artifacts) available quickly,
# while still offering the easier-to-download Release. Scoped to claude/** branches and manual
# runs so it never touches main's releases.
release:
needs: build
if: startsWith(github.ref_name, 'claude/') || github.event_name == 'workflow_dispatch'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Manual runs on non-claude/* branches still enter the release job, which can create/update debug releases for mainline branches. Tightening the condition to branch scope keeps behavior aligned with the stated release isolation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/build-apk.yml, line 104:

<comment>Manual runs on non-`claude/*` branches still enter the release job, which can create/update debug releases for mainline branches. Tightening the condition to branch scope keeps behavior aligned with the stated release isolation.</comment>

<file context>
@@ -0,0 +1,150 @@
+  # runs so it never touches main's releases.
+  release:
+    needs: build
+    if: startsWith(github.ref_name, 'claude/') || github.event_name == 'workflow_dispatch'
+    runs-on: ubuntu-latest
+    permissions:
</file context>

runs-on: ubuntu-latest
permissions:
contents: write # create/update the Release
actions: read # download-artifact reads this run's artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}

steps:
- name: Download built APKs
uses: actions/download-artifact@v4
with:
path: apks

- name: Publish debug APKs as a rolling prerelease
run: |
set -euo pipefail
TAG="debug-$(printf '%s' "$GITHUB_REF_NAME" | tr '/' '-')"
NOTES="Automated debug APKs for branch \`$GITHUB_REF_NAME\` at commit \`${GITHUB_SHA:0:8}\`.

- **gamenative-modern-debug** — most modern phones.
- **gamenative-legacy-debug** — legacy / glibc path (larger).

Debug-signed; install may require enabling \"install from unknown sources\"."

# IMPORTANT: never delete the release/tag. Deleting them leaves a multi-minute window
# (while the ~800 MB of APKs re-upload) where the download URLs 404 — that was the
# "the link keeps disappearing" problem. Instead keep the rolling release alive and
# overwrite the assets in place, so the download URLs stay valid across every build.
if gh release view "$TAG" >/dev/null 2>&1; then
gh release edit "$TAG" \
--prerelease \
--title "Debug build — $GITHUB_REF_NAME" \
--notes "$NOTES"
else
gh release create "$TAG" \
--target "$GITHUB_SHA" \
--prerelease \
--title "Debug build — $GITHUB_REF_NAME" \
--notes "$NOTES"
fi

# --clobber replaces each asset in place (stable names → stable, always-live URLs).
# Upload the smaller modern APK first so the most-used download refreshes soonest.
gh release upload "$TAG" apks/gamenative-modern-debug-apk/*.apk --clobber
gh release upload "$TAG" apks/gamenative-legacy-debug-apk/*.apk --clobber
4 changes: 2 additions & 2 deletions .github/workflows/pluvia-pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
if: github.event.pull_request.head.repo.full_name == github.repository
run: |
cat <<EOF > local.properties
POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}
POSTHOG_HOST=${{ secrets.POSTHOG_HOST }}
POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY || 'dummy' }}
POSTHOG_HOST=${{ secrets.POSTHOG_HOST || 'https://us.i.posthog.com' }}
EOF
- name: Inject dummy credentials
if: github.event.pull_request.head.repo.full_name != github.repository
Expand Down
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ android {
testOptions {
unitTests {
isIncludeAndroidResources = true
// Return defaults for unmocked android.jar calls (e.g. android.util.Log) instead
// of throwing, so plain-JVM unit tests that touch logging don't fail spuriously.
isReturnDefaultValues = true
}
}

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.INTERNET" />
Expand Down
9 changes: 8 additions & 1 deletion app/src/main/assets/box64_env_vars.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
{"name" : "BOX64_DYNAREC_FORWARD", "values" : ["0", "128", "256", "512", "1024"], "defaultValue" : "128"},
{"name" : "BOX64_DYNAREC_CALLRET", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"},
{"name" : "BOX64_DYNAREC_WAIT", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"},
{"name" : "BOX64_DYNAREC_WEAKBARRIER", "values" : ["0", "1", "2"], "defaultValue" : "0"},
{"name" : "BOX64_DYNAREC_PAUSE", "values" : ["0", "1", "2", "3"], "defaultValue" : "0"},
{"name" : "BOX64_DYNAREC_ALIGNED_ATOMICS", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "0"},
{"name" : "BOX64_DYNAREC_BLEEDING_EDGE", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "0"},
{"name" : "BOX64_AVX", "values" : ["0", "1", "2"], "defaultValue" : "1"},
{"name" : "BOX64_MAXCPU", "values" : ["4", "8", "16", "32", "64"], "defaultValue" : "8"},
{"name" : "BOX64_SSE42", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"},
{"name" : "BOX64_SHAEXT", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"},
{"name" : "BOX64_MMAP32", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"},
{"name" : "BOX64_MAXCPU", "values" : ["0", "4", "8", "16", "32", "64"], "defaultValue" : "8"},
{"name" : "BOX64_UNITYPLAYER", "values" : ["0", "1"], "toggleSwitch" : true, "defaultValue" : "1"}
]
32 changes: 24 additions & 8 deletions app/src/main/cpp/asurfacerenderer/drawable.c
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,34 @@ Java_com_winlator_xserver_Drawable_copyArea(JNIEnv *env, jclass obj, jshort srcX
}
}
} else {
/* Fast path when not using ASR - direct copy without conversion */
/* Fast path when not using ASR - direct copy without conversion.
Use memmove, not memcpy: an X11 CopyArea within a single drawable (e.g. scrolling a
window onto itself) makes srcData and dstData the same buffer with overlapping source
and destination regions, and memcpy on overlapping ranges is undefined behaviour. */
int sameBuffer = (srcDataAddr == dstDataAddr);
if (width == srcStride && width == dstStride) {
/* One contiguous block: memmove handles any overlap correctly. */
size_t bytes = (size_t)height * dstStride * 4;
memcpy(dstDataAddr + (dstX + dstY * dstStride) * 4,
srcDataAddr + (srcX + srcY * srcStride) * 4,
bytes);
memmove(dstDataAddr + (dstX + dstY * dstStride) * 4,
srcDataAddr + (srcX + srcY * srcStride) * 4,
bytes);
} else {
size_t rowBytes = (size_t)width * 4;
for (int16_t y = 0; y < height; y++) {
memcpy(dstDataAddr + (dstX + (y + dstY) * dstStride) * 4,
srcDataAddr + (srcX + (y + srcY) * srcStride) * 4,
rowBytes);
/* memmove protects overlap within a row; across rows we must also pick a safe
direction. When copying downward in the same buffer (dstY > srcY), iterate
bottom-to-top so a source row isn't overwritten before it is read. */
if (sameBuffer && dstY > srcY) {
for (int16_t y = height - 1; y >= 0; y--) {
memmove(dstDataAddr + (dstX + (y + dstY) * dstStride) * 4,
srcDataAddr + (srcX + (y + srcY) * srcStride) * 4,
rowBytes);
}
} else {
for (int16_t y = 0; y < height; y++) {
memmove(dstDataAddr + (dstX + (y + dstY) * dstStride) * 4,
srcDataAddr + (srcX + (y + srcY) * srcStride) * 4,
rowBytes);
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/cpp/patchelf/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ add_library(patchelf SHARED
src/patchelf.cc)

target_link_libraries(patchelf)

# Align ELF LOAD segments to 16 KB for Android 15+ devices with 16 KB page size.
target_link_options(patchelf PRIVATE -Wl,-z,max-page-size=16384)
5 changes: 4 additions & 1 deletion app/src/main/cpp/proot/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,7 @@ target_link_libraries(libproot.so
talloc)

add_library(proot-loader SHARED
src/loader/loader.c)
src/loader/loader.c)
# Align ELF LOAD segments to 16 KB for Android 15+ devices with 16 KB page size.
target_link_options(libproot.so PRIVATE -Wl,-z,max-page-size=16384)
target_link_options(proot-loader PRIVATE -Wl,-z,max-page-size=16384)
4 changes: 3 additions & 1 deletion app/src/main/cpp/virglrenderer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ target_link_libraries(virglrenderer
android
EGL
GLESv2
GLESv3)
GLESv3)
# Align ELF LOAD segments to 16 KB for Android 15+ devices with 16 KB page size.
target_link_options(virglrenderer PRIVATE -Wl,-z,max-page-size=16384)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Missing -Wl,-z,common-page-size=16384 alongside max-page-size. The official Android docs for 16 KB page size support recommend both flags for NDK r27 and lower. Without common-page-size, the ELF program header p_align field may not be set to 16 KB, which can cause the loader to fail on 16 KB page size devices. Several targets in this repo (asurfacerenderer, evshim, steambootstrap) already set both flags.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/cpp/virglrenderer/CMakeLists.txt, line 57:

<comment>Missing -Wl,-z,common-page-size=16384 alongside max-page-size. The official Android docs for 16 KB page size support recommend both flags for NDK r27 and lower. Without common-page-size, the ELF program header p_align field may not be set to 16 KB, which can cause the loader to fail on 16 KB page size devices. Several targets in this repo (asurfacerenderer, evshim, steambootstrap) already set both flags.</comment>

<file context>
@@ -52,4 +52,6 @@ target_link_libraries(virglrenderer
\ No newline at end of file
+                      GLESv3)
+# Align ELF LOAD segments to 16 KB for Android 15+ devices with 16 KB page size.
+target_link_options(virglrenderer PRIVATE -Wl,-z,max-page-size=16384)
</file context>
Suggested change
target_link_options(virglrenderer PRIVATE -Wl,-z,max-page-size=16384)
target_link_options(virglrenderer PRIVATE -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384)

6 changes: 4 additions & 2 deletions app/src/main/cpp/winlator/xconnector_epoll.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ typedef struct {

static FdTracker fd_tracking[MAX_TRACKED_FDS] = {0};

struct epoll_event events[MAX_EVENTS];

static int waitForEpollEvents(jint epollFd, struct epoll_event *epollEvents, int maxEvents) {
while (true) {
int numFds = epoll_wait(epollFd, epollEvents, maxEvents, -1);
Expand Down Expand Up @@ -160,6 +158,10 @@ Java_com_winlator_xconnector_XConnectorEpoll_doEpollIndefinitely(JNIEnv *env, jo
jmethodID handleExistingConnection =
(*env)->GetMethodID(env, cls, "handleExistingConnection", "(I)V");

// Stack-local, not file-scope: multiple connector threads (X server, VirGL, Vortek, ALSA, SysV
// SHM) each run this function concurrently, so a shared global buffer was a data race that could
// corrupt event dispatch across connectors.
struct epoll_event events[MAX_EVENTS];
int numFds = waitForEpollEvents(epollFd, events, MAX_EVENTS);
if (numFds < 0) {
return JNI_FALSE;
Expand Down
32 changes: 32 additions & 0 deletions app/src/main/java/app/gamenative/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -629,13 +629,45 @@ class MainActivity : ComponentActivity() {
}
}

/**
* Maps an allowed-orientation set to the Android sensor constant that lets the OS rotate
* the game freely within it — so a phone held in either landscape (or either portrait)
* auto-rotates to match. Returns null for mixed/single sets that need the manual pick below.
*/
private fun sensorOrientationFor(conformTo: EnumSet<Orientation>): Int? {
val landscapes = EnumSet.of(Orientation.LANDSCAPE, Orientation.REVERSE_LANDSCAPE)
val portraits = EnumSet.of(Orientation.PORTRAIT, Orientation.REVERSE_PORTRAIT)
return when {
conformTo.contains(Orientation.UNSPECIFIED) -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
conformTo == landscapes || conformTo.containsAll(landscapes) &&
!conformTo.contains(Orientation.PORTRAIT) && !conformTo.contains(Orientation.REVERSE_PORTRAIT) ->
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
conformTo == portraits || conformTo.containsAll(portraits) &&
!conformTo.contains(Orientation.LANDSCAPE) && !conformTo.contains(Orientation.REVERSE_LANDSCAPE) ->
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
conformTo.size >= 3 -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Orientation constraints can be broadened incorrectly for mixed 3-orientation sets, so the app may rotate into a disallowed orientation. This comes from mapping any conformTo.size >= 3 to SCREEN_ORIENTATION_FULL_SENSOR before the manual allowlist-based selection runs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/MainActivity.kt, line 648:

<comment>Orientation constraints can be broadened incorrectly for mixed 3-orientation sets, so the app may rotate into a disallowed orientation. This comes from mapping any `conformTo.size >= 3` to `SCREEN_ORIENTATION_FULL_SENSOR` before the manual allowlist-based selection runs.</comment>

<file context>
@@ -629,13 +629,45 @@ class MainActivity : ComponentActivity() {
+            conformTo == portraits || conformTo.containsAll(portraits) &&
+                !conformTo.contains(Orientation.LANDSCAPE) && !conformTo.contains(Orientation.REVERSE_LANDSCAPE) ->
+                ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
+            conformTo.size >= 3 -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
+            else -> null
+        }
</file context>
Suggested change
conformTo.size >= 3 -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
conformTo.containsAll(landscapes) && conformTo.containsAll(portraits) ->
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR

else -> null
Comment on lines +648 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not widen three-orientation sets to FULL_SENSOR.

A set such as landscape + reverse-landscape + portrait excludes reverse-portrait, but FULL_SENSOR allows it. Only use FULL_SENSOR when all four physical orientations are allowed; leave partial mixed sets on the manual path.

Proposed fix
-            conformTo.size >= 3 -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
+            conformTo.containsAll(
+                EnumSet.of(
+                    Orientation.LANDSCAPE,
+                    Orientation.REVERSE_LANDSCAPE,
+                    Orientation.PORTRAIT,
+                    Orientation.REVERSE_PORTRAIT,
+                ),
+            ) -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
conformTo.size >= 3 -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
else -> null
conformTo.containsAll(
EnumSet.of(
Orientation.LANDSCAPE,
Orientation.REVERSE_LANDSCAPE,
Orientation.PORTRAIT,
Orientation.REVERSE_PORTRAIT,
),
) -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
else -> null
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/MainActivity.kt` around lines 648 - 649,
Update the orientation-selection logic in the relevant method so
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR is returned only when conformTo
contains all four physical orientations; change the conformTo.size >= 3 branch
to preserve partial mixed sets for the manual handling path.

}
}

private fun setOrientationTo(orientation: Int, conformTo: EnumSet<Orientation>) {
if (isHeadset(this)) {
if (requestedOrientation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
return
}

// Let Android's own sensor drive rotation whenever the allowed set is a natural
// sensor group. The custom OrientationEventListener (startOrientator) is disabled
// because it leaked/restarted the Activity, which left currentOrientationChangeValue
// stuck at 0 — so the manual angle math below locked the game to ONE fixed landscape.
// SENSOR_LANDSCAPE flips freely between the two landscapes as the phone turns (and the
// same for portrait / full sensor), which is exactly the "follow the device" behaviour.
sensorOrientationFor(conformTo)?.let { sensor ->
if (requestedOrientation != sensor) requestedOrientation = sensor
return
}
// Log.d("MainActivity$index", "Setting orientation to conform")

// reverse direction of orientation
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/app/gamenative/PluviaApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ class PluviaApp : SplitCompatApplication() {
Timber.plant(ReleaseTree())
}

// Bounded on-device session log (survives kills; capped at ~4 MB, rotating). In debug we
// persist everything; in release only WARN+ to keep the hot path cheap.
app.gamenative.utils.SessionLogger.init(this)
Timber.plant(
app.gamenative.utils.SessionLogger.Tree(
persistFromPriority = if (BuildConfig.DEBUG) android.util.Log.DEBUG else android.util.Log.WARN,
),
)

NetworkMonitor.init(this)

// Init our custom crash handler.
Expand Down
Loading