Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
789dacedca | ||
|
|
35f6afb031 | ||
|
|
38561cd7e3 | ||
|
|
05f536694a | ||
|
|
444109c251 | ||
|
|
0ce2ec3b36 | ||
|
|
d8bfc691d1 | ||
|
|
39a46d755f | ||
|
|
b60d0aabf0 | ||
|
|
aeb2aec13b | ||
|
|
cb3521272f |
@@ -149,7 +149,7 @@ if (YUZU_USE_BUNDLED_VCPKG)
|
||||
set(VCPKG_DOWNLOADS_PATH ${PROJECT_SOURCE_DIR}/externals/vcpkg/downloads)
|
||||
set(NASM_VERSION "2.16.01")
|
||||
set(NASM_DESTINATION_PATH ${VCPKG_DOWNLOADS_PATH}/nasm-${NASM_VERSION}-win64.zip)
|
||||
set(NASM_DOWNLOAD_URL "https://git.eden-emu.dev/eden-emu/ext-windows-bin/raw/master/nasm/nasm-${NASM_VERSION}-win64.zip")
|
||||
set(NASM_DOWNLOAD_URL "https://github.com/eden-emulator/ext-windows-bin/raw/master/nasm/nasm-${NASM_VERSION}-win64.zip")
|
||||
|
||||
if (NOT EXISTS ${NASM_DESTINATION_PATH})
|
||||
file(DOWNLOAD ${NASM_DOWNLOAD_URL} ${NASM_DESTINATION_PATH} SHOW_PROGRESS STATUS NASM_STATUS)
|
||||
@@ -548,7 +548,7 @@ if (NOT CLANG_FORMAT)
|
||||
message(STATUS "Clang format not found! Downloading...")
|
||||
set(CLANG_FORMAT "${PROJECT_BINARY_DIR}/externals/clang-format${CLANG_FORMAT_POSTFIX}.exe")
|
||||
file(DOWNLOAD
|
||||
https://git.eden-emu.dev/eden-emu/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
|
||||
https://github.com/eden-emulator/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
|
||||
"${CLANG_FORMAT}" SHOW_PROGRESS
|
||||
STATUS DOWNLOAD_SUCCESS)
|
||||
if (NOT DOWNLOAD_SUCCESS EQUAL 0)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||
function(download_bundled_external remote_path lib_name prefix_var)
|
||||
|
||||
set(package_base_url "https://git.eden-emu.dev/eden-emu/")
|
||||
set(package_base_url "https://github.com/eden-emulator/")
|
||||
set(package_repo "no_platform")
|
||||
set(package_extension "no_platform")
|
||||
if (WIN32)
|
||||
|
||||
11
README.md
11
README.md
@@ -1,14 +1,15 @@
|
||||
<!--
|
||||
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 EDEN Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<!-- lang: en-GB -->
|
||||
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://git.eden-emu.dev/eden-emu/eden"><img src="https://git.eden-emu.dev/eden-emu/eden/raw/branch/master/dist/qt_themes/default/icons/256x256/eden_named.png" alt="Eden" width="200"></a>
|
||||
<a href="https://github.com/pflyly/eden-mirror"><img src="https://github.com/pflyly/eden-mirror/raw/branch/master/dist/qt_themes/default/icons/256x256/eden_named.png" alt="Eden" width="200"></a>
|
||||
<br>
|
||||
<b>Eden</b>
|
||||
<br>
|
||||
@@ -46,7 +47,7 @@ Check out our [website](https://eden-emulator.github.io) for the latest news on
|
||||
|
||||
## Development
|
||||
|
||||
Most of the development happens on our Git server. It is also where [our central repository](https://git.eden-emu.dev/eden-emu/eden) is hosted. For development discussions, please join us on [Discord](https://discord.gg/ynGGJAN4Rx).
|
||||
Most of the development happens on our Git server. It is also where [our central repository](https://github.com/pflyly/eden-mirror) is hosted. For development discussions, please join us on [Discord](https://discord.gg/edenemu).
|
||||
|
||||
If you would like to contribute, we are open to new developers and pull requests. Please ensure that your work is of a high standard and properly documented.
|
||||
You can also contact any of the developers on Discord to learn more about the current state of the emulator.
|
||||
@@ -73,7 +74,7 @@ Any donations received will go towards things such as:
|
||||
* Additional hardware (e.g. GPUs as needed to improve rendering support, other peripherals to add support for, etc.)
|
||||
* CI Infrastructure
|
||||
|
||||
If you would prefer to support us in a different way, please join our [Discord](https://discord.gg/ynGGJAN4Rx), once public, and talk to Camille or any of our other developers.
|
||||
If you would prefer to support us in a different way, please join our [Discord](https://discord.gg/edenemu), once public, and talk to Camille or any of our other developers.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ android {
|
||||
applicationIdSuffix = ".relWithDebInfo"
|
||||
isJniDebuggable = true
|
||||
}
|
||||
|
||||
|
||||
// Signed by debug key disallowing distribution on Play Store.
|
||||
// Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
|
||||
debug {
|
||||
|
||||
@@ -25,6 +25,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" android:required="false" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||
|
||||
<application
|
||||
android:name="org.yuzu.yuzu_emu.YuzuApplication"
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class GameAdapter(private val activity: AppCompatActivity) :
|
||||
AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) {
|
||||
@@ -49,7 +50,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
var cardSize: Int = 0
|
||||
public var cardSize: Int = 0
|
||||
private set
|
||||
|
||||
fun setCardSize(size: Int) {
|
||||
@@ -63,7 +64,6 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||
|
||||
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
// Always reset scale/alpha for recycled views
|
||||
when (getItemViewType(position)) {
|
||||
VIEW_TYPE_LIST -> {
|
||||
val listBinding = holder.binding as CardGameListBinding
|
||||
@@ -85,14 +85,9 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||
}
|
||||
VIEW_TYPE_CAROUSEL -> {
|
||||
val carouselBinding = holder.binding as CardGameCarouselBinding
|
||||
carouselBinding.cardGameCarousel.scaleX = 1f
|
||||
carouselBinding.cardGameCarousel.scaleY = 1f
|
||||
//soothens transient flickering
|
||||
carouselBinding.cardGameCarousel.scaleY = 0f
|
||||
carouselBinding.cardGameCarousel.alpha = 0f
|
||||
// Set square size for carousel
|
||||
if (cardSize > 0) {
|
||||
carouselBinding.root.layoutParams.width = cardSize
|
||||
carouselBinding.root.layoutParams.height = cardSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,16 +153,6 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||
private fun bindCarouselView(model: Game) {
|
||||
val carouselBinding = binding as CardGameCarouselBinding
|
||||
|
||||
// Remove padding from the root LinearLayout
|
||||
(carouselBinding.root.getChildAt(0) as? LinearLayout)?.setPadding(0, 0, 0, 0)
|
||||
|
||||
// Always set square size and remove margins for carousel
|
||||
val params = carouselBinding.root.layoutParams
|
||||
params.width = cardSize
|
||||
params.height = cardSize
|
||||
if (params is ViewGroup.MarginLayoutParams) params.setMargins(0, 0, 0, 0)
|
||||
carouselBinding.root.layoutParams = params
|
||||
|
||||
carouselBinding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
GameIconUtils.loadGameIcon(model, carouselBinding.imageGameScreen)
|
||||
|
||||
@@ -178,6 +163,9 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||
|
||||
carouselBinding.imageGameScreen.contentDescription =
|
||||
binding.root.context.getString(R.string.game_image_desc, model.title)
|
||||
|
||||
// Ensure zero-heighted-full-width cards for carousel
|
||||
carouselBinding.root.layoutParams.width = cardSize
|
||||
}
|
||||
|
||||
fun onClick(game: Game) {
|
||||
|
||||
@@ -361,7 +361,7 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
|
||||
|
||||
// setup listeners etc
|
||||
val roomNameWatcher = object : TextValidatorWatcher(
|
||||
binding.btnConfirm, // TODO(alekpop, crueter): Figure out a better way to deal with this?
|
||||
binding.btnConfirm,
|
||||
binding.layoutRoomName,
|
||||
context.getString(
|
||||
R.string.multiplayer_room_name_error
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.features.fetcher
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
@@ -17,7 +20,9 @@ import androidx.transition.TransitionManager
|
||||
import androidx.transition.TransitionSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||
|
||||
class DriverGroupAdapter(
|
||||
@@ -25,43 +30,61 @@ class DriverGroupAdapter(
|
||||
private val driverViewModel: DriverViewModel
|
||||
) : RecyclerView.Adapter<DriverGroupAdapter.DriverGroupViewHolder>() {
|
||||
private var driverGroups: List<DriverGroup> = emptyList()
|
||||
private val adapterJobs = mutableMapOf<Int, Job>()
|
||||
|
||||
inner class DriverGroupViewHolder(
|
||||
private val binding: ItemDriverGroupBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(group: DriverGroup) {
|
||||
binding.textGroupName.text = group.name
|
||||
|
||||
if (binding.recyclerReleases.layoutManager == null) {
|
||||
binding.recyclerReleases.layoutManager = LinearLayoutManager(activity)
|
||||
binding.recyclerReleases.addItemDecoration(
|
||||
SpacingItemDecoration(
|
||||
(activity.resources.displayMetrics.density * 8).toInt()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val onClick = {
|
||||
adapterJobs[bindingAdapterPosition]?.cancel()
|
||||
|
||||
TransitionManager.beginDelayedTransition(
|
||||
binding.root,
|
||||
TransitionSet().addTransition(Fade()).addTransition(ChangeBounds())
|
||||
.setDuration(200)
|
||||
)
|
||||
|
||||
val isVisible = binding.recyclerReleases.isVisible
|
||||
|
||||
if (!isVisible && binding.recyclerReleases.adapter == null) {
|
||||
val job = CoroutineScope(Dispatchers.Main).launch {
|
||||
// It prevents blocking the ui thread.
|
||||
var adapter: ReleaseAdapter?
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
adapter = ReleaseAdapter(group.releases, activity, driverViewModel)
|
||||
}
|
||||
|
||||
binding.recyclerReleases.adapter = adapter
|
||||
}
|
||||
|
||||
adapterJobs[bindingAdapterPosition] = job
|
||||
}
|
||||
|
||||
binding.recyclerReleases.visibility = if (isVisible) View.GONE else View.VISIBLE
|
||||
binding.imageDropdownArrow.rotation = if (isVisible) 0f else 180f
|
||||
|
||||
if (!isVisible && binding.recyclerReleases.adapter == null) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
binding.recyclerReleases.layoutManager =
|
||||
LinearLayoutManager(binding.root.context)
|
||||
binding.recyclerReleases.adapter =
|
||||
ReleaseAdapter(group.releases, activity, driverViewModel)
|
||||
|
||||
binding.recyclerReleases.addItemDecoration(
|
||||
SpacingItemDecoration(
|
||||
(activity.resources.displayMetrics.density * 8).toInt()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.textGroupName.text = group.name
|
||||
binding.textGroupName.setOnClickListener { onClick() }
|
||||
|
||||
binding.imageDropdownArrow.setOnClickListener { onClick() }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
adapterJobs[bindingAdapterPosition]?.cancel()
|
||||
adapterJobs.remove(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverGroupViewHolder {
|
||||
|
||||
@@ -378,7 +378,9 @@ abstract class SettingsItem(
|
||||
IntSetting.RENDERER_RESOLUTION,
|
||||
titleId = R.string.renderer_resolution,
|
||||
choicesId = R.array.rendererResolutionNames,
|
||||
valuesId = R.array.rendererResolutionValues
|
||||
valuesId = R.array.rendererResolutionValues,
|
||||
warnChoices = (5..7).toList(),
|
||||
warningMessage = R.string.warning_resolution
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ class SingleChoiceSetting(
|
||||
@StringRes descriptionId: Int = 0,
|
||||
descriptionString: String = "",
|
||||
@ArrayRes val choicesId: Int,
|
||||
@ArrayRes val valuesId: Int
|
||||
@ArrayRes val valuesId: Int,
|
||||
val warnChoices: List<Int> = ArrayList(),
|
||||
@StringRes val warningMessage: Int = 0,
|
||||
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
|
||||
override val type = TYPE_SINGLE_CHOICE
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
@@ -113,6 +112,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||
SettingsItem.TYPE_SINGLE_CHOICE -> {
|
||||
val item = settingsViewModel.clickedItem as SingleChoiceSetting
|
||||
val value = getSelectionForSingleChoiceValue(item)
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(item.title)
|
||||
.setSingleChoiceItems(item.choicesId, value, this)
|
||||
@@ -125,6 +125,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||
|
||||
settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units)
|
||||
sliderBinding.slider.apply {
|
||||
stepSize = 1.0f
|
||||
valueFrom = item.min.toFloat()
|
||||
valueTo = item.max.toFloat()
|
||||
value = settingsViewModel.sliderProgress.value.toFloat()
|
||||
@@ -244,6 +245,15 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||
is SingleChoiceSetting -> {
|
||||
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
||||
val value = getValueForSingleChoiceSelection(scSetting, which)
|
||||
|
||||
if (value in scSetting.warnChoices) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(scSetting.warningMessage)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
scSetting.setSelectedValue(value)
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ class SettingsFragmentPresenter(
|
||||
}
|
||||
|
||||
val pairedSettingKey = item.setting.pairedSettingKey
|
||||
|
||||
if (pairedSettingKey.isNotEmpty()) {
|
||||
val pairedSettingValue = NativeConfig.getBoolean(
|
||||
pairedSettingKey,
|
||||
@@ -220,7 +221,6 @@ class SettingsFragmentPresenter(
|
||||
|
||||
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
|
||||
sl.apply {
|
||||
// TODO(crueter): reorganize this, this is awful
|
||||
add(HeaderSetting(R.string.backend))
|
||||
|
||||
add(IntSetting.RENDERER_ACCURACY.key)
|
||||
@@ -436,7 +436,6 @@ class SettingsFragmentPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(alekpop): sort these into headers.
|
||||
private fun addEdenVeilSettings(sl: ArrayList<SettingsItem>) {
|
||||
sl.apply {
|
||||
add(HeaderSetting(R.string.veil_extensions))
|
||||
|
||||
@@ -22,6 +22,7 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
|
||||
binding.textSettingDescription.setVisible(setting.description.isNotEmpty())
|
||||
binding.textSettingDescription.text = setting.description
|
||||
|
||||
// TODO(alekpop): A race condition occurs here if the button is clicked too fast
|
||||
binding.switchWidget.setOnCheckedChangeListener(null)
|
||||
binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
|
||||
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
@@ -28,10 +30,12 @@ import org.yuzu.yuzu_emu.databinding.FragmentDriverFetcherBinding
|
||||
import org.yuzu.yuzu_emu.features.fetcher.DriverGroupAdapter
|
||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||
import org.yuzu.yuzu_emu.utils.Log
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import kotlin.getValue
|
||||
|
||||
class DriverFetcherFragment : Fragment() {
|
||||
@@ -49,17 +53,22 @@ class DriverFetcherFragment : Fragment() {
|
||||
private val recommendedDriver: String
|
||||
get() = driverMap.firstOrNull { adrenoModel in it.first }?.second ?: "Unsupported"
|
||||
|
||||
enum class SortMode {
|
||||
Default, PublishTime,
|
||||
}
|
||||
|
||||
private data class DriverRepo(
|
||||
val name: String = "",
|
||||
val path: String = "",
|
||||
val sort: Int = 0,
|
||||
val useTagName: Boolean = false
|
||||
val useTagName: Boolean = false,
|
||||
val sortMode: SortMode = SortMode.Default,
|
||||
)
|
||||
|
||||
private val repoList: List<DriverRepo> = listOf(
|
||||
DriverRepo("Mr. Purple Turnip", "MrPurple666/purple-turnip", 0),
|
||||
DriverRepo("GameHub Adreno 8xx", "crueter/GameHub-8Elite-Drivers", 1),
|
||||
DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true),
|
||||
DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true, SortMode.PublishTime),
|
||||
DriverRepo("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3),
|
||||
)
|
||||
|
||||
@@ -78,7 +87,7 @@ class DriverFetcherFragment : Fragment() {
|
||||
private lateinit var driverGroupAdapter: DriverGroupAdapter
|
||||
private val driverViewModel: DriverViewModel by activityViewModels()
|
||||
|
||||
fun parseAdrenoModel(): Int {
|
||||
private fun parseAdrenoModel(): Int {
|
||||
if (gpuModel == null) {
|
||||
return 0
|
||||
}
|
||||
@@ -115,9 +124,8 @@ class DriverFetcherFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentDriverFetcherBinding.inflate(inflater)
|
||||
binding.badgeRecommendedDriver.text = recommendedDriver
|
||||
binding.badgeGpuModel.text = gpuModel
|
||||
@@ -150,11 +158,12 @@ class DriverFetcherFragment : Fragment() {
|
||||
val name = driver.name
|
||||
val path = driver.path
|
||||
val useTagName = driver.useTagName
|
||||
val sortMode = driver.sortMode
|
||||
val sort = driver.sort
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val request = Request.Builder()
|
||||
.url("https://api.github.com/repos/$path/releases")
|
||||
.build()
|
||||
val request =
|
||||
Request.Builder().url("https://api.github.com/repos/$path/releases").build()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
var releases: ArrayList<Release>
|
||||
@@ -165,28 +174,25 @@ class DriverFetcherFragment : Fragment() {
|
||||
}
|
||||
|
||||
val body = response.body?.string() ?: return@withContext
|
||||
releases = Release.fromJsonArray(body, useTagName)
|
||||
releases = Release.fromJsonArray(body, useTagName, sortMode)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
MaterialAlertDialogBuilder(requireActivity().applicationContext)
|
||||
.setTitle(getString(R.string.error_during_fetch))
|
||||
MaterialAlertDialogBuilder(requireActivity()).setTitle(getString(R.string.error_during_fetch))
|
||||
.setMessage("${getString(R.string.failed_to_fetch)} ${name}:\n${e.message}")
|
||||
.setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.cancel() }
|
||||
.show()
|
||||
|
||||
releases = ArrayList<Release>()
|
||||
releases = ArrayList()
|
||||
}
|
||||
}
|
||||
|
||||
val driver = DriverGroup(
|
||||
name,
|
||||
releases,
|
||||
sort
|
||||
val group = DriverGroup(
|
||||
name, releases, sort
|
||||
)
|
||||
|
||||
synchronized(driverGroups) {
|
||||
driverGroups.add(driver)
|
||||
driverGroups.add(group)
|
||||
driverGroups.sortBy {
|
||||
it.sort
|
||||
}
|
||||
@@ -204,39 +210,41 @@ class DriverFetcherFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInsets() =
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||
private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||
|
||||
val leftInsets = barInsets.left + cutoutInsets.left
|
||||
val rightInsets = barInsets.right + cutoutInsets.right
|
||||
val leftInsets = barInsets.left + cutoutInsets.left
|
||||
val rightInsets = barInsets.right + cutoutInsets.right
|
||||
|
||||
binding.toolbarDrivers.updateMargins(left = leftInsets, right = rightInsets)
|
||||
binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets)
|
||||
binding.toolbarDrivers.updateMargins(left = leftInsets, right = rightInsets)
|
||||
binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets)
|
||||
|
||||
binding.listDrivers.updatePadding(
|
||||
bottom = barInsets.bottom +
|
||||
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||
)
|
||||
binding.listDrivers.updatePadding(
|
||||
bottom = barInsets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||
)
|
||||
|
||||
windowInsets
|
||||
}
|
||||
windowInsets
|
||||
}
|
||||
|
||||
data class Artifact(val url: URL, val name: String)
|
||||
|
||||
data class Release(
|
||||
var tagName: String = "",
|
||||
var titleName: String = "",
|
||||
var title: String = "",
|
||||
var body: String = "",
|
||||
var artifacts: List<Artifact> = ArrayList<Artifact>(),
|
||||
var artifacts: List<Artifact> = ArrayList(),
|
||||
var prerelease: Boolean = false,
|
||||
var latest: Boolean = false
|
||||
var latest: Boolean = false,
|
||||
var publishTime: LocalDateTime = LocalDateTime.now(),
|
||||
) {
|
||||
companion object {
|
||||
fun fromJsonArray(jsonString: String, useTagName: Boolean): ArrayList<Release> {
|
||||
fun fromJsonArray(
|
||||
jsonString: String, useTagName: Boolean, sortMode: SortMode
|
||||
): ArrayList<Release> {
|
||||
val mapper = jacksonObjectMapper()
|
||||
|
||||
try {
|
||||
@@ -256,39 +264,55 @@ class DriverFetcherFragment : Fragment() {
|
||||
}
|
||||
|
||||
releases.add(release)
|
||||
|
||||
println(release.publishTime)
|
||||
}
|
||||
}
|
||||
|
||||
when (sortMode) {
|
||||
SortMode.PublishTime -> releases.sortByDescending {
|
||||
it.publishTime
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
return releases
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return ArrayList<Release>()
|
||||
return ArrayList()
|
||||
}
|
||||
}
|
||||
|
||||
fun fromJson(node: JsonNode, useTagName: Boolean): Release {
|
||||
private fun fromJson(node: JsonNode, useTagName: Boolean): Release {
|
||||
try {
|
||||
val tagName = node.get("tag_name").toString().removeSurrounding("\"")
|
||||
val body = node.get("body").toString().removeSurrounding("\"")
|
||||
val prerelease = node.get("prerelease").toString().toBoolean()
|
||||
val title = if (useTagName) tagName else node.get("name").toString().removeSurrounding("\"")
|
||||
val titleName = node.get("name").toString().removeSurrounding("\"")
|
||||
|
||||
val published = node.get("published_at").toString().removeSurrounding("\"")
|
||||
val instantTime: Instant? = Instant.parse(published)
|
||||
val localTime = instantTime?.atZone(ZoneId.systemDefault())?.toLocalDateTime() ?: LocalDateTime.now()
|
||||
|
||||
val title = if (useTagName) tagName else titleName
|
||||
|
||||
val assets = node.get("assets")
|
||||
val artifacts = ArrayList<Artifact>()
|
||||
if (assets?.isArray == true) {
|
||||
assets.forEach { node ->
|
||||
val urlStr =
|
||||
node.get("browser_download_url").toString().removeSurrounding("\"")
|
||||
assets.forEach { subNode ->
|
||||
val urlStr = subNode.get("browser_download_url").toString()
|
||||
.removeSurrounding("\"")
|
||||
|
||||
val url = URL(urlStr)
|
||||
val name = node.get("name").toString().removeSurrounding("\"")
|
||||
val name = subNode.get("name").toString().removeSurrounding("\"")
|
||||
|
||||
val artifact = Artifact(url, name)
|
||||
artifacts.add(artifact)
|
||||
}
|
||||
}
|
||||
|
||||
return Release(tagName, title, body, artifacts, prerelease)
|
||||
return Release(tagName, titleName, title, body, artifacts, prerelease, false, localTime)
|
||||
} catch (e: Exception) {
|
||||
// TODO: handle malformed input.
|
||||
e.printStackTrace()
|
||||
|
||||
@@ -105,6 +105,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
private var isInFoldableLayout = false
|
||||
|
||||
private lateinit var gpuModel: String
|
||||
private lateinit var fwVersion: String
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
@@ -186,6 +187,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
gpuModel = GpuDriverHelper.getGpuModel().toString()
|
||||
fwVersion = NativeLibrary.firmwareVersion()
|
||||
|
||||
binding.surfaceEmulation.holder.addCallback(this)
|
||||
binding.doneControlConfig.setOnClickListener { stopConfiguringControls() }
|
||||
@@ -755,7 +757,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
|
||||
if (BooleanSetting.SHOW_FW_VERSION.getBoolean(NativeConfig.isPerGameConfigLoaded())) {
|
||||
if (sb.isNotEmpty()) sb.append(" | ")
|
||||
sb.append(NativeLibrary.firmwareVersion())
|
||||
sb.append(fwVersion)
|
||||
}
|
||||
|
||||
binding.showSocOverlayText.text = sb.toString()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.ui
|
||||
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
@@ -21,8 +22,9 @@ class MidScreenSwipeRefreshLayout @JvmOverloads constructor(
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
startX = ev.x
|
||||
val width = width
|
||||
val leftBound = width / 3
|
||||
val rightBound = width * 2 / 3
|
||||
val center_fraction = resources.getFraction(R.fraction.carousel_midscreenswipe_width_fraction, 1, 1).coerceIn(0f, 1f)
|
||||
val leftBound = ((1 - center_fraction) / 2) * width
|
||||
val rightBound = leftBound + (width * center_fraction)
|
||||
allowRefresh = startX >= leftBound && startX <= rightBound
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,16 @@ class GamesViewModel : ViewModel() {
|
||||
val searchFocused: StateFlow<Boolean> get() = _searchFocused
|
||||
private val _searchFocused = MutableStateFlow(false)
|
||||
|
||||
val shouldScrollAfterReload: StateFlow<Boolean> get() = _shouldScrollAfterReload
|
||||
private val _shouldScrollAfterReload = MutableStateFlow(false)
|
||||
|
||||
private val _folders = MutableStateFlow(mutableListOf<GameDir>())
|
||||
val folders = _folders.asStateFlow()
|
||||
|
||||
private val _filteredGames = MutableStateFlow<List<Game>>(emptyList())
|
||||
|
||||
var lastScrollPosition: Int = 0
|
||||
|
||||
init {
|
||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||
NativeLibrary.reloadKeys()
|
||||
@@ -74,6 +79,10 @@ class GamesViewModel : ViewModel() {
|
||||
_shouldScrollToTop.value = shouldScroll
|
||||
}
|
||||
|
||||
fun setShouldScrollAfterReload(shouldScroll: Boolean) {
|
||||
_shouldScrollAfterReload.value = shouldScroll
|
||||
}
|
||||
|
||||
fun setSearchFocused(searchFocused: Boolean) {
|
||||
_searchFocused.value = searchFocused
|
||||
}
|
||||
@@ -123,6 +132,7 @@ class GamesViewModel : ViewModel() {
|
||||
setGames(GameHelper.getGames())
|
||||
reloading.set(false)
|
||||
_isReloading.value = false
|
||||
_shouldScrollAfterReload.value = true
|
||||
|
||||
if (directoriesChanged) {
|
||||
setShouldSwapData(true)
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageButton
|
||||
import android.widget.PopupMenu
|
||||
@@ -48,6 +49,8 @@ import java.util.Locale
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||
import android.view.ViewParent
|
||||
import androidx.core.view.doOnNextLayout
|
||||
|
||||
class GamesFragment : Fragment() {
|
||||
private var _binding: FragmentGamesBinding? = null
|
||||
@@ -62,8 +65,6 @@ class GamesFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
private const val SEARCH_TEXT = "SearchText"
|
||||
private const val PREF_VIEW_TYPE_PORTRAIT = "GamesViewTypePortrait"
|
||||
private const val PREF_VIEW_TYPE_LANDSCAPE = "GamesViewTypeLandscape"
|
||||
private const val PREF_SORT_TYPE = "GamesSortType"
|
||||
}
|
||||
|
||||
@@ -84,14 +85,14 @@ class GamesFragment : Fragment() {
|
||||
|
||||
private fun getCurrentViewType(): Int {
|
||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
val key = if (isLandscape) PREF_VIEW_TYPE_LANDSCAPE else PREF_VIEW_TYPE_PORTRAIT
|
||||
val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT
|
||||
val fallback = if (isLandscape) GameAdapter.VIEW_TYPE_CAROUSEL else GameAdapter.VIEW_TYPE_GRID
|
||||
return preferences.getInt(key, fallback)
|
||||
}
|
||||
|
||||
private fun setCurrentViewType(type: Int) {
|
||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
val key = if (isLandscape) PREF_VIEW_TYPE_LANDSCAPE else PREF_VIEW_TYPE_PORTRAIT
|
||||
val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT
|
||||
preferences.edit { putInt(key, type) }
|
||||
}
|
||||
override fun onCreateView(
|
||||
@@ -150,7 +151,9 @@ class GamesFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
gamesViewModel.games.collect(viewLifecycleOwner) {
|
||||
setAdapter(it)
|
||||
if (it.size > 0) {
|
||||
setAdapter(it)
|
||||
}
|
||||
}
|
||||
gamesViewModel.shouldSwapData.collect(
|
||||
viewLifecycleOwner,
|
||||
@@ -165,6 +168,16 @@ class GamesFragment : Fragment() {
|
||||
resetState = { gamesViewModel.setShouldScrollToTop(false) }
|
||||
) { if (it) scrollToTop() }
|
||||
|
||||
gamesViewModel.shouldScrollAfterReload.collect(viewLifecycleOwner) { shouldScroll ->
|
||||
if (shouldScroll) {
|
||||
binding.gridGames.post {
|
||||
(binding.gridGames as? CarouselRecyclerView)?.pendingScrollAfterReload = true
|
||||
gameAdapter.notifyDataSetChanged()
|
||||
}
|
||||
gamesViewModel.setShouldScrollAfterReload(false)
|
||||
}
|
||||
}
|
||||
|
||||
setupTopView()
|
||||
|
||||
binding.addDirectory.setOnClickListener {
|
||||
@@ -176,17 +189,12 @@ class GamesFragment : Fragment() {
|
||||
|
||||
val applyGridGamesBinding = {
|
||||
(binding.gridGames as? RecyclerView)?.apply {
|
||||
val savedViewType = getCurrentViewType()
|
||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
val effectiveViewType = if (!isLandscape && savedViewType == GameAdapter.VIEW_TYPE_CAROUSEL) {
|
||||
GameAdapter.VIEW_TYPE_GRID
|
||||
} else {
|
||||
savedViewType
|
||||
}
|
||||
gameAdapter.setViewType(effectiveViewType)
|
||||
val currentViewType = getCurrentViewType()
|
||||
val savedViewType = if (isLandscape || currentViewType != GameAdapter.VIEW_TYPE_CAROUSEL) currentViewType else GameAdapter.VIEW_TYPE_GRID
|
||||
|
||||
gameAdapter.setViewType(savedViewType)
|
||||
currentFilter = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
|
||||
val overlapPx = resources.getDimensionPixelSize(R.dimen.carousel_overlap)
|
||||
|
||||
// Set the correct layout manager
|
||||
layoutManager = when (savedViewType) {
|
||||
@@ -203,23 +211,14 @@ class GamesFragment : Fragment() {
|
||||
}
|
||||
else -> throw IllegalArgumentException("Invalid view type: $savedViewType")
|
||||
}
|
||||
|
||||
// Carousel mode: wait for layout, then set card size and enable carousel features
|
||||
if (savedViewType == GameAdapter.VIEW_TYPE_CAROUSEL) {
|
||||
post {
|
||||
val insets = ViewCompat.getRootWindowInsets(this)
|
||||
val bottomInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom ?: 0
|
||||
val size = (resources.getFraction(R.fraction.carousel_card_size_multiplier, 1, 1) * (height - bottomInset)).toInt()
|
||||
if (size > 0) {
|
||||
gameAdapter.setCardSize(size)
|
||||
(this as? JukeboxRecyclerView)?.setCarouselMode(true, overlapPx, size)
|
||||
}
|
||||
doOnNextLayout {
|
||||
(this as? CarouselRecyclerView)?.setCarouselMode(true, gameAdapter)
|
||||
adapter = gameAdapter
|
||||
}
|
||||
} else {
|
||||
// Disable carousel features in other modes
|
||||
(this as? JukeboxRecyclerView)?.setCarouselMode(false, overlapPx, 0)
|
||||
(this as? CarouselRecyclerView)?.setCarouselMode(false)
|
||||
}
|
||||
|
||||
adapter = gameAdapter
|
||||
lastViewType = savedViewType
|
||||
}
|
||||
@@ -232,12 +231,34 @@ class GamesFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
|
||||
gamesViewModel.lastScrollPosition = (binding.gridGames as? CarouselRecyclerView)?.getClosestChildPosition() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
|
||||
(binding.gridGames as? CarouselRecyclerView)?.restoreScrollState(gamesViewModel.lastScrollPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private var lastSearchText: String = ""
|
||||
private var lastFilter: Int = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
|
||||
|
||||
private fun setAdapter(games: List<Game>) {
|
||||
val currentSearchText = binding.searchText.text.toString()
|
||||
val currentFilter = binding.filterButton.id
|
||||
|
||||
if (currentSearchText.isNotEmpty() || currentFilter != View.NO_ID) {
|
||||
val searchChanged = currentSearchText != lastSearchText
|
||||
val filterChanged = currentFilter != lastFilter
|
||||
|
||||
if (searchChanged || filterChanged) {
|
||||
filterAndSearch(games)
|
||||
lastSearchText = currentSearchText
|
||||
lastFilter = currentFilter
|
||||
} else {
|
||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(games)
|
||||
gamesViewModel.setFilteredGames(games)
|
||||
@@ -292,6 +313,7 @@ class GamesFragment : Fragment() {
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.view_grid -> {
|
||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
|
||||
setCurrentViewType(GameAdapter.VIEW_TYPE_GRID)
|
||||
applyGridGamesBinding()
|
||||
item.isChecked = true
|
||||
@@ -299,6 +321,7 @@ class GamesFragment : Fragment() {
|
||||
}
|
||||
|
||||
R.id.view_list -> {
|
||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
|
||||
setCurrentViewType(GameAdapter.VIEW_TYPE_LIST)
|
||||
applyGridGamesBinding()
|
||||
item.isChecked = true
|
||||
@@ -306,9 +329,12 @@ class GamesFragment : Fragment() {
|
||||
}
|
||||
|
||||
R.id.view_carousel -> {
|
||||
setCurrentViewType(GameAdapter.VIEW_TYPE_CAROUSEL)
|
||||
applyGridGamesBinding()
|
||||
item.isChecked = true
|
||||
if (!item.isChecked || getCurrentViewType() != GameAdapter.VIEW_TYPE_CAROUSEL) {
|
||||
setCurrentViewType(GameAdapter.VIEW_TYPE_CAROUSEL)
|
||||
applyGridGamesBinding()
|
||||
item.isChecked = true
|
||||
onResume()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -402,7 +428,7 @@ class GamesFragment : Fragment() {
|
||||
|
||||
private fun scrollToTop() {
|
||||
if (_binding != null) {
|
||||
(binding.gridGames as? JukeboxRecyclerView)?.smoothScrollToPosition(0)
|
||||
(binding.gridGames as? CarouselRecyclerView)?.smoothScrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.KeyEvent
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.math.abs
|
||||
import org.yuzu.yuzu_emu.R
|
||||
|
||||
/**
|
||||
* JukeboxRecyclerView encapsulates all carousel/grid/list logic for the games UI.
|
||||
* It manages overlapping cards, center snapping, custom drawing order, and mid-screen swipe-to-refresh.
|
||||
* Use setCarouselMode(enabled, overlapPx) to toggle carousel features.
|
||||
*/
|
||||
class JukeboxRecyclerView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0
|
||||
) : RecyclerView(context, attrs, defStyle) {
|
||||
|
||||
// Carousel/overlap/snap state
|
||||
private var overlapPx: Int = 0
|
||||
private var overlapDecoration: OverlappingDecoration? = null
|
||||
private var pagerSnapHelper: PagerSnapHelper? = null
|
||||
|
||||
var flingMultiplier: Float = resources.getFraction(R.fraction.carousel_fling_multiplier, 1, 1)
|
||||
|
||||
var useCustomDrawingOrder: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
setChildrenDrawingOrderEnabled(value)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
init {
|
||||
setChildrenDrawingOrderEnabled(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the horizontal center given width and paddings.
|
||||
*/
|
||||
private fun calculateCenter(width: Int, paddingStart: Int, paddingEnd: Int): Int {
|
||||
return paddingStart + (width - paddingStart - paddingEnd) / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the horizontal center of this RecyclerView, accounting for padding.
|
||||
*/
|
||||
private fun getRecyclerViewCenter(): Float {
|
||||
return calculateCenter(width, paddingLeft, paddingRight).toFloat()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the horizontal center of a LayoutManager, accounting for padding.
|
||||
*/
|
||||
private fun getLayoutManagerCenter(layoutManager: RecyclerView.LayoutManager): Int {
|
||||
return if (layoutManager is LinearLayoutManager) {
|
||||
calculateCenter(layoutManager.width, layoutManager.paddingStart, layoutManager.paddingEnd)
|
||||
} else {
|
||||
width / 2
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChildScalesAndAlpha() {
|
||||
val center = getRecyclerViewCenter()
|
||||
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
val childCenter = (child.left + child.right) / 2f
|
||||
val distance = abs(center - childCenter)
|
||||
val minScale = resources.getFraction(R.fraction.carousel_min_scale, 1, 1)
|
||||
val scale = minScale + (1f - minScale) * (1f - distance / center).coerceAtMost(1f)
|
||||
child.scaleX = scale
|
||||
child.scaleY = scale
|
||||
|
||||
val maxDistance = width / 2f
|
||||
val norm = (distance / maxDistance).coerceIn(0f, 1f)
|
||||
val minAlpha = resources.getFraction(R.fraction.carousel_min_alpha, 1, 1)
|
||||
val alpha = minAlpha + (1f - minAlpha) * kotlin.math.cos(norm * Math.PI).toFloat()
|
||||
child.alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable carousel mode.
|
||||
* When enabled, applies overlap, snap, and custom drawing order.
|
||||
*/
|
||||
fun setCarouselMode(enabled: Boolean, overlapPx: Int = 0, cardSize: Int = 0) {
|
||||
this.overlapPx = overlapPx
|
||||
if (enabled) {
|
||||
// Add overlap decoration if not present
|
||||
if (overlapDecoration == null) {
|
||||
overlapDecoration = OverlappingDecoration(overlapPx)
|
||||
addItemDecoration(overlapDecoration!!)
|
||||
}
|
||||
// Attach PagerSnapHelper
|
||||
if (pagerSnapHelper == null) {
|
||||
pagerSnapHelper = CenterPagerSnapHelper()
|
||||
pagerSnapHelper!!.attachToRecyclerView(this)
|
||||
}
|
||||
useCustomDrawingOrder = true
|
||||
flingMultiplier = resources.getFraction(R.fraction.carousel_fling_multiplier, 1, 1)
|
||||
|
||||
// Center first/last card
|
||||
post {
|
||||
if (cardSize > 0) {
|
||||
val sidePadding = (width - cardSize) / 2
|
||||
setPadding(sidePadding, 0, sidePadding, 0)
|
||||
clipToPadding = false
|
||||
}
|
||||
}
|
||||
// Handle bottom insets for keyboard/navigation bar only
|
||||
androidx.core.view.ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
|
||||
val imeInset = insets.getInsets(androidx.core.view.WindowInsetsCompat.Type.ime()).bottom
|
||||
val navInset = insets.getInsets(androidx.core.view.WindowInsetsCompat.Type.navigationBars()).bottom
|
||||
// Only adjust bottom padding, keep top at 0
|
||||
view.setPadding(view.paddingLeft, 0, view.paddingRight, maxOf(imeInset, navInset))
|
||||
insets
|
||||
}
|
||||
} else {
|
||||
// Remove overlap decoration
|
||||
overlapDecoration?.let { removeItemDecoration(it) }
|
||||
overlapDecoration = null
|
||||
// Detach PagerSnapHelper
|
||||
pagerSnapHelper?.attachToRecyclerView(null)
|
||||
pagerSnapHelper = null
|
||||
useCustomDrawingOrder = false
|
||||
// Reset padding and fling
|
||||
setPadding(0, 0, 0, 0)
|
||||
clipToPadding = true
|
||||
flingMultiplier = 1.0f
|
||||
// Reset scaling
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
child?.scaleX = 1f
|
||||
child?.scaleY = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trap past boundaries navigation
|
||||
override fun focusSearch(focused: View, direction: Int): View? {
|
||||
val lm = layoutManager as? LinearLayoutManager ?: return super.focusSearch(focused, direction)
|
||||
val vh = findContainingViewHolder(focused) ?: return super.focusSearch(focused, direction)
|
||||
val position = vh.bindingAdapterPosition
|
||||
val itemCount = adapter?.itemCount ?: return super.focusSearch(focused, direction)
|
||||
|
||||
return when (direction) {
|
||||
View.FOCUS_LEFT -> {
|
||||
if (position > 0) {
|
||||
findViewHolderForAdapterPosition(position - 1)?.itemView ?: super.focusSearch(focused, direction)
|
||||
} else {
|
||||
focused
|
||||
}
|
||||
}
|
||||
View.FOCUS_RIGHT -> {
|
||||
if (position < itemCount - 1) {
|
||||
findViewHolderForAdapterPosition(position + 1)?.itemView ?: super.focusSearch(focused, direction)
|
||||
} else {
|
||||
focused
|
||||
}
|
||||
}
|
||||
else -> super.focusSearch(focused, direction)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom fling multiplier for carousel
|
||||
override fun fling(velocityX: Int, velocityY: Int): Boolean {
|
||||
val newVelocityX = (velocityX * flingMultiplier).toInt()
|
||||
val newVelocityY = (velocityY * flingMultiplier).toInt()
|
||||
return super.fling(newVelocityX, newVelocityY)
|
||||
}
|
||||
|
||||
private var scaleUpdatePosted = false
|
||||
// Custom drawing order for carousel (for alpha fade)
|
||||
override fun getChildDrawingOrder(childCount: Int, i: Int): Int {
|
||||
if (!useCustomDrawingOrder || childCount == 0) return i
|
||||
val center = getRecyclerViewCenter()
|
||||
val children = (0 until childCount).map { idx ->
|
||||
val child = getChildAt(idx)
|
||||
val childCenter = (child.left + child.right) / 2f
|
||||
val distance = abs(childCenter - center)
|
||||
Pair(idx, distance)
|
||||
}
|
||||
val sorted = children.sortedWith(
|
||||
compareByDescending<Pair<Int, Float>> { it.second }
|
||||
.thenBy { it.first }
|
||||
)
|
||||
// Post scale update once per frame
|
||||
if (!scaleUpdatePosted && i == childCount - 1) {
|
||||
scaleUpdatePosted = true
|
||||
post {
|
||||
updateChildScalesAndAlpha()
|
||||
scaleUpdatePosted = false
|
||||
}
|
||||
}
|
||||
//Log.d("JukeboxRecyclerView", "Child $i got order ${sorted[i].first} at distance ${sorted[i].second} from center $center")
|
||||
return sorted[i].first
|
||||
}
|
||||
|
||||
// --- OverlappingDecoration (inner class) ---
|
||||
inner class OverlappingDecoration(private val overlapPx: Int) : ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect, view: View, parent: RecyclerView, state: State
|
||||
) {
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
if (position > 0) {
|
||||
outRect.left = -overlapPx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable proper center snapping
|
||||
inner class CenterPagerSnapHelper : PagerSnapHelper() {
|
||||
|
||||
// NEEDED: fixes center snapping, but introduces ghost movement
|
||||
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
|
||||
if (layoutManager !is LinearLayoutManager) return null
|
||||
val center = (this@JukeboxRecyclerView).getLayoutManagerCenter(layoutManager)
|
||||
var minDistance = Int.MAX_VALUE
|
||||
var closestChild: View? = null
|
||||
for (i in 0 until layoutManager.childCount) {
|
||||
val child = layoutManager.getChildAt(i) ?: continue
|
||||
val childCenter = (child.left + child.right) / 2
|
||||
val distance = kotlin.math.abs(childCenter - center)
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance
|
||||
closestChild = child
|
||||
}
|
||||
}
|
||||
return closestChild
|
||||
}
|
||||
|
||||
//NEEDED: fixes ghost movement when snapping, but breaks inertial scrolling
|
||||
override fun calculateDistanceToFinalSnap(
|
||||
layoutManager: RecyclerView.LayoutManager,
|
||||
targetView: View
|
||||
): IntArray? {
|
||||
if (layoutManager !is LinearLayoutManager) return super.calculateDistanceToFinalSnap(layoutManager, targetView)
|
||||
val out = IntArray(2)
|
||||
val center = (this@JukeboxRecyclerView).getLayoutManagerCenter(layoutManager)
|
||||
val childCenter = (targetView.left + targetView.right) / 2
|
||||
out[0] = childCenter - center
|
||||
out[1] = 0
|
||||
return out
|
||||
}
|
||||
|
||||
// NEEDED: fixes inertial scrolling (broken by calculateDistanceToFinalSnap)
|
||||
override fun findTargetSnapPosition(
|
||||
layoutManager: RecyclerView.LayoutManager,
|
||||
velocityX: Int,
|
||||
velocityY: Int
|
||||
): Int {
|
||||
if (layoutManager !is LinearLayoutManager) return RecyclerView.NO_POSITION
|
||||
val firstVisible = layoutManager.findFirstVisibleItemPosition()
|
||||
val lastVisible = layoutManager.findLastVisibleItemPosition()
|
||||
val center = (this@JukeboxRecyclerView).getLayoutManagerCenter(layoutManager)
|
||||
|
||||
var closestChild: View? = null
|
||||
var minDistance = Int.MAX_VALUE
|
||||
var closestPosition = RecyclerView.NO_POSITION
|
||||
for (i in firstVisible..lastVisible) {
|
||||
val child = layoutManager.findViewByPosition(i) ?: continue
|
||||
val childCenter = (child.left + child.right) / 2
|
||||
val distance = kotlin.math.abs(childCenter - center)
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance
|
||||
closestChild = child
|
||||
closestPosition = i
|
||||
}
|
||||
}
|
||||
|
||||
val flingCount = if (velocityX == 0) 0 else velocityX / 2000
|
||||
var targetPos = closestPosition + flingCount
|
||||
val itemCount = layoutManager.itemCount
|
||||
targetPos = targetPos.coerceIn(0, itemCount - 1)
|
||||
return targetPos
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,36 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
private val CHECKED_FIRMWARE = "CheckedFirmware"
|
||||
private var checkedFirmware = false
|
||||
|
||||
private val requestBluetoothPermissionsLauncher =
|
||||
registerForActivityResult(androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
val granted = permissions.entries.all { it.value }
|
||||
if (granted) {
|
||||
// Permissions were granted.
|
||||
android.widget.Toast.makeText(this, "Bluetooth permissions granted.", android.widget.Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
// Permissions were denied.
|
||||
android.widget.Toast.makeText(this, "Bluetooth permissions denied. Controller support may be limited.", android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAndRequestBluetoothPermissions() {
|
||||
// This check is only necessary for Android 12 (API level 31) and above.
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
||||
val permissionsToRequest = arrayOf(
|
||||
android.Manifest.permission.BLUETOOTH_SCAN,
|
||||
android.Manifest.permission.BLUETOOTH_CONNECT
|
||||
)
|
||||
|
||||
val permissionsNotGranted = permissionsToRequest.filter {
|
||||
checkSelfPermission(it) != android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (permissionsNotGranted.isNotEmpty()) {
|
||||
requestBluetoothPermissionsLauncher.launch(permissionsNotGranted.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
|
||||
@@ -75,8 +105,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
NativeLibrary.initMultiplayer()
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
checkAndRequestBluetoothPermissions()
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
checkedDecryption = savedInstanceState.getBoolean(CHECKED_DECRYPTION)
|
||||
checkedFirmware = savedInstanceState.getBoolean(CHECKED_FIRMWARE)
|
||||
@@ -335,7 +370,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
homeViewModel.setCheckKeys(true)
|
||||
homeViewModel.setCheckFirmware(true)
|
||||
|
||||
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||
if (!firstTimeSetup) {
|
||||
homeViewModel.setCheckFirmware(true)
|
||||
}
|
||||
|
||||
gamesViewModel.reloadGames(true)
|
||||
return true
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.math.abs
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import androidx.preference.PreferenceManager
|
||||
|
||||
/**
|
||||
* CarouselRecyclerView encapsulates all carousel logic for the games UI.
|
||||
* It manages overlapping cards, center snapping, custom drawing order,
|
||||
* joypad & fling navigation and mid-screen swipe-to-refresh.
|
||||
*/
|
||||
class CarouselRecyclerView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0
|
||||
) : RecyclerView(context, attrs, defStyle) {
|
||||
|
||||
private var overlapFactor: Float = 0f
|
||||
private var overlapPx: Int = 0
|
||||
private var overlapDecoration: OverlappingDecoration? = null
|
||||
private var pagerSnapHelper: PagerSnapHelper? = null
|
||||
private var scalingScrollListener: OnScrollListener? = null
|
||||
|
||||
companion object {
|
||||
private const val CAROUSEL_CARD_SIZE_FACTOR = "CarouselCardSizeMultiplier"
|
||||
private const val CAROUSEL_BORDERCARDS_SCALE = "CarouselBorderCardsScale"
|
||||
private const val CAROUSEL_BORDERCARDS_ALPHA = "CarouselBorderCardsAlpha"
|
||||
private const val CAROUSEL_OVERLAP_FACTOR = "CarouselOverlapFactor"
|
||||
private const val CAROUSEL_MAX_FLING_COUNT = "CarouselMaxFlingCount"
|
||||
private const val CAROUSEL_FLING_MULTIPLIER = "CarouselFlingMultiplier"
|
||||
private const val CAROUSEL_CARDS_SCALING_SHAPE = "CarouselCardsScalingShape"
|
||||
private const val CAROUSEL_CARDS_ALPHA_SHAPE = "CarouselCardsAlphaShape"
|
||||
const val CAROUSEL_LAST_SCROLL_POSITION = "CarouselLastScrollPosition"
|
||||
const val CAROUSEL_VIEW_TYPE_PORTRAIT = "GamesViewTypePortrait"
|
||||
const val CAROUSEL_VIEW_TYPE_LANDSCAPE = "GamesViewTypeLandscape"
|
||||
}
|
||||
|
||||
private val preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
|
||||
var flingMultiplier: Float = 1f
|
||||
|
||||
public var pendingScrollAfterReload: Boolean = false
|
||||
|
||||
var useCustomDrawingOrder: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
setChildrenDrawingOrderEnabled(value)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
init {
|
||||
setChildrenDrawingOrderEnabled(true)
|
||||
}
|
||||
|
||||
private fun calculateCenter(width: Int, paddingStart: Int, paddingEnd: Int): Int {
|
||||
return paddingStart + (width - paddingStart - paddingEnd) / 2
|
||||
}
|
||||
|
||||
private fun getRecyclerViewCenter(): Float {
|
||||
return calculateCenter(width, paddingLeft, paddingRight).toFloat()
|
||||
}
|
||||
|
||||
private fun getLayoutManagerCenter(layoutManager: RecyclerView.LayoutManager): Int {
|
||||
return if (layoutManager is LinearLayoutManager) {
|
||||
calculateCenter(layoutManager.width, layoutManager.paddingStart, layoutManager.paddingEnd)
|
||||
} else {
|
||||
width / 2
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChildDistanceToCenter(view: View): Float {
|
||||
return 0.5f * (view.left + view.right) - getRecyclerViewCenter()
|
||||
}
|
||||
|
||||
fun restoreScrollState(position: Int = 0, attempts: Int = 0) {
|
||||
val lm = layoutManager as? LinearLayoutManager ?: return
|
||||
if (lm.findLastVisibleItemPosition() == RecyclerView.NO_POSITION && attempts < 10) {
|
||||
post { restoreScrollState(position, attempts + 1) }
|
||||
return
|
||||
}
|
||||
scrollToPosition(position)
|
||||
}
|
||||
|
||||
fun getClosestChildPosition(fullRange: Boolean = false): Int {
|
||||
val lm = layoutManager as? LinearLayoutManager ?: return RecyclerView.NO_POSITION
|
||||
var minDistance = Int.MAX_VALUE
|
||||
var closestPosition = RecyclerView.NO_POSITION
|
||||
val start = if (fullRange) 0 else lm.findFirstVisibleItemPosition()
|
||||
val end = if (fullRange) lm.childCount - 1 else lm.findLastVisibleItemPosition()
|
||||
for (i in start..end) {
|
||||
val child = lm.findViewByPosition(i) ?: continue
|
||||
val distance = kotlin.math.abs(getChildDistanceToCenter(child).toInt())
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance
|
||||
closestPosition = i
|
||||
}
|
||||
}
|
||||
return closestPosition
|
||||
}
|
||||
|
||||
fun updateChildScalesAndAlpha() {
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i) ?: continue
|
||||
updateChildScaleAndAlphaForPosition(child)
|
||||
}
|
||||
}
|
||||
|
||||
fun shapingFunction(x: Float, option: Int = 0): Float {
|
||||
return when (option) {
|
||||
0 -> 1f //Off
|
||||
1 -> 1f - x //linear descending
|
||||
2 -> (1f - x) * (1f - x) //Ease out
|
||||
3 -> if (x < 0.05f) 1f else (1f-x) * 0.8f
|
||||
4 -> kotlin.math.cos(x * Math.PI).toFloat() //Cosine
|
||||
5 -> kotlin.math.cos( (1.5f * x).coerceIn(0f, 1f) * Math.PI).toFloat() //Cosine 1.5x trimmed
|
||||
else -> 1f //Default to Off
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChildScaleAndAlphaForPosition(child: View) {
|
||||
val cardSize = (adapter as? GameAdapter ?: return).cardSize
|
||||
val position = getChildViewHolder(child).bindingAdapterPosition
|
||||
if (position == RecyclerView.NO_POSITION || cardSize <= 0) {
|
||||
return // No valid position or card size
|
||||
}
|
||||
child.layoutParams.width = cardSize
|
||||
child.layoutParams.height = cardSize
|
||||
|
||||
val center = getRecyclerViewCenter()
|
||||
val distance = abs(getChildDistanceToCenter(child))
|
||||
val internalBorderScale = resources.getFraction(R.fraction.carousel_bordercards_scale, 1, 1)
|
||||
val borderScale = preferences.getFloat(CAROUSEL_BORDERCARDS_SCALE, internalBorderScale).coerceIn(0f, 1f)
|
||||
|
||||
val shapeInput = (distance / center).coerceIn(0f, 1f)
|
||||
val internalShapeSetting = resources.getInteger(R.integer.carousel_cards_scaling_shape)
|
||||
val scalingShapeSetting = preferences.getInt(CAROUSEL_CARDS_SCALING_SHAPE, internalShapeSetting)
|
||||
val shapedScaling = shapingFunction(shapeInput, scalingShapeSetting)
|
||||
val scale = (borderScale + (1f - borderScale) * shapedScaling).coerceIn(0f, 1f)
|
||||
|
||||
val maxDistance = width / 2f
|
||||
val alphaInput = (distance / maxDistance).coerceIn(0f, 1f)
|
||||
val internalBordersAlpha = resources.getFraction(R.fraction.carousel_bordercards_alpha, 1, 1)
|
||||
val borderAlpha = preferences.getFloat(CAROUSEL_BORDERCARDS_ALPHA, internalBordersAlpha).coerceIn(0f, 1f)
|
||||
val internalAlphaShapeSetting = resources.getInteger(R.integer.carousel_cards_alpha_shape)
|
||||
val alphaShapeSetting = preferences.getInt(CAROUSEL_CARDS_ALPHA_SHAPE, internalAlphaShapeSetting)
|
||||
val shapedAlpha = shapingFunction(alphaInput, alphaShapeSetting)
|
||||
val alpha = (borderAlpha + (1f - borderAlpha) * shapedAlpha).coerceIn(0f, 1f)
|
||||
|
||||
child.animate().cancel()
|
||||
child.alpha = alpha
|
||||
child.scaleX = scale
|
||||
child.scaleY = scale
|
||||
}
|
||||
|
||||
fun focusCenteredCard() {
|
||||
val centeredPos = getClosestChildPosition()
|
||||
if (centeredPos != RecyclerView.NO_POSITION) {
|
||||
val vh = findViewHolderForAdapterPosition(centeredPos)
|
||||
vh?.itemView?.let { child ->
|
||||
child.isFocusable = true
|
||||
child.isFocusableInTouchMode = true
|
||||
child.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setCarouselMode(enabled: Boolean, gameAdapter: GameAdapter? = null) {
|
||||
if (enabled) {
|
||||
useCustomDrawingOrder = true
|
||||
|
||||
val insets = rootWindowInsets
|
||||
val bottomInset = insets?.getInsets(android.view.WindowInsets.Type.systemBars())?.bottom ?: 0
|
||||
val internalFactor = resources.getFraction(R.fraction.carousel_card_size_factor, 1, 1)
|
||||
val userFactor = preferences.getFloat(CAROUSEL_CARD_SIZE_FACTOR, internalFactor).coerceIn(0f, 1f)
|
||||
val cardSize = (userFactor * (height - bottomInset)).toInt()
|
||||
gameAdapter?.setCardSize(cardSize)
|
||||
|
||||
val internalOverlapFactor = resources.getFraction(R.fraction.carousel_overlap_factor, 1, 1)
|
||||
overlapFactor = preferences.getFloat(CAROUSEL_OVERLAP_FACTOR, internalOverlapFactor).coerceIn(0f, 1f)
|
||||
overlapPx = (cardSize * overlapFactor).toInt()
|
||||
|
||||
val internalFlingMultiplier = resources.getFraction(R.fraction.carousel_fling_multiplier, 1, 1)
|
||||
flingMultiplier = preferences.getFloat(CAROUSEL_FLING_MULTIPLIER, internalFlingMultiplier).coerceIn(1f, 5f)
|
||||
|
||||
gameAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onChanged() {
|
||||
if (pendingScrollAfterReload) {
|
||||
post {
|
||||
jigglyScroll()
|
||||
pendingScrollAfterReload = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Detach SnapHelper during setup
|
||||
pagerSnapHelper?.attachToRecyclerView(null)
|
||||
|
||||
// Add overlap decoration if not present
|
||||
if (overlapDecoration == null) {
|
||||
overlapDecoration = OverlappingDecoration(overlapPx)
|
||||
addItemDecoration(overlapDecoration!!)
|
||||
}
|
||||
|
||||
// Gradual scalingAdd commentMore actions
|
||||
if (scalingScrollListener == null) {
|
||||
scalingScrollListener = object : OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
updateChildScalesAndAlpha()
|
||||
}
|
||||
}
|
||||
addOnScrollListener(scalingScrollListener!!)
|
||||
}
|
||||
|
||||
if (cardSize > 0) {
|
||||
val topPadding = ((height - bottomInset - cardSize) / 2).coerceAtLeast(0) // Center vertically
|
||||
val sidePadding = (width - cardSize) / 2 // Center first/last card
|
||||
setPadding(sidePadding, topPadding, sidePadding, 0)
|
||||
clipToPadding = false
|
||||
}
|
||||
|
||||
if (pagerSnapHelper == null) {
|
||||
pagerSnapHelper = CenterPagerSnapHelper()
|
||||
pagerSnapHelper!!.attachToRecyclerView(this)
|
||||
}
|
||||
} else {
|
||||
// Remove overlap decoration
|
||||
overlapDecoration?.let { removeItemDecoration(it) }
|
||||
overlapDecoration = null
|
||||
// Remove scaling scroll listener
|
||||
scalingScrollListener?.let { removeOnScrollListener(it) }
|
||||
scalingScrollListener = null
|
||||
// Detach PagerSnapHelper
|
||||
pagerSnapHelper?.attachToRecyclerView(null)
|
||||
pagerSnapHelper = null
|
||||
useCustomDrawingOrder = false
|
||||
// Reset padding and fling
|
||||
setPadding(0, 0, 0, 0)
|
||||
clipToPadding = true
|
||||
flingMultiplier = 1f
|
||||
// Reset scaling
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
child?.scaleX = 1f
|
||||
child?.scaleY = 1f
|
||||
child?.alpha = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(state: Int) {
|
||||
super.onScrollStateChanged(state)
|
||||
if (state == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
focusCenteredCard()
|
||||
}
|
||||
}
|
||||
|
||||
override fun scrollToPosition(position: Int) {
|
||||
super.scrollToPosition(position)
|
||||
(layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(position, overlapPx)
|
||||
doOnNextLayout {
|
||||
updateChildScalesAndAlpha()
|
||||
focusCenteredCard()
|
||||
}
|
||||
}
|
||||
|
||||
private var lastFocusSearchTime: Long = 0
|
||||
override fun focusSearch(focused: View, direction: Int): View? {
|
||||
if (layoutManager !is LinearLayoutManager) return super.focusSearch(focused, direction)
|
||||
val vh = findContainingViewHolder(focused) ?: return super.focusSearch(focused, direction)
|
||||
val itemCount = adapter?.itemCount ?: return super.focusSearch(focused, direction)
|
||||
val position = vh.bindingAdapterPosition
|
||||
|
||||
return when (direction) {
|
||||
View.FOCUS_LEFT -> {
|
||||
if (position > 0) {
|
||||
val now = System.currentTimeMillis()
|
||||
val repeatDetected = (now - lastFocusSearchTime) < resources.getInteger(R.integer.carousel_focus_search_repeat_threshold_ms)
|
||||
lastFocusSearchTime = now
|
||||
if (!repeatDetected) { //ensures the first run
|
||||
val offset = focused.width - overlapPx
|
||||
smoothScrollBy(-offset, 0)
|
||||
}
|
||||
findViewHolderForAdapterPosition(position - 1)?.itemView ?: super.focusSearch(focused, direction)
|
||||
} else {
|
||||
focused
|
||||
}
|
||||
}
|
||||
View.FOCUS_RIGHT -> {
|
||||
if (position < itemCount - 1) {
|
||||
findViewHolderForAdapterPosition(position + 1)?.itemView ?: super.focusSearch(focused, direction)
|
||||
} else {
|
||||
focused
|
||||
}
|
||||
}
|
||||
else -> super.focusSearch(focused, direction)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom fling multiplier for carousel
|
||||
override fun fling(velocityX: Int, velocityY: Int): Boolean {
|
||||
val newVelocityX = (velocityX * flingMultiplier).toInt()
|
||||
val newVelocityY = (velocityY * flingMultiplier).toInt()
|
||||
return super.fling(newVelocityX, newVelocityY)
|
||||
}
|
||||
|
||||
// Custom drawing order for carousel (for alpha fade)
|
||||
override fun getChildDrawingOrder(childCount: Int, i: Int): Int {
|
||||
if (!useCustomDrawingOrder || childCount == 0) return i
|
||||
val children = (0 until childCount).map { idx ->
|
||||
val distance = abs(getChildDistanceToCenter(getChildAt(idx)))
|
||||
Pair(idx, distance)
|
||||
}
|
||||
val sorted = children.sortedWith(
|
||||
compareByDescending<Pair<Int, Float>> { it.second }
|
||||
.thenBy { it.first }
|
||||
)
|
||||
return sorted[i].first
|
||||
}
|
||||
|
||||
fun jigglyScroll() {
|
||||
scrollBy(-1, 0)
|
||||
scrollBy(1, 0)
|
||||
focusCenteredCard()
|
||||
}
|
||||
|
||||
inner class OverlappingDecoration(private val overlap: Int) : ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect, view: View, parent: RecyclerView, state: State
|
||||
) {
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
if (position > 0) {
|
||||
outRect.left = -overlap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class VerticalCenterDecoration : ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: android.graphics.Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
val parentHeight = parent.height
|
||||
val childHeight = view.layoutParams.height.takeIf { it > 0 }
|
||||
?: view.measuredHeight.takeIf { it > 0 }
|
||||
?: view.height
|
||||
|
||||
if (parentHeight > 0 && childHeight > 0) {
|
||||
val verticalPadding = ((parentHeight - childHeight) / 2).coerceAtLeast(0)
|
||||
outRect.top = verticalPadding
|
||||
outRect.bottom = verticalPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class CenterPagerSnapHelper : PagerSnapHelper() {
|
||||
|
||||
// NEEDED: fixes center snapping, but introduces ghost movement
|
||||
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
|
||||
if (layoutManager !is LinearLayoutManager) return null
|
||||
return layoutManager.findViewByPosition(getClosestChildPosition())
|
||||
}
|
||||
|
||||
//NEEDED: fixes ghost movement when snapping, but breaks inertial scrolling
|
||||
override fun calculateDistanceToFinalSnap(
|
||||
layoutManager: RecyclerView.LayoutManager,
|
||||
targetView: View
|
||||
): IntArray? {
|
||||
if (layoutManager !is LinearLayoutManager) return super.calculateDistanceToFinalSnap(layoutManager, targetView)
|
||||
val out = IntArray(2)
|
||||
out[0] = getChildDistanceToCenter(targetView).toInt()
|
||||
out[1] = 0
|
||||
return out
|
||||
}
|
||||
|
||||
// NEEDED: fixes inertial scrolling (broken by calculateDistanceToFinalSnap)
|
||||
override fun findTargetSnapPosition(
|
||||
layoutManager: RecyclerView.LayoutManager,
|
||||
velocityX: Int,
|
||||
velocityY: Int
|
||||
): Int {
|
||||
if (layoutManager !is LinearLayoutManager) return RecyclerView.NO_POSITION
|
||||
val closestPosition = this@CarouselRecyclerView.getClosestChildPosition()
|
||||
val internalMaxFling = resources.getInteger(R.integer.carousel_max_fling_count)
|
||||
val maxFling = preferences.getInt(CAROUSEL_MAX_FLING_COUNT, internalMaxFling).coerceIn(1, 10)
|
||||
val rawFlingCount = if (velocityX == 0) 0 else velocityX / 2000
|
||||
val flingCount = rawFlingCount.coerceIn(-maxFling, maxFling)
|
||||
var targetPos = (closestPosition + flingCount).coerceIn(0, layoutManager.itemCount - 1)
|
||||
return targetPos
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,22 +127,22 @@ namespace AndroidSettings {
|
||||
Settings::Setting<bool> show_device_model{linkage, true, "show_device_model",
|
||||
Settings::Category::Overlay,
|
||||
Settings::Specialization::Default, true, true,
|
||||
&show_performance_overlay};
|
||||
&show_soc_overlay};
|
||||
|
||||
Settings::Setting<bool> show_gpu_model{linkage, true, "show_gpu_model",
|
||||
Settings::Category::Overlay,
|
||||
Settings::Specialization::Default, true, true,
|
||||
&show_performance_overlay};
|
||||
&show_soc_overlay};
|
||||
|
||||
Settings::Setting<bool> show_soc_model{linkage, true, "show_soc_model",
|
||||
Settings::Category::Overlay,
|
||||
Settings::Specialization::Default, true, true,
|
||||
&show_performance_overlay};
|
||||
&show_soc_overlay};
|
||||
|
||||
Settings::Setting<bool> show_fw_version{linkage, true, "show_firmware_version",
|
||||
Settings::Category::Overlay,
|
||||
Settings::Specialization::Default, true, true,
|
||||
&show_performance_overlay};
|
||||
&show_soc_overlay};
|
||||
|
||||
Settings::Setting<bool> soc_overlay_background{linkage, false, "soc_overlay_background",
|
||||
Settings::Category::Overlay,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/card_game_carousel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp"
|
||||
android:layout_margin="0dp"
|
||||
app:strokeColor="@android:color/transparent"
|
||||
app:strokeWidth="0dp"
|
||||
android:alpha="0">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="4dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_game_screen"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/game_image_desc"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_game_title"
|
||||
/>
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_game_title"
|
||||
style="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="0dp"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintTop_toBottomOf="@+id/image_game_screen"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:text="Game Title" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -185,7 +185,7 @@
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<org.yuzu.yuzu_emu.ui.JukeboxRecyclerView
|
||||
<org.yuzu.yuzu_emu.ui.CarouselRecyclerView
|
||||
android:id="@+id/grid_games"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -196,7 +196,6 @@
|
||||
android:fadeScrollbars="true"
|
||||
/>
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</org.yuzu.yuzu_emu.ui.MidScreenSwipeRefreshLayout>
|
||||
|
||||
@@ -678,6 +678,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (بطيء)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (بطيء)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (بطيء)</string>
|
||||
|
||||
@@ -636,6 +636,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (خاو)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (خاو)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (خاو)</string>
|
||||
|
||||
@@ -577,6 +577,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Pomalé)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Pomalé)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Pomalé)</string>
|
||||
|
||||
@@ -721,6 +721,7 @@ Wirklich fortfahren?</string>
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Langsam)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Langsam)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Langsam)</string>
|
||||
|
||||
@@ -786,6 +786,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">x1 (720p/1080p)</string>
|
||||
<string name="resolution_three_half">x1 (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Lento)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Lento)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Lento)</string>
|
||||
|
||||
@@ -785,6 +785,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (کند)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (کند)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (کند)</string>
|
||||
|
||||
@@ -834,6 +834,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Lent)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Lent)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Lent)</string>
|
||||
|
||||
@@ -698,6 +698,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (איטי)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (איטי)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (איטי)</string>
|
||||
|
||||
@@ -822,6 +822,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Lassú)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Lassú)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Lassú)</string>
|
||||
|
||||
@@ -778,6 +778,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Lambat)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Lambat)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Lambat)</string>
|
||||
|
||||
@@ -736,6 +736,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Slow)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Slow)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Slow)</string>
|
||||
|
||||
@@ -686,6 +686,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (低速)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (低速)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (低速)</string>
|
||||
|
||||
@@ -777,6 +777,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (느림)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (느림)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (느림)</string>
|
||||
|
||||
@@ -645,6 +645,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Slow)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Slow)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Slow)</string>
|
||||
|
||||
@@ -643,6 +643,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Wolno)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Wolno)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Wolno)</string>
|
||||
|
||||
@@ -835,6 +835,7 @@ uma tentativa de mapeamento automático</string>
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Lento)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Lento)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Lento)</string>
|
||||
|
||||
@@ -835,6 +835,7 @@ uma tentativa de mapeamento automático</string>
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Lento)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Lento)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Lento)</string>
|
||||
|
||||
@@ -836,6 +836,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Медленно)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Медленно)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Медленно)</string>
|
||||
|
||||
@@ -279,6 +279,7 @@
|
||||
<string name="about">О томе</string>
|
||||
<string name="about_description">Изградите верзију, кредите и још много тога</string>
|
||||
<string name="warning_help">Помоћи</string>
|
||||
<string name="warning">упозорење</string>
|
||||
<string name="warning_skip">Прескочити</string>
|
||||
<string name="warning_cancel">Отказати</string>
|
||||
<string name="install_amiibo_keys">Инсталирајте Амиибо Кеис</string>
|
||||
@@ -465,6 +466,8 @@
|
||||
<string name="anisotropic_filtering">Анисотропни филтрирање</string>
|
||||
<string name="anisotropic_filtering_description">Побољшава квалитет текстура када се посматра у косим угловима</string>
|
||||
|
||||
<string name="warning_resolution">Познато је да скалирање резолуције изнад 2x изазива проблеме и може довести до значајног успоравања вашег уређаја.</string>
|
||||
|
||||
<!-- Debug settings strings -->
|
||||
<string name="cpu">ЦПУ</string>
|
||||
<string name="cpu_debug_mode">ЦПУ уклањање погрешака</string>
|
||||
@@ -852,13 +855,14 @@
|
||||
<string name="vram_usage_aggressive">Агресиван</string>
|
||||
|
||||
<!-- Resolutions -->
|
||||
<string name="resolution_quarter">0,25к (180п / 270п)</string>
|
||||
<string name="resolution_half">0,5к (360п / 540п)</string>
|
||||
<string name="resolution_three_quarter">0,75к (540п / 810п)</string>
|
||||
<string name="resolution_one">1к (720п / 1080п)</string>
|
||||
<string name="resolution_two">2к (1440п / 2160п) (споро)</string>
|
||||
<string name="resolution_three">3к (2160п / 3240п) (споро)</string>
|
||||
<string name="resolution_four">4к (2880п / 4320п) (споро)</string>
|
||||
<string name="resolution_quarter">0.25X (180p/270p)</string>
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (споро)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (споро)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (споро)</string>
|
||||
|
||||
<!-- Renderer VSync -->
|
||||
<string name="renderer_vsync_immediate">Непосредан (искључен)</string>
|
||||
|
||||
@@ -625,6 +625,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Повільно)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Повільно)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Повільно)</string>
|
||||
|
||||
@@ -648,6 +648,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Chậm)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Chậm)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Chậm)</string>
|
||||
|
||||
@@ -829,6 +829,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (慢速)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (慢速)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (慢速)</string>
|
||||
|
||||
@@ -835,6 +835,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (慢)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (慢)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (慢)</string>
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
<item>@string/resolution_half</item>
|
||||
<item>@string/resolution_three_quarter</item>
|
||||
<item>@string/resolution_one</item>
|
||||
<item>@string/resolution_three_half</item>
|
||||
<item>@string/resolution_two</item>
|
||||
<item>@string/resolution_three</item>
|
||||
<item>@string/resolution_four</item>
|
||||
@@ -200,6 +201,7 @@
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
</integer-array>
|
||||
|
||||
<integer-array name="rendererVSyncValues">
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<dimen name="icon_inset">24dp</dimen>
|
||||
<dimen name="spacing_bottom_list_fab">96dp</dimen>
|
||||
<dimen name="spacing_fab">24dp</dimen>
|
||||
<dimen name="carousel_overlap">150dp</dimen>
|
||||
<dimen name="dialog_margin">20dp</dimen>
|
||||
<dimen name="elevated_app_bar">3dp</dimen>
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<resources>
|
||||
<fraction name="carousel_min_scale">60%</fraction>
|
||||
<fraction name="carousel_min_alpha">60%</fraction>
|
||||
<fraction name="carousel_bordercards_scale">60%</fraction>
|
||||
<fraction name="carousel_bordercards_alpha">60%</fraction>
|
||||
<fraction name="carousel_overlap_factor">60%</fraction>
|
||||
<fraction name="carousel_card_size_factor">95%</fraction>
|
||||
<fraction name="carousel_fling_multiplier">200%</fraction>
|
||||
<fraction name="carousel_card_size_multiplier">100%</fraction>
|
||||
<fraction name="carousel_midscreenswipe_width_fraction">20%</fraction>
|
||||
</resources>
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
<integer name="grid_columns">1</integer>
|
||||
<integer name="game_columns_list">1</integer>
|
||||
<integer name="game_columns_grid">2</integer>
|
||||
<integer name="carousel_max_fling_count">4</integer>
|
||||
<integer name="carousel_focus_search_repeat_threshold_ms">100</integer>
|
||||
<integer name="carousel_cards_scaling_shape">1</integer>
|
||||
<integer name="carousel_cards_alpha_shape">4</integer>
|
||||
|
||||
<!-- Default SWITCH landscape layout -->
|
||||
<integer name="BUTTON_A_X">760</integer>
|
||||
|
||||
@@ -290,6 +290,7 @@
|
||||
<string name="about">About</string>
|
||||
<string name="about_description">Build version, credits, and more</string>
|
||||
<string name="warning_help">Help</string>
|
||||
<string name="warning">Warning</string>
|
||||
<string name="warning_skip">Skip</string>
|
||||
<string name="warning_cancel">Cancel</string>
|
||||
<string name="install_amiibo_keys">Install Amiibo keys</string>
|
||||
@@ -491,6 +492,8 @@
|
||||
<string name="anisotropic_filtering">Anisotropic filtering</string>
|
||||
<string name="anisotropic_filtering_description">Improves the quality of textures when viewed at oblique angles</string>
|
||||
|
||||
<string name="warning_resolution">Resolution scaling above 2x is known to cause issues, and may result in significant slowdowns of your device.</string>
|
||||
|
||||
<!-- Debug settings strings -->
|
||||
<string name="cpu">CPU</string>
|
||||
<string name="cpu_debug_mode">CPU Debugging</string>
|
||||
@@ -884,6 +887,7 @@
|
||||
<string name="resolution_half">0.5X (360p/540p)</string>
|
||||
<string name="resolution_three_quarter">0.75X (540p/810p)</string>
|
||||
<string name="resolution_one">1X (720p/1080p)</string>
|
||||
<string name="resolution_three_half">1.5X (1080p/1620p)</string>
|
||||
<string name="resolution_two">2X (1440p/2160p) (Slow)</string>
|
||||
<string name="resolution_three">3X (2160p/3240p) (Slow)</string>
|
||||
<string name="resolution_four">4X (2880p/4320p) (Slow)</string>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
// Uncomment this to disable microprofile. This will get you cleaner profiles when using
|
||||
// Use this to disable microprofile. This will get you cleaner profiles when using
|
||||
// external sampling profilers like "Very Sleepy", and will improve performance somewhat.
|
||||
// #define MICROPROFILE_ENABLED 0
|
||||
#ifdef ANDROID
|
||||
#define MICROPROFILE_ENABLED 0
|
||||
#define MICROPROFILEUI_ENABLED 0
|
||||
#define MicroProfileOnThreadExit() do{}while(0)
|
||||
#define MICROPROFILE_TOKEN(x) 0
|
||||
#define MicroProfileEnter(x) 0
|
||||
#define MicroProfileLeave(x, y) ignore_all(x, y)
|
||||
#endif
|
||||
|
||||
// Customized Citra settings.
|
||||
// This file wraps the MicroProfile header so that these are consistent everywhere.
|
||||
@@ -19,6 +29,12 @@
|
||||
typedef void* HANDLE;
|
||||
#endif
|
||||
|
||||
#include <tuple>
|
||||
template <typename... Args>
|
||||
void ignore_all(Args&&... args) {
|
||||
(static_cast<void>(std::ignore = args), ...);
|
||||
}
|
||||
|
||||
#include <microprofile.h>
|
||||
|
||||
#define MP_RGB(r, g, b) ((r) << 16 | (g) << 8 | (b) << 0)
|
||||
|
||||
@@ -227,7 +227,7 @@ HaltReason ArmNce::RunThread(Kernel::KThread* thread) {
|
||||
if (auto it = post_handlers.find(m_guest_ctx.pc); it != post_handlers.end()) {
|
||||
hr = ReturnToRunCodeByTrampoline(thread_params, &m_guest_ctx, it->second);
|
||||
} else {
|
||||
hr = ReturnToRunCodeByExceptionLevelChange(m_thread_id, thread_params);
|
||||
hr = ReturnToRunCodeByExceptionLevelChange(m_thread_id, thread_params); // Android: Use "process handle SIGUSR2 -n true -p true -s false" (and SIGURG) in LLDB when debugging
|
||||
}
|
||||
|
||||
// Critical section for thread cleanup
|
||||
|
||||
@@ -53,6 +53,16 @@ enum class NetDbError : s32 {
|
||||
NoData = 4,
|
||||
};
|
||||
|
||||
const std::vector<std::string> blockedDomains = {"srv.nintendo.net", "battle.net",
|
||||
"microsoft.com", "mojang.com",
|
||||
"xboxlive.com", "minecraftservices.com"};
|
||||
|
||||
static bool IsBlockedHost(const std::string& host) {
|
||||
return std::any_of(
|
||||
blockedDomains.begin(), blockedDomains.end(),
|
||||
[&host](const std::string& domain) { return host.find(domain) != std::string::npos; });
|
||||
}
|
||||
|
||||
static NetDbError GetAddrInfoErrorToNetDbError(GetAddrInfoError result) {
|
||||
// These combinations have been verified on console (but are not
|
||||
// exhaustive).
|
||||
@@ -154,7 +164,7 @@ static std::pair<u32, GetAddrInfoError> GetHostByNameRequestImpl(HLERequestConte
|
||||
// For now, ignore options, which are in input buffer 1 for GetHostByNameRequestWithOptions.
|
||||
|
||||
// Prevent resolution of Nintendo servers
|
||||
if (host.find("srv.nintendo.net") != std::string::npos) {
|
||||
if (IsBlockedHost(host)) {
|
||||
LOG_WARNING(Network, "Resolution of hostname {} requested, returning EAI_AGAIN", host);
|
||||
return {0, GetAddrInfoError::AGAIN};
|
||||
}
|
||||
@@ -271,7 +281,7 @@ static std::pair<u32, GetAddrInfoError> GetAddrInfoRequestImpl(HLERequestContext
|
||||
const std::string host = Common::StringFromBuffer(host_buffer);
|
||||
|
||||
// Prevent resolution of Nintendo servers
|
||||
if (host.find("srv.nintendo.net") != std::string::npos) {
|
||||
if (IsBlockedHost(host)) {
|
||||
LOG_WARNING(Network, "Resolution of hostname {} requested, returning EAI_AGAIN", host);
|
||||
return {0, GetAddrInfoError::AGAIN};
|
||||
}
|
||||
@@ -359,5 +369,4 @@ void SFDNSRES::ResolverSetOptionRequest(HLERequestContext& ctx) {
|
||||
rb.Push(ResultSuccess);
|
||||
rb.Push<s32>(0); // bsd errno
|
||||
}
|
||||
|
||||
} // namespace Service::Sockets
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "common/bit_field.h"
|
||||
#include "common/common_types.h"
|
||||
#include "shader_recompiler/frontend/maxwell/translate/impl/impl.h"
|
||||
#include <fstream>
|
||||
|
||||
namespace Shader::Maxwell {
|
||||
namespace {
|
||||
@@ -36,6 +37,17 @@ enum class ShuffleMode : u64 {
|
||||
}
|
||||
}
|
||||
|
||||
bool IsKONA() {
|
||||
std::ifstream machineFile("/sys/devices/soc0/machine");
|
||||
if (machineFile.is_open()) {
|
||||
std::string line;
|
||||
std::getline(machineFile, line);
|
||||
if (line == "KONA")
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Shuffle(TranslatorVisitor& v, u64 insn, const IR::U32& index, const IR::U32& mask) {
|
||||
union {
|
||||
u64 insn;
|
||||
@@ -47,7 +59,10 @@ void Shuffle(TranslatorVisitor& v, u64 insn, const IR::U32& index, const IR::U32
|
||||
|
||||
const IR::U32 result{ShuffleOperation(v.ir, v.X(shfl.src_reg), index, mask, shfl.mode)};
|
||||
v.ir.SetPred(shfl.pred, v.ir.GetInBoundsFromOp(result));
|
||||
v.X(shfl.dest_reg, result);
|
||||
if (IsKONA())
|
||||
v.X(shfl.dest_reg, v.ir.Imm32(0xffffffff)); // This fixes the freeze for Retroid / Snapdragon SD865
|
||||
else
|
||||
v.X(shfl.dest_reg, result);
|
||||
}
|
||||
} // Anonymous namespace
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -351,7 +354,7 @@ std::optional<StorageBufferAddr> Track(const IR::Value& value, const Bias* bias)
|
||||
.index = index.U32(),
|
||||
.offset = offset.U32(),
|
||||
};
|
||||
const u32 alignment{bias ? bias->alignment : 8U};
|
||||
const u32 alignment{bias ? bias->alignment : 16U};
|
||||
if (!Common::IsAligned(storage_buffer.offset, alignment)) {
|
||||
// The SSBO pointer has to be aligned
|
||||
return std::nullopt;
|
||||
@@ -372,9 +375,9 @@ void CollectStorageBuffers(IR::Block& block, IR::Inst& inst, StorageInfo& info)
|
||||
// avoid getting false positives
|
||||
static constexpr Bias nvn_bias{
|
||||
.index = 0,
|
||||
.offset_begin = 0x100,
|
||||
.offset_end = 0x700,
|
||||
.alignment = 16,
|
||||
.offset_begin = 0x110,
|
||||
.offset_end = 0x800,
|
||||
.alignment = 32,
|
||||
};
|
||||
// Track the low address of the instruction
|
||||
const std::optional<LowAddrInfo> low_addr_info{TrackLowAddress(&inst)};
|
||||
@@ -426,7 +429,10 @@ IR::U32 StorageOffset(IR::Block& block, IR::Inst& inst, StorageBufferAddr buffer
|
||||
|
||||
// Align the offset base to match the host alignment requirements
|
||||
low_cbuf = ir.BitwiseAnd(low_cbuf, ir.Imm32(~(alignment - 1U)));
|
||||
return ir.ISub(offset, low_cbuf);
|
||||
|
||||
// It aligns the memory strongly
|
||||
IR::U32 res = ir.ISub(offset, low_cbuf);
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Replace a global memory load instruction with its storage buffer equivalent
|
||||
|
||||
@@ -26,7 +26,9 @@ BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, R
|
||||
void(slot_buffers.insert(runtime, NullBufferParams{}));
|
||||
gpu_modified_ranges.Clear();
|
||||
inline_buffer_id = NULL_BUFFER_ID;
|
||||
|
||||
#ifdef ANDROID
|
||||
immediately_free = (Settings::values.vram_usage_mode.GetValue() == Settings::VramUsageMode::Aggressive);
|
||||
#endif
|
||||
if (!runtime.CanReportMemoryUsage()) {
|
||||
minimum_memory = DEFAULT_EXPECTED_MEMORY;
|
||||
critical_memory = DEFAULT_CRITICAL_MEMORY;
|
||||
@@ -1383,6 +1385,8 @@ void BufferCache<P>::JoinOverlap(BufferId new_buffer_id, BufferId overlap_id,
|
||||
});
|
||||
new_buffer.MarkUsage(copies[0].dst_offset, copies[0].size);
|
||||
runtime.CopyBuffer(new_buffer, overlap, copies, true);
|
||||
if (immediately_free)
|
||||
runtime.Finish();
|
||||
DeleteBuffer(overlap_id, true);
|
||||
}
|
||||
|
||||
@@ -1674,7 +1678,9 @@ void BufferCache<P>::DeleteBuffer(BufferId buffer_id, bool do_not_mark) {
|
||||
}
|
||||
|
||||
Unregister(buffer_id);
|
||||
delayed_destruction_ring.Push(std::move(slot_buffers[buffer_id]));
|
||||
|
||||
if (!do_not_mark || !immediately_free)
|
||||
delayed_destruction_ring.Push(std::move(slot_buffers[buffer_id]));
|
||||
slot_buffers.erase(buffer_id);
|
||||
|
||||
if constexpr (HAS_PERSISTENT_UNIFORM_BUFFER_BINDINGS) {
|
||||
|
||||
@@ -159,7 +159,11 @@ template <class P>
|
||||
class BufferCache : public VideoCommon::ChannelSetupCaches<BufferCacheChannelInfo> {
|
||||
// Page size for caching purposes.
|
||||
// This is unrelated to the CPU page size and it can be changed as it seems optimal.
|
||||
#ifdef ANDROID
|
||||
static constexpr u32 CACHING_PAGEBITS = 12;
|
||||
#else
|
||||
static constexpr u32 CACHING_PAGEBITS = 16;
|
||||
#endif
|
||||
static constexpr u64 CACHING_PAGESIZE = u64{1} << CACHING_PAGEBITS;
|
||||
|
||||
static constexpr bool IS_OPENGL = P::IS_OPENGL;
|
||||
@@ -173,9 +177,15 @@ class BufferCache : public VideoCommon::ChannelSetupCaches<BufferCacheChannelInf
|
||||
static constexpr bool SEPARATE_IMAGE_BUFFERS_BINDINGS = P::SEPARATE_IMAGE_BUFFER_BINDINGS;
|
||||
static constexpr bool USE_MEMORY_MAPS_FOR_UPLOADS = P::USE_MEMORY_MAPS_FOR_UPLOADS;
|
||||
|
||||
#ifdef ANDROID
|
||||
static constexpr s64 DEFAULT_EXPECTED_MEMORY = 512_MiB;
|
||||
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB;
|
||||
static constexpr s64 TARGET_THRESHOLD = 3_GiB;
|
||||
#else
|
||||
static constexpr s64 DEFAULT_EXPECTED_MEMORY = 512_MiB;
|
||||
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB;
|
||||
static constexpr s64 TARGET_THRESHOLD = 4_GiB;
|
||||
#endif
|
||||
|
||||
// Debug Flags.
|
||||
|
||||
@@ -451,7 +461,12 @@ private:
|
||||
Tegra::MaxwellDeviceMemoryManager& device_memory;
|
||||
|
||||
Common::SlotVector<Buffer> slot_buffers;
|
||||
DelayedDestructionRing<Buffer, 8> delayed_destruction_ring;
|
||||
#ifdef ANDROID
|
||||
static constexpr size_t TICKS_TO_DESTROY = 6;
|
||||
#else
|
||||
static constexpr size_t TICKS_TO_DESTROY = 8;
|
||||
#endif
|
||||
DelayedDestructionRing<Buffer, TICKS_TO_DESTROY> delayed_destruction_ring;
|
||||
|
||||
const Tegra::Engines::DrawManager::IndirectParams* current_draw_indirect{};
|
||||
|
||||
@@ -483,6 +498,7 @@ private:
|
||||
u64 minimum_memory = 0;
|
||||
u64 critical_memory = 0;
|
||||
BufferId inline_buffer_id;
|
||||
bool immediately_free = false;
|
||||
|
||||
std::array<BufferId, ((1ULL << 34) >> CACHING_PAGEBITS)> page_table;
|
||||
Common::ScratchBuffer<u8> tmp_buffer;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
@@ -18,9 +21,11 @@ Host1x::~Host1x() = default;
|
||||
void Host1x::StartDevice(s32 fd, ChannelType type, u32 syncpt) {
|
||||
switch (type) {
|
||||
case ChannelType::NvDec:
|
||||
std::call_once(nvdec_first_init, []() {std::this_thread::sleep_for(std::chrono::milliseconds{500});}); // HACK: For Astroneer
|
||||
devices[fd] = std::make_unique<Tegra::Host1x::Nvdec>(*this, fd, syncpt, frame_queue);
|
||||
break;
|
||||
case ChannelType::VIC:
|
||||
std::call_once(vic_first_init, []() {std::this_thread::sleep_for(std::chrono::milliseconds{500});}); // HACK: For Astroneer
|
||||
devices[fd] = std::make_unique<Tegra::Host1x::Vic>(*this, fd, syncpt, frame_queue);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
@@ -201,6 +204,8 @@ private:
|
||||
std::unique_ptr<Common::FlatAllocator<u32, 0, 32>> allocator;
|
||||
FrameQueue frame_queue;
|
||||
std::unordered_map<s32, std::unique_ptr<CDmaPusher>> devices;
|
||||
std::once_flag nvdec_first_init;
|
||||
std::once_flag vic_first_init;
|
||||
};
|
||||
|
||||
} // namespace Tegra::Host1x
|
||||
|
||||
@@ -110,10 +110,17 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
|
||||
|
||||
static constexpr size_t UNSET_CHANNEL{std::numeric_limits<size_t>::max()};
|
||||
|
||||
#ifdef ANDROID
|
||||
static constexpr s64 TARGET_THRESHOLD = 3_GiB;
|
||||
static constexpr s64 DEFAULT_EXPECTED_MEMORY = 1_GiB + 125_MiB;
|
||||
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB + 625_MiB;
|
||||
static constexpr size_t GC_EMERGENCY_COUNTS = 2;
|
||||
#else
|
||||
static constexpr s64 TARGET_THRESHOLD = 4_GiB;
|
||||
static constexpr s64 DEFAULT_EXPECTED_MEMORY = 1_GiB + 125_MiB;
|
||||
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB + 625_MiB;
|
||||
static constexpr size_t GC_EMERGENCY_COUNTS = 2;
|
||||
#endif
|
||||
|
||||
using Runtime = typename P::Runtime;
|
||||
using Image = typename P::Image;
|
||||
@@ -479,7 +486,11 @@ private:
|
||||
};
|
||||
Common::LeastRecentlyUsedCache<LRUItemParams> lru_cache;
|
||||
|
||||
#ifdef ANDROID
|
||||
static constexpr size_t TICKS_TO_DESTROY = 6;
|
||||
#else
|
||||
static constexpr size_t TICKS_TO_DESTROY = 8;
|
||||
#endif
|
||||
DelayedDestructionRing<Image, TICKS_TO_DESTROY> sentenced_images;
|
||||
DelayedDestructionRing<ImageView, TICKS_TO_DESTROY> sentenced_image_view;
|
||||
DelayedDestructionRing<Framebuffer, TICKS_TO_DESTROY> sentenced_framebuffers;
|
||||
|
||||
@@ -271,7 +271,10 @@ vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo& ci, MemoryUsa
|
||||
VmaAllocation allocation{};
|
||||
VkMemoryPropertyFlags property_flags{};
|
||||
|
||||
vk::Check(vmaCreateBuffer(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info));
|
||||
VkResult result = vmaCreateBuffer(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info);
|
||||
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY) {
|
||||
LOG_ERROR(Render_Vulkan, "Out of memory creating buffer (size: {})", ci.size);
|
||||
}
|
||||
vmaGetAllocationMemoryProperties(allocator, allocation, &property_flags);
|
||||
|
||||
u8* data = reinterpret_cast<u8*>(alloc_info.pMappedData);
|
||||
|
||||
@@ -62,7 +62,7 @@ std::string DiscordImpl::GetGameString(const std::string& title) {
|
||||
|
||||
void DiscordImpl::UpdateGameStatus(bool use_default) {
|
||||
const std::string default_text = "eden is an emulator for the Nintendo Switch";
|
||||
const std::string default_image = "https://git.eden-emu.dev/eden-emu/eden/raw/branch/master/"
|
||||
const std::string default_image = "https://github.com/pflyly/eden-mirror/raw/branch/master/"
|
||||
"dist/qt_themes/default/icons/256x256/eden_named.png";
|
||||
const std::string url = use_default ? default_image : game_url;
|
||||
s64 start_time = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
|
||||
Reference in New Issue
Block a user