diff options
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()); |