[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>
This commit is contained in:
MrPurple666 2026-01-09 23:22:59 +01:00 committed by crueter
parent 1370f23675
commit 87d4c67386
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
22 changed files with 1391 additions and 13 deletions

View File

@ -71,9 +71,9 @@
"hash": "9697e80a7d5d9bcb3ce51051a9a24962fb90ca79d215f1f03ae6b58da8ba13a63b5dda1b4dde3d26ac6445029696b8ef2883f4e5a777b342bba01283ed293856"
},
"libadrenotools": {
"repo": "bylaws/libadrenotools",
"sha": "8fae8ce254",
"hash": "db4a74ce15559c75e01d1868a90701519b655d77f2a343bbee283a42f8332dc9046960fb022dc969f205e457348a3f99cb8be6e1cd91264d2ae1235294b9f9b2",
"repo": "eden-emulator/libadrenotools",
"sha": "8ba23b42d7",
"hash": "f6526620cb752876edc5ed4c0925d57b873a8218ee09ad10859ee476e9333259784f61c1dcc55a2bcba597352d18aff22cd2e4c1925ec2ae94074e09d7da2265",
"patches": [
"0001-linkerns-cpm.patch"
]

View File

@ -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()

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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);
}

View File

@ -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) {

View File

@ -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)

View File

@ -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()

View File

@ -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())

View File

@ -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
)

View File

@ -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(

View File

@ -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))

View File

@ -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
)
}

View File

@ -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

View File

@ -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),

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>