summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/android-build.yml79
-rw-r--r--.github/workflows/android-merge.js218
-rw-r--r--.github/workflows/android-publish.yml57
-rw-r--r--.github/workflows/verify.yml4
-rw-r--r--src/android/app/src/main/AndroidManifest.xml1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt35
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt69
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt18
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt410
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt120
-rw-r--r--src/android/app/src/main/res/drawable/button_l3.xml128
-rw-r--r--src/android/app/src/main/res/drawable/button_l3_depressed.xml75
-rw-r--r--src/android/app/src/main/res/drawable/button_r3.xml128
-rw-r--r--src/android/app/src/main/res/drawable/button_r3_depressed.xml75
-rw-r--r--src/android/app/src/main/res/values/arrays.xml2
-rw-r--r--src/android/app/src/main/res/values/integers.xml12
-rw-r--r--src/android/app/src/main/res/values/strings.xml1
-rw-r--r--src/core/core.cpp32
-rw-r--r--src/core/core.h11
-rw-r--r--src/core/core_timing.cpp9
-rw-r--r--src/core/core_timing.h8
-rw-r--r--src/core/file_sys/fsmitm_romfsbuild.cpp38
-rw-r--r--src/core/gpu_dirty_memory_manager.h122
-rw-r--r--src/core/hid/emulated_controller.cpp12
-rw-r--r--src/core/hid/emulated_controller.h8
-rw-r--r--src/core/hle/kernel/k_thread.h10
-rw-r--r--src/core/hle/kernel/svc/svc_ipc.cpp37
-rw-r--r--src/core/hle/kernel/svc/svc_synchronization.cpp41
-rw-r--r--src/core/hle/service/nfc/common/device.cpp32
-rw-r--r--src/core/memory.cpp40
-rw-r--r--src/core/memory.h6
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h39
-rw-r--r--src/video_core/buffer_cache/buffer_cache_base.h5
-rw-r--r--src/video_core/compatible_formats.cpp6
-rw-r--r--src/video_core/fence_manager.h2
-rw-r--r--src/video_core/gpu.cpp12
-rw-r--r--src/video_core/gpu.h4
-rw-r--r--src/video_core/gpu_thread.cpp6
-rw-r--r--src/video_core/rasterizer_interface.h4
-rw-r--r--src/video_core/renderer_null/null_rasterizer.cpp5
-rw-r--r--src/video_core/renderer_null/null_rasterizer.h3
-rw-r--r--src/video_core/renderer_opengl/gl_rasterizer.cpp35
-rw-r--r--src/video_core/renderer_opengl/gl_rasterizer.h3
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp34
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.h3
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp45
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h5
-rw-r--r--src/video_core/shader_cache.cpp2
-rw-r--r--src/video_core/shader_cache.h2
-rw-r--r--src/video_core/texture_cache/texture_cache.h16
-rw-r--r--src/video_core/texture_cache/types.h1
-rw-r--r--src/video_core/texture_cache/util.cpp9
-rw-r--r--src/video_core/vulkan_common/vulkan_device.cpp43
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h14
-rw-r--r--src/yuzu/main.cpp4
-rw-r--r--src/yuzu_cmd/yuzu.cpp8
58 files changed, 1795 insertions, 359 deletions
diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml
new file mode 100644
index 000000000..e639e965a
--- /dev/null
+++ b/.github/workflows/android-build.yml
@@ -0,0 +1,79 @@
+# SPDX-FileCopyrightText: 2022 yuzu Emulator Project
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+name: 'yuzu-android-build'
+
+on:
+ push:
+ tags: [ "*" ]
+
+jobs:
+ android:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ submodules: recursive
+ fetch-depth: 0
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ - name: Set up cache
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ ~/.ccache
+ key: ${{ runner.os }}-android-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-android-
+ - name: Query tag name
+ uses: olegtarasov/get-tag@v2.1.2
+ id: tagName
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y ccache apksigner glslang-dev glslang-tools
+ - name: Build
+ run: ./.ci/scripts/android/build.sh
+ - name: Copy and sign artifacts
+ env:
+ ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
+ ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
+ ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }}
+ run: ./.ci/scripts/android/upload.sh
+ - name: Upload
+ uses: actions/upload-artifact@v3
+ with:
+ name: android
+ path: artifacts/
+ # release steps
+ release-android:
+ runs-on: ubuntu-latest
+ needs: [android]
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/download-artifact@v3
+ - name: Query tag name
+ uses: olegtarasov/get-tag@v2.1.2
+ id: tagName
+ - name: Create release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ steps.tagName.outputs.tag }}
+ release_name: ${{ steps.tagName.outputs.tag }}
+ draft: false
+ prerelease: false
+ - name: Upload artifacts
+ uses: alexellis/upload-assets@0.2.3
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ asset_paths: '["./**/*.apk","./**/*.aab"]'
diff --git a/.github/workflows/android-merge.js b/.github/workflows/android-merge.js
new file mode 100644
index 000000000..7e02dc9e5
--- /dev/null
+++ b/.github/workflows/android-merge.js
@@ -0,0 +1,218 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Note: This is a GitHub Actions script
+// It is not meant to be executed directly on your machine without modifications
+
+const fs = require("fs");
+// which label to check for changes
+const CHANGE_LABEL = 'android-merge';
+// how far back in time should we consider the changes are "recent"? (default: 24 hours)
+const DETECTION_TIME_FRAME = (parseInt(process.env.DETECTION_TIME_FRAME)) || (24 * 3600 * 1000);
+
+async function checkBaseChanges(github, context) {
+ // query the commit date of the latest commit on this branch
+ const query = `query($owner:String!, $name:String!, $ref:String!) {
+ repository(name:$name, owner:$owner) {
+ ref(qualifiedName:$ref) {
+ target {
+ ... on Commit { id pushedDate oid }
+ }
+ }
+ }
+ }`;
+ const variables = {
+ owner: context.repo.owner,
+ name: context.repo.repo,
+ ref: 'refs/heads/master',
+ };
+ const result = await github.graphql(query, variables);
+ const pushedAt = result.repository.ref.target.pushedDate;
+ console.log(`Last commit pushed at ${pushedAt}.`);
+ const delta = new Date() - new Date(pushedAt);
+ if (delta <= DETECTION_TIME_FRAME) {
+ console.info('New changes detected, triggering a new build.');
+ return true;
+ }
+ console.info('No new changes detected.');
+ return false;
+}
+
+async function checkAndroidChanges(github, context) {
+ if (checkBaseChanges(github, context)) return true;
+ const query = `query($owner:String!, $name:String!, $label:String!) {
+ repository(name:$name, owner:$owner) {
+ pullRequests(labels: [$label], states: OPEN, first: 100) {
+ nodes { number headRepository { pushedAt } }
+ }
+ }
+ }`;
+ const variables = {
+ owner: context.repo.owner,
+ name: context.repo.repo,
+ label: CHANGE_LABEL,
+ };
+ const result = await github.graphql(query, variables);
+ const pulls = result.repository.pullRequests.nodes;
+ for (let i = 0; i < pulls.length; i++) {
+ let pull = pulls[i];
+ if (new Date() - new Date(pull.headRepository.pushedAt) <= DETECTION_TIME_FRAME) {
+ console.info(`${pull.number} updated at ${pull.headRepository.pushedAt}`);
+ return true;
+ }
+ }
+ console.info("No changes detected in any tagged pull requests.");
+ return false;
+}
+
+async function tagAndPush(github, owner, repo, execa, commit=false) {
+ let altToken = process.env.ALT_GITHUB_TOKEN;
+ if (!altToken) {
+ throw `Please set ALT_GITHUB_TOKEN environment variable. This token should have write access to ${owner}/${repo}.`;
+ }
+ const query = `query ($owner:String!, $name:String!) {
+ repository(name:$name, owner:$owner) {
+ refs(refPrefix: "refs/tags/", orderBy: {field: TAG_COMMIT_DATE, direction: DESC}, first: 10) {
+ nodes { name }
+ }
+ }
+ }`;
+ const variables = {
+ owner: owner,
+ name: repo,
+ };
+ const tags = await github.graphql(query, variables);
+ const tagList = tags.repository.refs.nodes;
+ const lastTag = tagList[0] ? tagList[0].name : 'dummy-0';
+ const tagNumber = /\w+-(\d+)/.exec(lastTag)[1] | 0;
+ const channel = repo.split('-')[1];
+ const newTag = `${channel}-${tagNumber + 1}`;
+ console.log(`New tag: ${newTag}`);
+ if (commit) {
+ let channelName = channel[0].toUpperCase() + channel.slice(1);
+ console.info(`Committing pending commit as ${channelName} #${tagNumber + 1}`);
+ await execa("git", ['commit', '-m', `${channelName} #${tagNumber + 1}`]);
+ }
+ console.info('Pushing tags to GitHub ...');
+ await execa("git", ['tag', newTag]);
+ await execa("git", ['remote', 'add', 'target', `https://${altToken}@github.com/${owner}/${repo}.git`]);
+ await execa("git", ['push', 'target', 'master', '-f']);
+ await execa("git", ['push', 'target', 'master', '--tags']);
+ console.info('Successfully pushed new changes.');
+}
+
+async function generateReadme(pulls, context, mergeResults, execa) {
+ let baseUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/`;
+ let output =
+ "| Pull Request | Commit | Title | Author | Merged? |\n|----|----|----|----|----|\n";
+ for (let pull of pulls) {
+ let pr = pull.number;
+ let result = mergeResults[pr];
+ output += `| [${pr}](${baseUrl}/pull/${pr}) | [\`${result.rev || "N/A"}\`](${baseUrl}/pull/${pr}/files) | ${pull.title} | [${pull.author.login}](https://github.com/${pull.author.login}/) | ${result.success ? "Yes" : "No"} |\n`;
+ }
+ output +=
+ "\n\nEnd of merge log. You can find the original README.md below the break.\n\n-----\n\n";
+ output += fs.readFileSync("./README.md");
+ fs.writeFileSync("./README.md", output);
+ await execa("git", ["add", "README.md"]);
+}
+
+async function fetchPullRequests(pulls, repoUrl, execa) {
+ console.log("::group::Fetch pull requests");
+ for (let pull of pulls) {
+ let pr = pull.number;
+ console.info(`Fetching PR ${pr} ...`);
+ await execa("git", [
+ "fetch",
+ "-f",
+ "--no-recurse-submodules",
+ repoUrl,
+ `pull/${pr}/head:pr-${pr}`,
+ ]);
+ }
+ console.log("::endgroup::");
+}
+
+async function mergePullRequests(pulls, execa) {
+ let mergeResults = {};
+ console.log("::group::Merge pull requests");
+ await execa("git", ["config", "--global", "user.name", "yuzubot"]);
+ await execa("git", [
+ "config",
+ "--global",
+ "user.email",
+ "yuzu\x40yuzu-emu\x2eorg", // prevent email harvesters from scraping the address
+ ]);
+ let hasFailed = false;
+ for (let pull of pulls) {
+ let pr = pull.number;
+ console.info(`Merging PR ${pr} ...`);
+ try {
+ const process1 = execa("git", [
+ "merge",
+ "--squash",
+ "--no-edit",
+ `pr-${pr}`,
+ ]);
+ process1.stdout.pipe(process.stdout);
+ await process1;
+
+ const process2 = execa("git", ["commit", "-m", `Merge PR ${pr}`]);
+ process2.stdout.pipe(process.stdout);
+ await process2;
+
+ const process3 = await execa("git", ["rev-parse", "--short", `pr-${pr}`]);
+ mergeResults[pr] = {
+ success: true,
+ rev: process3.stdout,
+ };
+ } catch (err) {
+ console.log(
+ `::error title=#${pr} not merged::Failed to merge pull request: ${pr}: ${err}`
+ );
+ mergeResults[pr] = { success: false };
+ hasFailed = true;
+ await execa("git", ["reset", "--hard"]);
+ }
+ }
+ console.log("::endgroup::");
+ if (hasFailed) {
+ throw 'There are merge failures. Aborting!';
+ }
+ return mergeResults;
+}
+
+async function mergebot(github, context, execa) {
+ const query = `query ($owner:String!, $name:String!, $label:String!) {
+ repository(name:$name, owner:$owner) {
+ pullRequests(labels: [$label], states: OPEN, first: 100) {
+ nodes {
+ number title author { login }
+ }
+ }
+ }
+ }`;
+ const variables = {
+ owner: context.repo.owner,
+ name: context.repo.repo,
+ label: CHANGE_LABEL,
+ };
+ const result = await github.graphql(query, variables);
+ const pulls = result.repository.pullRequests.nodes;
+ let displayList = [];
+ for (let i = 0; i < pulls.length; i++) {
+ let pull = pulls[i];
+ displayList.push({ PR: pull.number, Title: pull.title });
+ }
+ console.info("The following pull requests will be merged:");
+ console.table(displayList);
+ await fetchPullRequests(pulls, "https://github.com/yuzu-emu/yuzu", execa);
+ const mergeResults = await mergePullRequests(pulls, execa);
+ await generateReadme(pulls, context, mergeResults, execa);
+ await tagAndPush(github, context.repo.owner, `${context.repo.repo}-android`, execa, true);
+}
+
+module.exports.mergebot = mergebot;
+module.exports.checkAndroidChanges = checkAndroidChanges;
+module.exports.tagAndPush = tagAndPush;
+module.exports.checkBaseChanges = checkBaseChanges;
diff --git a/.github/workflows/android-publish.yml b/.github/workflows/android-publish.yml
new file mode 100644
index 000000000..8f46fcf74
--- /dev/null
+++ b/.github/workflows/android-publish.yml
@@ -0,0 +1,57 @@
+# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+name: yuzu-android-publish
+
+on:
+ schedule:
+ - cron: '37 0 * * *'
+ workflow_dispatch:
+ inputs:
+ android:
+ description: 'Whether to trigger an Android build (true/false/auto)'
+ required: false
+ default: 'true'
+
+jobs:
+ android:
+ runs-on: ubuntu-latest
+ if: ${{ github.event.inputs.android != 'false' && github.repository == 'yuzu-emu/yuzu' }}
+ steps:
+ # this checkout is required to make sure the GitHub Actions scripts are available
+ - uses: actions/checkout@v3
+ name: Pre-checkout
+ with:
+ submodules: false
+ - uses: actions/github-script@v6
+ id: check-changes
+ name: 'Check for new changes'
+ env:
+ # 24 hours
+ DETECTION_TIME_FRAME: 86400000
+ with:
+ script: |
+ if (context.payload.inputs && context.payload.inputs.android === 'true') return true;
+ const checkAndroidChanges = require('./.github/workflows/android-merge.js').checkAndroidChanges;
+ return checkAndroidChanges(github, context);
+ - run: npm install execa@5
+ if: ${{ steps.check-changes.outputs.result == 'true' }}
+ - uses: actions/checkout@v3
+ name: Checkout
+ if: ${{ steps.check-changes.outputs.result == 'true' }}
+ with:
+ path: 'yuzu-merge'
+ fetch-depth: 0
+ submodules: true
+ token: ${{ secrets.ALT_GITHUB_TOKEN }}
+ - uses: actions/github-script@v5
+ name: 'Check and merge Android changes'
+ if: ${{ steps.check-changes.outputs.result == 'true' }}
+ env:
+ ALT_GITHUB_TOKEN: ${{ secrets.ALT_GITHUB_TOKEN }}
+ with:
+ script: |
+ const execa = require("execa");
+ const mergebot = require('./.github/workflows/android-merge.js').mergebot;
+ process.chdir('${{ github.workspace }}/yuzu-merge');
+ mergebot(github, context, execa);
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index bd4141f56..b5d338199 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -129,11 +129,12 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
+ fetch-depth: 0
- name: set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
- distribution: 'adopt'
+ distribution: 'temurin'
- name: Set up cache
uses: actions/cache@v3
with:
@@ -151,7 +152,6 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y ccache apksigner glslang-dev glslang-tools
- git -C ./externals/vcpkg/ fetch --all --unshallow
- name: Build
run: ./.ci/scripts/android/build.sh
- name: Copy and sign artifacts
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 51d949d65..742685fb0 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -54,6 +54,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<activity
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
android:theme="@style/Theme.Yuzu.Main"
+ android:launchMode="singleTop"
android:screenOrientation="userLandscape"
android:supportsPictureInPicture="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
index ae665ed2e..7461fb093 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -34,11 +34,14 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.navigation.fragment.NavHostFragment
+import androidx.preference.PreferenceManager
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
+import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
@@ -47,6 +50,7 @@ import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.MemoryUtil
import org.yuzu.yuzu_emu.utils.NfcReader
import org.yuzu.yuzu_emu.utils.ThemeHelper
+import java.text.NumberFormat
import kotlin.math.roundToInt
class EmulationActivity : AppCompatActivity(), SensorEventListener {
@@ -106,17 +110,26 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
inputHandler = InputHandler()
inputHandler.initialize()
- val memoryUtil = MemoryUtil(this)
- if (memoryUtil.isLessThan(8, MemoryUtil.Gb)) {
- Toast.makeText(
- this,
- getString(
- R.string.device_memory_inadequate,
- memoryUtil.getDeviceRAM(),
- "8 ${getString(R.string.memory_gigabyte)}"
- ),
- Toast.LENGTH_LONG
- ).show()
+ val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) {
+ if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.Gb)) {
+ Toast.makeText(
+ this,
+ getString(
+ R.string.device_memory_inadequate,
+ MemoryUtil.getDeviceRAM(),
+ getString(
+ R.string.memory_formatted,
+ NumberFormat.getInstance().format(MemoryUtil.REQUIRED_MEMORY),
+ getString(R.string.memory_gigabyte)
+ )
+ ),
+ Toast.LENGTH_LONG
+ ).show()
+ preferences.edit()
+ .putBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, true)
+ .apply()
+ }
}
// Start a foreground service to prevent the app from getting killed in the background
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index 88afb2223..a6251bafd 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -110,25 +110,38 @@ class Settings {
const val SECTION_THEME = "Theme"
const val SECTION_DEBUG = "Debug"
- const val PREF_OVERLAY_INIT = "OverlayInit"
+ const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
+
+ const val PREF_OVERLAY_VERSION = "OverlayVersion"
+ const val PREF_LANDSCAPE_OVERLAY_VERSION = "LandscapeOverlayVersion"
+ const val PREF_PORTRAIT_OVERLAY_VERSION = "PortraitOverlayVersion"
+ const val PREF_FOLDABLE_OVERLAY_VERSION = "FoldableOverlayVersion"
+ val overlayLayoutPrefs = listOf(
+ PREF_LANDSCAPE_OVERLAY_VERSION,
+ PREF_PORTRAIT_OVERLAY_VERSION,
+ PREF_FOLDABLE_OVERLAY_VERSION
+ )
+
const val PREF_CONTROL_SCALE = "controlScale"
const val PREF_CONTROL_OPACITY = "controlOpacity"
const val PREF_TOUCH_ENABLED = "isTouchEnabled"
- const val PREF_BUTTON_TOGGLE_0 = "buttonToggle0"
- const val PREF_BUTTON_TOGGLE_1 = "buttonToggle1"
- const val PREF_BUTTON_TOGGLE_2 = "buttonToggle2"
- const val PREF_BUTTON_TOGGLE_3 = "buttonToggle3"
- const val PREF_BUTTON_TOGGLE_4 = "buttonToggle4"
- const val PREF_BUTTON_TOGGLE_5 = "buttonToggle5"
- const val PREF_BUTTON_TOGGLE_6 = "buttonToggle6"
- const val PREF_BUTTON_TOGGLE_7 = "buttonToggle7"
- const val PREF_BUTTON_TOGGLE_8 = "buttonToggle8"
- const val PREF_BUTTON_TOGGLE_9 = "buttonToggle9"
- const val PREF_BUTTON_TOGGLE_10 = "buttonToggle10"
- const val PREF_BUTTON_TOGGLE_11 = "buttonToggle11"
- const val PREF_BUTTON_TOGGLE_12 = "buttonToggle12"
- const val PREF_BUTTON_TOGGLE_13 = "buttonToggle13"
- const val PREF_BUTTON_TOGGLE_14 = "buttonToggle14"
+ const val PREF_BUTTON_A = "buttonToggle0"
+ const val PREF_BUTTON_B = "buttonToggle1"
+ const val PREF_BUTTON_X = "buttonToggle2"
+ const val PREF_BUTTON_Y = "buttonToggle3"
+ const val PREF_BUTTON_L = "buttonToggle4"
+ const val PREF_BUTTON_R = "buttonToggle5"
+ const val PREF_BUTTON_ZL = "buttonToggle6"
+ const val PREF_BUTTON_ZR = "buttonToggle7"
+ const val PREF_BUTTON_PLUS = "buttonToggle8"
+ const val PREF_BUTTON_MINUS = "buttonToggle9"
+ const val PREF_BUTTON_DPAD = "buttonToggle10"
+ const val PREF_STICK_L = "buttonToggle11"
+ const val PREF_STICK_R = "buttonToggle12"
+ const val PREF_BUTTON_STICK_L = "buttonToggle13"
+ const val PREF_BUTTON_STICK_R = "buttonToggle14"
+ const val PREF_BUTTON_HOME = "buttonToggle15"
+ const val PREF_BUTTON_SCREENSHOT = "buttonToggle16"
const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
@@ -143,6 +156,30 @@ class Settings {
private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
+ val overlayPreferences = listOf(
+ PREF_OVERLAY_VERSION,
+ PREF_CONTROL_SCALE,
+ PREF_CONTROL_OPACITY,
+ PREF_TOUCH_ENABLED,
+ PREF_BUTTON_A,
+ PREF_BUTTON_B,
+ PREF_BUTTON_X,
+ PREF_BUTTON_Y,
+ PREF_BUTTON_L,
+ PREF_BUTTON_R,
+ PREF_BUTTON_ZL,
+ PREF_BUTTON_ZR,
+ PREF_BUTTON_PLUS,
+ PREF_BUTTON_MINUS,
+ PREF_BUTTON_DPAD,
+ PREF_STICK_L,
+ PREF_STICK_R,
+ PREF_BUTTON_HOME,
+ PREF_BUTTON_SCREENSHOT,
+ PREF_BUTTON_STICK_L,
+ PREF_BUTTON_STICK_R
+ )
+
const val LayoutOption_Unspecified = 0
const val LayoutOption_MobilePortrait = 4
const val LayoutOption_MobileLandscape = 5
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 09976db62..0e7c1ba88 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -212,9 +212,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
if (!isInFoldableLayout) {
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
- binding.surfaceInputOverlay.orientation = InputOverlay.PORTRAIT
+ binding.surfaceInputOverlay.layout = InputOverlay.PORTRAIT
} else {
- binding.surfaceInputOverlay.orientation = InputOverlay.LANDSCAPE
+ binding.surfaceInputOverlay.layout = InputOverlay.LANDSCAPE
}
}
if (!binding.surfaceInputOverlay.isInEditMode) {
@@ -260,7 +260,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
.remove(Settings.PREF_CONTROL_SCALE)
.remove(Settings.PREF_CONTROL_OPACITY)
.apply()
- binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.resetButtonPlacement() }
+ binding.surfaceInputOverlay.post {
+ binding.surfaceInputOverlay.resetLayoutVisibilityAndPlacement()
+ }
}
private fun updateShowFpsOverlay() {
@@ -337,7 +339,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.inGameMenu.layoutParams.height = it.bounds.bottom
isInFoldableLayout = true
- binding.surfaceInputOverlay.orientation = InputOverlay.FOLDABLE
+ binding.surfaceInputOverlay.layout = InputOverlay.FOLDABLE
refreshInputOverlay()
}
}
@@ -410,9 +412,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
R.id.menu_toggle_controls -> {
val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
- val optionsArray = BooleanArray(15)
- for (i in 0..14) {
- optionsArray[i] = preferences.getBoolean("buttonToggle$i", i < 13)
+ val optionsArray = BooleanArray(Settings.overlayPreferences.size)
+ Settings.overlayPreferences.forEachIndexed { i, _ ->
+ optionsArray[i] = preferences.getBoolean("buttonToggle$i", i < 15)
}
val dialog = MaterialAlertDialogBuilder(requireContext())
@@ -436,7 +438,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
.setOnClickListener {
val isChecked = !optionsArray[0]
- for (i in 0..14) {
+ Settings.overlayPreferences.forEachIndexed { i, _ ->
optionsArray[i] = isChecked
dialog.listView.setItemChecked(i, isChecked)
preferences.edit()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
index 6251ec783..c055c2e35 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -51,15 +51,23 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
private lateinit var windowInsets: WindowInsets
- var orientation = LANDSCAPE
+ var layout = LANDSCAPE
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
windowInsets = rootWindowInsets
- if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
- defaultOverlay()
+ val overlayVersion = preferences.getInt(Settings.PREF_OVERLAY_VERSION, 0)
+ if (overlayVersion != OVERLAY_VERSION) {
+ resetAllLayouts()
+ } else {
+ val layoutIndex = overlayLayouts.indexOf(layout)
+ val currentLayoutVersion =
+ preferences.getInt(Settings.overlayLayoutPrefs[layoutIndex], 0)
+ if (currentLayoutVersion != overlayLayoutVersions[layoutIndex]) {
+ resetCurrentLayout()
+ }
}
// Load the controls.
@@ -266,10 +274,10 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured === button) {
// Persist button position by saving new place.
saveControlPosition(
- buttonBeingConfigured!!.buttonId,
+ buttonBeingConfigured!!.prefId,
buttonBeingConfigured!!.bounds.centerX(),
buttonBeingConfigured!!.bounds.centerY(),
- orientation
+ layout
)
buttonBeingConfigured = null
}
@@ -299,10 +307,10 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured === dpad) {
// Persist button position by saving new place.
saveControlPosition(
- dpadBeingConfigured!!.upId,
+ Settings.PREF_BUTTON_DPAD,
dpadBeingConfigured!!.bounds.centerX(),
dpadBeingConfigured!!.bounds.centerY(),
- orientation
+ layout
)
dpadBeingConfigured = null
}
@@ -330,10 +338,10 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) {
saveControlPosition(
- joystickBeingConfigured!!.buttonId,
+ joystickBeingConfigured!!.prefId,
joystickBeingConfigured!!.bounds.centerX(),
joystickBeingConfigured!!.bounds.centerY(),
- orientation
+ layout
)
joystickBeingConfigured = null
}
@@ -343,9 +351,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
return true
}
- private fun addOverlayControls(orientation: String) {
+ private fun addOverlayControls(layout: String) {
val windowSize = getSafeScreenSize(context)
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_0, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_A, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -353,11 +361,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.facebutton_a,
R.drawable.facebutton_a_depressed,
ButtonType.BUTTON_A,
- orientation
+ Settings.PREF_BUTTON_A,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_1, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_B, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -365,11 +374,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.facebutton_b,
R.drawable.facebutton_b_depressed,
ButtonType.BUTTON_B,
- orientation
+ Settings.PREF_BUTTON_B,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_2, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_X, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -377,11 +387,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.facebutton_x,
R.drawable.facebutton_x_depressed,
ButtonType.BUTTON_X,
- orientation
+ Settings.PREF_BUTTON_X,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_3, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_Y, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -389,11 +400,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.facebutton_y,
R.drawable.facebutton_y_depressed,
ButtonType.BUTTON_Y,
- orientation
+ Settings.PREF_BUTTON_Y,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_4, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_L, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -401,11 +413,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.l_shoulder,
R.drawable.l_shoulder_depressed,
ButtonType.TRIGGER_L,
- orientation
+ Settings.PREF_BUTTON_L,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_5, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_R, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -413,11 +426,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.r_shoulder,
R.drawable.r_shoulder_depressed,
ButtonType.TRIGGER_R,
- orientation
+ Settings.PREF_BUTTON_R,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_6, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_ZL, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -425,11 +439,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.zl_trigger,
R.drawable.zl_trigger_depressed,
ButtonType.TRIGGER_ZL,
- orientation
+ Settings.PREF_BUTTON_ZL,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_7, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_ZR, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -437,11 +452,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.zr_trigger,
R.drawable.zr_trigger_depressed,
ButtonType.TRIGGER_ZR,
- orientation
+ Settings.PREF_BUTTON_ZR,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_8, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_PLUS, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -449,11 +465,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.facebutton_plus,
R.drawable.facebutton_plus_depressed,
ButtonType.BUTTON_PLUS,
- orientation
+ Settings.PREF_BUTTON_PLUS,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_9, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_MINUS, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -461,11 +478,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.facebutton_minus,
R.drawable.facebutton_minus_depressed,
ButtonType.BUTTON_MINUS,
- orientation
+ Settings.PREF_BUTTON_MINUS,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_10, true)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_DPAD, true)) {
overlayDpads.add(
initializeOverlayDpad(
context,
@@ -473,11 +491,11 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.dpad_standard,
R.drawable.dpad_standard_cardinal_depressed,
R.drawable.dpad_standard_diagonal_depressed,
- orientation
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_11, true)) {
+ if (preferences.getBoolean(Settings.PREF_STICK_L, true)) {
overlayJoysticks.add(
initializeOverlayJoystick(
context,
@@ -487,11 +505,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.joystick_depressed,
StickType.STICK_L,
ButtonType.STICK_L,
- orientation
+ Settings.PREF_STICK_L,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_12, true)) {
+ if (preferences.getBoolean(Settings.PREF_STICK_R, true)) {
overlayJoysticks.add(
initializeOverlayJoystick(
context,
@@ -501,11 +520,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.joystick_depressed,
StickType.STICK_R,
ButtonType.STICK_R,
- orientation
+ Settings.PREF_STICK_R,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_13, false)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_HOME, false)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -513,11 +533,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.facebutton_home,
R.drawable.facebutton_home_depressed,
ButtonType.BUTTON_HOME,
- orientation
+ Settings.PREF_BUTTON_HOME,
+ layout
)
)
}
- if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_14, false)) {
+ if (preferences.getBoolean(Settings.PREF_BUTTON_SCREENSHOT, false)) {
overlayButtons.add(
initializeOverlayButton(
context,
@@ -525,7 +546,34 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.facebutton_screenshot,
R.drawable.facebutton_screenshot_depressed,
ButtonType.BUTTON_CAPTURE,
- orientation
+ Settings.PREF_BUTTON_SCREENSHOT,
+ layout
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_STICK_L, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.button_l3,
+ R.drawable.button_l3_depressed,
+ ButtonType.STICK_L,
+ Settings.PREF_BUTTON_STICK_L,
+ layout
+ )
+ )
+ }
+ if (preferences.getBoolean(Settings.PREF_BUTTON_STICK_R, true)) {
+ overlayButtons.add(
+ initializeOverlayButton(
+ context,
+ windowSize,
+ R.drawable.button_r3,
+ R.drawable.button_r3_depressed,
+ ButtonType.STICK_R,
+ Settings.PREF_BUTTON_STICK_R,
+ layout
)
)
}
@@ -539,18 +587,18 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// Add all the enabled overlay items back to the HashSet.
if (EmulationMenuSettings.showOverlay) {
- addOverlayControls(orientation)
+ addOverlayControls(layout)
}
invalidate()
}
- private fun saveControlPosition(sharedPrefsId: Int, x: Int, y: Int, orientation: String) {
+ private fun saveControlPosition(prefId: String, x: Int, y: Int, layout: String) {
val windowSize = getSafeScreenSize(context)
val min = windowSize.first
val max = windowSize.second
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
- .putFloat("$sharedPrefsId-X$orientation", (x - min.x).toFloat() / max.x)
- .putFloat("$sharedPrefsId-Y$orientation", (y - min.y).toFloat() / max.y)
+ .putFloat("$prefId-X$layout", (x - min.x).toFloat() / max.x)
+ .putFloat("$prefId-Y$layout", (y - min.y).toFloat() / max.y)
.apply()
}
@@ -558,19 +606,31 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
inEditMode = editMode
}
- private fun defaultOverlay() {
- if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
- defaultOverlayByLayout(orientation)
- }
-
- resetButtonPlacement()
+ private fun resetCurrentLayout() {
+ defaultOverlayByLayout(layout)
+ val layoutIndex = overlayLayouts.indexOf(layout)
preferences.edit()
- .putBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", true)
+ .putInt(Settings.overlayLayoutPrefs[layoutIndex], overlayLayoutVersions[layoutIndex])
.apply()
}
- fun resetButtonPlacement() {
- defaultOverlayByLayout(orientation)
+ private fun resetAllLayouts() {
+ val editor = preferences.edit()
+ overlayLayouts.forEachIndexed { i, layout ->
+ defaultOverlayByLayout(layout)
+ editor.putInt(Settings.overlayLayoutPrefs[i], overlayLayoutVersions[i])
+ }
+ editor.putInt(Settings.PREF_OVERLAY_VERSION, OVERLAY_VERSION)
+ editor.apply()
+ }
+
+ fun resetLayoutVisibilityAndPlacement() {
+ defaultOverlayByLayout(layout)
+ val editor = preferences.edit()
+ Settings.overlayPreferences.forEachIndexed { _, pref ->
+ editor.remove(pref)
+ }
+ editor.apply()
refreshControls()
}
@@ -604,7 +664,11 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.integer.SWITCH_STICK_R_X,
R.integer.SWITCH_STICK_R_Y,
R.integer.SWITCH_STICK_L_X,
- R.integer.SWITCH_STICK_L_Y
+ R.integer.SWITCH_STICK_L_Y,
+ R.integer.SWITCH_BUTTON_STICK_L_X,
+ R.integer.SWITCH_BUTTON_STICK_L_Y,
+ R.integer.SWITCH_BUTTON_STICK_R_X,
+ R.integer.SWITCH_BUTTON_STICK_R_Y
)
private val portraitResources = arrayOf(
@@ -637,7 +701,11 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.integer.SWITCH_STICK_R_X_PORTRAIT,
R.integer.SWITCH_STICK_R_Y_PORTRAIT,
R.integer.SWITCH_STICK_L_X_PORTRAIT,
- R.integer.SWITCH_STICK_L_Y_PORTRAIT
+ R.integer.SWITCH_STICK_L_Y_PORTRAIT,
+ R.integer.SWITCH_BUTTON_STICK_L_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_STICK_L_Y_PORTRAIT,
+ R.integer.SWITCH_BUTTON_STICK_R_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_STICK_R_Y_PORTRAIT
)
private val foldableResources = arrayOf(
@@ -670,139 +738,159 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.integer.SWITCH_STICK_R_X_FOLDABLE,
R.integer.SWITCH_STICK_R_Y_FOLDABLE,
R.integer.SWITCH_STICK_L_X_FOLDABLE,
- R.integer.SWITCH_STICK_L_Y_FOLDABLE
+ R.integer.SWITCH_STICK_L_Y_FOLDABLE,
+ R.integer.SWITCH_BUTTON_STICK_L_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_STICK_L_Y_FOLDABLE,
+ R.integer.SWITCH_BUTTON_STICK_R_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_STICK_R_Y_FOLDABLE
)
- private fun getResourceValue(orientation: String, position: Int): Float {
- return when (orientation) {
+ private fun getResourceValue(layout: String, position: Int): Float {
+ return when (layout) {
PORTRAIT -> resources.getInteger(portraitResources[position]).toFloat() / 1000
FOLDABLE -> resources.getInteger(foldableResources[position]).toFloat() / 1000
else -> resources.getInteger(landscapeResources[position]).toFloat() / 1000
}
}
- private fun defaultOverlayByLayout(orientation: String) {
+ private fun defaultOverlayByLayout(layout: String) {
// Each value represents the position of the button in relation to the screen size without insets.
preferences.edit()
.putFloat(
- ButtonType.BUTTON_A.toString() + "-X$orientation",
- getResourceValue(orientation, 0)
+ "${Settings.PREF_BUTTON_A}-X$layout",
+ getResourceValue(layout, 0)
+ )
+ .putFloat(
+ "${Settings.PREF_BUTTON_A}-Y$layout",
+ getResourceValue(layout, 1)
+ )
+ .putFloat(
+ "${Settings.PREF_BUTTON_B}-X$layout",
+ getResourceValue(layout, 2)
+ )
+ .putFloat(
+ "${Settings.PREF_BUTTON_B}-Y$layout",
+ getResourceValue(layout, 3)
+ )
+ .putFloat(
+ "${Settings.PREF_BUTTON_X}-X$layout",
+ getResourceValue(layout, 4)
)
.putFloat(
- ButtonType.BUTTON_A.toString() + "-Y$orientation",
- getResourceValue(orientation, 1)
+ "${Settings.PREF_BUTTON_X}-Y$layout",
+ getResourceValue(layout, 5)
)
.putFloat(
- ButtonType.BUTTON_B.toString() + "-X$orientation",
- getResourceValue(orientation, 2)
+ "${Settings.PREF_BUTTON_Y}-X$layout",
+ getResourceValue(layout, 6)
)
.putFloat(
- ButtonType.BUTTON_B.toString() + "-Y$orientation",
- getResourceValue(orientation, 3)
+ "${Settings.PREF_BUTTON_Y}-Y$layout",
+ getResourceValue(layout, 7)
)
.putFloat(
- ButtonType.BUTTON_X.toString() + "-X$orientation",
- getResourceValue(orientation, 4)
+ "${Settings.PREF_BUTTON_ZL}-X$layout",
+ getResourceValue(layout, 8)
)
.putFloat(
- ButtonType.BUTTON_X.toString() + "-Y$orientation",
- getResourceValue(orientation, 5)
+ "${Settings.PREF_BUTTON_ZL}-Y$layout",
+ getResourceValue(layout, 9)
)
.putFloat(
- ButtonType.BUTTON_Y.toString() + "-X$orientation",
- getResourceValue(orientation, 6)
+ "${Settings.PREF_BUTTON_ZR}-X$layout",
+ getResourceValue(layout, 10)
)
.putFloat(
- ButtonType.BUTTON_Y.toString() + "-Y$orientation",
- getResourceValue(orientation, 7)
+ "${Settings.PREF_BUTTON_ZR}-Y$layout",
+ getResourceValue(layout, 11)
)
.putFloat(
- ButtonType.TRIGGER_ZL.toString() + "-X$orientation",
- getResourceValue(orientation, 8)
+ "${Settings.PREF_BUTTON_DPAD}-X$layout",
+ getResourceValue(layout, 12)
)
.putFloat(
- ButtonType.TRIGGER_ZL.toString() + "-Y$orientation",
- getResourceValue(orientation, 9)
+ "${Settings.PREF_BUTTON_DPAD}-Y$layout",
+ getResourceValue(layout, 13)
)
.putFloat(
- ButtonType.TRIGGER_ZR.toString() + "-X$orientation",
- getResourceValue(orientation, 10)
+ "${Settings.PREF_BUTTON_L}-X$layout",
+ getResourceValue(layout, 14)
)
.putFloat(
- ButtonType.TRIGGER_ZR.toString() + "-Y$orientation",
- getResourceValue(orientation, 11)
+ "${Settings.PREF_BUTTON_L}-Y$layout",
+ getResourceValue(layout, 15)
)
.putFloat(
- ButtonType.DPAD_UP.toString() + "-X$orientation",
- getResourceValue(orientation, 12)
+ "${Settings.PREF_BUTTON_R}-X$layout",
+ getResourceValue(layout, 16)
)
.putFloat(
- ButtonType.DPAD_UP.toString() + "-Y$orientation",
- getResourceValue(orientation, 13)
+ "${Settings.PREF_BUTTON_R}-Y$layout",
+ getResourceValue(layout, 17)
)
.putFloat(
- ButtonType.TRIGGER_L.toString() + "-X$orientation",
- getResourceValue(orientation, 14)
+ "${Settings.PREF_BUTTON_PLUS}-X$layout",
+ getResourceValue(layout, 18)
)
.putFloat(
- ButtonType.TRIGGER_L.toString() + "-Y$orientation",
- getResourceValue(orientation, 15)
+ "${Settings.PREF_BUTTON_PLUS}-Y$layout",
+ getResourceValue(layout, 19)
)
.putFloat(
- ButtonType.TRIGGER_R.toString() + "-X$orientation",
- getResourceValue(orientation, 16)
+ "${Settings.PREF_BUTTON_MINUS}-X$layout",
+ getResourceValue(layout, 20)
)
.putFloat(
- ButtonType.TRIGGER_R.toString() + "-Y$orientation",
- getResourceValue(orientation, 17)
+ "${Settings.PREF_BUTTON_MINUS}-Y$layout",
+ getResourceValue(layout, 21)
)
.putFloat(
- ButtonType.BUTTON_PLUS.toString() + "-X$orientation",
- getResourceValue(orientation, 18)
+ "${Settings.PREF_BUTTON_HOME}-X$layout",
+ getResourceValue(layout, 22)
)
.putFloat(
- ButtonType.BUTTON_PLUS.toString() + "-Y$orientation",
- getResourceValue(orientation, 19)
+ "${Settings.PREF_BUTTON_HOME}-Y$layout",
+ getResourceValue(layout, 23)
)
.putFloat(
- ButtonType.BUTTON_MINUS.toString() + "-X$orientation",
- getResourceValue(orientation, 20)
+ "${Settings.PREF_BUTTON_SCREENSHOT}-X$layout",
+ getResourceValue(layout, 24)
)
.putFloat(
- ButtonType.BUTTON_MINUS.toString() + "-Y$orientation",
- getResourceValue(orientation, 21)
+ "${Settings.PREF_BUTTON_SCREENSHOT}-Y$layout",
+ getResourceValue(layout, 25)
)
.putFloat(
- ButtonType.BUTTON_HOME.toString() + "-X$orientation",
- getResourceValue(orientation, 22)
+ "${Settings.PREF_STICK_R}-X$layout",
+ getResourceValue(layout, 26)
)
.putFloat(
- ButtonType.BUTTON_HOME.toString() + "-Y$orientation",
- getResourceValue(orientation, 23)
+ "${Settings.PREF_STICK_R}-Y$layout",
+ getResourceValue(layout, 27)
)
.putFloat(
- ButtonType.BUTTON_CAPTURE.toString() + "-X$orientation",
- getResourceValue(orientation, 24)
+ "${Settings.PREF_STICK_L}-X$layout",
+ getResourceValue(layout, 28)
)
.putFloat(
- ButtonType.BUTTON_CAPTURE.toString() + "-Y$orientation",
- getResourceValue(orientation, 25)
+ "${Settings.PREF_STICK_L}-Y$layout",
+ getResourceValue(layout, 29)
)
.putFloat(
- ButtonType.STICK_R.toString() + "-X$orientation",
- getResourceValue(orientation, 26)
+ "${Settings.PREF_BUTTON_STICK_L}-X$layout",
+ getResourceValue(layout, 30)
)
.putFloat(
- ButtonType.STICK_R.toString() + "-Y$orientation",
- getResourceValue(orientation, 27)
+ "${Settings.PREF_BUTTON_STICK_L}-Y$layout",
+ getResourceValue(layout, 31)
)
.putFloat(
- ButtonType.STICK_L.toString() + "-X$orientation",
- getResourceValue(orientation, 28)
+ "${Settings.PREF_BUTTON_STICK_R}-X$layout",
+ getResourceValue(layout, 32)
)
.putFloat(
- ButtonType.STICK_L.toString() + "-Y$orientation",
- getResourceValue(orientation, 29)
+ "${Settings.PREF_BUTTON_STICK_R}-Y$layout",
+ getResourceValue(layout, 33)
)
.apply()
}
@@ -812,12 +900,30 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
}
companion object {
+ // Increase this number every time there is a breaking change to every overlay layout
+ const val OVERLAY_VERSION = 1
+
+ // Increase the corresponding layout version number whenever that layout has a breaking change
+ private const val LANDSCAPE_OVERLAY_VERSION = 1
+ private const val PORTRAIT_OVERLAY_VERSION = 1
+ private const val FOLDABLE_OVERLAY_VERSION = 1
+ val overlayLayoutVersions = listOf(
+ LANDSCAPE_OVERLAY_VERSION,
+ PORTRAIT_OVERLAY_VERSION,
+ FOLDABLE_OVERLAY_VERSION
+ )
+
private val preferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
- const val LANDSCAPE = ""
+ const val LANDSCAPE = "_Landscape"
const val PORTRAIT = "_Portrait"
const val FOLDABLE = "_Foldable"
+ val overlayLayouts = listOf(
+ LANDSCAPE,
+ PORTRAIT,
+ FOLDABLE
+ )
/**
* Resizes a [Bitmap] by a given scale factor
@@ -948,6 +1054,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
* @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State).
* @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State).
* @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
+ * @param prefId Identifier for determining where a button appears on screen.
+ * @param layout The current screen layout as determined by [LANDSCAPE], [PORTRAIT], or [FOLDABLE].
* @return An [InputOverlayDrawableButton] with the correct drawing bounds set.
*/
private fun initializeOverlayButton(
@@ -956,7 +1064,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
defaultResId: Int,
pressedResId: Int,
buttonId: Int,
- orientation: String
+ prefId: String,
+ layout: String
): InputOverlayDrawableButton {
// Resources handle for fetching the initial Drawable resource.
val res = context.resources
@@ -964,17 +1073,20 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
- // Decide scale based on button ID and user preference
- var scale: Float = when (buttonId) {
- ButtonType.BUTTON_HOME,
- ButtonType.BUTTON_CAPTURE,
- ButtonType.BUTTON_PLUS,
- ButtonType.BUTTON_MINUS -> 0.07f
+ // Decide scale based on button preference ID and user preference
+ var scale: Float = when (prefId) {
+ Settings.PREF_BUTTON_HOME,
+ Settings.PREF_BUTTON_SCREENSHOT,
+ Settings.PREF_BUTTON_PLUS,
+ Settings.PREF_BUTTON_MINUS -> 0.07f
- ButtonType.TRIGGER_L,
- ButtonType.TRIGGER_R,
- ButtonType.TRIGGER_ZL,
- ButtonType.TRIGGER_ZR -> 0.26f
+ Settings.PREF_BUTTON_L,
+ Settings.PREF_BUTTON_R,
+ Settings.PREF_BUTTON_ZL,
+ Settings.PREF_BUTTON_ZR -> 0.26f
+
+ Settings.PREF_BUTTON_STICK_L,
+ Settings.PREF_BUTTON_STICK_R -> 0.155f
else -> 0.11f
}
@@ -984,8 +1096,13 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// Initialize the InputOverlayDrawableButton.
val defaultStateBitmap = getBitmap(context, defaultResId, scale)
val pressedStateBitmap = getBitmap(context, pressedResId, scale)
- val overlayDrawable =
- InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId)
+ val overlayDrawable = InputOverlayDrawableButton(
+ res,
+ defaultStateBitmap,
+ pressedStateBitmap,
+ buttonId,
+ prefId
+ )
// Get the minimum and maximum coordinates of the screen where the button can be placed.
val min = windowSize.first
@@ -993,8 +1110,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
- val xKey = "$buttonId-X$orientation"
- val yKey = "$buttonId-Y$orientation"
+ val xKey = "$prefId-X$layout"
+ val yKey = "$prefId-Y$layout"
val drawableXPercent = sPrefs.getFloat(xKey, 0f)
val drawableYPercent = sPrefs.getFloat(yKey, 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
@@ -1029,7 +1146,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
* @param defaultResId The [Bitmap] resource ID of the default state.
* @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed state in one direction.
* @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed state in two directions.
- * @return the initialized [InputOverlayDrawableDpad]
+ * @param layout The current screen layout as determined by [LANDSCAPE], [PORTRAIT], or [FOLDABLE].
+ * @return The initialized [InputOverlayDrawableDpad]
*/
private fun initializeOverlayDpad(
context: Context,
@@ -1037,7 +1155,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
defaultResId: Int,
pressedOneDirectionResId: Int,
pressedTwoDirectionsResId: Int,
- orientation: String
+ layout: String
): InputOverlayDrawableDpad {
// Resources handle for fetching the initial Drawable resource.
val res = context.resources
@@ -1074,8 +1192,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
// These were set in the input overlay configuration menu.
- val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-X$orientation", 0f)
- val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-Y$orientation", 0f)
+ val drawableXPercent = sPrefs.getFloat("${Settings.PREF_BUTTON_DPAD}-X$layout", 0f)
+ val drawableYPercent = sPrefs.getFloat("${Settings.PREF_BUTTON_DPAD}-Y$layout", 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).toInt()
val width = overlayDrawable.width
@@ -1107,7 +1225,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
* @param pressedResInner Resource ID for the pressed inner image of the joystick.
* @param joystick Identifier for which joystick this is.
* @param button Identifier for which joystick button this is.
- * @return the initialized [InputOverlayDrawableJoystick].
+ * @param prefId Identifier for determining where a button appears on screen.
+ * @param layout The current screen layout as determined by [LANDSCAPE], [PORTRAIT], or [FOLDABLE].
+ * @return The initialized [InputOverlayDrawableJoystick].
*/
private fun initializeOverlayJoystick(
context: Context,
@@ -1117,7 +1237,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
pressedResInner: Int,
joystick: Int,
button: Int,
- orientation: String
+ prefId: String,
+ layout: String
): InputOverlayDrawableJoystick {
// Resources handle for fetching the initial Drawable resource.
val res = context.resources
@@ -1141,8 +1262,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
- val drawableXPercent = sPrefs.getFloat("$button-X$orientation", 0f)
- val drawableYPercent = sPrefs.getFloat("$button-Y$orientation", 0f)
+ val drawableXPercent = sPrefs.getFloat("$prefId-X$layout", 0f)
+ val drawableYPercent = sPrefs.getFloat("$prefId-Y$layout", 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).toInt()
val outerScale = 1.66f
@@ -1168,7 +1289,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
outerRect,
innerRect,
joystick,
- button
+ button,
+ prefId
)
// Need to set the image's position
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
index 4a93e0b14..2c28dda88 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
@@ -24,7 +24,8 @@ class InputOverlayDrawableButton(
res: Resources,
defaultStateBitmap: Bitmap,
pressedStateBitmap: Bitmap,
- val buttonId: Int
+ val buttonId: Int,
+ val prefId: String
) {
// The ID value what motion event is tracking
var trackId: Int
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
index fb48f584d..518b1e783 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
@@ -37,7 +37,8 @@ class InputOverlayDrawableJoystick(
rectOuter: Rect,
rectInner: Rect,
val joystickId: Int,
- val buttonId: Int
+ val buttonId: Int,
+ val prefId: String
) {
// The ID value what motion event is tracking
var trackId = -1
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt
index 18e5fa0b0..aa4a5539a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt
@@ -5,35 +5,101 @@ package org.yuzu.yuzu_emu.utils
import android.app.ActivityManager
import android.content.Context
+import android.os.Build
import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
import java.util.Locale
+import kotlin.math.ceil
-class MemoryUtil(val context: Context) {
+object MemoryUtil {
+ private val context get() = YuzuApplication.appContext
- private val Long.floatForm: String
- get() = String.format(Locale.ROOT, "%.2f", this.toDouble())
+ private val Float.hundredths: String
+ get() = String.format(Locale.ROOT, "%.2f", this)
- private fun bytesToSizeUnit(size: Long): String {
- return when {
- size < Kb -> "${size.floatForm} ${context.getString(R.string.memory_byte)}"
- size < Mb -> "${(size / Kb).floatForm} ${context.getString(R.string.memory_kilobyte)}"
- size < Gb -> "${(size / Mb).floatForm} ${context.getString(R.string.memory_megabyte)}"
- size < Tb -> "${(size / Gb).floatForm} ${context.getString(R.string.memory_gigabyte)}"
- size < Pb -> "${(size / Tb).floatForm} ${context.getString(R.string.memory_terabyte)}"
- size < Eb -> "${(size / Pb).floatForm} ${context.getString(R.string.memory_petabyte)}"
- else -> "${(size / Eb).floatForm} ${context.getString(R.string.memory_exabyte)}"
+ // Required total system memory
+ const val REQUIRED_MEMORY = 8
+
+ const val Kb: Float = 1024F
+ const val Mb = Kb * 1024
+ const val Gb = Mb * 1024
+ const val Tb = Gb * 1024
+ const val Pb = Tb * 1024
+ const val Eb = Pb * 1024
+
+ private fun bytesToSizeUnit(size: Float): String =
+ when {
+ size < Kb -> {
+ context.getString(
+ R.string.memory_formatted,
+ size.hundredths,
+ context.getString(R.string.memory_byte)
+ )
+ }
+ size < Mb -> {
+ context.getString(
+ R.string.memory_formatted,
+ (size / Kb).hundredths,
+ context.getString(R.string.memory_kilobyte)
+ )
+ }
+ size < Gb -> {
+ context.getString(
+ R.string.memory_formatted,
+ (size / Mb).hundredths,
+ context.getString(R.string.memory_megabyte)
+ )
+ }
+ size < Tb -> {
+ context.getString(
+ R.string.memory_formatted,
+ (size / Gb).hundredths,
+ context.getString(R.string.memory_gigabyte)
+ )
+ }
+ size < Pb -> {
+ context.getString(
+ R.string.memory_formatted,
+ (size / Tb).hundredths,
+ context.getString(R.string.memory_terabyte)
+ )
+ }
+ size < Eb -> {
+ context.getString(
+ R.string.memory_formatted,
+ (size / Pb).hundredths,
+ context.getString(R.string.memory_petabyte)
+ )
+ }
+ else -> {
+ context.getString(
+ R.string.memory_formatted,
+ (size / Eb).hundredths,
+ context.getString(R.string.memory_exabyte)
+ )
+ }
}
- }
- private val totalMemory =
- with(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) {
+ // Devices are unlikely to have 0.5GB increments of memory so we'll just round up to account for
+ // the potential error created by memInfo.totalMem
+ private val totalMemory: Float
+ get() {
val memInfo = ActivityManager.MemoryInfo()
- getMemoryInfo(memInfo)
- memInfo.totalMem
+ with(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) {
+ getMemoryInfo(memInfo)
+ }
+
+ return ceil(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ memInfo.advertisedMem.toFloat()
+ } else {
+ memInfo.totalMem.toFloat()
+ }
+ )
}
- fun isLessThan(minimum: Int, size: Long): Boolean {
- return when (size) {
+ fun isLessThan(minimum: Int, size: Float): Boolean =
+ when (size) {
Kb -> totalMemory < Mb && totalMemory < minimum
Mb -> totalMemory < Gb && (totalMemory / Mb) < minimum
Gb -> totalMemory < Tb && (totalMemory / Gb) < minimum
@@ -42,18 +108,6 @@ class MemoryUtil(val context: Context) {
Eb -> totalMemory / Eb < minimum
else -> totalMemory < Kb && totalMemory < minimum
}
- }
-
- fun getDeviceRAM(): String {
- return bytesToSizeUnit(totalMemory)
- }
-
- companion object {
- const val Kb: Long = 1024
- const val Mb = Kb * 1024
- const val Gb = Mb * 1024
- const val Tb = Gb * 1024
- const val Pb = Tb * 1024
- const val Eb = Pb * 1024
- }
+
+ fun getDeviceRAM(): String = bytesToSizeUnit(totalMemory)
}
diff --git a/src/android/app/src/main/res/drawable/button_l3.xml b/src/android/app/src/main/res/drawable/button_l3.xml
new file mode 100644
index 000000000..0cb28836e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/button_l3.xml
@@ -0,0 +1,128 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="34.963dp"
+ android:height="37.265dp"
+ android:viewportWidth="34.963"
+ android:viewportHeight="37.265">
+ <path
+ android:fillAlpha="0.5"
+ android:pathData="M19.451,19.024A3.498,3.498 0,0 0,21.165 19.508c1.336,0 1.749,-0.852 1.738,-1.49 0,-1.077 -0.982,-1.537 -1.987,-1.537L20.327,16.481L20.327,15.7L20.901,15.7c0.757,0 1.714,-0.392 1.714,-1.302C22.621,13.785 22.224,13.229 21.271,13.229a2.834,2.834 0,0 0,-1.537 0.529l-0.265,-0.757a3.662,3.662 0,0 1,2.008 -0.59c1.513,0 2.201,0.897 2.201,1.834 0,0.794 -0.474,1.466 -1.421,1.807l0,0.024c0.947,0.19 1.714,0.9 1.714,1.976C23.967,19.27 23.017,20.346 21.165,20.346a3.929,3.929 135,0 1,-1.998 -0.529z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="21.568"
+ android:endY="33.938"
+ android:startX="21.568"
+ android:startY="16.14"
+ android:type="linear">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0" />
+ <item
+ android:color="#FFC5C6C6"
+ android:offset="0.03" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.19" />
+ <item
+ android:color="#DBB5B5B5"
+ android:offset="0.44" />
+ <item
+ android:color="#7F878787"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillAlpha="0.5"
+ android:pathData="M16.062,9.353 L9.624,3.405A1.963,1.963 0,0 1,10.955 0l12.88,0a1.963,1.963 135,0 1,1.323 3.405L18.726,9.353a1.961,1.961 135,0 1,-2.664 0z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="17.395"
+ android:endY="18.74"
+ android:startX="17.395"
+ android:startY="-1.296"
+ android:type="linear">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0" />
+ <item
+ android:color="#FFC5C6C6"
+ android:offset="0.03" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.19" />
+ <item
+ android:color="#DBB5B5B5"
+ android:offset="0.44" />
+ <item
+ android:color="#7F878787"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillAlpha="0.5"
+ android:pathData="m25.79,5.657l0,0a2.09,2.09 45,0 0,0.23 3.262c3.522,2.402 4.762,5.927 4.741,10.52A13.279,13.279 135,0 1,4.206 19.365c0,-4.516 0.931,-7.71 4.374,-10.107a2.098,2.098 0,0 0,0.233 -3.265l0,0a2.101,2.101 135,0 0,-2.646 -0.169C1.433,9.133 -0.266,13.941 0.033,20.233a17.468,17.468 0,0 0,34.925 -0.868c0,-6.006 -1.971,-10.771 -6.585,-13.917a2.088,2.088 45,0 0,-2.582 0.209z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:centerX="17.477"
+ android:centerY="19.92"
+ android:gradientRadius="17.201"
+ android:type="radial">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0.58" />
+ <item
+ android:color="#FFC6C6C6"
+ android:offset="0.84" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.88" />
+ <item
+ android:color="#FFC2C2C2"
+ android:offset="0.91" />
+ <item
+ android:color="#FFB5B5B5"
+ android:offset="0.94" />
+ <item
+ android:color="#FF9E9E9E"
+ android:offset="0.98" />
+ <item
+ android:color="#FF8F8F8F"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillAlpha="0.5"
+ android:pathData="m12.516,12.729l2,0l0,13.822l6.615,0l0,1.68L12.516,28.231Z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="16.829"
+ android:endY="46.882"
+ android:startX="16.829"
+ android:startY="20.479"
+ android:type="linear">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0" />
+ <item
+ android:color="#FFC5C6C6"
+ android:offset="0.03" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.19" />
+ <item
+ android:color="#DBB5B5B5"
+ android:offset="0.44" />
+ <item
+ android:color="#7F878787"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+</vector>
diff --git a/src/android/app/src/main/res/drawable/button_l3_depressed.xml b/src/android/app/src/main/res/drawable/button_l3_depressed.xml
new file mode 100644
index 000000000..b078dedc9
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/button_l3_depressed.xml
@@ -0,0 +1,75 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="34.963dp"
+ android:height="37.265dp"
+ android:viewportWidth="34.963"
+ android:viewportHeight="37.265">
+ <path
+ android:fillAlpha="0.3"
+ android:fillColor="#151515"
+ android:pathData="M16.062,9.353 L9.624,3.405A1.963,1.963 0,0 1,10.955 0l12.88,0a1.963,1.963 135,0 1,1.323 3.405L18.726,9.353a1.961,1.961 135,0 1,-2.664 0z"
+ android:strokeAlpha="0.3" />
+ <path
+ android:fillAlpha="0.6"
+ android:fillColor="#151515"
+ android:pathData="m25.79,5.657l0,0a2.09,2.09 45,0 0,0.23 3.262c3.522,2.402 4.762,5.927 4.741,10.52A13.279,13.279 135,0 1,4.206 19.365c0,-4.516 0.931,-7.71 4.374,-10.107a2.098,2.098 0,0 0,0.233 -3.265l0,0a2.101,2.101 135,0 0,-2.646 -0.169C1.433,9.133 -0.266,13.941 0.033,20.233a17.468,17.468 0,0 0,34.925 -0.868c0,-6.006 -1.971,-10.771 -6.585,-13.917a2.088,2.088 45,0 0,-2.582 0.209z"
+ android:strokeAlpha="0.6" />
+ <path
+ android:fillAlpha="0.6"
+ android:pathData="M19.451,19.024A3.498,3.498 0,0 0,21.165 19.508c1.336,0 1.749,-0.852 1.738,-1.49 0,-1.077 -0.982,-1.537 -1.987,-1.537L20.327,16.481L20.327,15.7L20.901,15.7c0.757,0 1.714,-0.392 1.714,-1.302C22.621,13.785 22.224,13.229 21.271,13.229a2.834,2.834 0,0 0,-1.537 0.529l-0.265,-0.757a3.662,3.662 0,0 1,2.008 -0.59c1.513,0 2.201,0.897 2.201,1.834 0,0.794 -0.474,1.466 -1.421,1.807l0,0.024c0.947,0.19 1.714,0.9 1.714,1.976C23.967,19.27 23.017,20.346 21.165,20.346a3.929,3.929 135,0 1,-1.998 -0.529z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="21.568"
+ android:endY="33.938"
+ android:startX="21.568"
+ android:startY="16.14"
+ android:type="linear">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0" />
+ <item
+ android:color="#FFC5C6C6"
+ android:offset="0.03" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.19" />
+ <item
+ android:color="#DBB5B5B5"
+ android:offset="0.44" />
+ <item
+ android:color="#7F878787"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillAlpha="0.6"
+ android:pathData="m12.516,12.729l2,0l0,13.822l6.615,0l0,1.68L12.516,28.231Z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="16.829"
+ android:endY="46.882"
+ android:startX="16.829"
+ android:startY="20.479"
+ android:type="linear">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0" />
+ <item
+ android:color="#FFC5C6C6"
+ android:offset="0.03" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.19" />
+ <item
+ android:color="#DBB5B5B5"
+ android:offset="0.44" />
+ <item
+ android:color="#7F878787"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+</vector>
diff --git a/src/android/app/src/main/res/drawable/button_r3.xml b/src/android/app/src/main/res/drawable/button_r3.xml
new file mode 100644
index 000000000..5c6864e26
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/button_r3.xml
@@ -0,0 +1,128 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="34.963dp"
+ android:height="37.265dp"
+ android:viewportWidth="34.963"
+ android:viewportHeight="37.265">
+ <path
+ android:fillAlpha="0.5"
+ android:pathData="m10.781,12.65a19.579,19.579 0,0 1,3.596 -0.302c2.003,0 3.294,0.368 4.199,1.185a3.622,3.622 0,0 1,1.14 2.757c0,1.916 -1.206,3.175 -2.733,3.704l0,0.063c1.119,0.386 1.786,1.421 2.117,2.929 0.474,2.024 0.818,3.424 1.119,3.982l-1.924,0c-0.238,-0.407 -0.561,-1.656 -0.968,-3.466 -0.431,-2.003 -1.206,-2.757 -2.91,-2.82l-1.762,0l0,6.286l-1.873,0zM12.654,19.264l1.916,0c2.003,0 3.273,-1.098 3.273,-2.757 0,-1.873 -1.357,-2.691 -3.336,-2.712a7.649,7.649 0,0 0,-1.852 0.172z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="15.506"
+ android:endY="48.977"
+ android:startX="15.506"
+ android:startY="19.659"
+ android:type="linear">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0" />
+ <item
+ android:color="#FFC5C6C6"
+ android:offset="0.03" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.19" />
+ <item
+ android:color="#DBB5B5B5"
+ android:offset="0.44" />
+ <item
+ android:color="#7F878787"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillAlpha="0.5"
+ android:pathData="M16.062,9.353 L9.624,3.405A1.963,1.963 0,0 1,10.955 0l12.88,0a1.963,1.963 135,0 1,1.323 3.405L18.726,9.353a1.961,1.961 135,0 1,-2.664 0z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="17.395"
+ android:endY="18.74"
+ android:startX="17.395"
+ android:startY="-1.296"
+ android:type="linear">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0" />
+ <item
+ android:color="#FFC5C6C6"
+ android:offset="0.03" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.19" />
+ <item
+ android:color="#DBB5B5B5"
+ android:offset="0.44" />
+ <item
+ android:color="#7F878787"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillAlpha="0.5"
+ android:pathData="m25.79,5.657l0,0a2.09,2.09 45,0 0,0.23 3.262c3.522,2.402 4.762,5.927 4.741,10.52A13.279,13.279 135,0 1,4.206 19.365c0,-4.516 0.931,-7.71 4.374,-10.107a2.098,2.098 0,0 0,0.233 -3.265l0,0a2.101,2.101 135,0 0,-2.646 -0.169C1.433,9.133 -0.266,13.941 0.033,20.233a17.468,17.468 0,0 0,34.925 -0.868c0,-6.006 -1.971,-10.771 -6.585,-13.917a2.088,2.088 45,0 0,-2.582 0.209z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:centerX="17.477"
+ android:centerY="19.92"
+ android:gradientRadius="17.201"
+ android:type="radial">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0.58" />
+ <item
+ android:color="#FFC6C6C6"
+ android:offset="0.84" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.88" />
+ <item
+ android:color="#FFC2C2C2"
+ android:offset="0.91" />
+ <item
+ android:color="#FFB5B5B5"
+ android:offset="0.94" />
+ <item
+ android:color="#FF9E9E9E"
+ android:offset="0.98" />
+ <item
+ android:color="#FF8F8F8F"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillAlpha="0.5"
+ android:pathData="M21.832,19.024A3.498,3.498 0,0 0,23.547 19.508c1.336,0 1.749,-0.852 1.738,-1.49 0,-1.077 -0.982,-1.537 -1.987,-1.537L22.708,16.481L22.708,15.7L23.282,15.7c0.757,0 1.714,-0.392 1.714,-1.302C25.002,13.785 24.605,13.229 23.652,13.229a2.834,2.834 0,0 0,-1.537 0.529l-0.265,-0.757a3.662,3.662 0,0 1,2.008 -0.59c1.513,0 2.201,0.897 2.201,1.834 0,0.794 -0.474,1.466 -1.421,1.807l0,0.024c0.947,0.19 1.714,0.9 1.714,1.976C26.349,19.27 25.399,20.346 23.547,20.346a3.929,3.929 135,0 1,-1.998 -0.529z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="23.949"
+ android:endY="33.938"
+ android:startX="23.949"
+ android:startY="16.14"
+ android:type="linear">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0" />
+ <item
+ android:color="#FFC5C6C6"
+ android:offset="0.03" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.19" />
+ <item
+ android:color="#DBB5B5B5"
+ android:offset="0.44" />
+ <item
+ android:color="#7F878787"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+</vector>
diff --git a/src/android/app/src/main/res/drawable/button_r3_depressed.xml b/src/android/app/src/main/res/drawable/button_r3_depressed.xml
new file mode 100644
index 000000000..20f480179
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/button_r3_depressed.xml
@@ -0,0 +1,75 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="34.963dp"
+ android:height="37.265dp"
+ android:viewportWidth="34.963"
+ android:viewportHeight="37.265">
+ <path
+ android:fillAlpha="0.3"
+ android:fillColor="#151515"
+ android:pathData="M16.062,9.353 L9.624,3.405A1.963,1.963 0,0 1,10.955 0l12.88,0a1.963,1.963 135,0 1,1.323 3.405L18.726,9.353a1.961,1.961 135,0 1,-2.664 0z"
+ android:strokeAlpha="0.3" />
+ <path
+ android:fillAlpha="0.6"
+ android:fillColor="#151515"
+ android:pathData="m25.79,5.657l0,0a2.09,2.09 45,0 0,0.23 3.262c3.522,2.402 4.762,5.927 4.741,10.52A13.279,13.279 135,0 1,4.206 19.365c0,-4.516 0.931,-7.71 4.374,-10.107a2.098,2.098 0,0 0,0.233 -3.265l0,0a2.101,2.101 135,0 0,-2.646 -0.169C1.433,9.133 -0.266,13.941 0.033,20.233a17.468,17.468 0,0 0,34.925 -0.868c0,-6.006 -1.971,-10.771 -6.585,-13.917a2.088,2.088 45,0 0,-2.582 0.209z"
+ android:strokeAlpha="0.6" />
+ <path
+ android:fillAlpha="0.6"
+ android:pathData="m10.781,12.65a19.579,19.579 0,0 1,3.596 -0.302c2.003,0 3.294,0.368 4.199,1.185a3.622,3.622 0,0 1,1.14 2.757c0,1.916 -1.206,3.175 -2.733,3.704l0,0.063c1.119,0.386 1.786,1.421 2.117,2.929 0.474,2.024 0.818,3.424 1.119,3.982l-1.924,0c-0.238,-0.407 -0.561,-1.656 -0.968,-3.466 -0.431,-2.003 -1.206,-2.757 -2.91,-2.82l-1.762,0l0,6.286l-1.873,0zM12.654,19.264l1.916,0c2.003,0 3.273,-1.098 3.273,-2.757 0,-1.873 -1.357,-2.691 -3.336,-2.712a7.649,7.649 0,0 0,-1.852 0.172z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="15.506"
+ android:endY="48.977"
+ android:startX="15.506"
+ android:startY="19.659"
+ android:type="linear">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0" />
+ <item
+ android:color="#FFC5C6C6"
+ android:offset="0.03" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.19" />
+ <item
+ android:color="#DBB5B5B5"
+ android:offset="0.44" />
+ <item
+ android:color="#7F878787"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillAlpha="0.6"
+ android:pathData="M21.832,19.024A3.498,3.498 0,0 0,23.547 19.508c1.336,0 1.749,-0.852 1.738,-1.49 0,-1.077 -0.982,-1.537 -1.987,-1.537L22.708,16.481L22.708,15.7L23.282,15.7c0.757,0 1.714,-0.392 1.714,-1.302C25.002,13.785 24.605,13.229 23.652,13.229a2.834,2.834 0,0 0,-1.537 0.529l-0.265,-0.757a3.662,3.662 0,0 1,2.008 -0.59c1.513,0 2.201,0.897 2.201,1.834 0,0.794 -0.474,1.466 -1.421,1.807l0,0.024c0.947,0.19 1.714,0.9 1.714,1.976C26.349,19.27 25.399,20.346 23.547,20.346a3.929,3.929 135,0 1,-1.998 -0.529z"
+ android:strokeAlpha="0.6">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="23.949"
+ android:endY="33.938"
+ android:startX="23.949"
+ android:startY="16.14"
+ android:type="linear">
+ <item
+ android:color="#FFC3C4C5"
+ android:offset="0" />
+ <item
+ android:color="#FFC5C6C6"
+ android:offset="0.03" />
+ <item
+ android:color="#FFC7C7C7"
+ android:offset="0.19" />
+ <item
+ android:color="#DBB5B5B5"
+ android:offset="0.44" />
+ <item
+ android:color="#7F878787"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+</vector>
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index 6d092f7a9..200b99185 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -205,6 +205,8 @@
<item>@string/gamepad_d_pad</item>
<item>@string/gamepad_left_stick</item>
<item>@string/gamepad_right_stick</item>
+ <item>L3</item>
+ <item>R3</item>
<item>@string/gamepad_home</item>
<item>@string/gamepad_screenshot</item>
</string-array>
diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml
index 2e93b408c..5e39bc7d9 100644
--- a/src/android/app/src/main/res/values/integers.xml
+++ b/src/android/app/src/main/res/values/integers.xml
@@ -33,6 +33,10 @@
<integer name="SWITCH_BUTTON_CAPTURE_Y">950</integer>
<integer name="SWITCH_BUTTON_DPAD_X">260</integer>
<integer name="SWITCH_BUTTON_DPAD_Y">790</integer>
+ <integer name="SWITCH_BUTTON_STICK_L_X">870</integer>
+ <integer name="SWITCH_BUTTON_STICK_L_Y">400</integer>
+ <integer name="SWITCH_BUTTON_STICK_R_X">960</integer>
+ <integer name="SWITCH_BUTTON_STICK_R_Y">430</integer>
<!-- Default SWITCH portrait layout -->
<integer name="SWITCH_BUTTON_A_X_PORTRAIT">840</integer>
@@ -65,6 +69,10 @@
<integer name="SWITCH_BUTTON_CAPTURE_Y_PORTRAIT">950</integer>
<integer name="SWITCH_BUTTON_DPAD_X_PORTRAIT">240</integer>
<integer name="SWITCH_BUTTON_DPAD_Y_PORTRAIT">840</integer>
+ <integer name="SWITCH_BUTTON_STICK_L_X_PORTRAIT">730</integer>
+ <integer name="SWITCH_BUTTON_STICK_L_Y_PORTRAIT">510</integer>
+ <integer name="SWITCH_BUTTON_STICK_R_X_PORTRAIT">900</integer>
+ <integer name="SWITCH_BUTTON_STICK_R_Y_PORTRAIT">540</integer>
<!-- Default SWITCH foldable layout -->
<integer name="SWITCH_BUTTON_A_X_FOLDABLE">840</integer>
@@ -97,5 +105,9 @@
<integer name="SWITCH_BUTTON_CAPTURE_Y_FOLDABLE">470</integer>
<integer name="SWITCH_BUTTON_DPAD_X_FOLDABLE">240</integer>
<integer name="SWITCH_BUTTON_DPAD_Y_FOLDABLE">390</integer>
+ <integer name="SWITCH_BUTTON_STICK_L_X_FOLDABLE">550</integer>
+ <integer name="SWITCH_BUTTON_STICK_L_Y_FOLDABLE">210</integer>
+ <integer name="SWITCH_BUTTON_STICK_R_X_FOLDABLE">550</integer>
+ <integer name="SWITCH_BUTTON_STICK_R_Y_FOLDABLE">280</integer>
</resources>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index af7450619..b3c737979 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -273,6 +273,7 @@
<string name="fatal_error_message">A fatal error occurred. Check the log for details.\nContinuing emulation may result in crashes and bugs.</string>
<string name="performance_warning">Turning off this setting will significantly reduce emulation performance! For the best experience, it is recommended that you leave this setting enabled.</string>
<string name="device_memory_inadequate">Device RAM: %1$s\nRecommended: %2$s</string>
+ <string name="memory_formatted">%1$s %2$s</string>
<!-- Region Names -->
<string name="region_japan">Japan</string>
diff --git a/src/core/core.cpp b/src/core/core.cpp
index b74fd0a58..9e3eb3795 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -27,6 +27,7 @@
#include "core/file_sys/savedata_factory.h"
#include "core/file_sys/vfs_concat.h"
#include "core/file_sys/vfs_real.h"
+#include "core/gpu_dirty_memory_manager.h"
#include "core/hid/hid_core.h"
#include "core/hle/kernel/k_memory_manager.h"
#include "core/hle/kernel/k_process.h"
@@ -130,7 +131,10 @@ FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs,
struct System::Impl {
explicit Impl(System& system)
: kernel{system}, fs_controller{system}, memory{system}, hid_core{}, room_network{},
- cpu_manager{system}, reporter{system}, applet_manager{system}, time_manager{system} {}
+ cpu_manager{system}, reporter{system}, applet_manager{system}, time_manager{system},
+ gpu_dirty_memory_write_manager{} {
+ memory.SetGPUDirtyManagers(gpu_dirty_memory_write_manager);
+ }
void Initialize(System& system) {
device_memory = std::make_unique<Core::DeviceMemory>();
@@ -234,6 +238,8 @@ struct System::Impl {
// Setting changes may require a full system reinitialization (e.g., disabling multicore).
ReinitializeIfNecessary(system);
+ memory.SetGPUDirtyManagers(gpu_dirty_memory_write_manager);
+
kernel.Initialize();
cpu_manager.Initialize();
@@ -540,6 +546,9 @@ struct System::Impl {
std::array<u64, Core::Hardware::NUM_CPU_CORES> dynarmic_ticks{};
std::array<MicroProfileToken, Core::Hardware::NUM_CPU_CORES> microprofile_cpu{};
+
+ std::array<Core::GPUDirtyMemoryManager, Core::Hardware::NUM_CPU_CORES>
+ gpu_dirty_memory_write_manager{};
};
System::System() : impl{std::make_unique<Impl>(*this)} {}
@@ -629,10 +638,31 @@ void System::PrepareReschedule(const u32 core_index) {
impl->kernel.PrepareReschedule(core_index);
}
+Core::GPUDirtyMemoryManager& System::CurrentGPUDirtyMemoryManager() {
+ const std::size_t core = impl->kernel.GetCurrentHostThreadID();
+ return impl->gpu_dirty_memory_write_manager[core < Core::Hardware::NUM_CPU_CORES
+ ? core
+ : Core::Hardware::NUM_CPU_CORES - 1];
+}
+
+/// Provides a constant reference to the current gou dirty memory manager.
+const Core::GPUDirtyMemoryManager& System::CurrentGPUDirtyMemoryManager() const {
+ const std::size_t core = impl->kernel.GetCurrentHostThreadID();
+ return impl->gpu_dirty_memory_write_manager[core < Core::Hardware::NUM_CPU_CORES
+ ? core
+ : Core::Hardware::NUM_CPU_CORES - 1];
+}
+
size_t System::GetCurrentHostThreadID() const {
return impl->kernel.GetCurrentHostThreadID();
}
+void System::GatherGPUDirtyMemory(std::function<void(VAddr, size_t)>& callback) {
+ for (auto& manager : impl->gpu_dirty_memory_write_manager) {
+ manager.Gather(callback);
+ }
+}
+
PerfStatsResults System::GetAndResetPerfStats() {
return impl->GetAndResetPerfStats();
}
diff --git a/src/core/core.h b/src/core/core.h
index 93afc9303..14b2f7785 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -108,9 +108,10 @@ class CpuManager;
class Debugger;
class DeviceMemory;
class ExclusiveMonitor;
-class SpeedLimiter;
+class GPUDirtyMemoryManager;
class PerfStats;
class Reporter;
+class SpeedLimiter;
class TelemetrySession;
struct PerfStatsResults;
@@ -225,6 +226,14 @@ public:
/// Prepare the core emulation for a reschedule
void PrepareReschedule(u32 core_index);
+ /// Provides a reference to the gou dirty memory manager.
+ [[nodiscard]] Core::GPUDirtyMemoryManager& CurrentGPUDirtyMemoryManager();
+
+ /// Provides a constant reference to the current gou dirty memory manager.
+ [[nodiscard]] const Core::GPUDirtyMemoryManager& CurrentGPUDirtyMemoryManager() const;
+
+ void GatherGPUDirtyMemory(std::function<void(VAddr, size_t)>& callback);
+
[[nodiscard]] size_t GetCurrentHostThreadID() const;
/// Gets and resets core performance statistics
diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp
index 4f0a3f8ea..e6112a3c9 100644
--- a/src/core/core_timing.cpp
+++ b/src/core/core_timing.cpp
@@ -253,9 +253,6 @@ void CoreTiming::ThreadLoop() {
auto wait_time = *next_time - GetGlobalTimeNs().count();
if (wait_time > 0) {
#ifdef _WIN32
- const auto timer_resolution_ns =
- Common::Windows::GetCurrentTimerResolution().count();
-
while (!paused && !event.IsSet() && wait_time > 0) {
wait_time = *next_time - GetGlobalTimeNs().count();
@@ -316,4 +313,10 @@ std::chrono::microseconds CoreTiming::GetGlobalTimeUs() const {
return std::chrono::microseconds{Common::WallClock::CPUTickToUS(cpu_ticks)};
}
+#ifdef _WIN32
+void CoreTiming::SetTimerResolutionNs(std::chrono::nanoseconds ns) {
+ timer_resolution_ns = ns.count();
+}
+#endif
+
} // namespace Core::Timing
diff --git a/src/core/core_timing.h b/src/core/core_timing.h
index 10db1de55..5bca1c78d 100644
--- a/src/core/core_timing.h
+++ b/src/core/core_timing.h
@@ -131,6 +131,10 @@ public:
/// Checks for events manually and returns time in nanoseconds for next event, threadsafe.
std::optional<s64> Advance();
+#ifdef _WIN32
+ void SetTimerResolutionNs(std::chrono::nanoseconds ns);
+#endif
+
private:
struct Event;
@@ -143,6 +147,10 @@ private:
s64 global_timer = 0;
+#ifdef _WIN32
+ s64 timer_resolution_ns;
+#endif
+
// The queue is a min-heap using std::make_heap/push_heap/pop_heap.
// We don't use std::priority_queue because we need to be able to serialize, unserialize and
// erase arbitrary events (RemoveEvent()) regardless of the queue order. These aren't
diff --git a/src/core/file_sys/fsmitm_romfsbuild.cpp b/src/core/file_sys/fsmitm_romfsbuild.cpp
index 1ff83c08c..e39c7b62b 100644
--- a/src/core/file_sys/fsmitm_romfsbuild.cpp
+++ b/src/core/file_sys/fsmitm_romfsbuild.cpp
@@ -105,19 +105,11 @@ static u64 romfs_get_hash_table_count(u64 num_entries) {
return count;
}
-void RomFSBuildContext::VisitDirectory(VirtualDir root_romfs, VirtualDir ext_dir,
+void RomFSBuildContext::VisitDirectory(VirtualDir romfs_dir, VirtualDir ext_dir,
std::shared_ptr<RomFSBuildDirectoryContext> parent) {
std::vector<std::shared_ptr<RomFSBuildDirectoryContext>> child_dirs;
- VirtualDir dir;
-
- if (parent->path_len == 0) {
- dir = root_romfs;
- } else {
- dir = root_romfs->GetDirectoryRelative(parent->path);
- }
-
- const auto entries = dir->GetEntries();
+ const auto entries = romfs_dir->GetEntries();
for (const auto& kv : entries) {
if (kv.second == VfsEntryType::Directory) {
@@ -127,7 +119,7 @@ void RomFSBuildContext::VisitDirectory(VirtualDir root_romfs, VirtualDir ext_dir
child->path_len = child->cur_path_ofs + static_cast<u32>(kv.first.size());
child->path = parent->path + "/" + kv.first;
- if (ext_dir != nullptr && ext_dir->GetFileRelative(child->path + ".stub") != nullptr) {
+ if (ext_dir != nullptr && ext_dir->GetFile(kv.first + ".stub") != nullptr) {
continue;
}
@@ -144,17 +136,17 @@ void RomFSBuildContext::VisitDirectory(VirtualDir root_romfs, VirtualDir ext_dir
child->path_len = child->cur_path_ofs + static_cast<u32>(kv.first.size());
child->path = parent->path + "/" + kv.first;
- if (ext_dir != nullptr && ext_dir->GetFileRelative(child->path + ".stub") != nullptr) {
+ if (ext_dir != nullptr && ext_dir->GetFile(kv.first + ".stub") != nullptr) {
continue;
}
// Sanity check on path_len
ASSERT(child->path_len < FS_MAX_PATH);
- child->source = root_romfs->GetFileRelative(child->path);
+ child->source = romfs_dir->GetFile(kv.first);
if (ext_dir != nullptr) {
- if (const auto ips = ext_dir->GetFileRelative(child->path + ".ips")) {
+ if (const auto ips = ext_dir->GetFile(kv.first + ".ips")) {
if (auto patched = PatchIPS(child->source, ips)) {
child->source = std::move(patched);
}
@@ -168,23 +160,27 @@ void RomFSBuildContext::VisitDirectory(VirtualDir root_romfs, VirtualDir ext_dir
}
for (auto& child : child_dirs) {
- this->VisitDirectory(root_romfs, ext_dir, child);
+ auto subdir_name = std::string_view(child->path).substr(child->cur_path_ofs);
+ auto child_romfs_dir = romfs_dir->GetSubdirectory(subdir_name);
+ auto child_ext_dir = ext_dir != nullptr ? ext_dir->GetSubdirectory(subdir_name) : nullptr;
+ this->VisitDirectory(child_romfs_dir, child_ext_dir, child);
}
}
bool RomFSBuildContext::AddDirectory(std::shared_ptr<RomFSBuildDirectoryContext> parent_dir_ctx,
std::shared_ptr<RomFSBuildDirectoryContext> dir_ctx) {
// Check whether it's already in the known directories.
- const auto existing = directories.find(dir_ctx->path);
- if (existing != directories.end())
+ const auto [it, is_new] = directories.emplace(dir_ctx->path, nullptr);
+ if (!is_new) {
return false;
+ }
// Add a new directory.
num_dirs++;
dir_table_size +=
sizeof(RomFSDirectoryEntry) + Common::AlignUp(dir_ctx->path_len - dir_ctx->cur_path_ofs, 4);
dir_ctx->parent = parent_dir_ctx;
- directories.emplace(dir_ctx->path, dir_ctx);
+ it->second = dir_ctx;
return true;
}
@@ -192,8 +188,8 @@ bool RomFSBuildContext::AddDirectory(std::shared_ptr<RomFSBuildDirectoryContext>
bool RomFSBuildContext::AddFile(std::shared_ptr<RomFSBuildDirectoryContext> parent_dir_ctx,
std::shared_ptr<RomFSBuildFileContext> file_ctx) {
// Check whether it's already in the known files.
- const auto existing = files.find(file_ctx->path);
- if (existing != files.end()) {
+ const auto [it, is_new] = files.emplace(file_ctx->path, nullptr);
+ if (!is_new) {
return false;
}
@@ -202,7 +198,7 @@ bool RomFSBuildContext::AddFile(std::shared_ptr<RomFSBuildDirectoryContext> pare
file_table_size +=
sizeof(RomFSFileEntry) + Common::AlignUp(file_ctx->path_len - file_ctx->cur_path_ofs, 4);
file_ctx->parent = parent_dir_ctx;
- files.emplace(file_ctx->path, file_ctx);
+ it->second = file_ctx;
return true;
}
diff --git a/src/core/gpu_dirty_memory_manager.h b/src/core/gpu_dirty_memory_manager.h
new file mode 100644
index 000000000..9687531e8
--- /dev/null
+++ b/src/core/gpu_dirty_memory_manager.h
@@ -0,0 +1,122 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <atomic>
+#include <bit>
+#include <functional>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+#include "core/memory.h"
+
+namespace Core {
+
+class GPUDirtyMemoryManager {
+public:
+ GPUDirtyMemoryManager() : current{default_transform} {
+ back_buffer.reserve(256);
+ front_buffer.reserve(256);
+ }
+
+ ~GPUDirtyMemoryManager() = default;
+
+ void Collect(VAddr address, size_t size) {
+ TransformAddress t = BuildTransform(address, size);
+ TransformAddress tmp, original;
+ do {
+ tmp = current.load(std::memory_order_acquire);
+ original = tmp;
+ if (tmp.address != t.address) {
+ if (IsValid(tmp.address)) {
+ std::scoped_lock lk(guard);
+ back_buffer.emplace_back(tmp);
+ current.exchange(t, std::memory_order_relaxed);
+ return;
+ }
+ tmp.address = t.address;
+ tmp.mask = 0;
+ }
+ if ((tmp.mask | t.mask) == tmp.mask) {
+ return;
+ }
+ tmp.mask |= t.mask;
+ } while (!current.compare_exchange_weak(original, tmp, std::memory_order_release,
+ std::memory_order_relaxed));
+ }
+
+ void Gather(std::function<void(VAddr, size_t)>& callback) {
+ {
+ std::scoped_lock lk(guard);
+ TransformAddress t = current.exchange(default_transform, std::memory_order_relaxed);
+ front_buffer.swap(back_buffer);
+ if (IsValid(t.address)) {
+ front_buffer.emplace_back(t);
+ }
+ }
+ for (auto& transform : front_buffer) {
+ size_t offset = 0;
+ u64 mask = transform.mask;
+ while (mask != 0) {
+ const size_t empty_bits = std::countr_zero(mask);
+ offset += empty_bits << align_bits;
+ mask = mask >> empty_bits;
+
+ const size_t continuous_bits = std::countr_one(mask);
+ callback((static_cast<VAddr>(transform.address) << page_bits) + offset,
+ continuous_bits << align_bits);
+ mask = continuous_bits < align_size ? (mask >> continuous_bits) : 0;
+ offset += continuous_bits << align_bits;
+ }
+ }
+ front_buffer.clear();
+ }
+
+private:
+ struct alignas(8) TransformAddress {
+ u32 address;
+ u32 mask;
+ };
+
+ constexpr static size_t page_bits = Memory::YUZU_PAGEBITS - 1;
+ constexpr static size_t page_size = 1ULL << page_bits;
+ constexpr static size_t page_mask = page_size - 1;
+
+ constexpr static size_t align_bits = 6U;
+ constexpr static size_t align_size = 1U << align_bits;
+ constexpr static size_t align_mask = align_size - 1;
+ constexpr static TransformAddress default_transform = {.address = ~0U, .mask = 0U};
+
+ bool IsValid(VAddr address) {
+ return address < (1ULL << 39);
+ }
+
+ template <typename T>
+ T CreateMask(size_t top_bit, size_t minor_bit) {
+ T mask = ~T(0);
+ mask <<= (sizeof(T) * 8 - top_bit);
+ mask >>= (sizeof(T) * 8 - top_bit);
+ mask >>= minor_bit;
+ mask <<= minor_bit;
+ return mask;
+ }
+
+ TransformAddress BuildTransform(VAddr address, size_t size) {
+ const size_t minor_address = address & page_mask;
+ const size_t minor_bit = minor_address >> align_bits;
+ const size_t top_bit = (minor_address + size + align_mask) >> align_bits;
+ TransformAddress result{};
+ result.address = static_cast<u32>(address >> page_bits);
+ result.mask = CreateMask<u32>(top_bit, minor_bit);
+ return result;
+ }
+
+ std::atomic<TransformAddress> current{};
+ std::mutex guard;
+ std::vector<TransformAddress> back_buffer;
+ std::vector<TransformAddress> front_buffer;
+};
+
+} // namespace Core
diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp
index 1ebc32c1e..94bd656fe 100644
--- a/src/core/hid/emulated_controller.cpp
+++ b/src/core/hid/emulated_controller.cpp
@@ -1243,10 +1243,12 @@ Common::Input::DriverResult EmulatedController::SetPollingMode(
auto& nfc_output_device = output_devices[3];
if (device_index == EmulatedDeviceIndex::LeftIndex) {
+ controller.left_polling_mode = polling_mode;
return left_output_device->SetPollingMode(polling_mode);
}
if (device_index == EmulatedDeviceIndex::RightIndex) {
+ controller.right_polling_mode = polling_mode;
const auto virtual_nfc_result = nfc_output_device->SetPollingMode(polling_mode);
const auto mapped_nfc_result = right_output_device->SetPollingMode(polling_mode);
@@ -1261,12 +1263,22 @@ Common::Input::DriverResult EmulatedController::SetPollingMode(
return mapped_nfc_result;
}
+ controller.left_polling_mode = polling_mode;
+ controller.right_polling_mode = polling_mode;
left_output_device->SetPollingMode(polling_mode);
right_output_device->SetPollingMode(polling_mode);
nfc_output_device->SetPollingMode(polling_mode);
return Common::Input::DriverResult::Success;
}
+Common::Input::PollingMode EmulatedController::GetPollingMode(
+ EmulatedDeviceIndex device_index) const {
+ if (device_index == EmulatedDeviceIndex::LeftIndex) {
+ return controller.left_polling_mode;
+ }
+ return controller.right_polling_mode;
+}
+
bool EmulatedController::SetCameraFormat(
Core::IrSensor::ImageTransferProcessorFormat camera_format) {
LOG_INFO(Service_HID, "Set camera format {}", camera_format);
diff --git a/src/core/hid/emulated_controller.h b/src/core/hid/emulated_controller.h
index d511e5fac..88d77db8d 100644
--- a/src/core/hid/emulated_controller.h
+++ b/src/core/hid/emulated_controller.h
@@ -143,6 +143,8 @@ struct ControllerStatus {
CameraState camera_state{};
RingSensorForce ring_analog_state{};
NfcState nfc_state{};
+ Common::Input::PollingMode left_polling_mode{};
+ Common::Input::PollingMode right_polling_mode{};
};
enum class ControllerTriggerType {
@@ -370,6 +372,12 @@ public:
*/
Common::Input::DriverResult SetPollingMode(EmulatedDeviceIndex device_index,
Common::Input::PollingMode polling_mode);
+ /**
+ * Get the current polling mode from a controller
+ * @param device_index index of the controller to set the polling mode
+ * @return current polling mode
+ */
+ Common::Input::PollingMode GetPollingMode(EmulatedDeviceIndex device_index) const;
/**
* Sets the desired camera format to be polled from a controller
diff --git a/src/core/hle/kernel/k_thread.h b/src/core/hle/kernel/k_thread.h
index dd662b3f8..d178c2453 100644
--- a/src/core/hle/kernel/k_thread.h
+++ b/src/core/hle/kernel/k_thread.h
@@ -338,6 +338,15 @@ public:
return m_parent != nullptr;
}
+ std::span<KSynchronizationObject*> GetSynchronizationObjectBuffer() {
+ return m_sync_object_buffer.sync_objects;
+ }
+
+ std::span<Handle> GetHandleBuffer() {
+ return {m_sync_object_buffer.handles.data() + Svc::ArgumentHandleCountMax,
+ Svc::ArgumentHandleCountMax};
+ }
+
u16 GetUserDisableCount() const;
void SetInterruptFlag();
void ClearInterruptFlag();
@@ -855,6 +864,7 @@ private:
u32* m_light_ipc_data{};
KProcessAddress m_tls_address{};
KLightLock m_activity_pause_lock;
+ SyncObjectBuffer m_sync_object_buffer{};
s64 m_schedule_count{};
s64 m_last_scheduled_tick{};
std::array<QueueEntry, Core::Hardware::NUM_CPU_CORES> m_per_core_priority_queue_entry{};
diff --git a/src/core/hle/kernel/svc/svc_ipc.cpp b/src/core/hle/kernel/svc/svc_ipc.cpp
index 60247df2e..bb94f6934 100644
--- a/src/core/hle/kernel/svc/svc_ipc.cpp
+++ b/src/core/hle/kernel/svc/svc_ipc.cpp
@@ -38,22 +38,31 @@ Result SendAsyncRequestWithUserBuffer(Core::System& system, Handle* out_event_ha
Result ReplyAndReceive(Core::System& system, s32* out_index, uint64_t handles_addr, s32 num_handles,
Handle reply_target, s64 timeout_ns) {
+ // Ensure number of handles is valid.
+ R_UNLESS(0 <= num_handles && num_handles <= ArgumentHandleCountMax, ResultOutOfRange);
+
+ // Get the synchronization context.
auto& kernel = system.Kernel();
auto& handle_table = GetCurrentProcess(kernel).GetHandleTable();
-
- R_UNLESS(0 <= num_handles && num_handles <= ArgumentHandleCountMax, ResultOutOfRange);
- R_UNLESS(GetCurrentMemory(kernel).IsValidVirtualAddressRange(
- handles_addr, static_cast<u64>(sizeof(Handle) * num_handles)),
- ResultInvalidPointer);
-
- std::array<Handle, Svc::ArgumentHandleCountMax> handles;
- GetCurrentMemory(kernel).ReadBlock(handles_addr, handles.data(), sizeof(Handle) * num_handles);
-
- // Convert handle list to object table.
- std::array<KSynchronizationObject*, Svc::ArgumentHandleCountMax> objs;
- R_UNLESS(handle_table.GetMultipleObjects<KSynchronizationObject>(objs.data(), handles.data(),
- num_handles),
- ResultInvalidHandle);
+ auto objs = GetCurrentThread(kernel).GetSynchronizationObjectBuffer();
+ auto handles = GetCurrentThread(kernel).GetHandleBuffer();
+
+ // Copy user handles.
+ if (num_handles > 0) {
+ // Ensure we can try to get the handles.
+ R_UNLESS(GetCurrentMemory(kernel).IsValidVirtualAddressRange(
+ handles_addr, static_cast<u64>(sizeof(Handle) * num_handles)),
+ ResultInvalidPointer);
+
+ // Get the handles.
+ GetCurrentMemory(kernel).ReadBlock(handles_addr, handles.data(),
+ sizeof(Handle) * num_handles);
+
+ // Convert the handles to objects.
+ R_UNLESS(handle_table.GetMultipleObjects<KSynchronizationObject>(
+ objs.data(), handles.data(), num_handles),
+ ResultInvalidHandle);
+ }
// Ensure handles are closed when we're done.
SCOPE_EXIT({
diff --git a/src/core/hle/kernel/svc/svc_synchronization.cpp b/src/core/hle/kernel/svc/svc_synchronization.cpp
index 53df5bcd8..f02d03f30 100644
--- a/src/core/hle/kernel/svc/svc_synchronization.cpp
+++ b/src/core/hle/kernel/svc/svc_synchronization.cpp
@@ -47,21 +47,35 @@ Result ResetSignal(Core::System& system, Handle handle) {
R_THROW(ResultInvalidHandle);
}
-static Result WaitSynchronization(Core::System& system, int32_t* out_index, const Handle* handles,
- int32_t num_handles, int64_t timeout_ns) {
+/// Wait for the given handles to synchronize, timeout after the specified nanoseconds
+Result WaitSynchronization(Core::System& system, int32_t* out_index, u64 user_handles,
+ int32_t num_handles, int64_t timeout_ns) {
+ LOG_TRACE(Kernel_SVC, "called user_handles={:#x}, num_handles={}, timeout_ns={}", user_handles,
+ num_handles, timeout_ns);
+
// Ensure number of handles is valid.
R_UNLESS(0 <= num_handles && num_handles <= Svc::ArgumentHandleCountMax, ResultOutOfRange);
// Get the synchronization context.
auto& kernel = system.Kernel();
auto& handle_table = GetCurrentProcess(kernel).GetHandleTable();
- std::array<KSynchronizationObject*, Svc::ArgumentHandleCountMax> objs;
+ auto objs = GetCurrentThread(kernel).GetSynchronizationObjectBuffer();
+ auto handles = GetCurrentThread(kernel).GetHandleBuffer();
// Copy user handles.
if (num_handles > 0) {
+ // Ensure we can try to get the handles.
+ R_UNLESS(GetCurrentMemory(kernel).IsValidVirtualAddressRange(
+ user_handles, static_cast<u64>(sizeof(Handle) * num_handles)),
+ ResultInvalidPointer);
+
+ // Get the handles.
+ GetCurrentMemory(kernel).ReadBlock(user_handles, handles.data(),
+ sizeof(Handle) * num_handles);
+
// Convert the handles to objects.
- R_UNLESS(handle_table.GetMultipleObjects<KSynchronizationObject>(objs.data(), handles,
- num_handles),
+ R_UNLESS(handle_table.GetMultipleObjects<KSynchronizationObject>(
+ objs.data(), handles.data(), num_handles),
ResultInvalidHandle);
}
@@ -80,23 +94,6 @@ static Result WaitSynchronization(Core::System& system, int32_t* out_index, cons
R_RETURN(res);
}
-/// Wait for the given handles to synchronize, timeout after the specified nanoseconds
-Result WaitSynchronization(Core::System& system, int32_t* out_index, u64 user_handles,
- int32_t num_handles, int64_t timeout_ns) {
- LOG_TRACE(Kernel_SVC, "called user_handles={:#x}, num_handles={}, timeout_ns={}", user_handles,
- num_handles, timeout_ns);
-
- // Ensure number of handles is valid.
- R_UNLESS(0 <= num_handles && num_handles <= Svc::ArgumentHandleCountMax, ResultOutOfRange);
- std::array<Handle, Svc::ArgumentHandleCountMax> handles;
- if (num_handles > 0) {
- GetCurrentMemory(system.Kernel())
- .ReadBlock(user_handles, handles.data(), num_handles * sizeof(Handle));
- }
-
- R_RETURN(WaitSynchronization(system, out_index, handles.data(), num_handles, timeout_ns));
-}
-
/// Resumes a thread waiting on WaitSynchronization
Result CancelSynchronization(Core::System& system, Handle handle) {
LOG_TRACE(Kernel_SVC, "called handle=0x{:X}", handle);
diff --git a/src/core/hle/service/nfc/common/device.cpp b/src/core/hle/service/nfc/common/device.cpp
index 5bf289818..2d633b03f 100644
--- a/src/core/hle/service/nfc/common/device.cpp
+++ b/src/core/hle/service/nfc/common/device.cpp
@@ -66,10 +66,6 @@ NfcDevice::~NfcDevice() {
};
void NfcDevice::NpadUpdate(Core::HID::ControllerTriggerType type) {
- if (!is_initalized) {
- return;
- }
-
if (type == Core::HID::ControllerTriggerType::Connected) {
Initialize();
availability_change_event->Signal();
@@ -77,12 +73,12 @@ void NfcDevice::NpadUpdate(Core::HID::ControllerTriggerType type) {
}
if (type == Core::HID::ControllerTriggerType::Disconnected) {
- device_state = DeviceState::Unavailable;
+ Finalize();
availability_change_event->Signal();
return;
}
- if (type != Core::HID::ControllerTriggerType::Nfc) {
+ if (!is_initalized) {
return;
}
@@ -90,6 +86,17 @@ void NfcDevice::NpadUpdate(Core::HID::ControllerTriggerType type) {
return;
}
+ // Ensure nfc mode is always active
+ if (npad_device->GetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex) ==
+ Common::Input::PollingMode::Active) {
+ npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
+ Common::Input::PollingMode::NFC);
+ }
+
+ if (type != Core::HID::ControllerTriggerType::Nfc) {
+ return;
+ }
+
const auto nfc_status = npad_device->GetNfc();
switch (nfc_status.state) {
case Common::Input::NfcState::NewAmiibo:
@@ -207,11 +214,14 @@ void NfcDevice::Initialize() {
}
void NfcDevice::Finalize() {
- if (device_state == DeviceState::TagMounted) {
- Unmount();
- }
- if (device_state == DeviceState::SearchingForTag || device_state == DeviceState::TagRemoved) {
- StopDetection();
+ if (npad_device->IsConnected()) {
+ if (device_state == DeviceState::TagMounted) {
+ Unmount();
+ }
+ if (device_state == DeviceState::SearchingForTag ||
+ device_state == DeviceState::TagRemoved) {
+ StopDetection();
+ }
}
if (device_state != DeviceState::Unavailable) {
diff --git a/src/core/memory.cpp b/src/core/memory.cpp
index 514ba0d66..257406f09 100644
--- a/src/core/memory.cpp
+++ b/src/core/memory.cpp
@@ -3,6 +3,7 @@
#include <algorithm>
#include <cstring>
+#include <span>
#include "common/assert.h"
#include "common/atomic_ops.h"
@@ -13,6 +14,7 @@
#include "common/swap.h"
#include "core/core.h"
#include "core/device_memory.h"
+#include "core/gpu_dirty_memory_manager.h"
#include "core/hardware_properties.h"
#include "core/hle/kernel/k_page_table.h"
#include "core/hle/kernel/k_process.h"
@@ -678,7 +680,7 @@ struct Memory::Impl {
LOG_ERROR(HW_Memory, "Unmapped Write{} @ 0x{:016X} = 0x{:016X}", sizeof(T) * 8,
GetInteger(vaddr), static_cast<u64>(data));
},
- [&]() { system.GPU().InvalidateRegion(GetInteger(vaddr), sizeof(T)); });
+ [&]() { HandleRasterizerWrite(GetInteger(vaddr), sizeof(T)); });
if (ptr) {
std::memcpy(ptr, &data, sizeof(T));
}
@@ -692,7 +694,7 @@ struct Memory::Impl {
LOG_ERROR(HW_Memory, "Unmapped WriteExclusive{} @ 0x{:016X} = 0x{:016X}",
sizeof(T) * 8, GetInteger(vaddr), static_cast<u64>(data));
},
- [&]() { system.GPU().InvalidateRegion(GetInteger(vaddr), sizeof(T)); });
+ [&]() { HandleRasterizerWrite(GetInteger(vaddr), sizeof(T)); });
if (ptr) {
const auto volatile_pointer = reinterpret_cast<volatile T*>(ptr);
return Common::AtomicCompareAndSwap(volatile_pointer, data, expected);
@@ -707,7 +709,7 @@ struct Memory::Impl {
LOG_ERROR(HW_Memory, "Unmapped WriteExclusive128 @ 0x{:016X} = 0x{:016X}{:016X}",
GetInteger(vaddr), static_cast<u64>(data[1]), static_cast<u64>(data[0]));
},
- [&]() { system.GPU().InvalidateRegion(GetInteger(vaddr), sizeof(u128)); });
+ [&]() { HandleRasterizerWrite(GetInteger(vaddr), sizeof(u128)); });
if (ptr) {
const auto volatile_pointer = reinterpret_cast<volatile u64*>(ptr);
return Common::AtomicCompareAndSwap(volatile_pointer, data, expected);
@@ -717,7 +719,7 @@ struct Memory::Impl {
void HandleRasterizerDownload(VAddr address, size_t size) {
const size_t core = system.GetCurrentHostThreadID();
- auto& current_area = rasterizer_areas[core];
+ auto& current_area = rasterizer_read_areas[core];
const VAddr end_address = address + size;
if (current_area.start_address <= address && end_address <= current_area.end_address)
[[likely]] {
@@ -726,9 +728,31 @@ struct Memory::Impl {
current_area = system.GPU().OnCPURead(address, size);
}
- Common::PageTable* current_page_table = nullptr;
- std::array<VideoCore::RasterizerDownloadArea, Core::Hardware::NUM_CPU_CORES> rasterizer_areas{};
+ void HandleRasterizerWrite(VAddr address, size_t size) {
+ const size_t core = system.GetCurrentHostThreadID();
+ auto& current_area = rasterizer_write_areas[core];
+ VAddr subaddress = address >> YUZU_PAGEBITS;
+ bool do_collection = current_area.last_address == subaddress;
+ if (!do_collection) [[unlikely]] {
+ do_collection = system.GPU().OnCPUWrite(address, size);
+ if (!do_collection) {
+ return;
+ }
+ current_area.last_address = subaddress;
+ }
+ gpu_dirty_managers[core].Collect(address, size);
+ }
+
+ struct GPUDirtyState {
+ VAddr last_address;
+ };
+
Core::System& system;
+ Common::PageTable* current_page_table = nullptr;
+ std::array<VideoCore::RasterizerDownloadArea, Core::Hardware::NUM_CPU_CORES>
+ rasterizer_read_areas{};
+ std::array<GPUDirtyState, Core::Hardware::NUM_CPU_CORES> rasterizer_write_areas{};
+ std::span<Core::GPUDirtyMemoryManager> gpu_dirty_managers;
};
Memory::Memory(Core::System& system_) : system{system_} {
@@ -876,6 +900,10 @@ void Memory::ZeroBlock(Common::ProcessAddress dest_addr, const std::size_t size)
impl->ZeroBlock(*system.ApplicationProcess(), dest_addr, size);
}
+void Memory::SetGPUDirtyManagers(std::span<Core::GPUDirtyMemoryManager> managers) {
+ impl->gpu_dirty_managers = managers;
+}
+
Result Memory::InvalidateDataCache(Common::ProcessAddress dest_addr, const std::size_t size) {
return impl->InvalidateDataCache(*system.ApplicationProcess(), dest_addr, size);
}
diff --git a/src/core/memory.h b/src/core/memory.h
index 72a0be813..ea01824f8 100644
--- a/src/core/memory.h
+++ b/src/core/memory.h
@@ -5,6 +5,7 @@
#include <cstddef>
#include <memory>
+#include <span>
#include <string>
#include "common/typed_address.h"
#include "core/hle/result.h"
@@ -15,7 +16,8 @@ struct PageTable;
namespace Core {
class System;
-}
+class GPUDirtyMemoryManager;
+} // namespace Core
namespace Kernel {
class PhysicalMemory;
@@ -458,6 +460,8 @@ public:
*/
void MarkRegionDebug(Common::ProcessAddress vaddr, u64 size, bool debug);
+ void SetGPUDirtyManagers(std::span<Core::GPUDirtyMemoryManager> managers);
+
private:
Core::System& system;
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index 58a45ab67..b5ed3380f 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -115,7 +115,34 @@ void BufferCache<P>::WriteMemory(VAddr cpu_addr, u64 size) {
template <class P>
void BufferCache<P>::CachedWriteMemory(VAddr cpu_addr, u64 size) {
- memory_tracker.CachedCpuWrite(cpu_addr, size);
+ const bool is_dirty = IsRegionRegistered(cpu_addr, size);
+ if (!is_dirty) {
+ return;
+ }
+ VAddr aligned_start = Common::AlignDown(cpu_addr, YUZU_PAGESIZE);
+ VAddr aligned_end = Common::AlignUp(cpu_addr + size, YUZU_PAGESIZE);
+ if (!IsRegionGpuModified(aligned_start, aligned_end - aligned_start)) {
+ WriteMemory(cpu_addr, size);
+ return;
+ }
+
+ tmp_buffer.resize_destructive(size);
+ cpu_memory.ReadBlockUnsafe(cpu_addr, tmp_buffer.data(), size);
+
+ InlineMemoryImplementation(cpu_addr, size, tmp_buffer);
+}
+
+template <class P>
+bool BufferCache<P>::OnCPUWrite(VAddr cpu_addr, u64 size) {
+ const bool is_dirty = IsRegionRegistered(cpu_addr, size);
+ if (!is_dirty) {
+ return false;
+ }
+ if (memory_tracker.IsRegionGpuModified(cpu_addr, size)) {
+ return true;
+ }
+ WriteMemory(cpu_addr, size);
+ return false;
}
template <class P>
@@ -1553,6 +1580,14 @@ bool BufferCache<P>::InlineMemory(VAddr dest_address, size_t copy_size,
return false;
}
+ InlineMemoryImplementation(dest_address, copy_size, inlined_buffer);
+
+ return true;
+}
+
+template <class P>
+void BufferCache<P>::InlineMemoryImplementation(VAddr dest_address, size_t copy_size,
+ std::span<const u8> inlined_buffer) {
const IntervalType subtract_interval{dest_address, dest_address + copy_size};
ClearDownload(subtract_interval);
common_ranges.subtract(subtract_interval);
@@ -1574,8 +1609,6 @@ bool BufferCache<P>::InlineMemory(VAddr dest_address, size_t copy_size,
} else {
buffer.ImmediateUpload(buffer.Offset(dest_address), inlined_buffer.first(copy_size));
}
-
- return true;
}
template <class P>
diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h
index fe6068cfe..460fc7551 100644
--- a/src/video_core/buffer_cache/buffer_cache_base.h
+++ b/src/video_core/buffer_cache/buffer_cache_base.h
@@ -245,6 +245,8 @@ public:
void CachedWriteMemory(VAddr cpu_addr, u64 size);
+ bool OnCPUWrite(VAddr cpu_addr, u64 size);
+
void DownloadMemory(VAddr cpu_addr, u64 size);
std::optional<VideoCore::RasterizerDownloadArea> GetFlushArea(VAddr cpu_addr, u64 size);
@@ -543,6 +545,9 @@ private:
void ClearDownload(IntervalType subtract_interval);
+ void InlineMemoryImplementation(VAddr dest_address, size_t copy_size,
+ std::span<const u8> inlined_buffer);
+
VideoCore::RasterizerInterface& rasterizer;
Core::Memory::Memory& cpu_memory;
diff --git a/src/video_core/compatible_formats.cpp b/src/video_core/compatible_formats.cpp
index ab4f4d407..87d69ebc5 100644
--- a/src/video_core/compatible_formats.cpp
+++ b/src/video_core/compatible_formats.cpp
@@ -272,6 +272,9 @@ constexpr Table MakeNonNativeBgrCopyTable() {
bool IsViewCompatible(PixelFormat format_a, PixelFormat format_b, bool broken_views,
bool native_bgr) {
+ if (format_a == format_b) {
+ return true;
+ }
if (broken_views) {
// If format views are broken, only accept formats that are identical.
return format_a == format_b;
@@ -282,6 +285,9 @@ bool IsViewCompatible(PixelFormat format_a, PixelFormat format_b, bool broken_vi
}
bool IsCopyCompatible(PixelFormat format_a, PixelFormat format_b, bool native_bgr) {
+ if (format_a == format_b) {
+ return true;
+ }
static constexpr Table BGR_TABLE = MakeNativeBgrCopyTable();
static constexpr Table NO_BGR_TABLE = MakeNonNativeBgrCopyTable();
return IsSupported(native_bgr ? BGR_TABLE : NO_BGR_TABLE, format_a, format_b);
diff --git a/src/video_core/fence_manager.h b/src/video_core/fence_manager.h
index 35d699bbf..ab20ff30f 100644
--- a/src/video_core/fence_manager.h
+++ b/src/video_core/fence_manager.h
@@ -69,7 +69,6 @@ public:
}
void SignalFence(std::function<void()>&& func) {
- rasterizer.InvalidateGPUCache();
bool delay_fence = Settings::IsGPULevelHigh();
if constexpr (!can_async_check) {
TryReleasePendingFences<false>();
@@ -96,6 +95,7 @@ public:
guard.unlock();
cv.notify_all();
}
+ rasterizer.InvalidateGPUCache();
}
void SignalSyncPoint(u32 value) {
diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp
index db385076d..c192e33b2 100644
--- a/src/video_core/gpu.cpp
+++ b/src/video_core/gpu.cpp
@@ -95,7 +95,9 @@ struct GPU::Impl {
/// Synchronizes CPU writes with Host GPU memory.
void InvalidateGPUCache() {
- rasterizer->InvalidateGPUCache();
+ std::function<void(VAddr, size_t)> callback_writes(
+ [this](VAddr address, size_t size) { rasterizer->OnCacheInvalidation(address, size); });
+ system.GatherGPUDirtyMemory(callback_writes);
}
/// Signal the ending of command list.
@@ -299,6 +301,10 @@ struct GPU::Impl {
gpu_thread.InvalidateRegion(addr, size);
}
+ bool OnCPUWrite(VAddr addr, u64 size) {
+ return rasterizer->OnCPUWrite(addr, size);
+ }
+
/// Notify rasterizer that any caches of the specified region should be flushed and invalidated
void FlushAndInvalidateRegion(VAddr addr, u64 size) {
gpu_thread.FlushAndInvalidateRegion(addr, size);
@@ -561,6 +567,10 @@ void GPU::InvalidateRegion(VAddr addr, u64 size) {
impl->InvalidateRegion(addr, size);
}
+bool GPU::OnCPUWrite(VAddr addr, u64 size) {
+ return impl->OnCPUWrite(addr, size);
+}
+
void GPU::FlushAndInvalidateRegion(VAddr addr, u64 size) {
impl->FlushAndInvalidateRegion(addr, size);
}
diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h
index e49c40cf2..ba2838b89 100644
--- a/src/video_core/gpu.h
+++ b/src/video_core/gpu.h
@@ -250,6 +250,10 @@ public:
/// Notify rasterizer that any caches of the specified region should be invalidated
void InvalidateRegion(VAddr addr, u64 size);
+ /// Notify rasterizer that CPU is trying to write this area. It returns true if the area is
+ /// sensible, false otherwise
+ bool OnCPUWrite(VAddr addr, u64 size);
+
/// Notify rasterizer that any caches of the specified region should be flushed and invalidated
void FlushAndInvalidateRegion(VAddr addr, u64 size);
diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp
index 889144f38..2f0f9f593 100644
--- a/src/video_core/gpu_thread.cpp
+++ b/src/video_core/gpu_thread.cpp
@@ -47,7 +47,7 @@ static void RunThread(std::stop_token stop_token, Core::System& system,
} else if (const auto* flush = std::get_if<FlushRegionCommand>(&next.data)) {
rasterizer->FlushRegion(flush->addr, flush->size);
} else if (const auto* invalidate = std::get_if<InvalidateRegionCommand>(&next.data)) {
- rasterizer->OnCPUWrite(invalidate->addr, invalidate->size);
+ rasterizer->OnCacheInvalidation(invalidate->addr, invalidate->size);
} else {
ASSERT(false);
}
@@ -102,12 +102,12 @@ void ThreadManager::TickGPU() {
}
void ThreadManager::InvalidateRegion(VAddr addr, u64 size) {
- rasterizer->OnCPUWrite(addr, size);
+ rasterizer->OnCacheInvalidation(addr, size);
}
void ThreadManager::FlushAndInvalidateRegion(VAddr addr, u64 size) {
// Skip flush on asynch mode, as FlushAndInvalidateRegion is not used for anything too important
- rasterizer->OnCPUWrite(addr, size);
+ rasterizer->OnCacheInvalidation(addr, size);
}
u64 ThreadManager::PushCommand(CommandData&& command_data, bool block) {
diff --git a/src/video_core/rasterizer_interface.h b/src/video_core/rasterizer_interface.h
index 7566a8c4e..cb8029a4f 100644
--- a/src/video_core/rasterizer_interface.h
+++ b/src/video_core/rasterizer_interface.h
@@ -109,7 +109,9 @@ public:
}
/// Notify rasterizer that any caches of the specified region are desync with guest
- virtual void OnCPUWrite(VAddr addr, u64 size) = 0;
+ virtual void OnCacheInvalidation(VAddr addr, u64 size) = 0;
+
+ virtual bool OnCPUWrite(VAddr addr, u64 size) = 0;
/// Sync memory between guest and host.
virtual void InvalidateGPUCache() = 0;
diff --git a/src/video_core/renderer_null/null_rasterizer.cpp b/src/video_core/renderer_null/null_rasterizer.cpp
index bf2ce4c49..92ecf6682 100644
--- a/src/video_core/renderer_null/null_rasterizer.cpp
+++ b/src/video_core/renderer_null/null_rasterizer.cpp
@@ -47,7 +47,10 @@ bool RasterizerNull::MustFlushRegion(VAddr addr, u64 size, VideoCommon::CacheTyp
return false;
}
void RasterizerNull::InvalidateRegion(VAddr addr, u64 size, VideoCommon::CacheType) {}
-void RasterizerNull::OnCPUWrite(VAddr addr, u64 size) {}
+bool RasterizerNull::OnCPUWrite(VAddr addr, u64 size) {
+ return false;
+}
+void RasterizerNull::OnCacheInvalidation(VAddr addr, u64 size) {}
VideoCore::RasterizerDownloadArea RasterizerNull::GetFlushArea(VAddr addr, u64 size) {
VideoCore::RasterizerDownloadArea new_area{
.start_address = Common::AlignDown(addr, Core::Memory::YUZU_PAGESIZE),
diff --git a/src/video_core/renderer_null/null_rasterizer.h b/src/video_core/renderer_null/null_rasterizer.h
index a8d35d2c1..93b9a6971 100644
--- a/src/video_core/renderer_null/null_rasterizer.h
+++ b/src/video_core/renderer_null/null_rasterizer.h
@@ -53,7 +53,8 @@ public:
VideoCommon::CacheType which = VideoCommon::CacheType::All) override;
void InvalidateRegion(VAddr addr, u64 size,
VideoCommon::CacheType which = VideoCommon::CacheType::All) override;
- void OnCPUWrite(VAddr addr, u64 size) override;
+ void OnCacheInvalidation(VAddr addr, u64 size) override;
+ bool OnCPUWrite(VAddr addr, u64 size) override;
VideoCore::RasterizerDownloadArea GetFlushArea(VAddr addr, u64 size) override;
void InvalidateGPUCache() override;
void UnmapMemory(VAddr addr, u64 size) override;
diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp
index edf527f2d..aadd6967c 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp
@@ -485,12 +485,33 @@ void RasterizerOpenGL::InvalidateRegion(VAddr addr, u64 size, VideoCommon::Cache
}
}
-void RasterizerOpenGL::OnCPUWrite(VAddr addr, u64 size) {
+bool RasterizerOpenGL::OnCPUWrite(VAddr addr, u64 size) {
+ MICROPROFILE_SCOPE(OpenGL_CacheManagement);
+ if (addr == 0 || size == 0) {
+ return false;
+ }
+
+ {
+ std::scoped_lock lock{buffer_cache.mutex};
+ if (buffer_cache.OnCPUWrite(addr, size)) {
+ return true;
+ }
+ }
+
+ {
+ std::scoped_lock lock{texture_cache.mutex};
+ texture_cache.WriteMemory(addr, size);
+ }
+
+ shader_cache.InvalidateRegion(addr, size);
+ return false;
+}
+
+void RasterizerOpenGL::OnCacheInvalidation(VAddr addr, u64 size) {
MICROPROFILE_SCOPE(OpenGL_CacheManagement);
if (addr == 0 || size == 0) {
return;
}
- shader_cache.OnCPUWrite(addr, size);
{
std::scoped_lock lock{texture_cache.mutex};
texture_cache.WriteMemory(addr, size);
@@ -499,15 +520,11 @@ void RasterizerOpenGL::OnCPUWrite(VAddr addr, u64 size) {
std::scoped_lock lock{buffer_cache.mutex};
buffer_cache.CachedWriteMemory(addr, size);
}
+ shader_cache.InvalidateRegion(addr, size);
}
void RasterizerOpenGL::InvalidateGPUCache() {
- MICROPROFILE_SCOPE(OpenGL_CacheManagement);
- shader_cache.SyncGuestHost();
- {
- std::scoped_lock lock{buffer_cache.mutex};
- buffer_cache.FlushCachedWrites();
- }
+ gpu.InvalidateGPUCache();
}
void RasterizerOpenGL::UnmapMemory(VAddr addr, u64 size) {
@@ -519,7 +536,7 @@ void RasterizerOpenGL::UnmapMemory(VAddr addr, u64 size) {
std::scoped_lock lock{buffer_cache.mutex};
buffer_cache.WriteMemory(addr, size);
}
- shader_cache.OnCPUWrite(addr, size);
+ shader_cache.OnCacheInvalidation(addr, size);
}
void RasterizerOpenGL::ModifyGPUMemory(size_t as_id, GPUVAddr addr, u64 size) {
diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h
index a73ad15c1..8eda2ddba 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.h
+++ b/src/video_core/renderer_opengl/gl_rasterizer.h
@@ -98,7 +98,8 @@ public:
VideoCore::RasterizerDownloadArea GetFlushArea(VAddr addr, u64 size) override;
void InvalidateRegion(VAddr addr, u64 size,
VideoCommon::CacheType which = VideoCommon::CacheType::All) override;
- void OnCPUWrite(VAddr addr, u64 size) override;
+ void OnCacheInvalidation(VAddr addr, u64 size) override;
+ bool OnCPUWrite(VAddr addr, u64 size) override;
void InvalidateGPUCache() override;
void UnmapMemory(VAddr addr, u64 size) override;
void ModifyGPUMemory(size_t as_id, GPUVAddr addr, u64 size) override;
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index f7c0d939a..456bb040e 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -566,11 +566,32 @@ void RasterizerVulkan::InnerInvalidation(std::span<const std::pair<VAddr, std::s
}
}
-void RasterizerVulkan::OnCPUWrite(VAddr addr, u64 size) {
+bool RasterizerVulkan::OnCPUWrite(VAddr addr, u64 size) {
+ if (addr == 0 || size == 0) {
+ return false;
+ }
+
+ {
+ std::scoped_lock lock{buffer_cache.mutex};
+ if (buffer_cache.OnCPUWrite(addr, size)) {
+ return true;
+ }
+ }
+
+ {
+ std::scoped_lock lock{texture_cache.mutex};
+ texture_cache.WriteMemory(addr, size);
+ }
+
+ pipeline_cache.InvalidateRegion(addr, size);
+ return false;
+}
+
+void RasterizerVulkan::OnCacheInvalidation(VAddr addr, u64 size) {
if (addr == 0 || size == 0) {
return;
}
- pipeline_cache.OnCPUWrite(addr, size);
+
{
std::scoped_lock lock{texture_cache.mutex};
texture_cache.WriteMemory(addr, size);
@@ -579,14 +600,11 @@ void RasterizerVulkan::OnCPUWrite(VAddr addr, u64 size) {
std::scoped_lock lock{buffer_cache.mutex};
buffer_cache.CachedWriteMemory(addr, size);
}
+ pipeline_cache.InvalidateRegion(addr, size);
}
void RasterizerVulkan::InvalidateGPUCache() {
- pipeline_cache.SyncGuestHost();
- {
- std::scoped_lock lock{buffer_cache.mutex};
- buffer_cache.FlushCachedWrites();
- }
+ gpu.InvalidateGPUCache();
}
void RasterizerVulkan::UnmapMemory(VAddr addr, u64 size) {
@@ -598,7 +616,7 @@ void RasterizerVulkan::UnmapMemory(VAddr addr, u64 size) {
std::scoped_lock lock{buffer_cache.mutex};
buffer_cache.WriteMemory(addr, size);
}
- pipeline_cache.OnCPUWrite(addr, size);
+ pipeline_cache.OnCacheInvalidation(addr, size);
}
void RasterizerVulkan::ModifyGPUMemory(size_t as_id, GPUVAddr addr, u64 size) {
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h
index b39710b3c..73257d964 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.h
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.h
@@ -96,7 +96,8 @@ public:
void InvalidateRegion(VAddr addr, u64 size,
VideoCommon::CacheType which = VideoCommon::CacheType::All) override;
void InnerInvalidation(std::span<const std::pair<VAddr, std::size_t>> sequences) override;
- void OnCPUWrite(VAddr addr, u64 size) override;
+ void OnCacheInvalidation(VAddr addr, u64 size) override;
+ bool OnCPUWrite(VAddr addr, u64 size) override;
void InvalidateGPUCache() override;
void UnmapMemory(VAddr addr, u64 size) override;
void ModifyGPUMemory(size_t as_id, GPUVAddr addr, u64 size) override;
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index 8385b5509..3aac3cfab 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -36,8 +36,10 @@ using VideoCommon::ImageFlagBits;
using VideoCommon::ImageInfo;
using VideoCommon::ImageType;
using VideoCommon::SubresourceRange;
+using VideoCore::Surface::BytesPerBlock;
using VideoCore::Surface::IsPixelFormatASTC;
using VideoCore::Surface::IsPixelFormatInteger;
+using VideoCore::Surface::SurfaceType;
namespace {
constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
@@ -130,7 +132,7 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
[[nodiscard]] VkImageCreateInfo MakeImageCreateInfo(const Device& device, const ImageInfo& info) {
const PixelFormat format = StorageFormat(info.format);
const auto format_info = MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, format);
- VkImageCreateFlags flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
+ VkImageCreateFlags flags{};
if (info.type == ImageType::e2D && info.resources.layers >= 6 &&
info.size.width == info.size.height && !device.HasBrokenCubeImageCompability()) {
flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
@@ -163,11 +165,24 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
}
[[nodiscard]] vk::Image MakeImage(const Device& device, const MemoryAllocator& allocator,
- const ImageInfo& info) {
+ const ImageInfo& info, std::span<const VkFormat> view_formats) {
if (info.type == ImageType::Buffer) {
return vk::Image{};
}
- return allocator.CreateImage(MakeImageCreateInfo(device, info));
+ VkImageCreateInfo image_ci = MakeImageCreateInfo(device, info);
+ const VkImageFormatListCreateInfo image_format_list = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO,
+ .pNext = nullptr,
+ .viewFormatCount = static_cast<u32>(view_formats.size()),
+ .pViewFormats = view_formats.data(),
+ };
+ if (view_formats.size() > 1) {
+ image_ci.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
+ if (device.IsKhrImageFormatListSupported()) {
+ image_ci.pNext = &image_format_list;
+ }
+ }
+ return allocator.CreateImage(image_ci);
}
[[nodiscard]] VkImageAspectFlags ImageAspectMask(PixelFormat format) {
@@ -806,6 +821,23 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched
astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool,
compute_pass_descriptor_queue, memory_allocator);
}
+ if (!device.IsKhrImageFormatListSupported()) {
+ return;
+ }
+ for (size_t index_a = 0; index_a < VideoCore::Surface::MaxPixelFormat; index_a++) {
+ const auto image_format = static_cast<PixelFormat>(index_a);
+ if (IsPixelFormatASTC(image_format) && !device.IsOptimalAstcSupported()) {
+ view_formats[index_a].push_back(VK_FORMAT_A8B8G8R8_UNORM_PACK32);
+ }
+ for (size_t index_b = 0; index_b < VideoCore::Surface::MaxPixelFormat; index_b++) {
+ const auto view_format = static_cast<PixelFormat>(index_b);
+ if (VideoCore::Surface::IsViewCompatible(image_format, view_format, false, true)) {
+ const auto view_info =
+ MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, true, view_format);
+ view_formats[index_a].push_back(view_info.format);
+ }
+ }
+ }
}
void TextureCacheRuntime::Finish() {
@@ -1265,8 +1297,8 @@ void TextureCacheRuntime::TickFrame() {}
Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu_addr_,
VAddr cpu_addr_)
: VideoCommon::ImageBase(info_, gpu_addr_, cpu_addr_), scheduler{&runtime_.scheduler},
- runtime{&runtime_},
- original_image(MakeImage(runtime_.device, runtime_.memory_allocator, info)),
+ runtime{&runtime_}, original_image(MakeImage(runtime_.device, runtime_.memory_allocator, info,
+ runtime->ViewFormats(info.format))),
aspect_mask(ImageAspectMask(info.format)) {
if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) {
if (Settings::values.async_astc.GetValue()) {
@@ -1471,7 +1503,8 @@ bool Image::ScaleUp(bool ignore) {
auto scaled_info = info;
scaled_info.size.width = scaled_width;
scaled_info.size.height = scaled_height;
- scaled_image = MakeImage(runtime->device, runtime->memory_allocator, scaled_info);
+ scaled_image = MakeImage(runtime->device, runtime->memory_allocator, scaled_info,
+ runtime->ViewFormats(info.format));
ignore = false;
}
current_image = *scaled_image;
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h
index 220943116..6621210ea 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.h
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.h
@@ -103,6 +103,10 @@ public:
[[nodiscard]] VkBuffer GetTemporaryBuffer(size_t needed_size);
+ std::span<const VkFormat> ViewFormats(PixelFormat format) {
+ return view_formats[static_cast<std::size_t>(format)];
+ }
+
void BarrierFeedbackLoop();
const Device& device;
@@ -113,6 +117,7 @@ public:
RenderPassCache& render_pass_cache;
std::optional<ASTCDecoderPass> astc_decoder_pass;
const Settings::ResolutionScalingInfo& resolution;
+ std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
static constexpr size_t indexing_slots = 8 * sizeof(size_t);
std::array<vk::Buffer, indexing_slots> buffers{};
diff --git a/src/video_core/shader_cache.cpp b/src/video_core/shader_cache.cpp
index 4db948b6d..01701201d 100644
--- a/src/video_core/shader_cache.cpp
+++ b/src/video_core/shader_cache.cpp
@@ -24,7 +24,7 @@ void ShaderCache::InvalidateRegion(VAddr addr, size_t size) {
RemovePendingShaders();
}
-void ShaderCache::OnCPUWrite(VAddr addr, size_t size) {
+void ShaderCache::OnCacheInvalidation(VAddr addr, size_t size) {
std::scoped_lock lock{invalidation_mutex};
InvalidatePagesInRegion(addr, size);
}
diff --git a/src/video_core/shader_cache.h b/src/video_core/shader_cache.h
index f3cc4c70b..de8e08002 100644
--- a/src/video_core/shader_cache.h
+++ b/src/video_core/shader_cache.h
@@ -62,7 +62,7 @@ public:
/// @brief Unmarks a memory region as cached and marks it for removal
/// @param addr Start address of the CPU write operation
/// @param size Number of bytes of the CPU write operation
- void OnCPUWrite(VAddr addr, size_t size);
+ void OnCacheInvalidation(VAddr addr, size_t size);
/// @brief Flushes delayed removal operations
void SyncGuestHost();
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 8190f3ba1..3a859139c 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -598,6 +598,10 @@ void TextureCache<P>::UnmapGPUMemory(size_t as_id, GPUVAddr gpu_addr, size_t siz
[&](ImageId id, Image&) { deleted_images.push_back(id); });
for (const ImageId id : deleted_images) {
Image& image = slot_images[id];
+ if (True(image.flags & ImageFlagBits::CpuModified)) {
+ continue;
+ }
+ image.flags |= ImageFlagBits::CpuModified;
if (True(image.flags & ImageFlagBits::Remapped)) {
continue;
}
@@ -865,11 +869,15 @@ void TextureCache<P>::PopAsyncFlushes() {
template <class P>
ImageId TextureCache<P>::DmaImageId(const Tegra::DMA::ImageOperand& operand, bool is_upload) {
const ImageInfo dst_info(operand);
- const ImageId image_id = FindDMAImage(dst_info, operand.address);
- if (!image_id) {
+ const ImageId dst_id = FindDMAImage(dst_info, operand.address);
+ if (!dst_id) {
+ return NULL_IMAGE_ID;
+ }
+ auto& image = slot_images[dst_id];
+ if (False(image.flags & ImageFlagBits::GpuModified)) {
+ // No need to waste time on an image that's synced with guest
return NULL_IMAGE_ID;
}
- auto& image = slot_images[image_id];
if (image.info.type == ImageType::e3D) {
// Don't accelerate 3D images.
return NULL_IMAGE_ID;
@@ -883,7 +891,7 @@ ImageId TextureCache<P>::DmaImageId(const Tegra::DMA::ImageOperand& operand, boo
if (!base) {
return NULL_IMAGE_ID;
}
- return image_id;
+ return dst_id;
}
template <class P>
diff --git a/src/video_core/texture_cache/types.h b/src/video_core/texture_cache/types.h
index a0e10643f..0453456b4 100644
--- a/src/video_core/texture_cache/types.h
+++ b/src/video_core/texture_cache/types.h
@@ -54,7 +54,6 @@ enum class RelaxedOptions : u32 {
Format = 1 << 1,
Samples = 1 << 2,
ForceBrokenViews = 1 << 3,
- FormatBpp = 1 << 4,
};
DECLARE_ENUM_FLAG_OPERATORS(RelaxedOptions)
diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp
index 9a618a57a..0de6ed09d 100644
--- a/src/video_core/texture_cache/util.cpp
+++ b/src/video_core/texture_cache/util.cpp
@@ -1201,8 +1201,7 @@ std::optional<SubresourceBase> FindSubresource(const ImageInfo& candidate, const
// Format checking is relaxed, but we still have to check for matching bytes per block.
// This avoids creating a view for blits on UE4 titles where formats with different bytes
// per block are aliased.
- if (BytesPerBlock(existing.format) != BytesPerBlock(candidate.format) &&
- False(options & RelaxedOptions::FormatBpp)) {
+ if (BytesPerBlock(existing.format) != BytesPerBlock(candidate.format)) {
return std::nullopt;
}
} else {
@@ -1233,11 +1232,7 @@ std::optional<SubresourceBase> FindSubresource(const ImageInfo& candidate, const
}
const bool strict_size = False(options & RelaxedOptions::Size);
if (!IsBlockLinearSizeCompatible(existing, candidate, base->level, 0, strict_size)) {
- if (False(options & RelaxedOptions::FormatBpp)) {
- return std::nullopt;
- } else if (!IsBlockLinearSizeCompatibleBPPRelaxed(existing, candidate, base->level, 0)) {
- return std::nullopt;
- }
+ return std::nullopt;
}
// TODO: compare block sizes
return base;
diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp
index 421e71e5a..e04852e01 100644
--- a/src/video_core/vulkan_common/vulkan_device.cpp
+++ b/src/video_core/vulkan_common/vulkan_device.cpp
@@ -485,7 +485,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
loaded_extensions.erase(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME);
}
}
- if (extensions.extended_dynamic_state2 && (is_radv || is_qualcomm)) {
+ if (extensions.extended_dynamic_state2 && is_radv) {
const u32 version = (properties.properties.driverVersion << 3) >> 3;
if (version < VK_MAKE_API_VERSION(0, 22, 3, 1)) {
LOG_WARNING(
@@ -498,6 +498,20 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
loaded_extensions.erase(VK_EXT_EXTENDED_DYNAMIC_STATE_2_EXTENSION_NAME);
}
}
+ if (extensions.extended_dynamic_state2 && is_qualcomm) {
+ const u32 version = (properties.properties.driverVersion << 3) >> 3;
+ if (version >= VK_MAKE_API_VERSION(0, 0, 676, 0) &&
+ version < VK_MAKE_API_VERSION(0, 0, 680, 0)) {
+ // Qualcomm Adreno 7xx drivers do not properly support extended_dynamic_state2.
+ LOG_WARNING(Render_Vulkan,
+ "Qualcomm Adreno 7xx drivers have broken VK_EXT_extended_dynamic_state2");
+ features.extended_dynamic_state2.extendedDynamicState2 = false;
+ features.extended_dynamic_state2.extendedDynamicState2LogicOp = false;
+ features.extended_dynamic_state2.extendedDynamicState2PatchControlPoints = false;
+ extensions.extended_dynamic_state2 = false;
+ loaded_extensions.erase(VK_EXT_EXTENDED_DYNAMIC_STATE_2_EXTENSION_NAME);
+ }
+ }
if (extensions.extended_dynamic_state3 && is_radv) {
LOG_WARNING(Render_Vulkan, "RADV has broken extendedDynamicState3ColorBlendEquation");
features.extended_dynamic_state3.extendedDynamicState3ColorBlendEnable = false;
@@ -512,8 +526,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
dynamic_state3_enables = false;
}
}
- if (extensions.vertex_input_dynamic_state && (is_radv || is_qualcomm)) {
- // Qualcomm S8gen2 drivers do not properly support vertex_input_dynamic_state.
+ if (extensions.vertex_input_dynamic_state && is_radv) {
// TODO(ameerj): Blacklist only offending driver versions
// TODO(ameerj): Confirm if RDNA1 is affected
const bool is_rdna2 =
@@ -526,6 +539,19 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
loaded_extensions.erase(VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_EXTENSION_NAME);
}
}
+ if (extensions.vertex_input_dynamic_state && is_qualcomm) {
+ const u32 version = (properties.properties.driverVersion << 3) >> 3;
+ if (version >= VK_MAKE_API_VERSION(0, 0, 676, 0) &&
+ version < VK_MAKE_API_VERSION(0, 0, 680, 0)) {
+ // Qualcomm Adreno 7xx drivers do not properly support vertex_input_dynamic_state.
+ LOG_WARNING(
+ Render_Vulkan,
+ "Qualcomm Adreno 7xx drivers have broken VK_EXT_vertex_input_dynamic_state");
+ features.vertex_input_dynamic_state.vertexInputDynamicState = false;
+ extensions.vertex_input_dynamic_state = false;
+ loaded_extensions.erase(VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_EXTENSION_NAME);
+ }
+ }
sets_per_pool = 64;
if (extensions.extended_dynamic_state3 && is_amd_driver &&
@@ -774,6 +800,17 @@ bool Device::ShouldBoostClocks() const {
return validated_driver && !is_steam_deck && !is_debugging;
}
+bool Device::HasTimelineSemaphore() const {
+ if (GetDriverID() == VK_DRIVER_ID_QUALCOMM_PROPRIETARY ||
+ GetDriverID() == VK_DRIVER_ID_MESA_TURNIP) {
+ // Timeline semaphores do not work properly on all Qualcomm drivers.
+ // They generally work properly with Turnip drivers, but are problematic on some devices
+ // (e.g. ZTE handsets with Snapdragon 870).
+ return false;
+ }
+ return features.timeline_semaphore.timelineSemaphore;
+}
+
bool Device::GetSuitability(bool requires_swapchain) {
// Assume we will be suitable.
bool suitable = true;
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h
index 1f17265d5..be3ed45ff 100644
--- a/src/video_core/vulkan_common/vulkan_device.h
+++ b/src/video_core/vulkan_common/vulkan_device.h
@@ -77,6 +77,7 @@ VK_DEFINE_HANDLE(VmaAllocator)
EXTENSION(KHR, SPIRV_1_4, spirv_1_4) \
EXTENSION(KHR, SWAPCHAIN, swapchain) \
EXTENSION(KHR, SWAPCHAIN_MUTABLE_FORMAT, swapchain_mutable_format) \
+ EXTENSION(KHR, IMAGE_FORMAT_LIST, image_format_list) \
EXTENSION(NV, DEVICE_DIAGNOSTICS_CONFIG, device_diagnostics_config) \
EXTENSION(NV, GEOMETRY_SHADER_PASSTHROUGH, geometry_shader_passthrough) \
EXTENSION(NV, VIEWPORT_ARRAY2, viewport_array2) \
@@ -408,6 +409,11 @@ public:
return extensions.workgroup_memory_explicit_layout;
}
+ /// Returns true if the device supports VK_KHR_image_format_list.
+ bool IsKhrImageFormatListSupported() const {
+ return extensions.image_format_list || instance_version >= VK_API_VERSION_1_2;
+ }
+
/// Returns true if the device supports VK_EXT_primitive_topology_list_restart.
bool IsTopologyListPrimitiveRestartSupported() const {
return features.primitive_topology_list_restart.primitiveTopologyListRestart;
@@ -522,13 +528,7 @@ public:
return extensions.shader_atomic_int64;
}
- bool HasTimelineSemaphore() const {
- if (GetDriverID() == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) {
- // Timeline semaphores do not work properly on all Qualcomm drivers.
- return false;
- }
- return features.timeline_semaphore.timelineSemaphore;
- }
+ bool HasTimelineSemaphore() const;
/// Returns the minimum supported version of SPIR-V.
u32 SupportedSpirvVersion() const {
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index e8418b302..20532416c 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -101,6 +101,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "common/settings.h"
#include "common/telemetry.h"
#include "core/core.h"
+#include "core/core_timing.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h"
#include "core/file_sys/common_funcs.h"
@@ -389,6 +390,7 @@ GMainWindow::GMainWindow(std::unique_ptr<Config> config_, bool has_broken_vulkan
std::chrono::duration_cast<std::chrono::duration<f64, std::milli>>(
Common::Windows::SetCurrentTimerResolutionToMaximum())
.count());
+ system->CoreTiming().SetTimerResolutionNs(Common::Windows::GetCurrentTimerResolution());
#endif
UpdateWindowTitle();
@@ -452,7 +454,7 @@ GMainWindow::GMainWindow(std::unique_ptr<Config> config_, bool has_broken_vulkan
// the user through their desktop environment.
//: TRANSLATORS: This string is shown to the user to explain why yuzu needs to prevent the
//: computer from sleeping
- QByteArray wakelock_reason = tr("Running a game").toLatin1();
+ QByteArray wakelock_reason = tr("Running a game").toUtf8();
SDL_SetHint(SDL_HINT_SCREENSAVER_INHIBIT_ACTIVITY_NAME, wakelock_reason.data());
// SDL disables the screen saver by default, and setting the hint
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp
index 7b6d49c63..d0433ffc6 100644
--- a/src/yuzu_cmd/yuzu.cpp
+++ b/src/yuzu_cmd/yuzu.cpp
@@ -21,6 +21,7 @@
#include "common/string_util.h"
#include "common/telemetry.h"
#include "core/core.h"
+#include "core/core_timing.h"
#include "core/cpu_manager.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/registered_cache.h"
@@ -316,8 +317,6 @@ int main(int argc, char** argv) {
#ifdef _WIN32
LocalFree(argv_w);
-
- Common::Windows::SetCurrentTimerResolutionToMaximum();
#endif
MicroProfileOnThreadCreate("EmuThread");
@@ -351,6 +350,11 @@ int main(int argc, char** argv) {
break;
}
+#ifdef _WIN32
+ Common::Windows::SetCurrentTimerResolutionToMaximum();
+ system.CoreTiming().SetTimerResolutionNs(Common::Windows::GetCurrentTimerResolution());
+#endif
+
system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
system.GetFileSystemController().CreateFactories(*system.GetFilesystem());