aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMrPurple666 <antoniosacramento666usa@gmail.com>2026-01-09 23:22:59 +0100
committercrueter <crueter@eden-emu.dev>2026-01-09 23:22:59 +0100
commit87d4c67386adfb73eb22254709d7d27c14b727f1 (patch)
treeacc5ab9446e68fdb7d4dc7ee324d8f9d31674230 /src
parent1370f2367529e2311223d9ab7be201660a42d53e (diff)
[turnip/android] Add environment variables settings for turnip drivers (#3205)
This PR brings a feature that has been needed for some time in the Android Switch emulation community: environment variables for Turnip/Freedreno drivers. These are available in PC emulators and can help fix some problems, especially the TU_DEBUG function, which can be set to gmem (thus allowing Adreno 710/720 users to run Turnip correctly), and noubwc, which fixes some problems for OneUI users. This could also help us debug Turnip in a "better way" in the future. Attached is a screenshot of a user, Ivan albio, using the gmem function on Adreno 710. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3205 Reviewed-by: CamilleLaVey <camillelavey99@gmail.com> Reviewed-by: DraVee <dravee@eden-emu.dev> Co-authored-by: MrPurple666 <antoniosacramento666usa@gmail.com> Co-committed-by: MrPurple666 <antoniosacramento666usa@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoPresetAdapter.kt58
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoVariableAdapter.kt61
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt12
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt17
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/FreedrenoSettingsFragment.kt204
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt22
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeFreedrenoConfig.kt164
-rw-r--r--src/android/app/src/main/jni/CMakeLists.txt1
-rw-r--r--src/android/app/src/main/jni/native.cpp19
-rw-r--r--src/android/app/src/main/jni/native_freedreno.cpp477
-rw-r--r--src/android/app/src/main/res/layout/fragment_freedreno_settings.xml196
-rw-r--r--src/android/app/src/main/res/layout/list_item_freedreno_preset.xml18
-rw-r--r--src/android/app/src/main/res/layout/list_item_freedreno_variable.xml62
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml13
-rw-r--r--src/android/app/src/main/res/navigation/settings_navigation.xml15
-rw-r--r--src/android/app/src/main/res/values/strings.xml22
21 files changed, 1388 insertions, 10 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
index 0ba8519f92..54fb45bd87 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
@@ -61,6 +61,11 @@ class YuzuApplication : Application() {
application = this
documentsTree = DocumentsTree()
DirectoryInitialization.start()
+
+ // Initialize Freedreno config BEFORE loading native library
+ // This ensures GPU driver environment variables are set before adrenotools initializes
+ GpuDriverHelper.initializeFreedrenoConfigEarly()
+
NativeLibrary.playTimeManagerInit()
GpuDriverHelper.initializeDriverParameters()
NativeInput.reloadInputDevices()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoPresetAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoPresetAdapter.kt
new file mode 100644
index 0000000000..242ae588f2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoPresetAdapter.kt
@@ -0,0 +1,58 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.utils.FreedrenoPreset
+import org.yuzu.yuzu_emu.databinding.ListItemFreedrenoPresetBinding
+
+/**
+ * Adapter for displaying Freedreno preset configurations in a horizontal list.
+ */
+class FreedrenoPresetAdapter(
+ private val onPresetClicked: (FreedrenoPreset) -> Unit
+) : ListAdapter<FreedrenoPreset, FreedrenoPresetAdapter.PresetViewHolder>(DiffCallback) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PresetViewHolder {
+ val binding = ListItemFreedrenoPresetBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ return PresetViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: PresetViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ inner class PresetViewHolder(private val binding: ListItemFreedrenoPresetBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(preset: FreedrenoPreset) {
+ binding.presetButton.apply {
+ text = preset.name
+ setOnClickListener {
+ onPresetClicked(preset)
+ }
+ contentDescription = "${preset.name}: ${preset.description}"
+ }
+ }
+ }
+
+ companion object {
+ private val DiffCallback = object : DiffUtil.ItemCallback<FreedrenoPreset>() {
+ override fun areItemsTheSame(oldItem: FreedrenoPreset, newItem: FreedrenoPreset): Boolean =
+ oldItem.name == newItem.name
+
+ override fun areContentsTheSame(oldItem: FreedrenoPreset, newItem: FreedrenoPreset): Boolean =
+ oldItem == newItem
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoVariableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoVariableAdapter.kt
new file mode 100644
index 0000000000..1288711a0d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoVariableAdapter.kt
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.databinding.ListItemFreedrenoVariableBinding
+import org.yuzu.yuzu_emu.fragments.FreedrenoVariable
+import org.yuzu.yuzu_emu.utils.NativeFreedrenoConfig
+
+/**
+ * Adapter for displaying currently set Freedreno environment variables in a list.
+ */
+class FreedrenoVariableAdapter(
+ private val context: Context,
+ private val onItemClicked: (FreedrenoVariable, () -> Unit) -> Unit
+) : ListAdapter<FreedrenoVariable, FreedrenoVariableAdapter.VariableViewHolder>(DiffCallback) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VariableViewHolder {
+ val binding = ListItemFreedrenoVariableBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ return VariableViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: VariableViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ inner class VariableViewHolder(private val binding: ListItemFreedrenoVariableBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(variable: FreedrenoVariable) {
+ binding.variableName.text = variable.name
+ binding.variableValue.text = variable.value
+
+ binding.buttonDelete.setOnClickListener {
+ onItemClicked(variable) {
+ NativeFreedrenoConfig.clearFreedrenoEnv(variable.name)
+ }
+ }
+ }
+ }
+
+ companion object {
+ private val DiffCallback = object : DiffUtil.ItemCallback<FreedrenoVariable>() {
+ override fun areItemsTheSame(oldItem: FreedrenoVariable, newItem: FreedrenoVariable): Boolean =
+ oldItem.name == newItem.name
+
+ override fun areContentsTheSame(oldItem: FreedrenoVariable, newItem: FreedrenoVariable): Boolean =
+ oldItem == newItem
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index df96b17bec..fdf9e8b32b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -27,6 +27,7 @@ object Settings {
SECTION_APP_SETTINGS(R.string.app_settings),
SECTION_CUSTOM_PATHS(R.string.preferences_custom_paths),
SECTION_DEBUG(R.string.preferences_debug),
+ SECTION_FREEDRENO(R.string.gpu_driver_settings),
SECTION_APPLETS(R.string.applets_menu);
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
index fac67dbb64..7c1a9c23cc 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -29,6 +29,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
+import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.features.settings.ui.viewholder.*
import org.yuzu.yuzu_emu.utils.ParamPackage
@@ -212,8 +213,15 @@ class SettingsAdapter(
}
fun onSubmenuClick(item: SubmenuSetting) {
- val action = SettingsNavigationDirections.actionGlobalSettingsFragment(item.menuKey, null)
- fragment.view?.findNavController()?.navigate(action)
+ // Check if this is the Freedreno Settings submenu
+ if (item.menuKey == Settings.MenuTag.SECTION_FREEDRENO) {
+ fragment.view?.findNavController()?.navigate(
+ R.id.action_settingsFragment_to_freedrenoSettingsFragment
+ )
+ } else {
+ val action = SettingsNavigationDirections.actionGlobalSettingsFragment(item.menuKey, null)
+ fragment.view?.findNavController()?.navigate(action)
+ }
}
fun onLaunchableClick(item: LaunchableSetting) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index 6efce38002..dc58e7d23b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -27,6 +27,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.features.settings.model.view.*
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
@@ -109,6 +110,7 @@ class SettingsFragmentPresenter(
MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7)
MenuTag.SECTION_APP_SETTINGS -> addThemeSettings(sl)
MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
+ MenuTag.SECTION_FREEDRENO -> addFreedrenoSettings(sl)
MenuTag.SECTION_APPLETS -> addAppletSettings(sl)
MenuTag.SECTION_CUSTOM_PATHS -> addCustomPathsSettings(sl)
}
@@ -181,6 +183,16 @@ class SettingsFragmentPresenter(
menuKey = MenuTag.SECTION_DEBUG
)
)
+ if (GpuDriverHelper.isAdrenoGpu() && !NativeConfig.isPerGameConfigLoaded()) {
+ add(
+ SubmenuSetting(
+ titleId = R.string.gpu_driver_settings,
+ descriptionId = R.string.freedreno_settings_title,
+ iconId = R.drawable.ic_graphics,
+ menuKey = MenuTag.SECTION_FREEDRENO
+ )
+ )
+ }
add(
SubmenuSetting(
titleId = R.string.applets_menu,
@@ -498,6 +510,11 @@ class SettingsFragmentPresenter(
}
}
+ private fun addFreedrenoSettings(sl: ArrayList<SettingsItem>) {
+ // No additional settings needed here - the SubmenuSetting handles navigation
+ // This method is kept for consistency with other menu sections
+ }
+
private fun addAppletSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(IntSetting.SWKBD_APPLET.key)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt
index dea762dc17..130b556c1f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt
@@ -45,7 +45,7 @@ class DriverFetcherFragment : Fragment() {
private val client = OkHttpClient()
private val gpuModel: String?
- get() = GpuDriverHelper.getGpuModel()
+ get() = GpuDriverHelper.hookLibPath?.let { GpuDriverHelper.getGpuModel(hookLibPath = it) }
private val adrenoModel: Int
get() = parseAdrenoModel()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index d92ca12e6d..1bac1bd1ed 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -87,6 +87,7 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.NativeConfig
+import org.yuzu.yuzu_emu.utils.NativeFreedrenoConfig
import org.yuzu.yuzu_emu.utils.ViewUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
@@ -303,6 +304,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
throw fallbackException
}
}
+ try {
+ if (GpuDriverHelper.isAdrenoGpu()) {
+ val programIdHex = game!!.programIdHex
+ if (NativeFreedrenoConfig.loadPerGameConfigWithGlobalFallback(programIdHex)) {
+ Log.info("[EmulationFragment] Loaded per-game Freedreno config for $programIdHex")
+ } else {
+ Log.info("[EmulationFragment] Using global Freedreno config for $programIdHex")
+ }
+ }
+ } catch (e: Exception) {
+ Log.warning("[EmulationFragment] Failed to load Freedreno config: ${e.message}")
+ }
emulationState = EmulationState(game!!.path) {
return@EmulationState driverViewModel.isInteractionAllowed.value
@@ -616,7 +629,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
Log.info("[EmulationFragment] Starting view setup for game: ${game?.title}")
- gpuModel = GpuDriverHelper.getGpuModel().toString()
+ gpuModel = GpuDriverHelper.hookLibPath?.let { GpuDriverHelper.getGpuModel(hookLibPath = it).toString() } ?: "Unknown"
fwVersion = NativeLibrary.firmwareVersion()
updateQuickOverlayMenuEntry(BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean())
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/FreedrenoSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/FreedrenoSettingsFragment.kt
new file mode 100644
index 0000000000..c86ef0b800
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/FreedrenoSettingsFragment.kt
@@ -0,0 +1,204 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.navArgs
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.transition.MaterialSharedAxis
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.FreedrenoPresetAdapter
+import org.yuzu.yuzu_emu.adapters.FreedrenoVariableAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentFreedrenoSettingsBinding
+import org.yuzu.yuzu_emu.model.Game
+import org.yuzu.yuzu_emu.utils.NativeFreedrenoConfig
+import org.yuzu.yuzu_emu.utils.FreedrenoPresets
+
+
+class FreedrenoSettingsFragment : Fragment() {
+ private var _binding: FragmentFreedrenoSettingsBinding? = null
+ private val binding get() = _binding!!
+ private val args by navArgs<FreedrenoSettingsFragmentArgs>()
+ private val game: Game? get() = args.game
+ private val isPerGameConfig: Boolean get() = game != null
+
+ private lateinit var presetAdapter: FreedrenoPresetAdapter
+ private lateinit var settingsAdapter: FreedrenoVariableAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentFreedrenoSettingsBinding.inflate(layoutInflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ NativeFreedrenoConfig.setFreedrenoBasePath(requireContext().cacheDir.absolutePath)
+ NativeFreedrenoConfig.initializeFreedrenoConfig()
+
+ if (isPerGameConfig) {
+ NativeFreedrenoConfig.loadPerGameConfig(game!!.programIdHex)
+ } else {
+ NativeFreedrenoConfig.reloadFreedrenoConfig()
+ }
+
+ setupToolbar()
+ setupAdapters()
+ loadCurrentSettings()
+ setupButtonListeners()
+ setupWindowInsets()
+ }
+
+ private fun setupToolbar() {
+ binding.toolbarFreedreno.setNavigationOnClickListener {
+ requireActivity().onBackPressedDispatcher.onBackPressed()
+ }
+ if (isPerGameConfig) {
+ binding.toolbarFreedreno.title = getString(R.string.freedreno_per_game_title)
+ binding.toolbarFreedreno.subtitle = game!!.title
+ }
+ }
+
+ private fun setupAdapters() {
+ // Setup presets adapter (horizontal list)
+ presetAdapter = FreedrenoPresetAdapter { preset ->
+ applyPreset(preset)
+ }
+ binding.listFreedrenoPresets.apply {
+ adapter = presetAdapter
+ layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
+ }
+ presetAdapter.submitList(FreedrenoPresets.ALL_PRESETS)
+
+ // Setup current settings adapter (vertical list)
+ settingsAdapter = FreedrenoVariableAdapter(requireContext()) { variable, onDelete ->
+ onDelete()
+ loadCurrentSettings() // Refresh list after deletion
+ }
+ binding.listFreedrenoSettings.apply {
+ adapter = settingsAdapter
+ layoutManager = LinearLayoutManager(requireContext())
+ }
+ }
+
+ private fun loadCurrentSettings() {
+ // Load all currently set environment variables
+ val variables = mutableListOf<FreedrenoVariable>()
+
+ // Common variables to check
+ val commonVars = listOf(
+ "TU_DEBUG", "FD_MESA_DEBUG", "IR3_SHADER_DEBUG",
+ "FD_RD_DUMP", "FD_RD_DUMP_FRAMES", "FD_RD_DUMP_TESTNAME",
+ "TU_BREADCRUMBS"
+ )
+
+ for (varName in commonVars) {
+ if (NativeFreedrenoConfig.isFreedrenoEnvSet(varName)) {
+ val value = NativeFreedrenoConfig.getFreedrenoEnv(varName)
+ variables.add(FreedrenoVariable(varName, value))
+ }
+ }
+
+ settingsAdapter.submitList(variables)
+ }
+
+ private fun setupButtonListeners() {
+ binding.buttonAddVariable.setOnClickListener {
+ val varName = binding.variableNameInput.text.toString().trim()
+ val varValue = binding.variableValueInput.text.toString().trim()
+
+ if (varName.isEmpty()) {
+ showSnackbar(getString(R.string.freedreno_error_empty_name))
+ return@setOnClickListener
+ }
+
+ if (NativeFreedrenoConfig.setFreedrenoEnv(varName, varValue)) {
+ showSnackbar(getString(R.string.freedreno_variable_added, varName))
+ binding.variableNameInput.text?.clear()
+ binding.variableValueInput.text?.clear()
+ loadCurrentSettings()
+ } else {
+ showSnackbar(getString(R.string.freedreno_error_setting_variable))
+ }
+ }
+
+ binding.buttonClearAll.setOnClickListener {
+ NativeFreedrenoConfig.clearAllFreedrenoEnv()
+ showSnackbar(getString(R.string.freedreno_cleared_all))
+ loadCurrentSettings()
+ }
+
+ binding.buttonSave.setOnClickListener {
+ if (isPerGameConfig) {
+ NativeFreedrenoConfig.savePerGameConfig(game!!.programIdHex)
+ showSnackbar(getString(R.string.freedreno_per_game_saved))
+ } else {
+ NativeFreedrenoConfig.saveFreedrenoConfig()
+ showSnackbar(getString(R.string.freedreno_saved))
+ }
+ }
+ }
+
+ private fun applyPreset(preset: org.yuzu.yuzu_emu.utils.FreedrenoPreset) {
+ // Clear all first for consistency
+ NativeFreedrenoConfig.clearAllFreedrenoEnv()
+
+ // Apply all variables in the preset
+ for ((varName, varValue) in preset.variables) {
+ NativeFreedrenoConfig.setFreedrenoEnv(varName, varValue)
+ }
+
+ showSnackbar(getString(R.string.freedreno_preset_applied, preset.name))
+ loadCurrentSettings()
+ }
+
+ private fun setupWindowInsets() {
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
+ val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ binding.root.updatePadding(
+ left = systemInsets.left,
+ right = systemInsets.right,
+ bottom = systemInsets.bottom
+ )
+ insets
+ }
+ }
+
+ private fun showSnackbar(message: String) {
+ Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
+
+/**
+ * Data class representing a Freedreno environment variable.
+ */
+data class FreedrenoVariable(
+ val name: String,
+ val value: String
+)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index 0aa2bf09e7..97b0470feb 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -325,6 +325,20 @@ class GamePropertiesFragment : Fragment() {
)
)
}
+ if (GpuDriverHelper.isAdrenoGpu()) {
+ add(
+ SubmenuProperty(
+ R.string.freedreno_per_game_title,
+ R.string.freedreno_per_game_description,
+ R.drawable.ic_graphics,
+ action = {
+ val action = GamePropertiesFragmentDirections
+ .actionPerGamePropertiesFragmentToFreedrenoSettingsFragment(args.game)
+ binding.root.findNavController().navigate(action)
+ }
+ )
+ )
+ }
if (!args.game.isHomebrew) {
add(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
index e5832d660e..13d5da3a5a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
@@ -23,10 +23,16 @@ object GpuDriverHelper {
private const val META_JSON_FILENAME = "meta.json"
private var fileRedirectionPath: String? = null
var driverInstallationPath: String? = null
- private var hookLibPath: String? = null
+ internal var hookLibPath: String? = null
val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/"
+ fun initializeFreedrenoConfigEarly() {
+ NativeFreedrenoConfig.setFreedrenoBasePath(YuzuApplication.appContext.cacheDir.absolutePath)
+ NativeFreedrenoConfig.initializeFreedrenoConfig()
+ NativeFreedrenoConfig.reloadFreedrenoConfig()
+ }
+
fun initializeDriverParameters() {
try {
// Initialize the file redirection directory.
@@ -40,11 +46,9 @@ object GpuDriverHelper {
throw RuntimeException(e)
}
- // Initialize directories.
initializeDirectories()
-
- // Initialize hook libraries directory.
hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/"
+ NativeFreedrenoConfig.reloadFreedrenoConfig()
// Initialize GPU driver.
NativeLibrary.initializeGpuDriver(
@@ -211,9 +215,17 @@ object GpuDriverHelper {
external fun getGpuModel(
surface: Surface = Surface(SurfaceTexture(true)),
- hookLibPath: String = GpuDriverHelper.hookLibPath!!
+ hookLibPath: String
): String?
+ fun isAdrenoGpu(): Boolean {
+ return try {
+ supportsCustomDriverLoading()
+ } catch (e: Exception) {
+ false
+ }
+ }
+
// Parse the custom driver metadata to retrieve the name.
val installedCustomDriverData: GpuDriverMetadata
get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeFreedrenoConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeFreedrenoConfig.kt
new file mode 100644
index 0000000000..44d5ac5b7f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeFreedrenoConfig.kt
@@ -0,0 +1,164 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+/**
+ * Provides access to Freedreno/Turnip driver configuration through JNI bindings.
+ *
+ * This class allows Java/Kotlin code to configure Freedreno environment variables
+ * for the GPU driver (Turnip/Freedreno) that runs in the emulator on Android.
+ *
+ * Variables must be set BEFORE starting emulation for them to take effect.
+ *
+ * See https://docs.mesa3d.org/drivers/freedreno.html for documentation.
+ */
+object NativeFreedrenoConfig {
+
+ @Synchronized
+ external fun setFreedrenoBasePath(basePath: String)
+
+ @Synchronized
+ external fun initializeFreedrenoConfig()
+
+ @Synchronized
+ external fun saveFreedrenoConfig()
+
+ @Synchronized
+ external fun reloadFreedrenoConfig()
+
+ @Synchronized
+ external fun setFreedrenoEnv(varName: String, value: String): Boolean
+
+ @Synchronized
+ external fun getFreedrenoEnv(varName: String): String
+
+ @Synchronized
+ external fun isFreedrenoEnvSet(varName: String): Boolean
+
+ @Synchronized
+ external fun clearFreedrenoEnv(varName: String): Boolean
+
+ @Synchronized
+ external fun clearAllFreedrenoEnv()
+
+ @Synchronized
+ external fun getFreedrenoEnvSummary(): String
+
+ @Synchronized
+ external fun setCurrentProgramId(programId: String)
+
+ @Synchronized
+ external fun loadPerGameConfig(programId: String): Boolean
+
+ @Synchronized
+ external fun loadPerGameConfigWithGlobalFallback(programId: String): Boolean
+
+ @Synchronized
+ external fun savePerGameConfig(programId: String): Boolean
+
+ @Synchronized
+ external fun hasPerGameConfig(programId: String): Boolean
+
+ @Synchronized
+ external fun deletePerGameConfig(programId: String): Boolean
+}
+
+/**
+ * Data class representing a Freedreno preset configuration.
+ * Presets are commonly used debugging/profiling configurations.
+ */
+data class FreedrenoPreset(
+ val name: String, // Display name (e.g., "Debug - CPU Memory")
+ val description: String, // Description of what this preset does
+ val icon: String, // Icon identifier
+ val variables: Map<String, String> // Map of env vars to set
+)
+
+/**
+ * Predefined Freedreno presets for quick configuration.
+ */
+object FreedrenoPresets {
+
+ val DEBUG_CPU_MEMORY = FreedrenoPreset(
+ name = "Debug - CPU Memory",
+ description = "Use CPU memory (slower but more stable)",
+ icon = "ic_debug_cpu",
+ variables = mapOf(
+ "TU_DEBUG" to "sysmem"
+ )
+ )
+
+ val DEBUG_UBWC_DISABLED = FreedrenoPreset(
+ name = "Debug - No UBWC",
+ description = "Disable UBWC compression for debugging",
+ icon = "ic_debug_ubwc",
+ variables = mapOf(
+ "TU_DEBUG" to "noubwc"
+ )
+ )
+
+ val DEBUG_NO_BINNING = FreedrenoPreset(
+ name = "Debug - No Binning",
+ description = "Disable binning optimization",
+ icon = "ic_debug_bin",
+ variables = mapOf(
+ "TU_DEBUG" to "nobin"
+ )
+ )
+
+ val CAPTURE_RENDERPASS = FreedrenoPreset(
+ name = "Capture - Renderpass",
+ description = "Capture command stream data for debugging",
+ icon = "ic_capture",
+ variables = mapOf(
+ "FD_RD_DUMP" to "enable"
+ )
+ )
+
+ val CAPTURE_FRAMES = FreedrenoPreset(
+ name = "Capture - First 100 Frames",
+ description = "Capture command stream for first 100 frames only",
+ icon = "ic_capture",
+ variables = mapOf(
+ "FD_RD_DUMP" to "enable",
+ "FD_RD_DUMP_FRAMES" to "0-100"
+ )
+ )
+
+ val SHADER_DEBUG = FreedrenoPreset(
+ name = "Shader Debug",
+ description = "Enable IR3 shader compiler debugging",
+ icon = "ic_shader",
+ variables = mapOf(
+ "IR3_SHADER_DEBUG" to "nouboopt,spillall"
+ )
+ )
+
+ val GPU_HANG_TRACE = FreedrenoPreset(
+ name = "GPU Hang Trace",
+ description = "Trace GPU progress for debugging hangs",
+ icon = "ic_hang_trace",
+ variables = mapOf(
+ "TU_BREADCRUMBS" to "1"
+ )
+ )
+
+ val PERFORMANCE_DEFAULT = FreedrenoPreset(
+ name = "Performance - Default",
+ description = "Clear all debug options for performance",
+ icon = "ic_performance",
+ variables = emptyMap() // Clears all when applied
+ )
+
+ val ALL_PRESETS = listOf(
+ DEBUG_CPU_MEMORY,
+ DEBUG_UBWC_DISABLED,
+ DEBUG_NO_BINNING,
+ CAPTURE_RENDERPASS,
+ CAPTURE_FRAMES,
+ SHADER_DEBUG,
+ GPU_HANG_TRACE,
+ PERFORMANCE_DEFAULT
+ )
+}
diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt
index cb17de46da..6cdacea320 100644
--- a/src/android/app/src/main/jni/CMakeLists.txt
+++ b/src/android/app/src/main/jni/CMakeLists.txt
@@ -10,6 +10,7 @@ add_library(yuzu-android SHARED
native.cpp
native.h
native_config.cpp
+ native_freedreno.cpp
android_settings.cpp
game_metadata.cpp
native_log.cpp
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index d2daef4eb2..2fd532870c 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -730,11 +730,28 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* en
return false;
}
-void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz,
+void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
jstring hook_lib_dir,
jstring custom_driver_dir,
jstring custom_driver_name,
jstring file_redirect_dir) {
+ // Log active Freedreno environment variables
+ const char* tu_debug = getenv("TU_DEBUG");
+ const char* fd_debug = getenv("FD_MESA_DEBUG");
+ const char* ir3_debug = getenv("IR3_SHADER_DEBUG");
+ const char* fd_rd_dump = getenv("FD_RD_DUMP");
+ const char* tu_breadcrumbs = getenv("TU_BREADCRUMBS");
+
+ if (tu_debug || fd_debug || ir3_debug || fd_rd_dump || tu_breadcrumbs) {
+ LOG_INFO(Frontend, "[Freedreno] Initializing GPU driver with configuration:");
+ if (tu_debug) LOG_INFO(Frontend, "[Freedreno] TU_DEBUG={}", tu_debug);
+ if (fd_debug) LOG_INFO(Frontend, "[Freedreno] FD_MESA_DEBUG={}", fd_debug);
+ if (ir3_debug) LOG_INFO(Frontend, "[Freedreno] IR3_SHADER_DEBUG={}", ir3_debug);
+ if (fd_rd_dump) LOG_INFO(Frontend, "[Freedreno] FD_RD_DUMP={}", fd_rd_dump);
+ if (tu_breadcrumbs) LOG_INFO(Frontend, "[Freedreno] TU_BREADCRUMBS={}", tu_breadcrumbs);
+ }
+
EmulationSession::GetInstance().InitializeGpuDriver(
Common::Android::GetJString(env, hook_lib_dir),
Common::Android::GetJString(env, custom_driver_dir),
diff --git a/src/android/app/src/main/jni/native_freedreno.cpp b/src/android/app/src/main/jni/native_freedreno.cpp
new file mode 100644
index 0000000000..0879a85ea8
--- /dev/null
+++ b/src/android/app/src/main/jni/native_freedreno.cpp
@@ -0,0 +1,477 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+/**
+ * @file native_freedreno.cpp
+ * @brief JNI bindings for Freedreno/Turnip GPU driver configuration.
+ *
+ * Provides runtime configuration of Mesa Freedreno environment variables
+ * for the Turnip Vulkan driver on Adreno GPUs.
+ *
+ * @see https://docs.mesa3d.org/drivers/freedreno.html
+ */
+
+#include <algorithm>
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+#include <map>
+#include <memory>
+#include <string>
+#include <sys/stat.h>
+
+#include <jni.h>
+
+#include "common/android/android_common.h"
+#include "common/logging/log.h"
+#include "native.h"
+
+namespace {
+
+struct FreedrenoConfig {
+ std::map<std::string, std::string> env_vars;
+ std::string config_file_path;
+};
+
+std::unique_ptr<FreedrenoConfig> g_config;
+std::string g_base_path;
+std::string g_current_program_id;
+
+constexpr const char* kConfigFileName = ".freedreno.conf";
+constexpr const char* kPerGameConfigDir = "freedreno_games";
+
+void LogActiveVariables() {
+ if (!g_config || g_config->env_vars.empty()) {
+ return;
+ }
+ for (const auto& [key, value] : g_config->env_vars) {
+ LOG_INFO(Frontend, "[Freedreno] {}={}", key, value);
+ }
+}
+
+bool ApplyEnvironmentVariable(const std::string& key, const std::string& value) {
+ if (setenv(key.c_str(), value.c_str(), 1) != 0) {
+ LOG_ERROR(Frontend, "[Freedreno] Failed to set {}={} (errno: {})", key, value, errno);
+ return false;
+ }
+ return true;
+}
+
+void ClearAllEnvironmentVariables() {
+ if (!g_config) return;
+ for (const auto& [key, value] : g_config->env_vars) {
+ unsetenv(key.c_str());
+ }
+ g_config->env_vars.clear();
+}
+
+std::string GetConfigPath() {
+ return g_base_path + "/" + kConfigFileName;
+}
+
+std::string GetPerGameConfigPath(const std::string& program_id) {
+ return g_base_path + "/" + kPerGameConfigDir + "/" + program_id + ".conf";
+}
+
+void EnsurePerGameConfigDir() {
+ std::string dir_path = g_base_path + "/" + kPerGameConfigDir;
+ mkdir(dir_path.c_str(), 0755);
+}
+
+bool LoadConfigFromFile(const std::string& config_path) {
+ if (!g_config) return false;
+
+ FILE* file = fopen(config_path.c_str(), "r");
+ if (!file) {
+ return false;
+ }
+
+ char line[512];
+ int count = 0;
+ while (fgets(line, sizeof(line), file)) {
+ size_t len = strlen(line);
+ if (len > 0 && line[len - 1] == '\n') {
+ line[len - 1] = '\0';
+ len--;
+ }
+
+ if (len == 0 || line[0] == '#') {
+ continue;
+ }
+
+ const char* eq = strchr(line, '=');
+ if (!eq) {
+ continue;
+ }
+
+ std::string key(line, eq - line);
+ std::string value(eq + 1);
+
+ g_config->env_vars[key] = value;
+ ApplyEnvironmentVariable(key, value);
+ count++;
+ }
+
+ fclose(file);
+ return count > 0;
+}
+
+bool SaveConfigToFile(const std::string& config_path) {
+ if (!g_config) return false;
+
+ FILE* file = fopen(config_path.c_str(), "w");
+ if (!file) {
+ LOG_ERROR(Frontend, "[Freedreno] Failed to open {} for writing", config_path);
+ return false;
+ }
+
+ fprintf(file, "# Freedreno/Turnip Configuration\n");
+ fprintf(file, "# Auto-generated by Eden Emulator\n\n");
+
+ for (const auto& [key, value] : g_config->env_vars) {
+ fprintf(file, "%s=%s\n", key.c_str(), value.c_str());
+ }
+
+ fclose(file);
+ return true;
+}
+
+} // anonymous namespace
+
+extern "C" {
+
+JNIEXPORT void JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoBasePath(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jbasePath) {
+ g_base_path = Common::Android::GetJString(env, jbasePath);
+}
+
+JNIEXPORT void JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_initializeFreedrenoConfig(
+ [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
+ if (!g_config) {
+ g_config = std::make_unique<FreedrenoConfig>();
+ LOG_INFO(Frontend, "[Freedreno] Configuration system initialized");
+ }
+}
+
+JNIEXPORT void JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_saveFreedrenoConfig(
+ [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
+ if (!g_config) {
+ LOG_WARNING(Frontend, "[Freedreno] Cannot save: not initialized");
+ return;
+ }
+
+ const std::string config_path = GetConfigPath();
+ FILE* file = fopen(config_path.c_str(), "w");
+ if (!file) {
+ LOG_ERROR(Frontend, "[Freedreno] Failed to open {} for writing", config_path);
+ return;
+ }
+
+ fprintf(file, "# Freedreno/Turnip Configuration\n");
+ fprintf(file, "# Auto-generated by Eden Emulator\n\n");
+
+ for (const auto& [key, value] : g_config->env_vars) {
+ fprintf(file, "%s=%s\n", key.c_str(), value.c_str());
+ }
+
+ fclose(file);
+ g_config->config_file_path = config_path;
+
+ LOG_INFO(Frontend, "[Freedreno] Saved {} variables to {}",
+ g_config->env_vars.size(), config_path);
+}
+
+JNIEXPORT void JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_reloadFreedrenoConfig(
+ [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
+ if (!g_config) {
+ LOG_WARNING(Frontend, "[Freedreno] Cannot reload: not initialized");
+ return;
+ }
+
+ const std::string config_path = GetConfigPath();
+ g_config->env_vars.clear();
+
+ FILE* file = fopen(config_path.c_str(), "r");
+ if (!file) {
+ LOG_DEBUG(Frontend, "[Freedreno] No config file found at {}", config_path);
+ return;
+ }
+
+ char line[512];
+ while (fgets(line, sizeof(line), file)) {
+ // Remove trailing newline
+ size_t len = strlen(line);
+ if (len > 0 && line[len - 1] == '\n') {
+ line[len - 1] = '\0';
+ len--;
+ }
+
+ // Skip empty lines and comments
+ if (len == 0 || line[0] == '#') {
+ continue;
+ }
+
+ // Parse key=value
+ const char* eq = strchr(line, '=');
+ if (!eq) {
+ continue;
+ }
+
+ std::string key(line, eq - line);
+ std::string value(eq + 1);
+
+ g_config->env_vars[key] = value;
+ ApplyEnvironmentVariable(key, value);
+ }
+
+ fclose(file);
+ g_config->config_file_path = config_path;
+
+ if (!g_config->env_vars.empty()) {
+ LOG_INFO(Frontend, "[Freedreno] Loaded {} variables:", g_config->env_vars.size());
+ LogActiveVariables();
+ }
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoEnv(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName, jstring jvalue) {
+ if (!g_config) {
+ return JNI_FALSE;
+ }
+
+ auto var_name = Common::Android::GetJString(env, jvarName);
+ auto value = Common::Android::GetJString(env, jvalue);
+
+ if (var_name.empty()) {
+ return JNI_FALSE;
+ }
+
+ g_config->env_vars[var_name] = value;
+
+ if (!ApplyEnvironmentVariable(var_name, value)) {
+ return JNI_FALSE;
+ }
+
+ LOG_INFO(Frontend, "[Freedreno] Set {}={}", var_name, value);
+ return JNI_TRUE;
+}
+
+JNIEXPORT jstring JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_getFreedrenoEnv(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName) {
+ if (!g_config) {
+ return env->NewStringUTF("");
+ }
+
+ auto var_name = Common::Android::GetJString(env, jvarName);
+ auto it = g_config->env_vars.find(var_name);
+
+ if (it != g_config->env_vars.end()) {
+ return env->NewStringUTF(it->second.c_str());
+ }
+
+ return env->NewStringUTF("");
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_isFreedrenoEnvSet(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName) {
+ if (!g_config) {
+ return JNI_FALSE;
+ }
+
+ auto var_name = Common::Android::GetJString(env, jvarName);
+ auto it = g_config->env_vars.find(var_name);
+
+ return (it != g_config->env_vars.end() && !it->second.empty()) ? JNI_TRUE : JNI_FALSE;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_clearFreedrenoEnv(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName) {
+ if (!g_config) {
+ return JNI_FALSE;
+ }
+
+ auto var_name = Common::Android::GetJString(env, jvarName);
+ auto it = g_config->env_vars.find(var_name);
+
+ if (it != g_config->env_vars.end()) {
+ g_config->env_vars.erase(it);
+ unsetenv(var_name.c_str());
+ LOG_INFO(Frontend, "[Freedreno] Cleared {}", var_name);
+ return JNI_TRUE;
+ }
+
+ return JNI_FALSE;
+}
+
+JNIEXPORT void JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_clearAllFreedrenoEnv(
+ [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
+ if (!g_config) {
+ return;
+ }
+
+ for (const auto& [key, value] : g_config->env_vars) {
+ unsetenv(key.c_str());
+ }
+
+ size_t count = g_config->env_vars.size();
+ g_config->env_vars.clear();
+
+ if (count > 0) {
+ LOG_INFO(Frontend, "[Freedreno] Cleared all {} variables", count);
+ }
+}
+
+JNIEXPORT jstring JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_getFreedrenoEnvSummary(
+ JNIEnv* env, [[maybe_unused]] jobject obj) {
+ if (!g_config || g_config->env_vars.empty()) {
+ return env->NewStringUTF("");
+ }
+
+ std::string summary;
+ for (const auto& [key, value] : g_config->env_vars) {
+ if (!summary.empty()) {
+ summary += ",";
+ }
+ summary += key + "=" + value;
+ }
+
+ return env->NewStringUTF(summary.c_str());
+}
+
+JNIEXPORT void JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setCurrentProgramId(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
+ g_current_program_id = Common::Android::GetJString(env, jprogramId);
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_loadPerGameConfig(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
+ if (!g_config) {
+ return JNI_FALSE;
+ }
+
+ auto program_id = Common::Android::GetJString(env, jprogramId);
+ if (program_id.empty()) {
+ return JNI_FALSE;
+ }
+
+ // Clear current environment variables first
+ ClearAllEnvironmentVariables();
+ g_current_program_id = program_id;
+
+ // Try to load per-game config - do NOT fall back to global
+ // Per-game config should start empty if no config exists yet
+ std::string per_game_path = GetPerGameConfigPath(program_id);
+ if (LoadConfigFromFile(per_game_path)) {
+ LOG_INFO(Frontend, "[Freedreno] Loaded per-game config for {}", program_id);
+ LogActiveVariables();
+ return JNI_TRUE;
+ }
+
+ // No per-game config exists - start with empty config
+ LOG_INFO(Frontend, "[Freedreno] No per-game config for {}, starting empty", program_id);
+ return JNI_FALSE;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_loadPerGameConfigWithGlobalFallback(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
+ if (!g_config) {
+ return JNI_FALSE;
+ }
+
+ auto program_id = Common::Android::GetJString(env, jprogramId);
+ if (program_id.empty()) {
+ return JNI_FALSE;
+ }
+
+ // Clear current environment variables first
+ ClearAllEnvironmentVariables();
+ g_current_program_id = program_id;
+
+ // Try to load per-game config first
+ std::string per_game_path = GetPerGameConfigPath(program_id);
+ if (LoadConfigFromFile(per_game_path)) {
+ LOG_INFO(Frontend, "[Freedreno] Loaded per-game config for {}", program_id);
+ LogActiveVariables();
+ return JNI_TRUE;
+ }
+
+ // Fall back to global config for emulation
+ std::string global_path = GetConfigPath();
+ if (LoadConfigFromFile(global_path)) {
+ LOG_INFO(Frontend, "[Freedreno] No per-game config for {}, using global for emulation", program_id);
+ LogActiveVariables();
+ }
+
+ return JNI_FALSE;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_savePerGameConfig(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
+ if (!g_config) {
+ return JNI_FALSE;
+ }
+
+ auto program_id = Common::Android::GetJString(env, jprogramId);
+ if (program_id.empty()) {
+ return JNI_FALSE;
+ }
+
+ EnsurePerGameConfigDir();
+ std::string config_path = GetPerGameConfigPath(program_id);
+
+ if (SaveConfigToFile(config_path)) {
+ LOG_INFO(Frontend, "[Freedreno] Saved per-game config for {}", program_id);
+ return JNI_TRUE;
+ }
+
+ return JNI_FALSE;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_hasPerGameConfig(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
+ auto program_id = Common::Android::GetJString(env, jprogramId);
+ if (program_id.empty()) {
+ return JNI_FALSE;
+ }
+
+ std::string config_path = GetPerGameConfigPath(program_id);
+ FILE* file = fopen(config_path.c_str(), "r");
+ if (file) {
+ fclose(file);
+ return JNI_TRUE;
+ }
+ return JNI_FALSE;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_deletePerGameConfig(
+ JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) {
+ auto program_id = Common::Android::GetJString(env, jprogramId);
+ if (program_id.empty()) {
+ return JNI_FALSE;
+ }
+
+ std::string config_path = GetPerGameConfigPath(program_id);
+ if (remove(config_path.c_str()) == 0) {
+ LOG_INFO(Frontend, "[Freedreno] Deleted per-game config for {}", program_id);
+ return JNI_TRUE;
+ }
+ return JNI_FALSE;
+}
+
+} // extern "C"
diff --git a/src/android/app/src/main/res/layout/fragment_freedreno_settings.xml b/src/android/app/src/main/res/layout/fragment_freedreno_settings.xml
new file mode 100644
index 0000000000..d0e10adc0d
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_freedreno_settings.xml
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/coordinator_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar_freedreno"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true"
+ android:touchscreenBlocksFocus="false"
+ app:elevation="0dp">
+
+ <com.google.android.material.appbar.CollapsingToolbarLayout
+ android:id="@+id/toolbar_freedreno_layout"
+ style="?attr/collapsingToolbarLayoutMediumStyle"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
+ app:contentScrim="?attr/colorSurface"
+ app:scrimVisibleHeightTrigger="100dp">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar_freedreno"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:touchscreenBlocksFocus="false"
+ app:layout_collapseMode="pin"
+ app:navigationIcon="@drawable/ic_back"
+ app:title="@string/gpu_driver_settings" />
+
+ </com.google.android.material.appbar.CollapsingToolbarLayout>
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.core.widget.NestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="16dp">
+
+ <!-- Presets Section -->
+ <com.google.android.material.textview.MaterialTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="8dp"
+ android:text="@string/freedreno_presets"
+ android:textAppearance="?attr/textAppearanceTitleMedium" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_freedreno_presets"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:scrollbars="horizontal"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
+
+ <!-- Current Settings Section -->
+ <com.google.android.material.textview.MaterialTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:layout_marginBottom="8dp"
+ android:text="@string/freedreno_current_settings"
+ android:textAppearance="?attr/textAppearanceTitleMedium" />
+
+ <!-- Settings List -->
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_freedreno_settings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
+
+ <!-- Debug Section -->
+ <com.google.android.material.textview.MaterialTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:layout_marginBottom="8dp"
+ android:text="@string/freedreno_debug"
+ android:textAppearance="?attr/textAppearanceTitleMedium" />
+
+ <!-- Manual Variable Input -->
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/variable_name_input_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:hint="@string/freedreno_var_name">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/variable_name_input"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="text" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/variable_value_input_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:hint="@string/freedreno_var_value">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/variable_value_input"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="text" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <Button
+ android:id="@+id/button_add_variable"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:text="@string/freedreno_add_variable" />
+
+ <!-- Action Buttons -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginTop="16dp"
+ android:spacing="8dp">
+
+ <Button
+ android:id="@+id/button_clear_all"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/freedreno_clear_all"
+ style="?attr/materialButtonOutlinedStyle" />
+
+ <Button
+ android:id="@+id/button_save"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/save" />
+
+ </LinearLayout>
+
+ <!-- Info Section -->
+ <com.google.android.material.card.MaterialCardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ app:cardElevation="0dp"
+ app:strokeWidth="1dp"
+ app:strokeColor="?attr/colorOutline">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="12dp">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/freedreno_info_title"
+ android:textAppearance="?attr/textAppearanceTitleSmall" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/freedreno_info_description"
+ android:textAppearance="?attr/textAppearanceBodySmall"
+ android:textColor="?attr/colorOnSurfaceVariant" />
+
+ </LinearLayout>
+
+ </com.google.android.material.card.MaterialCardView>
+
+ </LinearLayout>
+
+ </androidx.core.widget.NestedScrollView>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_freedreno_preset.xml b/src/android/app/src/main/res/layout/list_item_freedreno_preset.xml
new file mode 100644
index 0000000000..172e1fcaab
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_freedreno_preset.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="4dp"
+ android:paddingEnd="4dp">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/preset_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="100dp"
+ android:text="Preset"
+ style="?attr/materialButtonOutlinedStyle" />
+
+</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_freedreno_variable.xml b/src/android/app/src/main/res/layout/list_item_freedreno_variable.xml
new file mode 100644
index 0000000000..2344b8868a
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_freedreno_variable.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="0dp"
+ android:layout_marginEnd="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ app:cardElevation="0dp"
+ app:strokeWidth="1dp"
+ app:strokeColor="?attr/colorOutline">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="12dp"
+ android:gravity="center_vertical">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/variable_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="VARIABLE_NAME"
+ android:textAppearance="?attr/textAppearanceTitleSmall"
+ android:textColor="?attr/colorOnSurface" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/variable_value"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:text="variable_value"
+ android:textAppearance="?attr/textAppearanceBodySmall"
+ android:textColor="?attr/colorOnSurfaceVariant"
+ android:maxLines="1"
+ android:ellipsize="end" />
+
+ </LinearLayout>
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button_delete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:text="@string/delete"
+ android:minWidth="0dp"
+ android:minHeight="36dp"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ style="?attr/materialButtonOutlinedStyle" />
+
+ </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index bfea21e557..97ff52a124 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -148,6 +148,9 @@
<action
android:id="@+id/action_perGamePropertiesFragment_to_driverManagerFragment"
app:destination="@id/driverManagerFragment" />
+ <action
+ android:id="@+id/action_perGamePropertiesFragment_to_freedrenoSettingsFragment"
+ app:destination="@id/freedrenoSettingsFragment" />
</fragment>
<action
android:id="@+id/action_global_perGamePropertiesFragment"
@@ -173,5 +176,15 @@
android:name="org.yuzu.yuzu_emu.fragments.DriverFetcherFragment"
android:label="fragment_driver_fetcher"
tools:layout="@layout/fragment_driver_fetcher" />
+ <fragment
+ android:id="@+id/freedrenoSettingsFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.FreedrenoSettingsFragment"
+ android:label="@string/freedreno_settings_title">
+ <argument
+ android:name="game"
+ app:argType="org.yuzu.yuzu_emu.model.Game"
+ app:nullable="true"
+ android:defaultValue="@null" />
+ </fragment>
</navigation>
diff --git a/src/android/app/src/main/res/navigation/settings_navigation.xml b/src/android/app/src/main/res/navigation/settings_navigation.xml
index e4c66e7d5a..845fc6c44c 100644
--- a/src/android/app/src/main/res/navigation/settings_navigation.xml
+++ b/src/android/app/src/main/res/navigation/settings_navigation.xml
@@ -29,4 +29,19 @@
android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsSearchFragment"
android:label="SettingsSearchFragment" />
+ <fragment
+ android:id="@+id/freedrenoSettingsFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.FreedrenoSettingsFragment"
+ android:label="@string/freedreno_settings_title">
+ <argument
+ android:name="game"
+ app:argType="org.yuzu.yuzu_emu.model.Game"
+ app:nullable="true"
+ android:defaultValue="@null" />
+ </fragment>
+
+ <action
+ android:id="@+id/action_settingsFragment_to_freedrenoSettingsFragment"
+ app:destination="@id/freedrenoSettingsFragment" />
+
</navigation>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 35f2489223..4bfa5afd01 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -1051,6 +1051,28 @@
<string name="cpu_accuracy_paranoid">Paranoid</string>
<string name="cpu_accuracy_debugging">Debugging</string>
+ <!-- Freedreno Settings -->
+ <string name="freedreno_settings_title">Freedreno Settings</string>
+ <string name="gpu_driver_settings">GPU Driver Settings</string>
+ <string name="freedreno_presets">Quick Presets</string>
+ <string name="freedreno_current_settings">Current Settings</string>
+ <string name="freedreno_debug">Advanced Settings</string>
+ <string name="freedreno_var_name">Variable Name (e.g., TU_DEBUG)</string>
+ <string name="freedreno_var_value">Variable Value</string>
+ <string name="freedreno_add_variable">Add Variable</string>
+ <string name="freedreno_clear_all">Clear All</string>
+ <string name="freedreno_saved">Freedreno configuration saved</string>
+ <string name="freedreno_cleared_all">All Freedreno variables cleared</string>
+ <string name="freedreno_variable_added">Variable %1$s added</string>
+ <string name="freedreno_preset_applied">Preset \'%1$s\' applied</string>
+ <string name="freedreno_error_empty_name">Variable name cannot be empty</string>
+ <string name="freedreno_error_setting_variable">Failed to set variable</string>
+ <string name="freedreno_info_title">About Freedreno Configuration</string>
+ <string name="freedreno_info_description">Configure Freedreno/Turnip GPU driver options for debugging, profiling, and performance optimization. Changes are saved automatically. See https://docs.mesa3d.org/drivers/freedreno.html for detailed documentation.</string>
+ <string name="freedreno_per_game_title">Freedreno Settings</string>
+ <string name="freedreno_per_game_description">Configure GPU driver settings for this game</string>
+ <string name="freedreno_per_game_saved">Freedreno configuration saved</string>
+
<!-- Gamepad Buttons -->
<string name="gamepad_d_pad">D-pad</string>
<string name="gamepad_left_stick">Left stick</string>