Browse Source
[turnip/android] Add environment variables settings for turnip drivers (#3205)
[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>pull/3263/head
committed by
crueter
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
22 changed files with 1391 additions and 13 deletions
-
6externals/cpmfile.json
-
5src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
-
58src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoPresetAdapter.kt
-
61src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoVariableAdapter.kt
-
1src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
-
12src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
-
17src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
-
2src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt
-
15src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
-
204src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/FreedrenoSettingsFragment.kt
-
14src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
-
22src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
-
164src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeFreedrenoConfig.kt
-
1src/android/app/src/main/jni/CMakeLists.txt
-
19src/android/app/src/main/jni/native.cpp
-
477src/android/app/src/main/jni/native_freedreno.cpp
-
196src/android/app/src/main/res/layout/fragment_freedreno_settings.xml
-
18src/android/app/src/main/res/layout/list_item_freedreno_preset.xml
-
62src/android/app/src/main/res/layout/list_item_freedreno_variable.xml
-
13src/android/app/src/main/res/navigation/home_navigation.xml
-
15src/android/app/src/main/res/navigation/settings_navigation.xml
-
22src/android/app/src/main/res/values/strings.xml
@ -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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
) |
||||
@ -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 |
||||
|
) |
||||
|
} |
||||
@ -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"
|
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue