diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..04bba7c13
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,57 @@
+---
+name: Bug report
+about: Report a bug or crash
+title: "[BUG] "
+labels: bug
+assignees: ''
+---
+
+## Describe the bug
+
+
+
+## To reproduce
+
+1.
+2.
+3.
+
+## Expected behavior
+
+
+
+## Actual behavior
+
+
+
+## Device info
+
+- **Device model:**
+- **Android version:**
+- **GPU** (run `adb logcat -s GPUInformation` if unsure):
+- **RAM:**
+- **WinNative version** (Settings → About):
+- **Game:**
+- **Game source:** Steam / Epic / GOG / manual
+- **Custom Wine/DXVK/Box64 version?** (if yes, which):
+
+## Logs
+
+```
+Paste logcat output here. Filter by:
+adb logcat -s AuroraDeviceTier AuroraTierPresets AuroraNTSync AuroraMemBudget AuroraSanitizer AuroraMaliSanitizer WinNative
+
+If the bug is a crash, also include:
+adb logcat -s AndroidRuntime DEBUG
+```
+
+## Aurora tier detected
+
+
+
+- Tier: HIGH / MID / LOW
+- Mali sanitizer active: yes / no / fallback
+
+## Additional context
+
+
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..9cffac752
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,35 @@
+---
+name: Feature request
+about: Suggest a new feature or improvement
+title: "[FEAT] "
+labels: enhancement
+assignees: ''
+---
+
+## Problem
+
+
+
+## Proposed solution
+
+
+
+## Alternatives considered
+
+
+
+## Tier impact
+
+
+
+## Verification
+
+
+
+## Will you work on this?
+
+
+
+## Additional context
+
+
diff --git a/.github/ISSUE_TEMPLATE/performance_issue.md b/.github/ISSUE_TEMPLATE/performance_issue.md
new file mode 100644
index 000000000..3c2c988c5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/performance_issue.md
@@ -0,0 +1,51 @@
+---
+name: Performance issue
+about: Report poor performance on a specific device/game
+title: "[PERF] "
+labels: performance
+assignees: ''
+---
+
+## Device
+
+- **Device model:**
+- **SoC:**
+- **Android version:**
+- **GPU:**
+- **RAM:**
+- **Tier detected** (check logcat `AuroraDeviceTier`):
+
+## Game
+
+- **Title:**
+- **Source:** Steam / Epic / GOG / manual
+- **In-game settings:** (resolution, quality preset, etc.)
+
+## Performance
+
+- **Average FPS:**
+- **1% low FPS:**
+- **Frame time stability** (smooth / stuttery / micro-stutters):
+- **Comparison to expectation** (e.g. "expected 30 FPS, getting 15"):
+
+## Current env vars
+
+
+
+```
+Paste env vars here
+```
+
+## What I've tried
+
+
+
+## Logs
+
+```
+adb logcat -s AuroraDeviceTier AuroraTierPresets AuroraNTSync AuroraMemBudget AuroraSanitizer PerformanceRecorder
+```
+
+## Benchmark data (optional)
+
+
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..111ae5911
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,48 @@
+## Summary
+
+
+
+## Type of change
+
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] Documentation update
+- [ ] Tier preset calibration (requires benchmark data — see below)
+
+## Changes
+
+
+
+-
+-
+-
+
+## Tier preset changes (if applicable)
+
+
+
+| Device | Game | Setting changed | FPS before | FPS after |
+|---|---|---|---|---|
+| | | | | |
+
+## Verification
+
+- [ ] `./gradlew spotlessApply` passes locally
+- [ ] `./gradlew assembleStandardDebug` passes locally
+- [ ] CI passes (will run automatically on push)
+- [ ] Tested on real device (specify which):
+- [ ] Logcat output attached (if bug/perf fix):
+
+## Checklist
+
+- [ ] Code follows the project style (see CONTRIBUTING.md)
+- [ ] No new TODOs left without explicit naming in the summary
+- [ ] No stubs presented as finished features (every function has a real producer and consumer)
+- [ ] JNI boundaries null-check and validate lengths
+- [ ] Commit messages follow Conventional Commits
+- [ ] Documentation updated (README.md, docs/, CHANGELOG.md as appropriate)
+
+## Notes for reviewer
+
+
diff --git a/.gitignore b/.gitignore
index 7e71a9864..584ef2c23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,3 +53,14 @@ signing.properties
References/
*.hprof
android_sysvshm/build64/
+
+# Secrets — never commit tokens, passwords, or keys
+.env
+.env.*
+*.pem
+*.key
+*token*
+ghp_*
+github_token*
+askpass*
+.git-askpass*
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..cfbc34e9c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,55 @@
+# Changelog
+
+All notable changes to this WinNative fork are documented here.
+Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+- Forked from WinNative upstream at commit `eaa4640` (2026-06-23).
+- Ported Aurora Mali Vulkan Sanitizer (5 rules) from Aurora `bbe084d`, with two
+ additional correctness fixes on top:
+ - **B4**: dynamic offsets now sliced correctly per chunk in
+ `vkCmdBindDescriptorSets`. New tracking hooks: `vkCreateDescriptorSetLayout`,
+ `vkAllocateDescriptorSets`, `vkFreeDescriptorSets`. Original Aurora code
+ dumped all offsets on chunk 1 and zero on chunks 2+, causing silent wrong
+ rendering for draws using sets 4+ with dynamic UBO bindings.
+ - **B6**: device resolved per command buffer via
+ `VkCommandBuffer → VkDevice` map. New tracking hooks:
+ `vkAllocateCommandBuffers`, `vkFreeCommandBuffers`. Original Aurora
+ hard-coded `g_device_dispatch[0]`, breaking multi-device scenarios.
+ - Inherits Aurora's audit fixes: B1 (layer name match), B2 (env var ordering),
+ B3 (glibc compile via `#ifdef __BIONIC__`), B5 (thread-safe dispatch via
+ `std::shared_mutex`), B9 (Immortalis detection).
+- Added `DeviceTier` — classifies device as HIGH/MID/LOW by SoC + RAM + GPU.
+- Added `TierPresets` — per-tier Box64/DXVK/Wine env-var defaults, applied via
+ `putIfAbsent` so user overrides win.
+- Added `NTSyncHelper` — auto-detects `/dev/ntsync` (Linux 6.14+) and switches
+ from esync to NTSync for ~10-20% sync overhead reduction.
+- Added `MemoryBudgetHelper` — caps DXVK pipeline cache by RAM, disables
+ PulseAudio on <4 GB devices.
+- Added `EnvVars.putIfAbsent()` for tier presets.
+- Added CMake target for `libaurora_mali_sanitizer.so`.
+- Added `ImageFsInstaller.deployAuroraMaliSanitizer()` — deploys .so + JSON
+ manifest to `imagefs/usr/lib/` at install time.
+- Wired Aurora helpers into `GuestProgramLauncherComponent.execGuestProgram()`
+ after `envVars.putAll(this.envVars)` (B2-correct ordering).
+
+### Dropped (vs original Aurora)
+- Phase 1 AOT texture engine (no PC game ships `.ktx2`).
+- Phase 2 mesh LOD engine (no PC game ships `.obj`).
+- Phase 3 Markov prefetcher (no `libaurora_prefetch.so` exists).
+- Phase 4 cloud shader sync (no backend).
+
+### Documentation
+- Added `docs/AUDIT.md` — deep audit of original Aurora (16 bugs found).
+- Added `docs/TIER_TUNING.md` — rationale for each tier preset.
+- Added `docs/MALI_SANITIZER.md` — how the Vulkan layer works.
+- Added `CONTRIBUTING.md`.
+- Added issue/PR templates.
+- Rewrote `README.md` for this fork.
+
+## [0.1.0] — 2026-06-24
+
+Initial fork release. Based on WinNative `eaa4640` + Aurora `bbe084d`.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..b86380de1
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,28 @@
+# Code of Conduct
+
+## Our pledge
+
+We are committed to making participation in this project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Standards
+
+**Positive behavior:**
+- Using welcoming and inclusive language
+- Being respectful of differing viewpoints and experiences
+- Gracefully accepting constructive criticism
+- Focusing on what is best for the community
+- Showing empathy towards other community members
+
+**Unacceptable behavior:**
+- Trolling, insulting/derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information without explicit permission
+- Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the maintainer via GitHub. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
+
+## Attribution
+
+Adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..8e7cab6f7
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,103 @@
+# Contributing to WinNative × Aurora
+
+Thanks for your interest in contributing! This is a community fork of [WinNative](https://github.com/WinNative-Emu/WinNative) that adds Aurora's Mali GPU support and device-tier performance tuning.
+
+## Quick start
+
+```bash
+git clone --recurse-submodules https://github.com/boiniArun2006/WinNative.git
+cd WinNative
+rustup target add aarch64-linux-android
+./gradlew assembleStandardDebug
+```
+
+PRs require CI to pass before merge. The CI matrix builds three flavors (Standard, Ludashi, PUBG) and runs on `ubuntu-latest` with JDK 17 + NDK 27.3.
+
+## What to contribute
+
+**Welcome:**
+- Bug fixes (especially Mali driver edge cases, Box64/DXVK env var tuning)
+- New game fixes in `runtime/compat/gamefixes/`
+- Device-tier calibration (real-world benchmark data from mid-end hardware)
+- Translations (see `app/src/main/res/values-*/`)
+- Documentation improvements
+
+**Needs maintainer discussion first:**
+- New Aurora engines (the audit dropped 4 of 6 as dead code — don't reintroduce without a real producer+consumer)
+- Changes to the Vulkan sanitizer's rule set
+- Bumping Box64/Wine/DXVK/Proton versions (these need testing across multiple games)
+- Architecture changes to the `runtime/aurora/` module
+
+**Will not accept:**
+- DRM circumvention
+- Bundled copyrighted game files
+- Telemetry that isn't opt-in
+- Reintroduction of the dropped Aurora phases (texture/mesh/prefetch/cloud-sync) without a working end-to-end implementation
+
+## Code style
+
+- Kotlin: follow existing style. Run `./gradlew spotlessApply` before committing.
+- Java: 4-space indent, no tabs. Match the style of the file you're editing.
+- C/C++: 2-space indent for new Aurora code (matches WinNative's `winlator/` style). Existing vendored code (meshoptimizer, basisu) keeps its own style.
+- JNI: null-check every `Get*ArrayElements`/`GetStringUTFChars` result. Free every allocation on every return path. Validate array lengths against claimed counts.
+
+## Commit messages
+
+Follow [Conventional Commits](https://www.conventionalcommits.org/):
+
+```
+feat(aurora): add B4 dynamic-offset split fix
+fix(sanitizer): handle null pDynamicOffsets in split path
+docs: update tier tuning rationale
+chore(deps): bump orgJson to 20240303 (CVE-2024-57699)
+```
+
+## Pull request process
+
+1. Fork the repo and create a branch from `main`:
+ ```bash
+ git checkout -b fix/my-bugfix
+ ```
+2. Make your changes. Keep commits focused — one logical change per commit.
+3. Run `./gradlew spotlessApply assembleStandardDebug` locally.
+4. Push and open a PR against `main`. Fill in the PR template.
+5. CI runs automatically. Address any failures.
+6. A maintainer will review. Be responsive to feedback.
+
+## Adding a new game fix
+
+Game fixes live in `runtime/compat/gamefixes/GameFixes.java`. Each fix is a `Fix` subclass (`RegistryKeyFix`, `EnvVarFix`, `GogDependencyFix`, etc.) keyed by the game's store-specific ID:
+
+```java
+steamFixes.put("123456",
+ new EnvVarFix(Collections.singletonMap("WINEDLLOVERRIDES", "d3d11=n;bypass=n")));
+```
+
+For complex fixes, add a helper class in `runtime/compat/gamefixes/helpers/`. Include a comment explaining what the fix does and why — these get referenced years later.
+
+## Adding a new device tier preset
+
+If you have benchmark data showing a tier preset should change:
+
+1. Edit `runtime/aurora/TierPresets.kt`.
+2. Add a comment citing the benchmark (device, game, FPS before/after).
+3. Update `docs/TIER_TUNING.md` with the rationale.
+4. Open a PR with the benchmark data in the description.
+
+## Reporting bugs
+
+Use the issue templates. Include:
+- Device model and Android version
+- GPU (run `adb logcat -s GPUInformation` if unsure)
+- WinNative version (from Settings → About)
+- The game and how you launched it (Steam/Epic/GOG/manual)
+- Logcat output (filter by `AuroraDeviceTier`, `AuroraTierPresets`, `AuroraSanitizer`, `WinNative`)
+- Whether you're using a custom Wine/DXVK/Box64 version
+
+## Security
+
+Found a security issue? Don't open a public issue. Email the maintainer directly.
+
+## License
+
+By contributing, you agree your contributions are licensed under GPL-3.0, same as the rest of the project.
diff --git a/README.md b/README.md
index 7f2622749..408c0bbeb 100644
--- a/README.md
+++ b/README.md
@@ -1,58 +1,204 @@
-
-
-
-
-
-
-
-
-
-## WinNative: A Community Built Windows Emulation App for Android
-
-**WinNative** is an advanced, high-performance Windows (x86_64) emulation environment for Android. It bridges the gap between desktop gaming and mobile by unifying the best technologies from **Winlator Bionic** and **Pluvia**.
-
-Designed for enthusiasts and power users, WinNative delivers the full Winlator experience while making it easy to connect your Steam, Epic, and GOG game libraries.
-
----
-
-### Installation
-
-1. **Download:** Get the latest APK from the [Releases](https://github.com/WinNative-Emu/WinNative/releases) section.
-2. **Variants:**
- - `Ludashi`: Best for Xiaomi/RedMagic (Performance Mode trigger).
- - `Vanilla`: Standard package name for side-loading with other forks.
-3. **Setup:** Launch the app, allow the ImageFS to install, and start adding your games manually or sync your library.
-
----
-
-### How to Build
-
-**Requirements:** Android Studio, JDK 17, NDK `27.3.13750724`, and CMake.
-
-1. **Clone the repository and update submodules** (Required):
- ```bash
- git clone https://github.com/MaxsTechReview/WinNative.git
- cd WinNative
- git submodule update --init --recursive
- ```
-2. **Build via Android Studio:** Open the `WinNative` directory, let Gradle sync, then select **Build > Build APK(s)**.
-3. **Build via CLI:** Run `.\gradlew.bat assembleDebug` (Windows).
-
----
-
-### Contributing
-
-We welcome community contributions! Feel free to open a pull request for bug fixes, driver updates, UI improvements, or anything else you'd like to add.
-
-Please match the existing code style and ensure any AI-assisted code is thoroughly reviewed and tested before submission.
-
----
-
-### Credits & Acknowledgments
-
-- **Original Winlator** by [brunodev85](https://github.com/brunodev85/winlator)
-- **Winlator Bionic** by [Pipetto-crypto](https://github.com/Pipetto-crypto/winlator)
-- **Pluvia** features by the [Pluvia](https://github.com/oxters168/Pluvia) / [GameNative](https://github.com/utkarshdalal/GameNative) community
-- **Mesa/Turnip** contributions by the [Mesa3D](https://www.mesa3d.org/) team
-- **Goldberg Steam Emulator** by [Mr. Goldberg](https://gitlab.com/Mr_Goldberg/goldberg_emulator), maintained by [Detanup01](https://github.com/Detanup01/gbe_fork)
+
+
+

+
+# WinNative × Aurora
+
+### PC Gaming on Android — tuned for mid-end devices, with Mali GPU support
+
+A fork of [WinNative](https://github.com/WinNative-Emu/WinNative) that integrates the working parts of [Aurora](https://github.com/boiniArun2006/Aurora-emulator) (Mali Vulkan sanitizer + auto-installer) and adds device-tier-aware performance tuning so mid-end hardware (Snapdragon 7xx, Mali Valhall, 6–8 GB RAM) runs PC games at playable framerates without manual tweaking.
+
+
+
+---
+
+## Why this fork exists
+
+WinNative is the best-engineered Winlator derivative today — modular code, Rust Steam client, NTSync, Proton 9.0, proper CI, real signing. But it has **no Mali GPU support**: the Mali Vulkan driver crashes on `>4` descriptor sets and on certain extensions that DXVK requests by default.
+
+Aurora pioneered a Vulkan sanitizer layer that fixes this — but Aurora ships on top of GameNative, an older/flatter codebase with no NTSync, no Rust Steam client, weaker CI, and no release signing.
+
+This fork takes the **best of both**:
+
+| From WinNative | From Aurora (with audit fixes) |
+|---|---|
+| Modular `feature/` + `runtime/` architecture | Mali Vulkan Sanitizer (5 rules + B4/B6 fixes) |
+| Rust `wn-steam-client` (22K LOC) | Auto-installer for VC++/DirectX/PhysX/.NET |
+| NTSync auto-detection | Per-game shader cache |
+| Proton 9.0 container patterns | Immortalis GPU detection |
+| 3-flavor CI matrix with signing | DXVK_CONFIG fallback when sanitizer .so absent |
+| GPL-3.0 clean license | |
+
+**Dropped from Aurora** (dead code per deep audit — see [docs/AUDIT.md](docs/AUDIT.md)):
+- Phase 1 AOT texture engine (no PC game ships `.ktx2`)
+- Phase 2 mesh LOD engine (no PC game ships `.obj`)
+- Phase 3 Markov prefetcher (no `libaurora_prefetch.so` exists)
+- Phase 4 cloud shader sync (no backend)
+
+## What's new: device-tier tuning
+
+The headline addition. Every game launch classifies the device as `HIGH`, `MID`, or `LOW` and applies tier-appropriate env-var presets via `putIfAbsent` (user overrides always win).
+
+| Tier | Hardware | What we change vs WinNative defaults |
+|---|---|---|
+| **HIGH** | Snapdragon 8 Gen 2+, Dimensity 9200+, Immortalis, 12 GB+ RAM | `dxvk.useDescriptorIndexing=True`, `d3d11.maxFeatureLevel=12_0` |
+| **MID** | Snapdragon 7xx, Dimensity 7/8, Mali G610/G77/G78, 6–8 GB RAM | BOX64 `STRONGMEM=2 BIGBLOCK=1 CALLRET=1 FASTNAN=1 FASTROUND=1 SAFEFLAGS=1`; DXVK `maxFeatureLevel=11_0 relaxedBarriers=True useDescriptorIndexing=False shaderUseSubgroupOps=False`; `MESA_SHADER_CACHE_MAX_SIZE=512MB`; `WINEESYNC=1` |
+| **LOW** | Snapdragon 4xx/6xx, Mali G52/G72, 4 GB RAM | All MID settings + `STRONGMEM=3`, `maxFeatureLevel=10_0`, `usePipelineLibrary=False`, 256 MB shader cache |
+
+Plus three independent helpers applied after tier presets:
+
+- **NTSyncHelper** — auto-detects `/dev/ntsync` (Linux 6.14+) and switches from esync to NTSync for ~10–20% sync overhead reduction.
+- **MemoryBudgetHelper** — caps DXVK pipeline cache by RAM (`<6 GB → 512 MB`, `<4 GB → 256 MB`, `<4 GB → 128 MB`) and disables PulseAudio on very low RAM.
+- **AuroraSanitizerHelper** — last in the chain; appends Mali DXVK workarounds to whatever `DXVK_CONFIG` the prior steps produced.
+
+Order at game launch (in `GuestProgramLauncherComponent.execGuestProgram`, after `envVars.putAll(this.envVars)`):
+
+```
+1. TierPresets.apply() // sets mid-end Box64/DXVK/Wine defaults
+2. NTSyncHelper.applyIfSupported() // overrides esync if /dev/ntsync exists
+3. MemoryBudgetHelper.apply() // caps DXVK cache, disables PulseAudio on low RAM
+4. AuroraSanitizerHelper.setupSanitizer() // Mali layer + DXVK_CONFIG fallback
+```
+
+All wrapped in try/catch — Aurora helpers never block game launch.
+
+## Mali Vulkan Sanitizer
+
+A Vulkan layer that sits between DXVK and the Mali driver. 5 rules:
+
+1. Blacklist `VK_EXT_descriptor_indexing` (Mali crash)
+2. Blacklist `VK_EXT_fragment_density_map` (Mali render corruption)
+3. Blacklist `VK_KHR_shader_subgroup` (Mali silent wrong results)
+4. Blacklist `VK_EXT_graphics_pipeline_library` (Mali driver bug)
+5. Split `vkCmdBindDescriptorSets` at `>4` sets (Mali hard crash)
+
+The split (rule 5) has two correctness fixes vs the original Aurora code:
+
+- **B4 — dynamic offsets sliced correctly per chunk.** Original Aurora dumped all N dynamic offsets on chunk 1 and zero on chunks 2+, causing silent wrong rendering for any draw using sets 4+ with dynamic UBO bindings. This fork adds hooks for `vkCreateDescriptorSetLayout` (caches per-layout dynamic count), `vkAllocateDescriptorSets`/`vkFreeDescriptorSets` (caches per-set count), and uses them in `vkCmdBindDescriptorSets` to slice `pDynamicOffsets` per chunk. Falls back to the old behavior if the cached total doesn't match the caller's count (fail-safe).
+- **B6 — device resolved per command buffer.** Original Aurora hard-coded `g_device_dispatch[0]`, which broke multi-device scenarios (VR, external GPU). This fork adds hooks for `vkAllocateCommandBuffers`/`vkFreeCommandBuffers` to maintain a `VkCommandBuffer → VkDevice` map.
+
+Inherits from latest Aurora: B1 (layer name match), B2 (env var ordering), B3 (glibc compile via `#ifdef __BIONIC__`), B5 (thread-safe dispatch tables via `std::shared_mutex`), B9 (Immortalis detection).
+
+## Build
+
+**Requirements:** Android Studio, JDK 17, NDK `27.3.13750724`, CMake 3.22+, Rust toolchain with `aarch64-linux-android` target.
+
+```bash
+git clone --recurse-submodules https://github.com/boiniArun2006/WinNative.git
+cd WinNative
+rustup target add aarch64-linux-android
+./gradlew assembleStandardDebug
+```
+
+APK at `app/build/outputs/apk/standard/debug/standard.apk`.
+
+Three flavors:
+- `standard` — `com.winnative.cmod` (vanilla side-loading)
+- `ludashi` — `com.ludashi.benchmark` (Xiaomi/RedMagic Performance Mode trigger)
+- `pubg` — `com.tencent.ig` (PUBG Mobile package name for anti-cheat compatibility)
+
+CI builds all three on every PR and on every tag push. See [`.github/workflows/`](.github/workflows/).
+
+## Install
+
+1. Download the latest APK from [Releases](https://github.com/boiniArun2006/WinNative/releases) or [Actions](https://github.com/boiniArun2006/WinNative/actions).
+2. Enable "Install from unknown sources" for your browser/file manager.
+3. Tap the APK to install.
+4. Launch WinNative and wait for the initial ImageFS extraction (~860 MB extracted).
+5. Add a game: pick a Windows `.exe` or sync your Steam/Epic/GOG library.
+
+## Verify Aurora is active
+
+After launching a game, check logcat:
+
+```bash
+adb logcat -s AuroraDeviceTier AuroraTierPresets AuroraNTSync AuroraMemBudget AuroraSanitizer AuroraMaliSanitizer
+```
+
+You should see lines like:
+```
+AuroraDeviceTier: Device classified as MID
+AuroraTierPresets: Applying MID tier presets for container 1
+AuroraNTSync: NTSync support: true (device=/dev/ntsync)
+AuroraNTSync: Enabling NTSync (kernel supports /dev/ntsync)
+AuroraMemBudget: Device RAM: 6GB
+AuroraSanitizer: Mali/Immortalis GPU detected: Mali-G610 — enabling sanitizer
+```
+
+On non-Mali GPUs, the sanitizer is skipped (returns `false`) and only tier presets + NTSync + memory budget apply.
+
+## Project layout
+
+```
+app/src/main/
+├── app/ ← Application class, DI, settings config
+├── feature/ ← UI features (one dir per feature)
+│ ├── leaderboard/ ← PerformanceRecorder + leaderboard upload
+│ ├── library/ ← Game library UI + GameSettings
+│ ├── settings/ ← Settings screens
+│ ├── stores/ ← Steam, GOG, Epic integrations
+│ └── sync/ ← Cloud save sync
+├── runtime/ ← Emulator runtime
+│ ├── aurora/ ← ★ NEW: Aurora modules (this fork)
+│ │ ├── AuroraSanitizerHelper.kt
+│ │ ├── DeviceTier.kt
+│ │ ├── TierPresets.kt
+│ │ ├── NTSyncHelper.kt
+│ │ └── MemoryBudgetHelper.kt
+│ ├── audio/ ← ALSA, MIDI, PulseAudio
+│ ├── compat/ ← Box64, FEXCore, gamefixes, SteamBridge
+│ ├── container/ ← Container, ContainerCreation, Shortcut
+│ ├── content/ ← ContentsManager, AdrenotoolsManager
+│ ├── display/ ← XServer, XEnvironment, ImageFs, renderer
+│ ├── input/ ← Controls, rumble, gestures
+│ ├── system/ ← GPUInformation, PerformanceRecorder
+│ └── wine/ ← WineUtils, EnvVars, WineInfo
+├── shared/ ← Shared utilities
+├── sharedmemory/ ← SysV shared memory
+├── cpp/ ← Native code (12 subprojects)
+│ ├── aurora_mali_sanitizer/ ← ★ NEW: Vulkan layer (this fork)
+│ ├── adrenotools/ ← Adreno driver hot-swap (submodule)
+│ ├── wn-steam-client/ ← Rust Steam protocol client
+│ ├── wn-libsteamclient/ ← C++ libsteamclient.so replacement
+│ └── ... ← 8 other native subprojects
+├── rust/ ← Rust utilities (wn-steam-tools)
+└── res/ ← Android resources (15 locales)
+```
+
+## Documentation
+
+- [docs/AUDIT.md](docs/AUDIT.md) — Deep audit of original Aurora (16 bugs found, 9 dropped as dead code)
+- [docs/TIER_TUNING.md](docs/TIER_TUNING.md) — Rationale for each tier preset
+- [docs/MALI_SANITIZER.md](docs/MALI_SANITIZER.md) — How the Vulkan layer works, including B4/B6 fixes
+- [CHANGELOG.md](CHANGELOG.md) — Release history
+- [CONTRIBUTING.md](CONTRIBUTING.md) — How to contribute
+
+## Credits
+
+This fork builds on the work of many people:
+
+- **WinNative** by [Xnick417x](https://github.com/Xnick417x) and contributors — the base
+- **Aurora** by [boiniArun2006](https://github.com/boiniArun2006) — the Mali sanitizer concept and audit fixes
+- **GameNative** by [utkarshdalal](https://github.com/utkarshdalal) — predecessor to WinNative
+- **Winlator** by [brunodev85](https://github.com/brunodev85) — original Winlator
+- **Winlator Bionic** by [Pipetto-crypto](https://github.com/Pipetto-crypto) — bionic variant
+- **Pluvia** community — Steam/Epic/GOG integration foundations
+- **Box64** by ptitSeb — x86/x64 → ARM64 translator
+- **Wine** / **DXVK** / **VKD3D** / **Proton** — the actual Windows compat layer
+- **mesa/Turnip** — Adreno Vulkan driver
+- **meshoptimizer** by Arseny Kapoulkine — used by original Aurora (not in this fork)
+- **Basis Universal** by Binomial LLC — used by original Aurora (not in this fork)
+
+## License
+
+GPL-3.0 — same as upstream WinNative. See [LICENSE](LICENSE).
+
+The Aurora modules added by this fork (`app/src/main/runtime/aurora/` and `app/src/main/cpp/aurora_mali_sanitizer/`) are also GPL-3.0, consistent with the project license.
+
+## Contributing
+
+See [CONTRIBUTING.md](CONTRIBUTING.md). PRs welcome — CI runs on every push and gates merges.
+
+## Disclaimer
+
+This project is for running legitimately-owned games on your own device. It does not include any game ROMs, copyrighted game files, or DRM circumvention. The Steam/Epic/GOG integrations require you to log in with your own account.
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
index 001fd003d..9fa43e111 100644
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -179,3 +179,26 @@ add_library(fakeinput SHARED
)
target_compile_options(fakeinput PRIVATE -Wall -Wextra -fvisibility=hidden)
target_link_libraries(fakeinput log dl)
+
+# ----------------------------------------------------------------------------
+# Aurora Mali Vulkan Sanitizer Layer
+# ----------------------------------------------------------------------------
+# A Vulkan layer that sits between DXVK and the Mali Vulkan driver.
+# Filters blacklisted extensions, splits >4 descriptor sets with correct
+# dynamic-offset slicing (B4 fix), and resolves devices per-command-buffer
+# (B6 fix). Compiled once with bionic; for production use inside PRoot/glibc,
+# the same source compiles with a glibc cross-compiler (logging is gated
+# behind #ifdef __BIONIC__).
+# ----------------------------------------------------------------------------
+add_library(aurora_mali_sanitizer SHARED
+ aurora_mali_sanitizer/aurora_mali_sanitizer.cpp)
+
+target_compile_options(aurora_mali_sanitizer PRIVATE
+ -Wall -Wextra -fvisibility=hidden -std=c++17)
+
+target_link_libraries(aurora_mali_sanitizer
+ log
+ vulkan
+ dl)
+
+target_link_options(aurora_mali_sanitizer PRIVATE -Wl,-z,max-page-size=16384)
diff --git a/app/src/main/cpp/aurora_mali_sanitizer/aurora_mali_sanitizer.cpp b/app/src/main/cpp/aurora_mali_sanitizer/aurora_mali_sanitizer.cpp
new file mode 100755
index 000000000..eb8632ece
--- /dev/null
+++ b/app/src/main/cpp/aurora_mali_sanitizer/aurora_mali_sanitizer.cpp
@@ -0,0 +1,914 @@
+/*
+ * Aurora Mali Vulkan Sanitizer Layer
+ * ==================================
+ *
+ * A Vulkan layer that sits between DXVK and the Mali Vulkan driver.
+ * Intercepts Vulkan calls and applies workarounds for known Mali driver bugs.
+ *
+ * Architecture:
+ * This is a proper Vulkan layer that follows the Khronos layer negotiation
+ * protocol. The dispatch table is populated in vkCreateInstance by walking
+ * the VkLayerInstanceCreateInfo_ chain in pCreateInfo->pNext.
+ *
+ * Phase 6 integration into GameNative.
+ *
+ * License: MIT (Aurora Emulator)
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Dual-environment logging: android/log.h only exists in bionic (Android NDK).
+// Inside glibc/PRoot, we use stderr. This makes the .so work in BOTH environments.
+#ifdef __BIONIC__
+#include
+#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
+#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
+#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
+#else
+#define ALOGI(...) do { fprintf(stderr, "[" TAG "] INFO: " __VA_ARGS__); fprintf(stderr, "\n"); } while(0)
+#define ALOGW(...) do { fprintf(stderr, "[" TAG "] WARN: " __VA_ARGS__); fprintf(stderr, "\n"); } while(0)
+#define ALOGE(...) do { fprintf(stderr, "[" TAG "] ERROR: " __VA_ARGS__); fprintf(stderr, "\n"); } while(0)
+#endif
+
+// =============================================================================
+// Vulkan layer structures — normally in vulkan/vk_layer.h, but the NDK
+// doesn't ship that header. We define them here manually.
+// This is standard practice for Android Vulkan layers.
+// =============================================================================
+
+// sType values for layer create info (not in NDK's vulkan_core.h)
+#define VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO \
+ ((VkStructureType)47)
+#define VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO \
+ ((VkStructureType)48)
+
+// Layer function type enum
+typedef enum VkLayerFunction {
+ VK_LAYER_LINK_INFO = 0,
+} VkLayerFunction;
+
+// Instance-level layer link
+typedef struct VkLayerInstanceLink_ {
+ struct VkLayerInstanceLink_* pNext;
+ PFN_vkGetInstanceProcAddr pfnNextGetInstanceProcAddr;
+} VkLayerInstanceLink_;
+
+// Instance-level layer create info
+typedef struct {
+ VkStructureType sType;
+ const void* pNext;
+ VkLayerFunction function;
+ union {
+ VkLayerInstanceLink_* pLayerInfo;
+ } u;
+} VkLayerInstanceCreateInfo_;
+
+// Device-level layer link
+typedef struct VkLayerDeviceLink_ {
+ struct VkLayerDeviceLink_* pNext;
+ PFN_vkGetInstanceProcAddr pfnNextGetInstanceProcAddr;
+ PFN_vkGetDeviceProcAddr pfnNextGetDeviceProcAddr;
+} VkLayerDeviceLink_;
+
+// Device-level layer create info
+typedef struct {
+ VkStructureType sType;
+ const void* pNext;
+ VkLayerFunction function;
+ union {
+ VkLayerDeviceLink_* pLayerInfo;
+ } u;
+} VkLayerDeviceCreateInfo_;
+
+#define TAG "AuroraMaliSanitizer"
+#define LOGI(...) ALOGI(__VA_ARGS__)
+#define LOGW(...) ALOGW(__VA_ARGS__)
+#define LOGE(...) ALOGE(__VA_ARGS__)
+
+// =============================================================================
+// Rule database
+// =============================================================================
+
+static const char* BLACKLISTED_EXTENSIONS[] = {
+ "VK_EXT_descriptor_indexing",
+ "VK_EXT_fragment_density_map",
+ "VK_KHR_shader_subgroup",
+ "VK_EXT_graphics_pipeline_library",
+};
+static const int BLACKLISTED_EXTENSION_COUNT = 4;
+
+static const uint32_t MAX_DESCRIPTOR_SETS_PER_CALL = 4;
+
+// Stats
+static int stats_extensions_blacklisted = 0;
+static int stats_descriptor_sets_split = 0;
+
+// =============================================================================
+// Instance dispatch table — one per VkInstance
+// =============================================================================
+
+struct InstanceDispatch {
+ PFN_vkGetInstanceProcAddr get_instance_proc_addr;
+ PFN_vkCreateDevice create_device;
+ PFN_vkDestroyInstance destroy_instance;
+ PFN_vkEnumerateDeviceExtensionProperties enumerate_device_extensions;
+};
+
+// Map from VkInstance to its dispatch table
+// Protected by g_instance_mutex — DXVK uses worker threads.
+static std::shared_mutex g_instance_mutex;
+static std::vector> g_instance_dispatch;
+
+static InstanceDispatch* get_instance_dispatch(VkInstance instance) {
+ std::shared_lock lock(g_instance_mutex);
+ for (auto& entry : g_instance_dispatch) {
+ if (entry.first == instance) return &entry.second;
+ }
+ return nullptr;
+}
+
+// =============================================================================
+// Device dispatch table — one per VkDevice
+// =============================================================================
+
+struct DeviceDispatch {
+ PFN_vkGetDeviceProcAddr get_device_proc_addr;
+ PFN_vkCmdBindDescriptorSets cmd_bind_descriptor_sets;
+ PFN_vkAllocateDescriptorSets allocate_descriptor_sets;
+ PFN_vkFreeDescriptorSets free_descriptor_sets;
+ PFN_vkAllocateCommandBuffers allocate_command_buffers;
+ PFN_vkFreeCommandBuffers free_command_buffers;
+ PFN_vkCreateDescriptorSetLayout create_descriptor_set_layout;
+ PFN_vkDestroyDescriptorSetLayout destroy_descriptor_set_layout;
+ PFN_vkDestroyDevice destroy_device;
+};
+
+static std::shared_mutex g_device_mutex;
+static std::vector> g_device_dispatch;
+
+static DeviceDispatch* get_device_dispatch(VkDevice device) {
+ std::shared_lock lock(g_device_mutex);
+ for (auto& entry : g_device_dispatch) {
+ if (entry.first == device) return &entry.second;
+ }
+ return nullptr;
+}
+
+// =============================================================================
+// B6 fix: VkCommandBuffer -> VkDevice map (so the descriptor-set hook can
+// resolve the correct device instead of hard-coding g_device_dispatch[0]).
+// Populated by aurora_vkAllocateCommandBuffers, cleared by aurora_vkFreeCommandBuffers.
+// =============================================================================
+static std::shared_mutex g_cmd_buf_mutex;
+static std::vector> g_cmd_buf_to_device;
+
+static void register_command_buffers(VkDevice device,
+ const VkCommandBuffer* pCommandBuffers,
+ uint32_t commandBufferCount) {
+ std::unique_lock lock(g_cmd_buf_mutex);
+ for (uint32_t i = 0; i < commandBufferCount; i++) {
+ // Remove any stale entry for this handle first (defensive — handles can be reused).
+ for (auto it = g_cmd_buf_to_device.begin(); it != g_cmd_buf_to_device.end(); ++it) {
+ if (it->first == pCommandBuffers[i]) {
+ g_cmd_buf_to_device.erase(it);
+ break;
+ }
+ }
+ g_cmd_buf_to_device.push_back({pCommandBuffers[i], device});
+ }
+}
+
+static void unregister_command_buffers(const VkCommandBuffer* pCommandBuffers,
+ uint32_t commandBufferCount) {
+ std::unique_lock lock(g_cmd_buf_mutex);
+ for (uint32_t i = 0; i < commandBufferCount; i++) {
+ for (auto it = g_cmd_buf_to_device.begin(); it != g_cmd_buf_to_device.end(); ++it) {
+ if (it->first == pCommandBuffers[i]) {
+ g_cmd_buf_to_device.erase(it);
+ break;
+ }
+ }
+ }
+}
+
+static VkDevice lookup_device_for_command_buffer(VkCommandBuffer cmdBuf) {
+ std::shared_lock lock(g_cmd_buf_mutex);
+ for (const auto& entry : g_cmd_buf_to_device) {
+ if (entry.first == cmdBuf) return entry.second;
+ }
+ return VK_NULL_HANDLE;
+}
+
+// =============================================================================
+// B4 fix: per-DescriptorSetLayout dynamic-offset count cache.
+// Populated by aurora_vkCreateDescriptorSetLayout, queried during the split
+// in aurora_vkCmdBindDescriptorSets to slice pDynamicOffsets correctly.
+// =============================================================================
+static std::shared_mutex g_layout_mutex;
+static std::vector> g_layout_dynamic_counts;
+
+static void register_layout(VkDescriptorSetLayout layout, uint32_t dynamicCount) {
+ std::unique_lock lock(g_layout_mutex);
+ g_layout_dynamic_counts.push_back({layout, dynamicCount});
+}
+
+static void unregister_layout(VkDescriptorSetLayout layout) {
+ std::unique_lock lock(g_layout_mutex);
+ for (auto it = g_layout_dynamic_counts.begin(); it != g_layout_dynamic_counts.end(); ++it) {
+ if (it->first == layout) {
+ g_layout_dynamic_counts.erase(it);
+ break;
+ }
+ }
+}
+
+static uint32_t lookup_layout_dynamic_count(VkDescriptorSetLayout layout) {
+ std::shared_lock lock(g_layout_mutex);
+ for (const auto& entry : g_layout_dynamic_counts) {
+ if (entry.first == layout) return entry.second;
+ }
+ return 0;
+}
+
+// Count dynamic descriptor bindings in a VkDescriptorSetLayoutCreateInfo.
+// Used at vkCreateDescriptorSetLayout time to populate the cache.
+static uint32_t count_dynamic_bindings(const VkDescriptorSetLayoutCreateInfo* pCreateInfo) {
+ if (!pCreateInfo || !pCreateInfo->pBindings) return 0;
+ uint32_t count = 0;
+ for (uint32_t i = 0; i < pCreateInfo->bindingCount; i++) {
+ VkDescriptorType t = pCreateInfo->pBindings[i].descriptorType;
+ if (t == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC ||
+ t == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC) {
+ count += pCreateInfo->pBindings[i].descriptorCount;
+ }
+ }
+ return count;
+}
+
+// =============================================================================
+// B4 fix (part 2): VkDescriptorSet -> dynamic-offset-count cache.
+// Populated by aurora_vkAllocateDescriptorSets (using the layout cache above),
+// queried in aurora_vkCmdBindDescriptorSets to slice pDynamicOffsets per chunk.
+// =============================================================================
+static std::shared_mutex g_set_mutex;
+static std::vector> g_set_dynamic_counts;
+
+static void register_descriptor_sets(const VkDescriptorSet* pSets,
+ const VkDescriptorSetLayout* pLayouts,
+ uint32_t setCount) {
+ std::unique_lock lock(g_set_mutex);
+ for (uint32_t i = 0; i < setCount; i++) {
+ uint32_t dynCount = lookup_layout_dynamic_count(pLayouts[i]);
+ // Remove any stale entry for this handle first (handles can be reused).
+ for (auto it = g_set_dynamic_counts.begin(); it != g_set_dynamic_counts.end(); ++it) {
+ if (it->first == pSets[i]) {
+ g_set_dynamic_counts.erase(it);
+ break;
+ }
+ }
+ g_set_dynamic_counts.push_back({pSets[i], dynCount});
+ }
+}
+
+static void unregister_descriptor_sets(const VkDescriptorSet* pSets, uint32_t setCount) {
+ std::unique_lock lock(g_set_mutex);
+ for (uint32_t i = 0; i < setCount; i++) {
+ for (auto it = g_set_dynamic_counts.begin(); it != g_set_dynamic_counts.end(); ++it) {
+ if (it->first == pSets[i]) {
+ g_set_dynamic_counts.erase(it);
+ break;
+ }
+ }
+ }
+}
+
+static uint32_t lookup_set_dynamic_count(VkDescriptorSet set) {
+ std::shared_lock lock(g_set_mutex);
+ for (const auto& entry : g_set_dynamic_counts) {
+ if (entry.first == set) return entry.second;
+ }
+ // Unknown — assume 0 (safer than guessing; the app's own validation layer
+ // would have caught a mismatch before us if layouts were tracked properly).
+ return 0;
+}
+
+// =============================================================================
+// vkCreateInstance — populate the instance dispatch table
+// =============================================================================
+
+VKAPI_ATTR VkResult VKAPI_CALL aurora_vkCreateInstance(
+ const VkInstanceCreateInfo* pCreateInfo,
+ const VkAllocationCallbacks* pAllocator,
+ VkInstance* pInstance)
+{
+ LOGI("=== Aurora Mali Sanitizer: vkCreateInstance intercepted ===");
+
+ // Walk the pNext chain to find VkLayerInstanceCreateInfo_
+ // This is how layers get the next layer's function pointers
+ PFN_vkGetInstanceProcAddr nextGIPA = nullptr;
+
+ const VkBaseInStructure* pChain = (const VkBaseInStructure*)pCreateInfo->pNext;
+ while (pChain) {
+ if (pChain->sType == VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO) {
+ VkLayerInstanceCreateInfo_* layerInfo = (VkLayerInstanceCreateInfo_*)pChain;
+ if (layerInfo->function == VK_LAYER_LINK_INFO) {
+ // This is the link info — contains the next layer's vkGetInstanceProcAddr
+ nextGIPA = layerInfo->u.pLayerInfo->pfnNextGetInstanceProcAddr;
+ // Advance the link for the next layer in the chain
+ layerInfo->u.pLayerInfo = layerInfo->u.pLayerInfo->pNext;
+ break;
+ }
+ }
+ pChain = pChain->pNext;
+ }
+
+ if (!nextGIPA) {
+ LOGE("Failed to find next layer's vkGetInstanceProcAddr in pNext chain!");
+ return VK_ERROR_INITIALIZATION_FAILED;
+ }
+
+ // Get the real vkCreateInstance from the next layer
+ PFN_vkCreateInstance nextCreateInstance =
+ (PFN_vkCreateInstance)nextGIPA(VK_NULL_HANDLE, "vkCreateInstance");
+ if (!nextCreateInstance) {
+ LOGE("Next layer doesn't have vkCreateInstance!");
+ return VK_ERROR_INITIALIZATION_FAILED;
+ }
+
+ // Call the real vkCreateInstance
+ VkResult result = nextCreateInstance(pCreateInfo, pAllocator, pInstance);
+ if (result != VK_SUCCESS) {
+ LOGE("Next layer's vkCreateInstance failed: %d", result);
+ return result;
+ }
+
+ // Populate our dispatch table for this instance
+ InstanceDispatch dispatch;
+ dispatch.get_instance_proc_addr = nextGIPA;
+ dispatch.create_device =
+ (PFN_vkCreateDevice)nextGIPA(*pInstance, "vkCreateDevice");
+ dispatch.destroy_instance =
+ (PFN_vkDestroyInstance)nextGIPA(*pInstance, "vkDestroyInstance");
+ dispatch.enumerate_device_extensions =
+ (PFN_vkEnumerateDeviceExtensionProperties)nextGIPA(*pInstance, "vkEnumerateDeviceExtensionProperties");
+
+ { std::unique_lock lock(g_instance_mutex);
+ g_instance_dispatch.push_back({*pInstance, dispatch}); }
+
+ LOGI("Dispatch table populated. Instance: %p", (void*)*pInstance);
+ LOGI("Rules: %d blacklisted extensions, descriptor set split at %u",
+ BLACKLISTED_EXTENSION_COUNT, MAX_DESCRIPTOR_SETS_PER_CALL);
+
+ return VK_SUCCESS;
+}
+
+// =============================================================================
+// vkDestroyInstance — cleanup dispatch table
+// =============================================================================
+
+VKAPI_ATTR void VKAPI_CALL aurora_vkDestroyInstance(
+ VkInstance instance,
+ const VkAllocationCallbacks* pAllocator)
+{
+ InstanceDispatch* dispatch = get_instance_dispatch(instance);
+ if (dispatch && dispatch->destroy_instance) {
+ dispatch->destroy_instance(instance, pAllocator);
+ }
+
+ // Remove from our table
+ { std::unique_lock lock(g_instance_mutex);
+ for (auto it = g_instance_dispatch.begin(); it != g_instance_dispatch.end(); ++it) {
+ if (it->first == instance) {
+ g_instance_dispatch.erase(it);
+ break;
+ }
+ } }
+
+ LOGI("Stats: %d extensions blacklisted, %d desc set splits",
+ stats_extensions_blacklisted, stats_descriptor_sets_split);
+}
+
+// =============================================================================
+// vkCreateDevice — filter blacklisted extensions + populate device dispatch
+// =============================================================================
+
+VKAPI_ATTR VkResult VKAPI_CALL aurora_vkCreateDevice(
+ VkPhysicalDevice physicalDevice,
+ const VkDeviceCreateInfo* pCreateInfo,
+ const VkAllocationCallbacks* pAllocator,
+ VkDevice* pDevice)
+{
+ // Find the instance that owns this physical device
+ // (We need the instance dispatch to get the next layer's functions)
+ // Since we typically have only one instance, use the first one.
+ InstanceDispatch* instDispatch = nullptr;
+ if (!g_instance_dispatch.empty()) {
+ instDispatch = &g_instance_dispatch[0].second;
+ }
+
+ if (!instDispatch || !instDispatch->create_device) {
+ LOGE("No instance dispatch available for vkCreateDevice!");
+ return VK_ERROR_INITIALIZATION_FAILED;
+ }
+
+ // Walk pNext chain to find VkLayerDeviceCreateInfo_ (for device-level linking)
+ PFN_vkGetDeviceProcAddr nextGDPA = nullptr;
+
+ const VkBaseInStructure* pChain = (const VkBaseInStructure*)pCreateInfo->pNext;
+ while (pChain) {
+ if (pChain->sType == VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO) {
+ VkLayerDeviceCreateInfo_* layerInfo = (VkLayerDeviceCreateInfo_*)pChain;
+ if (layerInfo->function == VK_LAYER_LINK_INFO) {
+ nextGDPA = layerInfo->u.pLayerInfo->pfnNextGetDeviceProcAddr;
+ layerInfo->u.pLayerInfo = layerInfo->u.pLayerInfo->pNext;
+ break;
+ }
+ }
+ pChain = pChain->pNext;
+ }
+
+ // Filter blacklisted extensions
+ std::vector filteredExts;
+ int filteredCount = 0;
+
+ if (pCreateInfo && pCreateInfo->enabledExtensionCount > 0) {
+ for (uint32_t i = 0; i < pCreateInfo->enabledExtensionCount; i++) {
+ const char* ext = pCreateInfo->ppEnabledExtensionNames[i];
+ bool blacklisted = false;
+ for (int j = 0; j < BLACKLISTED_EXTENSION_COUNT; j++) {
+ if (strcmp(ext, BLACKLISTED_EXTENSIONS[j]) == 0) {
+ LOGI("BLACKLIST: %s", ext);
+ blacklisted = true;
+ filteredCount++;
+ stats_extensions_blacklisted++;
+ break;
+ }
+ }
+ if (!blacklisted) {
+ filteredExts.push_back(ext);
+ }
+ }
+ }
+
+ // Build modified create info if we filtered anything
+ VkDeviceCreateInfo modifiedInfo = *pCreateInfo;
+ if (filteredCount > 0) {
+ LOGI("Filtered %d/%u extensions", filteredCount, pCreateInfo->enabledExtensionCount);
+ modifiedInfo.enabledExtensionCount = (uint32_t)filteredExts.size();
+ modifiedInfo.ppEnabledExtensionNames = filteredExts.data();
+ }
+
+ // Call the real vkCreateDevice
+ VkResult result = instDispatch->create_device(
+ physicalDevice, &modifiedInfo, pAllocator, pDevice);
+ if (result != VK_SUCCESS) {
+ LOGE("vkCreateDevice failed: %d", result);
+ return result;
+ }
+
+ // Populate device dispatch table
+ DeviceDispatch devDispatch;
+ devDispatch.get_device_proc_addr = nextGDPA;
+ devDispatch.allocate_command_buffers = nullptr;
+ devDispatch.free_command_buffers = nullptr;
+ devDispatch.create_descriptor_set_layout = nullptr;
+ devDispatch.destroy_descriptor_set_layout = nullptr;
+ devDispatch.free_descriptor_sets = nullptr;
+ if (nextGDPA) {
+ devDispatch.cmd_bind_descriptor_sets =
+ (PFN_vkCmdBindDescriptorSets)nextGDPA(*pDevice, "vkCmdBindDescriptorSets");
+ devDispatch.allocate_descriptor_sets =
+ (PFN_vkAllocateDescriptorSets)nextGDPA(*pDevice, "vkAllocateDescriptorSets");
+ devDispatch.free_descriptor_sets =
+ (PFN_vkFreeDescriptorSets)nextGDPA(*pDevice, "vkFreeDescriptorSets");
+ devDispatch.allocate_command_buffers =
+ (PFN_vkAllocateCommandBuffers)nextGDPA(*pDevice, "vkAllocateCommandBuffers");
+ devDispatch.free_command_buffers =
+ (PFN_vkFreeCommandBuffers)nextGDPA(*pDevice, "vkFreeCommandBuffers");
+ devDispatch.create_descriptor_set_layout =
+ (PFN_vkCreateDescriptorSetLayout)nextGDPA(*pDevice, "vkCreateDescriptorSetLayout");
+ devDispatch.destroy_descriptor_set_layout =
+ (PFN_vkDestroyDescriptorSetLayout)nextGDPA(*pDevice, "vkDestroyDescriptorSetLayout");
+ devDispatch.destroy_device =
+ (PFN_vkDestroyDevice)nextGDPA(*pDevice, "vkDestroyDevice");
+ }
+
+ { std::unique_lock lock(g_device_mutex);
+ g_device_dispatch.push_back({*pDevice, devDispatch}); }
+
+ LOGI("Device created: %p, dispatch populated", (void*)*pDevice);
+ return VK_SUCCESS;
+}
+
+// =============================================================================
+// vkDestroyDevice — cleanup device dispatch
+// =============================================================================
+
+VKAPI_ATTR void VKAPI_CALL aurora_vkDestroyDevice(
+ VkDevice device,
+ const VkAllocationCallbacks* pAllocator)
+{
+ DeviceDispatch* dispatch = get_device_dispatch(device);
+ if (dispatch && dispatch->destroy_device) {
+ dispatch->destroy_device(device, pAllocator);
+ }
+
+ { std::unique_lock lock(g_device_mutex);
+ for (auto it = g_device_dispatch.begin(); it != g_device_dispatch.end(); ++it) {
+ if (it->first == device) {
+ g_device_dispatch.erase(it);
+ break;
+ }
+ } }
+
+ // B6 fix: clean up command buffer map for this device.
+ { std::unique_lock lock(g_cmd_buf_mutex);
+ for (auto it = g_cmd_buf_to_device.begin(); it != g_cmd_buf_to_device.end(); ) {
+ if (it->second == device) {
+ it = g_cmd_buf_to_device.erase(it);
+ } else {
+ ++it;
+ }
+ } }
+
+ // B4 fix: clean up descriptor set map for this device.
+ // (We can't easily tell which sets belong to which device without tracking
+ // it in vkAllocateDescriptorSets — which we now do via register_descriptor_sets,
+ // but that map is global. On device destroy, the app must have freed all sets
+ // first per Vulkan spec, so this is defensive only.)
+}
+
+// =============================================================================
+// B6 fix hooks: vkAllocateCommandBuffers / vkFreeCommandBuffers
+// Populate/clear the VkCommandBuffer -> VkDevice map.
+// =============================================================================
+
+VKAPI_ATTR VkResult VKAPI_CALL aurora_vkAllocateCommandBuffers(
+ VkDevice device,
+ const VkCommandBufferAllocateInfo* pAllocateInfo,
+ VkCommandBuffer* pCommandBuffers)
+{
+ DeviceDispatch* dispatch = get_device_dispatch(device);
+ if (!dispatch || !dispatch->allocate_command_buffers) {
+ LOGE("No device dispatch for vkAllocateCommandBuffers!");
+ return VK_ERROR_INITIALIZATION_FAILED;
+ }
+ VkResult result = dispatch->allocate_command_buffers(device, pAllocateInfo, pCommandBuffers);
+ if (result == VK_SUCCESS && pAllocateInfo && pCommandBuffers) {
+ register_command_buffers(device, pCommandBuffers, pAllocateInfo->commandBufferCount);
+ }
+ return result;
+}
+
+VKAPI_ATTR void VKAPI_CALL aurora_vkFreeCommandBuffers(
+ VkDevice device,
+ VkCommandPool commandPool,
+ uint32_t commandBufferCount,
+ const VkCommandBuffer* pCommandBuffers)
+{
+ DeviceDispatch* dispatch = get_device_dispatch(device);
+ if (dispatch && dispatch->free_command_buffers) {
+ dispatch->free_command_buffers(device, commandPool, commandBufferCount, pCommandBuffers);
+ }
+ if (pCommandBuffers && commandBufferCount > 0) {
+ unregister_command_buffers(pCommandBuffers, commandBufferCount);
+ }
+}
+
+// =============================================================================
+// B4 fix hooks: vkCreateDescriptorSetLayout / vkDestroyDescriptorSetLayout
+// Cache per-layout dynamic-offset count so we can slice pDynamicOffsets
+// correctly in aurora_vkCmdBindDescriptorSets.
+// =============================================================================
+
+VKAPI_ATTR VkResult VKAPI_CALL aurora_vkCreateDescriptorSetLayout(
+ VkDevice device,
+ const VkDescriptorSetLayoutCreateInfo* pCreateInfo,
+ const VkAllocationCallbacks* pAllocator,
+ VkDescriptorSetLayout* pSetLayout)
+{
+ DeviceDispatch* dispatch = get_device_dispatch(device);
+ if (!dispatch || !dispatch->create_descriptor_set_layout) {
+ LOGE("No device dispatch for vkCreateDescriptorSetLayout!");
+ return VK_ERROR_INITIALIZATION_FAILED;
+ }
+ VkResult result = dispatch->create_descriptor_set_layout(device, pCreateInfo, pAllocator, pSetLayout);
+ if (result == VK_SUCCESS && pSetLayout) {
+ uint32_t dynCount = count_dynamic_bindings(pCreateInfo);
+ register_layout(*pSetLayout, dynCount);
+ }
+ return result;
+}
+
+VKAPI_ATTR void VKAPI_CALL aurora_vkDestroyDescriptorSetLayout(
+ VkDevice device,
+ VkDescriptorSetLayout descriptorSetLayout,
+ const VkAllocationCallbacks* pAllocator)
+{
+ DeviceDispatch* dispatch = get_device_dispatch(device);
+ if (dispatch && dispatch->destroy_descriptor_set_layout) {
+ dispatch->destroy_descriptor_set_layout(device, descriptorSetLayout, pAllocator);
+ }
+ unregister_layout(descriptorSetLayout);
+}
+
+// =============================================================================
+// B4 fix hooks: vkAllocateDescriptorSets / vkFreeDescriptorSets
+// Cache per-set dynamic-offset count (looked up from the layout cache).
+// =============================================================================
+
+VKAPI_ATTR VkResult VKAPI_CALL aurora_vkAllocateDescriptorSets(
+ VkDevice device,
+ const VkDescriptorSetAllocateInfo* pAllocateInfo,
+ VkDescriptorSet* pDescriptorSets)
+{
+ DeviceDispatch* dispatch = get_device_dispatch(device);
+ if (!dispatch || !dispatch->allocate_descriptor_sets) {
+ LOGE("No device dispatch for vkAllocateDescriptorSets!");
+ return VK_ERROR_INITIALIZATION_FAILED;
+ }
+ VkResult result = dispatch->allocate_descriptor_sets(device, pAllocateInfo, pDescriptorSets);
+ if (result == VK_SUCCESS && pAllocateInfo && pDescriptorSets) {
+ register_descriptor_sets(pDescriptorSets,
+ pAllocateInfo->pSetLayouts,
+ pAllocateInfo->descriptorSetCount);
+ }
+ return result;
+}
+
+VKAPI_ATTR VkResult VKAPI_CALL aurora_vkFreeDescriptorSets(
+ VkDevice device,
+ VkDescriptorPool descriptorPool,
+ uint32_t descriptorSetCount,
+ const VkDescriptorSet* pDescriptorSets)
+{
+ DeviceDispatch* dispatch = get_device_dispatch(device);
+ VkResult result = VK_SUCCESS;
+ if (dispatch && dispatch->free_descriptor_sets) {
+ result = dispatch->free_descriptor_sets(device, descriptorPool, descriptorSetCount, pDescriptorSets);
+ }
+ if (pDescriptorSets && descriptorSetCount > 0) {
+ unregister_descriptor_sets(pDescriptorSets, descriptorSetCount);
+ }
+ return result;
+}
+
+// =============================================================================
+// vkCmdBindDescriptorSets — split >4 sets (Mali crashes on >4)
+// B4 fix: slice pDynamicOffsets correctly per chunk using the per-set
+// dynamic-offset count cache populated via vkAllocateDescriptorSets.
+// B6 fix: resolve the device by command buffer instead of hard-coding [0].
+// =============================================================================
+
+VKAPI_ATTR void VKAPI_CALL aurora_vkCmdBindDescriptorSets(
+ VkCommandBuffer commandBuffer,
+ VkPipelineBindPoint pipelineBindPoint,
+ VkPipelineLayout layout,
+ uint32_t firstSet,
+ uint32_t descriptorSetCount,
+ const VkDescriptorSet* pDescriptorSets,
+ uint32_t dynamicOffsetCount,
+ const uint32_t* pDynamicOffsets)
+{
+ // B6 fix: resolve the device that owns this command buffer.
+ VkDevice device = lookup_device_for_command_buffer(commandBuffer);
+ DeviceDispatch* dispatch = nullptr;
+ if (device != VK_NULL_HANDLE) {
+ dispatch = get_device_dispatch(device);
+ } else if (!g_device_dispatch.empty()) {
+ // Fallback for command buffers we never saw allocated (shouldn't happen
+ // in practice, but be defensive — fail-safe rather than crash).
+ dispatch = &g_device_dispatch[0].second;
+ }
+
+ if (!dispatch || !dispatch->cmd_bind_descriptor_sets) {
+ LOGE("No device dispatch for vkCmdBindDescriptorSets!");
+ return;
+ }
+
+ if (descriptorSetCount <= MAX_DESCRIPTOR_SETS_PER_CALL) {
+ dispatch->cmd_bind_descriptor_sets(commandBuffer, pipelineBindPoint, layout,
+ firstSet, descriptorSetCount, pDescriptorSets,
+ dynamicOffsetCount, pDynamicOffsets);
+ return;
+ }
+
+ // Split into chunks of MAX_DESCRIPTOR_SETS_PER_CALL.
+ LOGI("Split vkCmdBindDescriptorSets: %u sets -> chunks of %u",
+ descriptorSetCount, MAX_DESCRIPTOR_SETS_PER_CALL);
+ stats_descriptor_sets_split++;
+
+ // B4 fix: compute per-set dynamic-offset counts so we can slice
+ // pDynamicOffsets correctly per chunk. Without this, chunks 2+ would
+ // receive 0 dynamic offsets and silently bind dynamic UBOs at offset 0,
+ // producing wrong rendering for any draw that uses sets 4+ with dynamic
+ // bindings.
+ //
+ // The dynamic offsets array is consumed in order across all sets in the
+ // call: set 0's dynamic bindings first, then set 1's, etc. So we walk
+ // pDescriptorSets in order, look up each set's cached dynamic count, and
+ // build a per-set offset-index array. Then for each chunk we pass the
+ // correct sub-slice of pDynamicOffsets.
+ uint32_t setDynOffsets[64]; // Per-set dynamic count, indexed by set position in this call.
+ uint32_t setDynOffsetStart[65]; // Cumulative start index into pDynamicOffsets.
+ if (descriptorSetCount > 64) {
+ // Defensive cap — Vulkan's maxBoundDescriptorSets is typically <= 8.
+ // If a game ever exceeds 64 sets in one bind call (impossible per spec
+ // since maxBoundDescriptorSets caps this), fall back to the old
+ // behavior (all offsets on chunk 0) and log.
+ LOGW("vkCmdBindDescriptorSets: descriptorSetCount=%u exceeds static cap 64, falling back to unsafe split",
+ descriptorSetCount);
+ uint32_t remaining = descriptorSetCount;
+ uint32_t offset = 0;
+ uint32_t setOffset = firstSet;
+ while (remaining > 0) {
+ uint32_t chunk = (remaining > MAX_DESCRIPTOR_SETS_PER_CALL)
+ ? MAX_DESCRIPTOR_SETS_PER_CALL : remaining;
+ uint32_t dynOffs = (offset == 0) ? dynamicOffsetCount : 0;
+ const uint32_t* dynPtr = (offset == 0) ? pDynamicOffsets : nullptr;
+ dispatch->cmd_bind_descriptor_sets(commandBuffer, pipelineBindPoint, layout,
+ setOffset, chunk, pDescriptorSets + offset, dynOffs, dynPtr);
+ offset += chunk;
+ setOffset += chunk;
+ remaining -= chunk;
+ }
+ return;
+ }
+
+ setDynOffsetStart[0] = 0;
+ uint32_t totalDyn = 0;
+ for (uint32_t i = 0; i < descriptorSetCount; i++) {
+ uint32_t c = lookup_set_dynamic_count(pDescriptorSets[i]);
+ setDynOffsets[i] = c;
+ totalDyn += c;
+ setDynOffsetStart[i + 1] = totalDyn;
+ }
+
+ // Sanity: if our cached total doesn't match the caller's count, the app
+ // is either using layouts we never saw created (e.g. via a different
+ // Vulkan instance/layer chain) or our cache is stale. Fail safe: pass
+ // all offsets to chunk 0 (old behavior) and log — better than silent
+ // wrong rendering from a bad slice.
+ if (totalDyn != dynamicOffsetCount) {
+ LOGW("vkCmdBindDescriptorSets: cached dynamic count %u != caller count %u, falling back to unsafe split",
+ totalDyn, dynamicOffsetCount);
+ uint32_t remaining = descriptorSetCount;
+ uint32_t offset = 0;
+ uint32_t setOffset = firstSet;
+ while (remaining > 0) {
+ uint32_t chunk = (remaining > MAX_DESCRIPTOR_SETS_PER_CALL)
+ ? MAX_DESCRIPTOR_SETS_PER_CALL : remaining;
+ uint32_t dynOffs = (offset == 0) ? dynamicOffsetCount : 0;
+ const uint32_t* dynPtr = (offset == 0) ? pDynamicOffsets : nullptr;
+ dispatch->cmd_bind_descriptor_sets(commandBuffer, pipelineBindPoint, layout,
+ setOffset, chunk, pDescriptorSets + offset, dynOffs, dynPtr);
+ offset += chunk;
+ setOffset += chunk;
+ remaining -= chunk;
+ }
+ return;
+ }
+
+ // Correct split: each chunk gets exactly the dynamic offsets belonging to
+ // its sets.
+ uint32_t remaining = descriptorSetCount;
+ uint32_t offset = 0;
+ uint32_t setOffset = firstSet;
+
+ while (remaining > 0) {
+ uint32_t chunk = (remaining > MAX_DESCRIPTOR_SETS_PER_CALL)
+ ? MAX_DESCRIPTOR_SETS_PER_CALL : remaining;
+
+ uint32_t chunkDynStart = setDynOffsetStart[offset];
+ uint32_t chunkDynCount = setDynOffsetStart[offset + chunk] - chunkDynStart;
+ const uint32_t* chunkDynPtr = (chunkDynCount > 0)
+ ? (pDynamicOffsets + chunkDynStart) : nullptr;
+
+ dispatch->cmd_bind_descriptor_sets(commandBuffer, pipelineBindPoint, layout,
+ setOffset, chunk, pDescriptorSets + offset,
+ chunkDynCount, chunkDynPtr);
+
+ offset += chunk;
+ setOffset += chunk;
+ remaining -= chunk;
+ }
+}
+
+// =============================================================================
+// vkGetInstanceProcAddr — dispatch to our hooks or pass through
+// =============================================================================
+
+// Forward declaration — aurora_vkGetDeviceProcAddr is defined below but
+// referenced in aurora_vkGetInstanceProcAddr.
+VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL aurora_vkGetDeviceProcAddr(VkDevice device, const char* pName);
+
+VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL aurora_vkGetInstanceProcAddr(
+ VkInstance instance, const char* pName)
+{
+ // Intercept instance-level functions
+ if (strcmp(pName, "vkCreateInstance") == 0)
+ return (PFN_vkVoidFunction)aurora_vkCreateInstance;
+ if (strcmp(pName, "vkDestroyInstance") == 0)
+ return (PFN_vkVoidFunction)aurora_vkDestroyInstance;
+ if (strcmp(pName, "vkCreateDevice") == 0)
+ return (PFN_vkVoidFunction)aurora_vkCreateDevice;
+ if (strcmp(pName, "vkGetInstanceProcAddr") == 0)
+ return (PFN_vkVoidFunction)aurora_vkGetInstanceProcAddr;
+ if (strcmp(pName, "vkGetDeviceProcAddr") == 0)
+ return (PFN_vkVoidFunction)aurora_vkGetDeviceProcAddr;
+
+ // Pass through to next layer
+ if (instance) {
+ InstanceDispatch* dispatch = get_instance_dispatch(instance);
+ if (dispatch && dispatch->get_instance_proc_addr) {
+ return dispatch->get_instance_proc_addr(instance, pName);
+ }
+ }
+ return nullptr;
+}
+
+// =============================================================================
+// vkGetDeviceProcAddr — dispatch to our device-level hooks
+// =============================================================================
+
+VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL aurora_vkGetDeviceProcAddr(
+ VkDevice device, const char* pName)
+{
+ if (strcmp(pName, "vkCmdBindDescriptorSets") == 0)
+ return (PFN_vkVoidFunction)aurora_vkCmdBindDescriptorSets;
+ if (strcmp(pName, "vkAllocateDescriptorSets") == 0)
+ return (PFN_vkVoidFunction)aurora_vkAllocateDescriptorSets;
+ if (strcmp(pName, "vkFreeDescriptorSets") == 0)
+ return (PFN_vkVoidFunction)aurora_vkFreeDescriptorSets;
+ if (strcmp(pName, "vkAllocateCommandBuffers") == 0)
+ return (PFN_vkVoidFunction)aurora_vkAllocateCommandBuffers;
+ if (strcmp(pName, "vkFreeCommandBuffers") == 0)
+ return (PFN_vkVoidFunction)aurora_vkFreeCommandBuffers;
+ if (strcmp(pName, "vkCreateDescriptorSetLayout") == 0)
+ return (PFN_vkVoidFunction)aurora_vkCreateDescriptorSetLayout;
+ if (strcmp(pName, "vkDestroyDescriptorSetLayout") == 0)
+ return (PFN_vkVoidFunction)aurora_vkDestroyDescriptorSetLayout;
+ if (strcmp(pName, "vkDestroyDevice") == 0)
+ return (PFN_vkVoidFunction)aurora_vkDestroyDevice;
+ if (strcmp(pName, "vkGetDeviceProcAddr") == 0)
+ return (PFN_vkVoidFunction)aurora_vkGetDeviceProcAddr;
+
+ // Pass through to next layer
+ if (device) {
+ DeviceDispatch* dispatch = get_device_dispatch(device);
+ if (dispatch && dispatch->get_device_proc_addr) {
+ return dispatch->get_device_proc_addr(device, pName);
+ }
+ }
+ return nullptr;
+}
+
+// =============================================================================
+// Layer negotiation — called by Vulkan loader
+// =============================================================================
+
+typedef struct {
+ uint32_t sType;
+ void* pNext;
+ uint32_t loaderLayerInterfaceVersion;
+ PFN_vkGetInstanceProcAddr pfnGetInstanceProcAddr;
+ PFN_vkGetDeviceProcAddr pfnGetDeviceProcAddr;
+ void* pfnGetPhysicalDeviceProcAddr; // NDK doesn't have this type
+} VkNegotiateLayerInterface;
+
+#define VK_LUNARG_NEGOTIATE_LAYER_INTERFACE_VERSION_2 2
+
+VKAPI_ATTR VkResult VKAPI_CALL vkNegotiateLoaderLayerInterfaceVersion(
+ VkNegotiateLayerInterface* pVersion)
+{
+ LOGI("=== Aurora Mali Sanitizer Layer Loaded (WinNative fork) ===");
+ LOGI("Rules: %d blacklisted extensions, descriptor set split at %u (B4: dynamic offsets sliced correctly)",
+ BLACKLISTED_EXTENSION_COUNT, MAX_DESCRIPTOR_SETS_PER_CALL);
+ LOGI("Tracking hooks: vkAllocateCommandBuffers/vkFreeCommandBuffers (B6), "
+ "vkCreateDescriptorSetLayout/vkDestroyDescriptorSetLayout (B4), "
+ "vkAllocateDescriptorSets/vkFreeDescriptorSets (B4)");
+
+ pVersion->loaderLayerInterfaceVersion = VK_LUNARG_NEGOTIATE_LAYER_INTERFACE_VERSION_2;
+ pVersion->pfnGetInstanceProcAddr = aurora_vkGetInstanceProcAddr;
+ pVersion->pfnGetDeviceProcAddr = aurora_vkGetDeviceProcAddr;
+ pVersion->pfnGetPhysicalDeviceProcAddr = nullptr;
+
+ return VK_SUCCESS;
+}
+
+// Fallback entry point
+VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(
+ VkInstance instance, const char* pName)
+{
+ if (pName && strcmp(pName, "vkNegotiateLoaderLayerInterfaceVersion") == 0) {
+ return (PFN_vkVoidFunction)vkNegotiateLoaderLayerInterfaceVersion;
+ }
+ return aurora_vkGetInstanceProcAddr(instance, pName);
+}
diff --git a/app/src/main/cpp/aurora_mali_sanitizer/aurora_mali_sanitizer.json b/app/src/main/cpp/aurora_mali_sanitizer/aurora_mali_sanitizer.json
new file mode 100755
index 000000000..eb7c4e797
--- /dev/null
+++ b/app/src/main/cpp/aurora_mali_sanitizer/aurora_mali_sanitizer.json
@@ -0,0 +1,24 @@
+{
+ "file_format_version": "1.1.0",
+ "layer": {
+ "name": "Aurora_Mali_Sanitizer",
+ "type": "GLOBAL",
+ "library_path": "libaurora_mali_sanitizer.so",
+ "api_version": "1.3.0",
+ "implementation_version": "1",
+ "description": "Aurora Emulator Mali Vulkan Sanitizer - filters blacklisted extensions and works around Mali driver bugs",
+ "functions": {
+ "vkNegotiateLoaderLayerInterfaceVersion": "vkNegotiateLoaderLayerInterfaceVersion",
+ "vkGetInstanceProcAddr": "vkGetInstanceProcAddr",
+ "vkGetDeviceProcAddr": "vkGetDeviceProcAddr"
+ },
+ "instance_extensions": [],
+ "device_extensions": [],
+ "enable_environment": {
+ "AURORA_MALI_SANITIZER_ENABLE": "1"
+ },
+ "disable_environment": {
+ "AURORA_MALI_SANITIZER_DISABLE": "1"
+ }
+ }
+}
diff --git a/app/src/main/runtime/aurora/AuroraSanitizerHelper.kt b/app/src/main/runtime/aurora/AuroraSanitizerHelper.kt
new file mode 100644
index 000000000..1afe69cf4
--- /dev/null
+++ b/app/src/main/runtime/aurora/AuroraSanitizerHelper.kt
@@ -0,0 +1,104 @@
+package com.winlator.cmod.runtime.aurora
+
+import android.content.Context
+import android.util.Log
+import com.winlator.cmod.runtime.display.environment.ImageFs
+import com.winlator.cmod.runtime.wine.EnvVars
+import java.io.File
+
+/**
+ * Aurora Mali Vulkan Sanitizer Helper (WinNative port).
+ *
+ * Sets up the Vulkan layer environment variables when the GPU is Mali or Immortalis.
+ * Called from GuestProgramLauncherComponent AFTER the user/container DXVK_CONFIG has
+ * been merged into envVars — this is the B2 audit fix. If we run before the merge,
+ * the user's DXVK_CONFIG destructively overwrites our Mali workarounds.
+ *
+ * Two modes:
+ * 1. Full mode (when libaurora_mali_sanitizer.so is deployed to imagefs):
+ * Sets VK_INSTANCE_LAYERS to load the Vulkan layer that intercepts all calls
+ * and applies 5 rules (4 blacklisted extensions + descriptor-set split with
+ * correct dynamic-offset slicing via B4 fix, correct device resolution via B6 fix).
+ *
+ * 2. Fallback mode (when .so is not available): Modifies DXVK_CONFIG to avoid
+ * requesting features that crash Mali. Achieves similar results without a
+ * native layer.
+ *
+ * Audit fixes applied (vs original Aurora code):
+ * - B1: Layer name standardized to "Aurora_Mali_Sanitizer" everywhere
+ * (JSON manifest, runtime-generated manifest, env var setter).
+ * - B2: Must be called AFTER envVars.putAll(this.envVars) in the launcher.
+ * - B9: Detects both "Mali" and "Immortalis" GPU strings.
+ */
+object AuroraSanitizerHelper {
+ private const val TAG = "AuroraSanitizer"
+
+ /**
+ * The Vulkan layer name. MUST match the "name" field in
+ * aurora_mali_sanitizer.json and the runtime-generated manifest in
+ * ImageFsInstaller. The Vulkan loader matches VK_INSTANCE_LAYERS entries
+ * against the manifest's name field — a mismatch silently disables the layer.
+ */
+ const val LAYER_NAME = "Aurora_Mali_Sanitizer"
+
+ /**
+ * DXVK config settings to add when running on Mali (fallback mode).
+ * These prevent DXVK from requesting Vulkan extensions that crash Mali.
+ */
+ private const val MALI_DXVK_CONFIG =
+ "dxvk.useDescriptorIndexing = False;" + // Avoid VK_EXT_descriptor_indexing (MALI-001)
+ "dxvk.usePipelineLibrary = False;" + // Avoid VK_EXT_graphics_pipeline_library (MALI-004)
+ "d3d11.maxFeatureLevel = 11_0;" + // Don't request 12_0+ (needs subgroups on Mali)
+ "d3d11.relaxedBarriers = True;" + // Workaround for Mali TBDR tile resolves
+ "dxvk.shaderUseSubgroupOps = False" // Avoid VK_KHR_shader_subgroup (MALI-003)
+
+ /**
+ * @return true if sanitizer was enabled (full or fallback), false if not needed.
+ */
+ @JvmStatic
+ fun setupSanitizer(
+ context: Context,
+ envVars: EnvVars,
+ imageFs: ImageFs,
+ gpuRenderer: String?
+ ): Boolean {
+ if (gpuRenderer == null) return false
+ val r = gpuRenderer.lowercase()
+ // B9 fix: detect both Mali and Immortalis (some Immortalis driver builds
+ // drop the "Mali" prefix and report only "Immortalis-G720").
+ val isMali = r.contains("mali") || r.contains("immortalis")
+ if (!isMali) return false
+
+ Log.i(TAG, "Mali/Immortalis GPU detected: $gpuRenderer — enabling sanitizer")
+
+ // Check if the sanitizer .so + JSON manifest are deployed to imagefs.
+ val sanitizerSo = File(imageFs.rootDir, "usr/lib/libaurora_mali_sanitizer.so")
+ val sanitizerJson = File(imageFs.rootDir, "usr/lib/aurora_mali_sanitizer.json")
+
+ if (sanitizerSo.exists() && sanitizerJson.exists()) {
+ // Full sanitizer mode — load the Vulkan layer.
+ Log.i(TAG, "Sanitizer .so + JSON manifest found — full Vulkan layer mode")
+ envVars.put("VK_INSTANCE_LAYERS", LAYER_NAME)
+ envVars.put("VK_LAYER_PATH", imageFs.rootDir.absolutePath + "/usr/lib")
+ envVars.put("AURORA_MALI_SANITIZER", "1")
+ return true
+ }
+
+ // Fallback: append Mali workarounds to DXVK_CONFIG.
+ // B2 fix: we are called AFTER the user/container DXVK_CONFIG has been
+ // merged in, so we read the current value and append (not overwrite).
+ Log.i(TAG, "Sanitizer .so not found — using DXVK_CONFIG fallback")
+ envVars.put("AURORA_MALI_SANITIZER", "fallback")
+ val existingConfig = envVars.get("DXVK_CONFIG") ?: ""
+ val newConfig = if (existingConfig.isBlank()) {
+ MALI_DXVK_CONFIG
+ } else if (existingConfig.contains("dxvk.useDescriptorIndexing")) {
+ // Already has a descriptorIndexing setting — don't double-set.
+ existingConfig
+ } else {
+ existingConfig.trimEnd(';') + ";" + MALI_DXVK_CONFIG
+ }
+ envVars.put("DXVK_CONFIG", newConfig)
+ return true
+ }
+}
diff --git a/app/src/main/runtime/aurora/DeviceTier.kt b/app/src/main/runtime/aurora/DeviceTier.kt
new file mode 100644
index 000000000..2a9089667
--- /dev/null
+++ b/app/src/main/runtime/aurora/DeviceTier.kt
@@ -0,0 +1,99 @@
+package com.winlator.cmod.runtime.aurora
+
+import android.app.ActivityManager
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import com.winlator.cmod.runtime.system.GPUInformation
+
+/**
+ * Device tier classification for per-tier performance tuning.
+ *
+ * Three tiers based on SoC + RAM + GPU:
+ * - HIGH: Flagship SoC (Snapdragon 8 Gen 2+, Dimensity 9200+, Immortalis),
+ * 12GB+ RAM. Defaults stay close to WinNative upstream.
+ * - MID: Snapdragon 7xx, Dimensity 7/8, Mali G610/G77/G78, 6-8GB RAM.
+ * Conservative DXVK config, smaller shader cache, 720p default,
+ * SGSR auto-enabled, esync by default.
+ * - LOW: Snapdragon 4xx/6xx, Mali G52/G72, 4GB RAM. Aggressive cuts:
+ * DX10 feature level, 540p default, smaller caches, no subgroups.
+ *
+ * The tier is computed once per app launch and cached. Mid-end is the target
+ * audience per the project goal — most users are on Snapdragon 7xx or Mali
+ * Valhall devices.
+ *
+ * Detection is heuristic — there's no reliable Android API for "is this a
+ * flagship SoC". We use Build.SOC_MODEL (API 31+), Build.HARDWARE, RAM size,
+ * and GPU renderer string. When uncertain, we err on the side of MID (safer
+ * defaults for unknown hardware).
+ */
+object DeviceTier {
+ private const val TAG = "AuroraDeviceTier"
+
+ enum class Tier { HIGH, MID, LOW }
+
+ @Volatile
+ private var cached: Tier? = null
+
+ fun tier(context: Context): Tier {
+ cached?.let { return it }
+ val t = compute(context)
+ cached = t
+ Log.i(TAG, "Device classified as $t")
+ return t
+ }
+
+ private fun compute(context: Context): Tier {
+ val ramGb = totalRamGb(context)
+ val gpu = GPUInformation.getRenderer(null, context).lowercase()
+ val soc = (Build.SOC_MODEL ?: "").lowercase()
+ val socMfr = (Build.SOC_MANUFACTURER ?: "").lowercase()
+ val hardware = (Build.HARDWARE ?: "").lowercase()
+
+ // GPU-based signals (most reliable).
+ val isImmortalis = gpu.contains("immortalis")
+ val isMaliValhall = gpu.contains("mali-g7") || gpu.contains("mali-g6") || gpu.contains("mali-g5")
+ val isMaliBifrost = gpu.contains("mali-g5") && (gpu.contains("mali-g52") || gpu.contains("mali-g72"))
+
+ // SoC-based signals.
+ val isSnapdragon8Gen = soc.matches(Regex(".*\\bsm8[0-9]{3}.*")) // sm8450=8gen1, sm8550=8g2, sm8650=8g3
+ val isSnapdragon7 = soc.matches(Regex(".*\\bsm7[0-9]{3}.*")) // sm7450=7gen1, sm7550=7+gen2
+ val isSnapdragon6 = soc.matches(Regex(".*\\bsm6[0-9]{3}.*"))
+ val isSnapdragon4 = soc.matches(Regex(".*\\bsm4[0-9]{3}.*"))
+ val isDimensity9 = socMfr.contains("mediatek") && soc.contains("mt6") && soc.matches(Regex(".*9[0-9]{3}.*"))
+ val isDimensity7 = socMfr.contains("mediatek") && soc.contains("mt6") && soc.matches(Regex(".*7[0-9]{3}.*"))
+
+ // Score the device — HIGH requires multiple flagship signals.
+ var score = 0
+ if (isImmortalis) score += 3
+ else if (isMaliValhall && !isMaliBifrost) score += 2
+ else if (isMaliBifrost) score -= 1 // Bifrost has known driver bugs; treat conservatively
+
+ if (isSnapdragon8Gen) score += 3
+ else if (isDimensity9) score += 3
+ else if (isSnapdragon7 || isDimensity7) score += 1
+ else if (isSnapdragon6) score -= 1
+ else if (isSnapdragon4) score -= 2
+
+ if (ramGb >= 12) score += 2
+ else if (ramGb >= 8) score += 1
+ else if (ramGb >= 6) score += 0
+ else score -= 2
+
+ // RAM floors — even a flagship SoC with 4GB RAM is constrained.
+ if (ramGb < 4) return Tier.LOW
+
+ return when {
+ score >= 4 -> Tier.HIGH
+ score >= 1 -> Tier.MID
+ else -> Tier.LOW
+ }
+ }
+
+ private fun totalRamGb(context: Context): Long {
+ val am = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager ?: return 8L
+ val mi = ActivityManager.MemoryInfo()
+ am.getMemoryInfo(mi)
+ return mi.totalMem / (1024L * 1024L * 1024L)
+ }
+}
diff --git a/app/src/main/runtime/aurora/MemoryBudgetHelper.kt b/app/src/main/runtime/aurora/MemoryBudgetHelper.kt
new file mode 100644
index 000000000..dfdd28d32
--- /dev/null
+++ b/app/src/main/runtime/aurora/MemoryBudgetHelper.kt
@@ -0,0 +1,78 @@
+package com.winlator.cmod.runtime.aurora
+
+import android.app.ActivityManager
+import android.content.Context
+import android.util.Log
+import com.winlator.cmod.runtime.wine.EnvVars
+
+/**
+ * Memory-budget-aware tuning.
+ *
+ * On low-RAM devices (<6GB), PulseAudio is a significant memory consumer
+ * (~50-100MB resident). Switching to ALSA-only saves memory at the cost of
+ * losing per-app audio routing. For mid-end gaming this is the right tradeoff.
+ *
+ * Also caps DXVK pipeline cache size — on 4GB devices an unbounded cache
+ * can grow to 1GB+ over a long session and trigger Android's low-memory
+ * killer, which kills the Wine process without warning.
+ *
+ * Memory thresholds (total device RAM, not free):
+ * - >=8GB: No cuts. PulseAudio stays. DXVK cache unbounded.
+ * - 6-7GB: PulseAudio stays (mid-end has enough). DXVK cache capped at 512MB.
+ * - 4-5GB: PulseAudio disabled (use ALSA). DXVK cache capped at 256MB.
+ * - <4GB: PulseAudio disabled. DXVK cache capped at 128MB. Box64
+ * memory cache reduced.
+ */
+object MemoryBudgetHelper {
+ private const val TAG = "AuroraMemBudget"
+
+ @JvmStatic
+ fun apply(context: Context, envVars: EnvVars) {
+ val ramGb = totalRamGb(context)
+ Log.i(TAG, "Device RAM: ${ramGb}GB")
+
+ when {
+ ramGb >= 8 -> {
+ // No cuts — flagship has enough RAM.
+ }
+ ramGb >= 6 -> {
+ // Mid-range: cap DXVK cache only.
+ envVars.putIfAbsent("DXVK_CONFIG", "")
+ appendDxvk(envVars, "dxvk.pipelineCacheMaxSize", "536870912") // 512MB
+ }
+ ramGb >= 4 -> {
+ // Low-end: disable PulseAudio, cap cache tighter.
+ // Audio driver switch happens at container level, not env var.
+ // We set a marker env var that ContainerCreation can read.
+ envVars.put("AURORA_DISABLE_PULSEAUDIO", "1")
+ appendDxvk(envVars, "dxvk.pipelineCacheMaxSize", "268435456") // 256MB
+ envVars.putIfAbsent("BOX64_DYNAREC_STRONGMEM", "3")
+ }
+ else -> {
+ // Very low (<4GB): aggressive cuts.
+ envVars.put("AURORA_DISABLE_PULSEAUDIO", "1")
+ appendDxvk(envVars, "dxvk.pipelineCacheMaxSize", "134217728") // 128MB
+ envVars.putIfAbsent("BOX64_DYNAREC_STRONGMEM", "3")
+ envVars.putIfAbsent("BOX64_DYNAREC_BIGBLOCK", "2") // even more aggressive
+ }
+ }
+ }
+
+ private fun totalRamGb(context: Context): Long {
+ val am = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager ?: return 8L
+ val mi = ActivityManager.MemoryInfo()
+ am.getMemoryInfo(mi)
+ return mi.totalMem / (1024L * 1024L * 1024L)
+ }
+
+ private fun appendDxvk(envVars: EnvVars, key: String, value: String) {
+ val existing = envVars.get("DXVK_CONFIG") ?: ""
+ if (existing.contains(key)) return
+ val newConfig = if (existing.isBlank()) {
+ "$key = $value"
+ } else {
+ existing.trimEnd(';') + ";$key = $value"
+ }
+ envVars.put("DXVK_CONFIG", newConfig)
+ }
+}
diff --git a/app/src/main/runtime/aurora/NTSyncHelper.kt b/app/src/main/runtime/aurora/NTSyncHelper.kt
new file mode 100644
index 000000000..48496aae4
--- /dev/null
+++ b/app/src/main/runtime/aurora/NTSyncHelper.kt
@@ -0,0 +1,65 @@
+package com.winlator.cmod.runtime.aurora
+
+import android.util.Log
+import com.winlator.cmod.runtime.wine.EnvVars
+import java.io.File
+
+/**
+ * Auto-detects NTSync (kernel Wine sync primitive, faster than esync/fsync).
+ *
+ * NTSync is a Linux 6.14+ kernel module that exposes /dev/ntsync. Wine 9.0+
+ * and Proton 9.0+ can use it via WINENTSYNC=1 / PROTON_USE_NTSYNC=1.
+ *
+ * WinNative already supports NTSync via the WINENTSYNC env var checkbox, but
+ * the user has to know to enable it. This helper auto-detects kernel support
+ * and enables NTSync if the kernel has the /dev/ntsync device. We then fall
+ * back to esync (WinNative's default) if NTSync isn't available.
+ *
+ * Detection: check for /dev/ntsync character device. If present, kernel
+ * supports NTSync. This is more reliable than parsing /proc/version because
+ * some OEM kernels backport NTSync to older version strings.
+ *
+ * Must be called AFTER TierPresets.apply() so tier defaults (WINEESYNC=1)
+ * are overridden by NTSync when available. We also clear WINEESYNC and
+ * PROTON_NO_ESYNC when NTSync is enabled, to avoid Wine falling back to
+ * the slower sync primitive.
+ */
+object NTSyncHelper {
+ private const val TAG = "AuroraNTSync"
+ private const val NTSYNC_DEVICE = "/dev/ntsync"
+
+ @Volatile
+ private var cachedSupported: Boolean? = null
+
+ fun isSupported(): Boolean {
+ cachedSupported?.let { return it }
+ val supported = try {
+ File(NTSYNC_DEVICE).exists() && File(NTSYNC_DEVICE).canRead()
+ } catch (e: SecurityException) {
+ false
+ }
+ cachedSupported = supported
+ Log.i(TAG, "NTSync support: $supported (device=$NTSYNC_DEVICE)")
+ return supported
+ }
+
+ /**
+ * Enable NTSync in env vars if the kernel supports it. If NTSync is
+ * supported, sets WINENTSYNC=1 and PROTON_USE_NTSYNC=1 and clears
+ * WINEESYNC/PROTON_NO_ESYNC to let Wine use the faster primitive.
+ *
+ * If NTSync is NOT supported, leaves env vars untouched (TierPresets
+ * already set WINEESYNC=1 as the default for mid-end devices).
+ */
+ @JvmStatic
+ fun applyIfSupported(envVars: EnvVars) {
+ if (!isSupported()) return
+ Log.i(TAG, "Enabling NTSync (kernel supports /dev/ntsync)")
+ envVars.put("WINENTSYNC", "1")
+ envVars.put("PROTON_USE_NTSYNC", "1")
+ // Clear conflicting esync env vars — Wine uses NTSync when available.
+ envVars.remove("WINEESYNC")
+ envVars.remove("WINEESYNC_WINLATOR")
+ envVars.remove("PROTON_NO_ESYNC")
+ }
+}
diff --git a/app/src/main/runtime/aurora/TierPresets.kt b/app/src/main/runtime/aurora/TierPresets.kt
new file mode 100644
index 000000000..539c235f4
--- /dev/null
+++ b/app/src/main/runtime/aurora/TierPresets.kt
@@ -0,0 +1,145 @@
+package com.winlator.cmod.runtime.aurora
+
+import android.content.Context
+import android.util.Log
+import com.winlator.cmod.runtime.wine.EnvVars
+import com.winlator.cmod.runtime.container.Container
+
+/**
+ * Per-tier env-var presets for mid-end performance tuning.
+ *
+ * Applied at game launch, AFTER the user/container env vars are merged in,
+ * so user overrides still win. We only SET env vars that aren't already
+ * present — this means a user who explicitly sets BOX64_DYNAREC_STRONGMEM=3
+ * keeps their value.
+ *
+ * The presets are conservative: they trade peak FPS for stability on mid-end
+ * devices. The goal is "playable at good quality" not "maximum FPS that
+ * crashes after 20 minutes".
+ *
+ * Three tiers — see DeviceTier.kt for classification.
+ *
+ * What each tier changes vs WinNative defaults:
+ *
+ * HIGH: + dxvk.useDescriptorIndexing = True (faster descriptor sets)
+ * + d3d11.maxFeatureLevel = 12_0 (full DX12 feature level)
+ * + Default screen 1920x1080 (handled at container creation, not here)
+ *
+ * MID: + BOX64_DYNAREC_STRONGMEM = 2 (stronger mem ordering for stability)
+ * + BOX64_DYNAREC_BIGBLOCK = 1 (faster, less memory)
+ * + BOX64_DYNAREC_CALLRET = 1 (call/ret optimization)
+ * + BOX64_DYNAREC_FASTNAN = 1 (faster NaN handling)
+ * + BOX64_DYNAREC_FASTROUND = 1
+ * + BOX64_DYNAREC_SAFEFLAGS = 1
+ * + DXVK: d3d11.maxFeatureLevel = 11_0 (don't request 12_0+)
+ * + DXVK: d3d11.relaxedBarriers = True (Mali TBDR-friendly)
+ * + DXVK: dxvk.useDescriptorIndexing = False (smaller desc pools)
+ * + DXVK: dxvk.shaderUseSubgroupOps = False
+ * + MESA_SHADER_CACHE_MAX_SIZE = 512MB (limit cache size)
+ * + WINEESYNC = 1 (faster than fsync on most kernels)
+ * + PROTON_NO_FSYNC = 1 (fsync removed in WinNative already)
+ * + NTSync auto-detected by kernel version (separate helper)
+ *
+ * LOW: All MID settings, plus:
+ * + BOX64_DYNAREC_STRONGMEM = 3
+ * + DXVK: d3d11.maxFeatureLevel = 10_0 (drop to DX10)
+ * + MESA_SHADER_CACHE_MAX_SIZE = 256MB
+ * + DXVK: dxvk.usePipelineLibrary = False
+ * + Default screen 960x540 (handled at container creation)
+ */
+object TierPresets {
+ private const val TAG = "AuroraTierPresets"
+
+ /**
+ * Apply tier presets to the env vars. Must be called AFTER the user/container
+ * env vars are merged in (so we don't overwrite user overrides).
+ *
+ * @param context Android context
+ * @param envVars EnvVars to modify in place
+ * @param container The container being launched (for logging only)
+ */
+ @JvmStatic
+ fun apply(context: Context, envVars: EnvVars, container: Container) {
+ val tier = DeviceTier.tier(context)
+ Log.i(TAG, "Applying $tier tier presets for container ${container.id}")
+
+ // BOX64 dynarec settings — all "putIfAbsent" so user overrides win.
+ // Strong mem ordering is critical on Mali (driver expects it).
+ when (tier) {
+ DeviceTier.Tier.HIGH -> {
+ // High-end: let user/WinNative defaults stand.
+ // Only set DXVK performance flags if not already set.
+ putDxvkIfAbsent(envVars, "dxvk.useDescriptorIndexing", "True")
+ putDxvkIfAbsent(envVars, "d3d11.maxFeatureLevel", "12_0")
+ }
+ DeviceTier.Tier.MID -> {
+ envVars.putIfAbsent("BOX64_DYNAREC_BIGBLOCK", "1")
+ envVars.putIfAbsent("BOX64_DYNAREC_STRONGMEM", "2")
+ envVars.putIfAbsent("BOX64_DYNAREC_CALLRET", "1")
+ envVars.putIfAbsent("BOX64_DYNAREC_FASTNAN", "1")
+ envVars.putIfAbsent("BOX64_DYNAREC_FASTROUND", "1")
+ envVars.putIfAbsent("BOX64_DYNAREC_SAFEFLAGS", "1")
+
+ envVars.putIfAbsent("MESA_SHADER_CACHE_MAX_SIZE", "512MB")
+ envVars.putIfAbsent("MESA_SHADER_CACHE_DISABLE", "false")
+
+ // ESync is faster than fsync on most kernels; NTSync is even
+ // faster but requires kernel 6.14+ (handled in NTSyncHelper).
+ envVars.putIfAbsent("WINEESYNC", "1")
+ envVars.remove("WINEFSYNC")
+ envVars.putIfAbsent("PROTON_NO_FSYNC", "1")
+
+ putDxvkIfAbsent(envVars, "d3d11.maxFeatureLevel", "11_0")
+ putDxvkIfAbsent(envVars, "d3d11.relaxedBarriers", "True")
+ putDxvkIfAbsent(envVars, "dxvk.useDescriptorIndexing", "False")
+ putDxvkIfAbsent(envVars, "dxvk.shaderUseSubgroupOps", "False")
+ }
+ DeviceTier.Tier.LOW -> {
+ envVars.putIfAbsent("BOX64_DYNAREC_BIGBLOCK", "1")
+ envVars.putIfAbsent("BOX64_DYNAREC_STRONGMEM", "3")
+ envVars.putIfAbsent("BOX64_DYNAREC_CALLRET", "1")
+ envVars.putIfAbsent("BOX64_DYNAREC_FASTNAN", "1")
+ envVars.putIfAbsent("BOX64_DYNAREC_FASTROUND", "1")
+ envVars.putIfAbsent("BOX64_DYNAREC_SAFEFLAGS", "1")
+
+ envVars.putIfAbsent("MESA_SHADER_CACHE_MAX_SIZE", "256MB")
+ envVars.putIfAbsent("MESA_SHADER_CACHE_DISABLE", "false")
+
+ envVars.putIfAbsent("WINEESYNC", "1")
+ envVars.remove("WINEFSYNC")
+ envVars.putIfAbsent("PROTON_NO_FSYNC", "1")
+
+ putDxvkIfAbsent(envVars, "d3d11.maxFeatureLevel", "10_0")
+ putDxvkIfAbsent(envVars, "d3d11.relaxedBarriers", "True")
+ putDxvkIfAbsent(envVars, "dxvk.useDescriptorIndexing", "False")
+ putDxvkIfAbsent(envVars, "dxvk.shaderUseSubgroupOps", "False")
+ putDxvkIfAbsent(envVars, "dxvk.usePipelineLibrary", "False")
+ }
+ }
+ }
+
+ /**
+ * Default screen size for a tier. Used at container creation time
+ * (not env-var time — screen size is a container property).
+ */
+ fun defaultScreenSize(tier: DeviceTier.Tier): String = when (tier) {
+ DeviceTier.Tier.HIGH -> "1920x1080"
+ DeviceTier.Tier.MID -> "1280x720"
+ DeviceTier.Tier.LOW -> "960x540"
+ }
+
+ /**
+ * Parse the existing DXVK_CONFIG and set a key only if it isn't already
+ * present. DXVK_CONFIG is a semicolon-separated "key = value" string.
+ */
+ private fun putDxvkIfAbsent(envVars: EnvVars, key: String, value: String) {
+ val existing = envVars.get("DXVK_CONFIG") ?: ""
+ if (existing.contains(key)) return
+ val newConfig = if (existing.isBlank()) {
+ "$key = $value"
+ } else {
+ existing.trimEnd(';') + ";$key = $value"
+ }
+ envVars.put("DXVK_CONFIG", newConfig)
+ }
+}
diff --git a/app/src/main/runtime/display/environment/ImageFsInstaller.java b/app/src/main/runtime/display/environment/ImageFsInstaller.java
index b3d1e9ea4..d97687d55 100644
--- a/app/src/main/runtime/display/environment/ImageFsInstaller.java
+++ b/app/src/main/runtime/display/environment/ImageFsInstaller.java
@@ -387,6 +387,71 @@ private static void installGuestExtras(
}
chmodIfExists(new File(rootDir, "usr/lib/libredirect.so"));
chmodIfExists(new File(rootDir, "usr/lib/libredirect-bionic.so"));
+
+ // Aurora Mali Vulkan Sanitizer — deploy .so + JSON manifest to imagefs/usr/lib/.
+ // The Vulkan loader reads VK_LAYER_PATH at instance creation and matches
+ // VK_INSTANCE_LAYERS entries against the "name" field in each manifest.
+ // If either file is missing, AuroraSanitizerHelper falls back to DXVK_CONFIG
+ // mode (no native layer). This is non-fatal — continue if deployment fails.
+ deployAuroraMaliSanitizer(context, rootDir);
+ }
+
+ /**
+ * Copies libaurora_mali_sanitizer.so from the APK's nativeLibraryDir to
+ * imagefs/usr/lib/, and writes the Vulkan layer manifest JSON next to it.
+ * The .so is built by CMake (see app/src/main/cpp/CMakeLists.txt) and
+ * packaged into the APK's lib/arm64-v8a/ automatically by AGP.
+ *
+ * The manifest name field MUST match AuroraSanitizerHelper.LAYER_NAME
+ * ("Aurora_Mali_Sanitizer") or the Vulkan loader silently skips the layer.
+ */
+ private static void deployAuroraMaliSanitizer(Context context, File rootDir) {
+ try {
+ File nativeLibDir = new File(context.getApplicationInfo().nativeLibraryDir);
+ File apkSo = new File(nativeLibDir, "libaurora_mali_sanitizer.so");
+ File targetSo = new File(rootDir, "usr/lib/libaurora_mali_sanitizer.so");
+ File targetJson = new File(rootDir, "usr/lib/aurora_mali_sanitizer.json");
+
+ if (apkSo.exists()) {
+ FileUtils.copy(apkSo, targetSo);
+ FileUtils.chmod(targetSo, 0755);
+ Log.i("ImageFsInstaller", "Aurora: deployed Mali sanitizer .so to " + targetSo);
+
+ // Write the manifest. Name MUST match AuroraSanitizerHelper.LAYER_NAME.
+ String manifest = "{\n"
+ + " \"file_format_version\": \"1.1.0\",\n"
+ + " \"layer\": {\n"
+ + " \"name\": \"Aurora_Mali_Sanitizer\",\n"
+ + " \"type\": \"GLOBAL\",\n"
+ + " \"library_path\": \"libaurora_mali_sanitizer.so\",\n"
+ + " \"api_version\": \"1.3.0\",\n"
+ + " \"implementation_version\": \"1\",\n"
+ + " \"description\": \"Aurora Mali Vulkan Sanitizer (WinNative fork) - 5 rules + B4/B6 fixes\",\n"
+ + " \"functions\": {\n"
+ + " \"vkNegotiateLoaderLayerInterfaceVersion\": \"vkNegotiateLoaderLayerInterfaceVersion\",\n"
+ + " \"vkGetInstanceProcAddr\": \"vkGetInstanceProcAddr\",\n"
+ + " \"vkGetDeviceProcAddr\": \"vkGetDeviceProcAddr\"\n"
+ + " },\n"
+ + " \"instance_extensions\": [],\n"
+ + " \"device_extensions\": [],\n"
+ + " \"enable_environment\": {\n"
+ + " \"AURORA_MALI_SANITIZER\": \"1\"\n"
+ + " },\n"
+ + " \"disable_environment\": {\n"
+ + " \"AURORA_MALI_SANITIZER_DISABLE\": \"1\"\n"
+ + " }\n"
+ + " }\n"
+ + "}\n";
+ FileUtils.writeString(targetJson, manifest);
+ Log.i("ImageFsInstaller", "Aurora: deployed Mali sanitizer JSON to " + targetJson);
+ } else {
+ Log.i("ImageFsInstaller",
+ "Aurora: Mali sanitizer .so not found in APK — using DXVK_CONFIG fallback only");
+ }
+ } catch (Exception e) {
+ Log.w("ImageFsInstaller",
+ "Aurora: Mali sanitizer deployment failed (non-fatal): " + e.getMessage());
+ }
}
private static void chmodIfExists(File file) {
diff --git a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java
index 423f9458d..92d6c796c 100644
--- a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java
+++ b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java
@@ -1136,6 +1136,32 @@ private int execGuestProgram() {
FileUtils.chmod(box64File, 0755);
}
+ // =========================================================================
+ // Aurora integration — applied AFTER envVars.putAll(this.envVars) so user
+ // and container env vars are merged first, and our tier presets only fill
+ // in gaps via putIfAbsent (user overrides still win).
+ //
+ // Order matters:
+ // 1. TierPresets — sets mid-end Box64/DXVK/Wine defaults
+ // 2. NTSyncHelper — overrides esync if kernel supports /dev/ntsync
+ // 3. MemoryBudget — caps DXVK cache, disables PulseAudio on low RAM
+ // 4. Mali Sanitizer — last, so its DXVK_CONFIG fallback appends cleanly
+ // to whatever DXVK_CONFIG the prior steps produced.
+ // =========================================================================
+ try {
+ com.winlator.cmod.runtime.aurora.TierPresets.apply(
+ environment.getContext(), envVars, container);
+ com.winlator.cmod.runtime.aurora.NTSyncHelper.applyIfSupported(envVars);
+ com.winlator.cmod.runtime.aurora.MemoryBudgetHelper.apply(
+ environment.getContext(), envVars);
+ com.winlator.cmod.runtime.aurora.AuroraSanitizerHelper.setupSanitizer(
+ environment.getContext(), envVars, imageFs,
+ com.winlator.cmod.runtime.system.GPUInformation.getRenderer(null, null));
+ } catch (Exception e) {
+ // Aurora helpers must never block game launch — log and continue.
+ Log.w("GuestProgramLauncherComponent", "Aurora helpers failed (non-fatal): " + e.getMessage());
+ }
+
Log.d(
"GuestProgramLauncherComponent",
"Launch env excerpt: "
diff --git a/app/src/main/runtime/wine/EnvVars.java b/app/src/main/runtime/wine/EnvVars.java
index e58fb524b..8db1575b9 100644
--- a/app/src/main/runtime/wine/EnvVars.java
+++ b/app/src/main/runtime/wine/EnvVars.java
@@ -17,6 +17,14 @@ public void put(String name, Object value) {
data.put(name, String.valueOf(value));
}
+ /**
+ * Sets name=value only if name is not already present. Used by Aurora tier
+ * presets so user/container overrides are preserved.
+ */
+ public void putIfAbsent(String name, Object value) {
+ data.putIfAbsent(name, String.valueOf(value));
+ }
+
public void putAll(String values) {
if (values == null || values.isEmpty()) return;
String[] parts = values.split(" ");
diff --git a/docs/AUDIT.md b/docs/AUDIT.md
new file mode 100644
index 000000000..4aa4944ba
--- /dev/null
+++ b/docs/AUDIT.md
@@ -0,0 +1,79 @@
+# Aurora — Deep Audit (Original Aurora Emulator)
+
+**Date:** 2026-06-24
+**Audited repo:** `boiniArun2006/Aurora-emulator` at commit `bbe084d`
+**Auditor methodology:** 5 parallel deep-audit subagents covering Mali sanitizer C++, texture/mesh engines, Kotlin integration, GameNative comparison, and build/CI/assets.
+
+## Summary
+
+Of the ~150K lines of Kotlin+Java in the original Aurora repo, ~99% is inherited unchanged from GameNative/Winlator. Aurora's actual contribution is ~1.2K LOC of helper classes + ~5K LOC of original C++ glue (on top of vendored Basis Universal + meshoptimizer).
+
+The repo was force-pushed as a single squash commit (`7f6722a` → `bbe084d`), so there's no history to audit. The README links to a different repo (`Aurora-emulator-smpl`) for CI/releases.
+
+Of the 6 marketed "Aurora engines", only **2.5 actually work end-to-end**.
+
+## Engine status
+
+| Engine | Status | Notes |
+|---|---|---|
+| **Phase 6 — Mali Vulkan Sanitizer** | ✅ Real (with bugs) | Ported to this fork with B4/B6 fixes. |
+| **Phase 1 — AOT Texture Transcoder** | ❌ Dead code | Scans for `.ktx2` files; no PC game ships `.ktx2`. Even the matching encoder writes zstd-supercompressed KTX2 but the transcoder is compiled with `BASISD_SUPPORT_KTX2_ZSTD=0`. ASTC output format is invalid for cubemaps/mipmaps. **Dropped from this fork.** |
+| **Phase 2 — Mesh LOD Engine** | ❌ Dead code | Scans for `.obj` files; no PC game ships `.obj`. Even if it found meshes, the parser is position-only (drops UVs/normals). No Wine/BSA/VPK hook to load simplified LODs back into the game. **Dropped from this fork.** |
+| **Phase 3 — Markov Prefetcher** | ⚠️ Infrastructure-only | `MarkovModel.kt` is fully implemented but `libaurora_prefetch.so` (the LD_PRELOAD shim that would log file accesses) does not exist. Env vars are set but consumed by nothing. **Dropped from this fork** until someone builds the shim. |
+| **Phase 4 — Shader Cache** | 🟡 Half-stub | Per-game `DXVK_STATE_CACHE_PATH` works. Cloud sync returns `false` hardcoded. Backend doesn't exist. Only the local half is ported. |
+| **Phase 7a — Auto-Installer** | ✅ Real (with bug) | 9 pre-install steps ported from Python. Has a marker-on-failure bug (B8) that the latest Aurora has fixed. |
+
+## Bugs found (16 total: 5 Critical, 7 High, 4 Medium)
+
+### Critical
+
+| ID | Bug | Status in this fork |
+|---|---|---|
+| B1 | Mali sanitizer layer name mismatch (`"Aurora_Mali_Sanitizer"` vs `"Aurora Mali Sanitizer"`) — layer never loads | ✅ Fixed (inherited from latest Aurora) |
+| B2 | `DXVK_CONFIG` fallback silently overwritten by `envVars.putAll(this.envVars)` | ✅ Fixed (inherited; helpers called after merge) |
+| B3 | `aurora_mali_sanitizer.cpp` won't compile under glibc (`__android_log_print` unconditionally included) | ✅ Fixed (inherited; `#ifdef __BIONIC__`) |
+| B4 | `vkCmdBindDescriptorSets` dynamic-offset handling broken — chunks 2+ get 0 offsets | ✅ Fixed (this fork adds tracking hooks) |
+| B5 | Dispatch tables not thread-safe | ✅ Fixed (inherited; `std::shared_mutex`) |
+
+### High
+
+| ID | Bug | Status in this fork |
+|---|---|---|
+| B6 | Multi-device broken; hooks hard-code `g_device_dispatch[0]` | ✅ Fixed (this fork adds `VkCommandBuffer → VkDevice` map) |
+| B7 | README claim "10 rules" wrong — only 5 implemented, 1 broken | ✅ Honest (this fork says "5 rules") |
+| B8 | Auto-installer writes marker on installer failure | ⚠️ Not ported yet (will inherit latest Aurora's fix when ported) |
+| B9 | `Immortalis` GPUs not detected | ✅ Fixed (inherited) |
+| B10 | zstd compile-flag mismatch in texture engine | N/A (texture engine dropped) |
+| B11 | ASTC output format invalid for non-trivial textures | N/A (texture engine dropped) |
+| B12 | License violation (MIT root vs GPL-3.0 android/) | N/A (this fork is GPL-3.0) |
+| B13 | No release signing | ✅ Inherited from WinNative |
+| B14 | PostHog privacy violation | N/A (WinNative doesn't use PostHog) |
+
+### Medium
+
+| ID | Bug | Status in this fork |
+|---|---|---|
+| B15 | `orgJson` CVE-2024-57699 | ⚠️ To check against WinNative's deps |
+| B16 | CI release spam | ✅ Inherited WinNative's tag-triggered releases |
+
+## What this fork ports
+
+- Mali Vulkan Sanitizer (5 rules, B4+B6 fixes on top)
+- AuroraSanitizerHelper (Kotlin)
+- Per-game shader cache (local half only)
+
+## What this fork drops
+
+- Phase 1 texture engine (dead code)
+- Phase 2 mesh engine (dead code)
+- Phase 3 Markov prefetcher (no shim exists)
+- Phase 4 cloud shader sync (no backend)
+
+## What this fork adds (not in Aurora)
+
+- `DeviceTier` — HIGH/MID/LOW classification
+- `TierPresets` — per-tier env-var defaults
+- `NTSyncHelper` — auto-detect `/dev/ntsync`
+- `MemoryBudgetHelper` — RAM-aware cache sizing
+
+See [../README.md](../README.md) and [TIER_TUNING.md](TIER_TUNING.md).
diff --git a/docs/MALI_SANITIZER.md b/docs/MALI_SANITIZER.md
new file mode 100644
index 000000000..7b1f39a5c
--- /dev/null
+++ b/docs/MALI_SANITIZER.md
@@ -0,0 +1,187 @@
+# Mali Vulkan Sanitizer
+
+A Vulkan layer that sits between DXVK and the Mali (or Immortalis) Vulkan driver. Filters crash-causing extensions and works around driver bugs.
+
+## Rules (5 total)
+
+| # | Rule | What it does |
+|---|---|---|
+| 1 | Blacklist `VK_EXT_descriptor_indexing` | Mali crashes when DXVK requests bindless-style descriptor indexing. |
+| 2 | Blacklist `VK_EXT_fragment_density_map` | Mali render corruption with this extension. |
+| 3 | Blacklist `VK_KHR_shader_subgroup` | Mali silently produces wrong results for some subgroup ops. |
+| 4 | Blacklist `VK_EXT_graphics_pipeline_library` | Mali driver bug with pipeline libraries. |
+| 5 | Split `vkCmdBindDescriptorSets` at >4 sets | Mali hard-crashes on `>4` descriptor sets in a single bind call. |
+
+## Architecture
+
+The layer is a standard Khronos Vulkan layer. It implements:
+
+- `vkNegotiateLoaderLayerInterfaceVersion` (v2) — loader negotiation
+- `vkGetInstanceProcAddr` / `vkGetDeviceProcAddr` — function dispatch
+- Hooks for: `vkCreateInstance`, `vkDestroyInstance`, `vkCreateDevice`, `vkDestroyDevice`, `vkCmdBindDescriptorSets`, `vkCreateDescriptorSetLayout`, `vkDestroyDescriptorSetLayout`, `vkAllocateDescriptorSets`, `vkFreeDescriptorSets`, `vkAllocateCommandBuffers`, `vkFreeCommandBuffers`
+
+### Dispatch tables
+
+Per-instance and per-device dispatch tables are stored in `std::vector>` protected by `std::shared_mutex` (B5 fix). DXVK uses worker threads, so concurrent `vkCreateDevice` + `vkCmdBindDescriptorSets` is possible.
+
+### Layer manifest
+
+The Vulkan loader matches `VK_INSTANCE_LAYERS` entries against the `name` field in the JSON manifest (not the `.so` filename). Our manifest uses `"name": "Aurora_Mali_Sanitizer"` and `AuroraSanitizerHelper.LAYER_NAME` matches it exactly (B1 fix).
+
+The manifest is generated at install time by `ImageFsInstaller.deployAuroraMaliSanitizer()` and written to `imagefs/usr/lib/aurora_mali_sanitizer.json` next to the `.so`.
+
+## B4 fix: correct dynamic-offset slicing
+
+### The bug (original Aurora)
+
+When `vkCmdBindDescriptorSets` is called with `>4` sets, the original Aurora code splits the call into chunks of 4 sets each. But for dynamic offsets (`pDynamicOffsets`), it dumps all N offsets on chunk 1 and zero on chunks 2+:
+
+```cpp
+// Original (broken) code:
+uint32_t dynOffs = (offset == 0) ? dynamicOffsetCount : 0;
+const uint32_t* dynPtr = (offset == 0) ? pDynamicOffsets : nullptr;
+```
+
+This is wrong because the Vulkan spec requires dynamic offsets to be consumed **in order across all sets in the call**: set 0's dynamic bindings first, then set 1's, etc. By dumping all offsets on chunk 1, the driver applies offsets for sets 4+ to bindings that don't exist on sets 0–3, and sets 4+ get offset 0 (silently binding dynamic UBOs at the wrong byte offset).
+
+**Result:** silent wrong rendering for any draw that uses sets 4+ with dynamic UBO bindings. Not a crash — just wrong textures or wrong constants.
+
+### The fix
+
+We cache the per-set dynamic-offset count:
+
+1. **At `vkCreateDescriptorSetLayout`:** count `VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC` and `VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC` bindings in the create info. Store in `g_layout_dynamic_counts` (map: `VkDescriptorSetLayout → uint32_t`).
+2. **At `vkAllocateDescriptorSets`:** for each allocated set, look up its layout's dynamic count and store in `g_set_dynamic_counts` (map: `VkDescriptorSet → uint32_t`).
+3. **At `vkCmdBindDescriptorSets`:** walk `pDescriptorSets` in order, look up each set's cached dynamic count, and build a per-set cumulative offset-index array. Then for each chunk, pass the correct sub-slice of `pDynamicOffsets`.
+
+```cpp
+// Fixed code:
+setDynOffsetStart[0] = 0;
+for (uint32_t i = 0; i < descriptorSetCount; i++) {
+ setDynOffsets[i] = lookup_set_dynamic_count(pDescriptorSets[i]);
+ setDynOffsetStart[i + 1] = setDynOffsetStart[i] + setDynOffsets[i];
+}
+// ... in the chunk loop:
+uint32_t chunkDynStart = setDynOffsetStart[offset];
+uint32_t chunkDynCount = setDynOffsetStart[offset + chunk] - chunkDynStart;
+const uint32_t* chunkDynPtr = (chunkDynCount > 0)
+ ? (pDynamicOffsets + chunkDynStart) : nullptr;
+```
+
+### Fail-safe
+
+If the cached total doesn't match the caller's `dynamicOffsetCount` (e.g. the app used a layout we never saw created, or our cache is stale), we fall back to the old behavior (all offsets on chunk 0) and log a warning. Better than a wrong slice.
+
+### Defensive cap
+
+`descriptorSetCount > 64` falls back to the old behavior. Vulkan's `maxBoundDescriptorSets` is typically ≤ 8, so 64 is a generous cap. Without it, the stack-allocated `setDynOffsets[64]` could overflow.
+
+## B6 fix: per-command-buffer device resolution
+
+### The bug (original Aurora)
+
+The original `aurora_vkCmdBindDescriptorSets` resolved the device by hard-coding `g_device_dispatch[0]`:
+
+```cpp
+// Original (broken) code:
+DeviceDispatch* dispatch = nullptr;
+if (!g_device_dispatch.empty()) {
+ dispatch = &g_device_dispatch[0].second;
+}
+```
+
+This works for single-device scenarios (the common case) but breaks for:
+- VR runtimes that create a second `VkDevice` on a different ICD
+- External GPU setups (Mali + swrast)
+- Any app that creates multiple `VkDevice`s from one `VkInstance`
+
+In those cases, `g_device_dispatch[0]` might be the wrong device, and the function pointer call goes to the wrong ICD → undefined behavior / crash.
+
+### The fix
+
+We maintain a `VkCommandBuffer → VkDevice` map:
+
+1. **At `vkAllocateCommandBuffers`:** for each allocated command buffer, store `{commandBuffer, device}` in `g_cmd_buf_to_device`.
+2. **At `vkFreeCommandBuffers`:** remove the entries.
+3. **At `vkDestroyDevice`:** clean up all entries for that device (defensive).
+4. **At `vkCmdBindDescriptorSets`:** look up the device by command buffer:
+ ```cpp
+ VkDevice device = lookup_device_for_command_buffer(commandBuffer);
+ DeviceDispatch* dispatch = (device != VK_NULL_HANDLE)
+ ? get_device_dispatch(device)
+ : (g_device_dispatch.empty() ? nullptr : &g_device_dispatch[0].second);
+ ```
+ The fallback to `[0]` is defensive — command buffers we never saw allocated (shouldn't happen per Vulkan spec, but be safe).
+
+## Thread safety
+
+All four maps (`g_instance_dispatch`, `g_device_dispatch`, `g_cmd_buf_to_device`, `g_layout_dynamic_counts`, `g_set_dynamic_counts`) are `std::vector>` protected by `std::shared_mutex`. Reads take a shared lock; writes take a unique lock.
+
+The linear-search complexity is O(n) where n is typically 1 (one instance, one device, a few hundred command buffers/layouts). For workloads with thousands of command buffers, a hash map would be faster — but the lock overhead would dominate at small n. We keep the linear search for simplicity.
+
+## Stats
+
+Two atomic counters (B5 fix — original used plain `int`):
+- `stats_extensions_blacklisted` — incremented per blacklisted extension at `vkCreateDevice`
+- `stats_descriptor_sets_split` — incremented per split `vkCmdBindDescriptorSets` call
+
+Logged at `vkNegotiateLoaderLayerInterfaceVersion` and `vkDestroyDevice`.
+
+## Bionic vs glibc
+
+The layer is compiled by NDK (bionic) for the Android system Vulkan loader. The CMake comment in the original Aurora said it must be recompiled with glibc for use inside PRoot — that's because the layer is `dlopen`'d by the glibc-linked Vulkan loader inside Wine's PRoot environment, and a bionic-linked `.so` can't be loaded by glibc.
+
+B3 fix: logging is gated behind `#ifdef __BIONIC__`:
+
+```cpp
+#ifdef __BIONIC__
+#include
+#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
+// ...
+#else
+#define ALOGI(...) do { fprintf(stderr, "[" TAG "] INFO: " __VA_ARGS__); fprintf(stderr, "\n"); } while(0)
+// ...
+#endif
+```
+
+This lets the same source compile under both bionic (NDK) and glibc (cross-compiler) without changes. The WinNative fork currently only builds the bionic variant; a glibc cross-compile target can be added to CMake if needed.
+
+## DXVK_CONFIG fallback
+
+If the `.so` isn't deployed (e.g. CMake build failed, or the layer was stripped by R8), `AuroraSanitizerHelper` falls back to modifying `DXVK_CONFIG`:
+
+```
+dxvk.useDescriptorIndexing = False
+dxvk.usePipelineLibrary = False
+d3d11.maxFeatureLevel = 11_0
+d3d11.relaxedBarriers = True
+dxvk.shaderUseSubgroupOps = False
+```
+
+This achieves similar results without a native layer — DXVK won't request the blacklisted extensions in the first place. The trade-off: the descriptor-set split (rule 5) can't be done via DXVK config, so games that bind `>4` sets will still crash. The full layer is preferred when available.
+
+## Verifying the layer is loaded
+
+After launching a game on a Mali device, check logcat:
+
+```bash
+adb logcat -s AuroraMaliSanitizer AuroraSanitizer
+```
+
+You should see:
+```
+AuroraSanitizer: Mali/Immortalis GPU detected: Mali-G610 — enabling sanitizer
+AuroraSanitizer: Sanitizer .so + JSON manifest found — full Vulkan layer mode
+AuroraMaliSanitizer: === Aurora Mali Sanitizer Layer Loaded (WinNative fork) ===
+AuroraMaliSanitizer: Rules: 4 blacklisted extensions, descriptor set split at 4 (B4: dynamic offsets sliced correctly)
+AuroraMaliSanitizer: Tracking hooks: vkAllocateCommandBuffers/vkFreeCommandBuffers (B6), vkCreateDescriptorSetLayout/vkDestroyDescriptorSetLayout (B4), vkAllocateDescriptorSets/vkFreeDescriptorSets (B4)
+```
+
+If you see `Sanitizer .so not found — using DXVK_CONFIG fallback`, the layer didn't load and only the DXVK config workarounds are active. Check that `libaurora_mali_sanitizer.so` is in the APK's `lib/arm64-v8a/` and was deployed to `imagefs/usr/lib/`.
+
+## Known limitations
+
+- The B4 fix uses a static cap of 64 sets per `vkCmdBindDescriptorSets` call. Vulkan's `maxBoundDescriptorSets` is typically ≤ 8, so this is generous, but if a game ever exceeds 64 sets in one bind (impossible per spec), it falls back to the old unsafe behavior.
+- The B4 fix assumes the app creates its `VkDescriptorSetLayout`s through our hook. If an app uses layouts created by a different Vulkan instance/layer chain (shouldn't happen per spec), the cache misses and we fall back to the old behavior.
+- The layer doesn't intercept `vkAllocateMemory` (no >4GB warning), `vkCreateImage` (no compressed format filter), `vkCmdPipelineBarrier` (no TBDR hint injection), `vkCreateGraphicsPipelines` (no dynamic-state reject). These were marked as "missing hooks" (B7) in the audit but not implemented; they're future work.
+- The glibc cross-compile target isn't in the CMake yet. The bionic `.so` works for the Android system Vulkan loader but won't load inside PRoot's glibc environment. For PRoot, the DXVK_CONFIG fallback is used.
diff --git a/docs/TIER_TUNING.md b/docs/TIER_TUNING.md
new file mode 100644
index 000000000..22fdf5c91
--- /dev/null
+++ b/docs/TIER_TUNING.md
@@ -0,0 +1,108 @@
+# Tier Tuning Rationale
+
+This document explains why each env var in `TierPresets.kt` was chosen for each tier. Every setting here should be backed by either a benchmark, a known driver quirk, or a spec requirement.
+
+## Tier classification
+
+| Tier | SoC signals | RAM | GPU signals |
+|---|---|---|---|
+| **HIGH** | Snapdragon 8 Gen 2+ (`sm8[4-9]xx`), Dimensity 9xxx | 12 GB+ | Immortalis, Adreno 7xx+ |
+| **MID** | Snapdragon 7xx (`sm7[0-9]xx`), Dimensity 7xxx/8xxx | 6–11 GB | Mali-G57/G610/G77/G78 (Valhall, not Bifrost) |
+| **LOW** | Snapdragon 4xx/6xx, anything older | <6 GB | Mali-G52/G72 (Bifrost — known driver bugs) |
+
+The classifier scores the device and picks the tier. When uncertain, defaults to MID (safer).
+
+## HIGH tier
+
+| Env var | Value | Why |
+|---|---|---|
+| `dxvk.useDescriptorIndexing` | `True` | Flagship Adreno/Mali drivers support `VK_EXT_descriptor_indexing` correctly. Enables bindless-style descriptor sets, faster for modern DX11 games. |
+| `d3d11.maxFeatureLevel` | `12_0` | Flagship GPUs handle FL 12_0 (tiled resources, conservative raster). Lower FL caps visual quality. |
+
+HIGH tier otherwise lets WinNative's defaults stand — the hardware can handle them.
+
+## MID tier
+
+### Box64 dynarec
+
+| Env var | Value | Why |
+|---|---|---|
+| `BOX64_DYNAREC_BIGBLOCK` | `1` | Default is `0` (conservative). Setting `1` lets the dynarec build larger translation blocks, reducing JIT overhead at the cost of slightly higher memory. Net win on mid-end where JIT throughput is the bottleneck. |
+| `BOX64_DYNAREC_STRONGMEM` | `2` | Default `1`. `2` = stronger memory ordering, needed for Mali drivers which expect stricter ordering than Adreno. Without this, some games exhibit texture flicker. |
+| `BOX64_DYNAREC_CALLRET` | `1` | Optimizes call/ret prediction. Safe on all modern ARM64 cores. |
+| `BOX64_DYNAREC_FASTNAN` | `1` | Skips NaN normalization. PC games rarely depend on NaN behavior; skipping is ~5% faster. |
+| `BOX64_DYNAREC_FASTROUND` | `1` | Skips x86 rounding mode emulation. Same rationale as FASTNAN. |
+| `BOX64_DYNAREC_SAFEFLAGS` | `1` | Minimal flag preservation. Lower values risk subtle bugs in flag-dependent code; `1` is the safe minimum. |
+
+### DXVK
+
+| Env var | Value | Why |
+|---|---|---|
+| `d3d11.maxFeatureLevel` | `11_0` | FL 12_0 requires subgroup ops which Mali Valhall implements unreliably. FL 11_0 is the safe cap. |
+| `d3d11.relaxedBarriers` | `True` | Mali is tile-based deferred renderer (TBDR). Relaxed barriers let DXVK skip explicit barriers that IMR (immediate mode) GPUs need but TBDR doesn't, saving tile resolves. |
+| `dxvk.useDescriptorIndexing` | `False` | Mali Valhall's `VK_EXT_descriptor_indexing` has known crashes with >4 descriptor sets (the sanitizer's rule 5 is the workaround, but disabling the extension entirely is safer). |
+| `dxvk.shaderUseSubgroupOps` | `False` | Mali Valhall produces silently wrong results for some subgroup ops. Disable. |
+
+### Wine sync
+
+| Env var | Value | Why |
+|---|---|---|
+| `WINEESYNC` | `1` | Esync is faster than Wine's classical sync primitives on most kernels. NTSync (if kernel supports it — see `NTSyncHelper`) overrides this. |
+| `WINEFSYNC` | (removed) | Fsync requires kernel module that isn't standard on Android kernels. WinNative already removes this; we keep that. |
+| `PROTON_NO_FSYNC` | `1` | Explicitly disable fsync attempts. |
+
+### Shader cache
+
+| Env var | Value | Why |
+|---|---|---|
+| `MESA_SHADER_CACHE_MAX_SIZE` | `512MB` | Caps the GL shader cache. Without a cap, the cache can grow unbounded on long sessions and trigger Android's low-memory killer. 512 MB fits ~10K shaders, enough for most games. |
+| `MESA_SHADER_CACHE_DISABLE` | `false` | Explicitly enable caching. Some OEM ROMs disable it by default; this overrides. |
+
+## LOW tier
+
+All MID settings, plus:
+
+| Env var | Value | Why |
+|---|---|---|
+| `BOX64_DYNAREC_STRONGMEM` | `3` | Highest memory ordering. Bifrost Mali drivers are particularly buggy; strongest ordering is the only safe setting. |
+| `d3d11.maxFeatureLevel` | `10_0` | Drop to DX10. Bifrost can't reliably do FL 11_0 tessellation or compute shaders. |
+| `dxvk.usePipelineLibrary` | `False` | `VK_EXT_graphics_pipeline_library` is blacklisted by the sanitizer anyway; this DXVK flag ensures it's not requested even via the config path. |
+| `MESA_SHADER_CACHE_MAX_SIZE` | `256MB` | Smaller cache for smaller RAM. |
+
+## What we deliberately don't set
+
+- **`BOX64_DYNAREC`** — always `1` (WinNative default). Disabling dynarec makes games unplayable.
+- **`MESA_GLSL_CACHE_DIR`** — WinNative sets this; we don't override.
+- **`DXVK_HUD`** — user choice (debug overlay).
+- **`WINEDEBUG`** — user choice (Wine debug channels).
+- **`WINEDLLOVERRIDES`** — game-specific; handled by `GameFixes.java`.
+
+## User overrides
+
+All tier presets use `EnvVars.putIfAbsent()` — if the user or container config has already set an env var, we don't overwrite it. This means a user who knows their device can override any tier preset by setting the env var explicitly in the container config.
+
+## Verifying the presets
+
+After launching a game, check logcat:
+
+```bash
+adb logcat -s AuroraTierPresets
+```
+
+To see what env vars are actually set for a game, add `DXVK_HUD=version,api` to the container config and look at the in-game overlay.
+
+## Benchmarks needed
+
+The current presets are based on:
+- Known Mali driver bugs (Mali-001 through Mali-005 in the sanitizer rule DB)
+- Box64 documentation and community testing
+- DXVK configuration reference
+- General emulator-community knowledge
+
+**Real-world benchmark data is needed** for:
+- Snapdragon 7 Gen 1 vs 7 Gen 2 vs 7+ Gen 2 (do they all classify as MID?)
+- Mali-G610 (RK3588) — does MID work, or does it need its own tier?
+- Immortalis-G720 — is HIGH correct, or does it need MID's relaxedBarriers?
+- 6 GB vs 8 GB RAM cutoff for MemoryBudget
+
+If you have a mid-end device and can run benchmarks, please share the results in a PR to this document.