diff --git a/externals/cpmfile.json b/externals/cpmfile.json index fc39d02065..bed53e683d 100644 --- a/externals/cpmfile.json +++ b/externals/cpmfile.json @@ -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" ] diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index 0ba8519f92..54fb45bd87 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt @@ -61,6 +61,11 @@ class YuzuApplication : Application() { application = this documentsTree = DocumentsTree() DirectoryInitialization.start() + + // Initialize Freedreno config BEFORE loading native library + // This ensures GPU driver environment variables are set before adrenotools initializes + GpuDriverHelper.initializeFreedrenoConfigEarly() + NativeLibrary.playTimeManagerInit() GpuDriverHelper.initializeDriverParameters() NativeInput.reloadInputDevices() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoPresetAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoPresetAdapter.kt new file mode 100644 index 0000000000..242ae588f2 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoPresetAdapter.kt @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.utils.FreedrenoPreset +import org.yuzu.yuzu_emu.databinding.ListItemFreedrenoPresetBinding + +/** + * Adapter for displaying Freedreno preset configurations in a horizontal list. + */ +class FreedrenoPresetAdapter( + private val onPresetClicked: (FreedrenoPreset) -> Unit +) : ListAdapter(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() { + override fun areItemsTheSame(oldItem: FreedrenoPreset, newItem: FreedrenoPreset): Boolean = + oldItem.name == newItem.name + + override fun areContentsTheSame(oldItem: FreedrenoPreset, newItem: FreedrenoPreset): Boolean = + oldItem == newItem + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoVariableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoVariableAdapter.kt new file mode 100644 index 0000000000..1288711a0d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FreedrenoVariableAdapter.kt @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.databinding.ListItemFreedrenoVariableBinding +import org.yuzu.yuzu_emu.fragments.FreedrenoVariable +import org.yuzu.yuzu_emu.utils.NativeFreedrenoConfig + +/** + * Adapter for displaying currently set Freedreno environment variables in a list. + */ +class FreedrenoVariableAdapter( + private val context: Context, + private val onItemClicked: (FreedrenoVariable, () -> Unit) -> Unit +) : ListAdapter(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() { + override fun areItemsTheSame(oldItem: FreedrenoVariable, newItem: FreedrenoVariable): Boolean = + oldItem.name == newItem.name + + override fun areContentsTheSame(oldItem: FreedrenoVariable, newItem: FreedrenoVariable): Boolean = + oldItem == newItem + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt index df96b17bec..fdf9e8b32b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt @@ -27,6 +27,7 @@ object Settings { SECTION_APP_SETTINGS(R.string.app_settings), SECTION_CUSTOM_PATHS(R.string.preferences_custom_paths), SECTION_DEBUG(R.string.preferences_debug), + SECTION_FREEDRENO(R.string.gpu_driver_settings), SECTION_APPLETS(R.string.applets_menu); } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index fac67dbb64..7c1a9c23cc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -29,6 +29,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.features.input.model.AnalogDirection import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting +import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* import org.yuzu.yuzu_emu.utils.ParamPackage @@ -212,8 +213,15 @@ class SettingsAdapter( } fun onSubmenuClick(item: SubmenuSetting) { - val action = SettingsNavigationDirections.actionGlobalSettingsFragment(item.menuKey, null) - fragment.view?.findNavController()?.navigate(action) + // Check if this is the Freedreno Settings submenu + if (item.menuKey == Settings.MenuTag.SECTION_FREEDRENO) { + fragment.view?.findNavController()?.navigate( + R.id.action_settingsFragment_to_freedrenoSettingsFragment + ) + } else { + val action = SettingsNavigationDirections.actionGlobalSettingsFragment(item.menuKey, null) + fragment.view?.findNavController()?.navigate(action) + } } fun onLaunchableClick(item: LaunchableSetting) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 6efce38002..dc58e7d23b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -27,6 +27,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag import org.yuzu.yuzu_emu.features.settings.model.ShortSetting import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.features.settings.model.view.* +import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.InputHandler import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.DirectoryInitialization @@ -109,6 +110,7 @@ class SettingsFragmentPresenter( MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7) MenuTag.SECTION_APP_SETTINGS -> addThemeSettings(sl) MenuTag.SECTION_DEBUG -> addDebugSettings(sl) + MenuTag.SECTION_FREEDRENO -> addFreedrenoSettings(sl) MenuTag.SECTION_APPLETS -> addAppletSettings(sl) MenuTag.SECTION_CUSTOM_PATHS -> addCustomPathsSettings(sl) } @@ -181,6 +183,16 @@ class SettingsFragmentPresenter( menuKey = MenuTag.SECTION_DEBUG ) ) + if (GpuDriverHelper.isAdrenoGpu() && !NativeConfig.isPerGameConfigLoaded()) { + add( + SubmenuSetting( + titleId = R.string.gpu_driver_settings, + descriptionId = R.string.freedreno_settings_title, + iconId = R.drawable.ic_graphics, + menuKey = MenuTag.SECTION_FREEDRENO + ) + ) + } add( SubmenuSetting( titleId = R.string.applets_menu, @@ -498,6 +510,11 @@ class SettingsFragmentPresenter( } } + private fun addFreedrenoSettings(sl: ArrayList) { + // No additional settings needed here - the SubmenuSetting handles navigation + // This method is kept for consistency with other menu sections + } + private fun addAppletSettings(sl: ArrayList) { sl.apply { add(IntSetting.SWKBD_APPLET.key) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt index dea762dc17..130b556c1f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt @@ -45,7 +45,7 @@ class DriverFetcherFragment : Fragment() { private val client = OkHttpClient() private val gpuModel: String? - get() = GpuDriverHelper.getGpuModel() + get() = GpuDriverHelper.hookLibPath?.let { GpuDriverHelper.getGpuModel(hookLibPath = it) } private val adrenoModel: Int get() = parseAdrenoModel() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index d92ca12e6d..1bac1bd1ed 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -87,6 +87,7 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.NativeFreedrenoConfig import org.yuzu.yuzu_emu.utils.ViewUtils import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.collect @@ -303,6 +304,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { throw fallbackException } } + try { + if (GpuDriverHelper.isAdrenoGpu()) { + val programIdHex = game!!.programIdHex + if (NativeFreedrenoConfig.loadPerGameConfigWithGlobalFallback(programIdHex)) { + Log.info("[EmulationFragment] Loaded per-game Freedreno config for $programIdHex") + } else { + Log.info("[EmulationFragment] Using global Freedreno config for $programIdHex") + } + } + } catch (e: Exception) { + Log.warning("[EmulationFragment] Failed to load Freedreno config: ${e.message}") + } emulationState = EmulationState(game!!.path) { return@EmulationState driverViewModel.isInteractionAllowed.value @@ -616,7 +629,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } Log.info("[EmulationFragment] Starting view setup for game: ${game?.title}") - gpuModel = GpuDriverHelper.getGpuModel().toString() + gpuModel = GpuDriverHelper.hookLibPath?.let { GpuDriverHelper.getGpuModel(hookLibPath = it).toString() } ?: "Unknown" fwVersion = NativeLibrary.firmwareVersion() updateQuickOverlayMenuEntry(BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/FreedrenoSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/FreedrenoSettingsFragment.kt new file mode 100644 index 0000000000..c86ef0b800 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/FreedrenoSettingsFragment.kt @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.transition.MaterialSharedAxis +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.FreedrenoPresetAdapter +import org.yuzu.yuzu_emu.adapters.FreedrenoVariableAdapter +import org.yuzu.yuzu_emu.databinding.FragmentFreedrenoSettingsBinding +import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.utils.NativeFreedrenoConfig +import org.yuzu.yuzu_emu.utils.FreedrenoPresets + + +class FreedrenoSettingsFragment : Fragment() { + private var _binding: FragmentFreedrenoSettingsBinding? = null + private val binding get() = _binding!! + private val args by navArgs() + 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() + + // Common variables to check + val commonVars = listOf( + "TU_DEBUG", "FD_MESA_DEBUG", "IR3_SHADER_DEBUG", + "FD_RD_DUMP", "FD_RD_DUMP_FRAMES", "FD_RD_DUMP_TESTNAME", + "TU_BREADCRUMBS" + ) + + for (varName in commonVars) { + if (NativeFreedrenoConfig.isFreedrenoEnvSet(varName)) { + val value = NativeFreedrenoConfig.getFreedrenoEnv(varName) + variables.add(FreedrenoVariable(varName, value)) + } + } + + settingsAdapter.submitList(variables) + } + + private fun setupButtonListeners() { + binding.buttonAddVariable.setOnClickListener { + val varName = binding.variableNameInput.text.toString().trim() + val varValue = binding.variableValueInput.text.toString().trim() + + if (varName.isEmpty()) { + showSnackbar(getString(R.string.freedreno_error_empty_name)) + return@setOnClickListener + } + + if (NativeFreedrenoConfig.setFreedrenoEnv(varName, varValue)) { + showSnackbar(getString(R.string.freedreno_variable_added, varName)) + binding.variableNameInput.text?.clear() + binding.variableValueInput.text?.clear() + loadCurrentSettings() + } else { + showSnackbar(getString(R.string.freedreno_error_setting_variable)) + } + } + + binding.buttonClearAll.setOnClickListener { + NativeFreedrenoConfig.clearAllFreedrenoEnv() + showSnackbar(getString(R.string.freedreno_cleared_all)) + loadCurrentSettings() + } + + binding.buttonSave.setOnClickListener { + if (isPerGameConfig) { + NativeFreedrenoConfig.savePerGameConfig(game!!.programIdHex) + showSnackbar(getString(R.string.freedreno_per_game_saved)) + } else { + NativeFreedrenoConfig.saveFreedrenoConfig() + showSnackbar(getString(R.string.freedreno_saved)) + } + } + } + + private fun applyPreset(preset: org.yuzu.yuzu_emu.utils.FreedrenoPreset) { + // Clear all first for consistency + NativeFreedrenoConfig.clearAllFreedrenoEnv() + + // Apply all variables in the preset + for ((varName, varValue) in preset.variables) { + NativeFreedrenoConfig.setFreedrenoEnv(varName, varValue) + } + + showSnackbar(getString(R.string.freedreno_preset_applied, preset.name)) + loadCurrentSettings() + } + + private fun setupWindowInsets() { + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets -> + val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + binding.root.updatePadding( + left = systemInsets.left, + right = systemInsets.right, + bottom = systemInsets.bottom + ) + insets + } + } + + private fun showSnackbar(message: String) { + Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} + +/** + * Data class representing a Freedreno environment variable. + */ +data class FreedrenoVariable( + val name: String, + val value: String +) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index 0aa2bf09e7..97b0470feb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt @@ -325,6 +325,20 @@ class GamePropertiesFragment : Fragment() { ) ) } + if (GpuDriverHelper.isAdrenoGpu()) { + add( + SubmenuProperty( + R.string.freedreno_per_game_title, + R.string.freedreno_per_game_description, + R.drawable.ic_graphics, + action = { + val action = GamePropertiesFragmentDirections + .actionPerGamePropertiesFragmentToFreedrenoSettingsFragment(args.game) + binding.root.findNavController().navigate(action) + } + ) + ) + } if (!args.game.isHomebrew) { add( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt index e5832d660e..13d5da3a5a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt @@ -23,10 +23,16 @@ object GpuDriverHelper { private const val META_JSON_FILENAME = "meta.json" private var fileRedirectionPath: String? = null var driverInstallationPath: String? = null - private var hookLibPath: String? = null + internal var hookLibPath: String? = null val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/" + fun initializeFreedrenoConfigEarly() { + NativeFreedrenoConfig.setFreedrenoBasePath(YuzuApplication.appContext.cacheDir.absolutePath) + NativeFreedrenoConfig.initializeFreedrenoConfig() + NativeFreedrenoConfig.reloadFreedrenoConfig() + } + fun initializeDriverParameters() { try { // Initialize the file redirection directory. @@ -40,11 +46,9 @@ object GpuDriverHelper { throw RuntimeException(e) } - // Initialize directories. initializeDirectories() - - // Initialize hook libraries directory. hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/" + NativeFreedrenoConfig.reloadFreedrenoConfig() // Initialize GPU driver. NativeLibrary.initializeGpuDriver( @@ -211,9 +215,17 @@ object GpuDriverHelper { external fun getGpuModel( surface: Surface = Surface(SurfaceTexture(true)), - hookLibPath: String = GpuDriverHelper.hookLibPath!! + hookLibPath: String ): String? + fun isAdrenoGpu(): Boolean { + return try { + supportsCustomDriverLoading() + } catch (e: Exception) { + false + } + } + // Parse the custom driver metadata to retrieve the name. val installedCustomDriverData: GpuDriverMetadata get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME)) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeFreedrenoConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeFreedrenoConfig.kt new file mode 100644 index 0000000000..44d5ac5b7f --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeFreedrenoConfig.kt @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.utils + +/** + * Provides access to Freedreno/Turnip driver configuration through JNI bindings. + * + * This class allows Java/Kotlin code to configure Freedreno environment variables + * for the GPU driver (Turnip/Freedreno) that runs in the emulator on Android. + * + * Variables must be set BEFORE starting emulation for them to take effect. + * + * See https://docs.mesa3d.org/drivers/freedreno.html for documentation. + */ +object NativeFreedrenoConfig { + + @Synchronized + external fun setFreedrenoBasePath(basePath: String) + + @Synchronized + external fun initializeFreedrenoConfig() + + @Synchronized + external fun saveFreedrenoConfig() + + @Synchronized + external fun reloadFreedrenoConfig() + + @Synchronized + external fun setFreedrenoEnv(varName: String, value: String): Boolean + + @Synchronized + external fun getFreedrenoEnv(varName: String): String + + @Synchronized + external fun isFreedrenoEnvSet(varName: String): Boolean + + @Synchronized + external fun clearFreedrenoEnv(varName: String): Boolean + + @Synchronized + external fun clearAllFreedrenoEnv() + + @Synchronized + external fun getFreedrenoEnvSummary(): String + + @Synchronized + external fun setCurrentProgramId(programId: String) + + @Synchronized + external fun loadPerGameConfig(programId: String): Boolean + + @Synchronized + external fun loadPerGameConfigWithGlobalFallback(programId: String): Boolean + + @Synchronized + external fun savePerGameConfig(programId: String): Boolean + + @Synchronized + external fun hasPerGameConfig(programId: String): Boolean + + @Synchronized + external fun deletePerGameConfig(programId: String): Boolean +} + +/** + * Data class representing a Freedreno preset configuration. + * Presets are commonly used debugging/profiling configurations. + */ +data class FreedrenoPreset( + val name: String, // Display name (e.g., "Debug - CPU Memory") + val description: String, // Description of what this preset does + val icon: String, // Icon identifier + val variables: Map // Map of env vars to set +) + +/** + * Predefined Freedreno presets for quick configuration. + */ +object FreedrenoPresets { + + val DEBUG_CPU_MEMORY = FreedrenoPreset( + name = "Debug - CPU Memory", + description = "Use CPU memory (slower but more stable)", + icon = "ic_debug_cpu", + variables = mapOf( + "TU_DEBUG" to "sysmem" + ) + ) + + val DEBUG_UBWC_DISABLED = FreedrenoPreset( + name = "Debug - No UBWC", + description = "Disable UBWC compression for debugging", + icon = "ic_debug_ubwc", + variables = mapOf( + "TU_DEBUG" to "noubwc" + ) + ) + + val DEBUG_NO_BINNING = FreedrenoPreset( + name = "Debug - No Binning", + description = "Disable binning optimization", + icon = "ic_debug_bin", + variables = mapOf( + "TU_DEBUG" to "nobin" + ) + ) + + val CAPTURE_RENDERPASS = FreedrenoPreset( + name = "Capture - Renderpass", + description = "Capture command stream data for debugging", + icon = "ic_capture", + variables = mapOf( + "FD_RD_DUMP" to "enable" + ) + ) + + val CAPTURE_FRAMES = FreedrenoPreset( + name = "Capture - First 100 Frames", + description = "Capture command stream for first 100 frames only", + icon = "ic_capture", + variables = mapOf( + "FD_RD_DUMP" to "enable", + "FD_RD_DUMP_FRAMES" to "0-100" + ) + ) + + val SHADER_DEBUG = FreedrenoPreset( + name = "Shader Debug", + description = "Enable IR3 shader compiler debugging", + icon = "ic_shader", + variables = mapOf( + "IR3_SHADER_DEBUG" to "nouboopt,spillall" + ) + ) + + val GPU_HANG_TRACE = FreedrenoPreset( + name = "GPU Hang Trace", + description = "Trace GPU progress for debugging hangs", + icon = "ic_hang_trace", + variables = mapOf( + "TU_BREADCRUMBS" to "1" + ) + ) + + val PERFORMANCE_DEFAULT = FreedrenoPreset( + name = "Performance - Default", + description = "Clear all debug options for performance", + icon = "ic_performance", + variables = emptyMap() // Clears all when applied + ) + + val ALL_PRESETS = listOf( + DEBUG_CPU_MEMORY, + DEBUG_UBWC_DISABLED, + DEBUG_NO_BINNING, + CAPTURE_RENDERPASS, + CAPTURE_FRAMES, + SHADER_DEBUG, + GPU_HANG_TRACE, + PERFORMANCE_DEFAULT + ) +} diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index cb17de46da..6cdacea320 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(yuzu-android SHARED native.cpp native.h native_config.cpp + native_freedreno.cpp android_settings.cpp game_metadata.cpp native_log.cpp diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index d2daef4eb2..2fd532870c 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -730,11 +730,28 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* en return false; } -void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz, +void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, + [[maybe_unused]] jclass clazz, jstring hook_lib_dir, jstring custom_driver_dir, jstring custom_driver_name, jstring file_redirect_dir) { + // Log active Freedreno environment variables + const char* tu_debug = getenv("TU_DEBUG"); + const char* fd_debug = getenv("FD_MESA_DEBUG"); + const char* ir3_debug = getenv("IR3_SHADER_DEBUG"); + const char* fd_rd_dump = getenv("FD_RD_DUMP"); + const char* tu_breadcrumbs = getenv("TU_BREADCRUMBS"); + + if (tu_debug || fd_debug || ir3_debug || fd_rd_dump || tu_breadcrumbs) { + LOG_INFO(Frontend, "[Freedreno] Initializing GPU driver with configuration:"); + if (tu_debug) LOG_INFO(Frontend, "[Freedreno] TU_DEBUG={}", tu_debug); + if (fd_debug) LOG_INFO(Frontend, "[Freedreno] FD_MESA_DEBUG={}", fd_debug); + if (ir3_debug) LOG_INFO(Frontend, "[Freedreno] IR3_SHADER_DEBUG={}", ir3_debug); + if (fd_rd_dump) LOG_INFO(Frontend, "[Freedreno] FD_RD_DUMP={}", fd_rd_dump); + if (tu_breadcrumbs) LOG_INFO(Frontend, "[Freedreno] TU_BREADCRUMBS={}", tu_breadcrumbs); + } + EmulationSession::GetInstance().InitializeGpuDriver( Common::Android::GetJString(env, hook_lib_dir), Common::Android::GetJString(env, custom_driver_dir), diff --git a/src/android/app/src/main/jni/native_freedreno.cpp b/src/android/app/src/main/jni/native_freedreno.cpp new file mode 100644 index 0000000000..0879a85ea8 --- /dev/null +++ b/src/android/app/src/main/jni/native_freedreno.cpp @@ -0,0 +1,477 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +/** + * @file native_freedreno.cpp + * @brief JNI bindings for Freedreno/Turnip GPU driver configuration. + * + * Provides runtime configuration of Mesa Freedreno environment variables + * for the Turnip Vulkan driver on Adreno GPUs. + * + * @see https://docs.mesa3d.org/drivers/freedreno.html + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "common/android/android_common.h" +#include "common/logging/log.h" +#include "native.h" + +namespace { + +struct FreedrenoConfig { + std::map env_vars; + std::string config_file_path; +}; + +std::unique_ptr 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(); + LOG_INFO(Frontend, "[Freedreno] Configuration system initialized"); + } +} + +JNIEXPORT void JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_saveFreedrenoConfig( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { + if (!g_config) { + LOG_WARNING(Frontend, "[Freedreno] Cannot save: not initialized"); + return; + } + + const std::string config_path = GetConfigPath(); + FILE* file = fopen(config_path.c_str(), "w"); + if (!file) { + LOG_ERROR(Frontend, "[Freedreno] Failed to open {} for writing", config_path); + return; + } + + fprintf(file, "# Freedreno/Turnip Configuration\n"); + fprintf(file, "# Auto-generated by Eden Emulator\n\n"); + + for (const auto& [key, value] : g_config->env_vars) { + fprintf(file, "%s=%s\n", key.c_str(), value.c_str()); + } + + fclose(file); + g_config->config_file_path = config_path; + + LOG_INFO(Frontend, "[Freedreno] Saved {} variables to {}", + g_config->env_vars.size(), config_path); +} + +JNIEXPORT void JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_reloadFreedrenoConfig( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { + if (!g_config) { + LOG_WARNING(Frontend, "[Freedreno] Cannot reload: not initialized"); + return; + } + + const std::string config_path = GetConfigPath(); + g_config->env_vars.clear(); + + FILE* file = fopen(config_path.c_str(), "r"); + if (!file) { + LOG_DEBUG(Frontend, "[Freedreno] No config file found at {}", config_path); + return; + } + + char line[512]; + while (fgets(line, sizeof(line), file)) { + // Remove trailing newline + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + len--; + } + + // Skip empty lines and comments + if (len == 0 || line[0] == '#') { + continue; + } + + // Parse key=value + const char* eq = strchr(line, '='); + if (!eq) { + continue; + } + + std::string key(line, eq - line); + std::string value(eq + 1); + + g_config->env_vars[key] = value; + ApplyEnvironmentVariable(key, value); + } + + fclose(file); + g_config->config_file_path = config_path; + + if (!g_config->env_vars.empty()) { + LOG_INFO(Frontend, "[Freedreno] Loaded {} variables:", g_config->env_vars.size()); + LogActiveVariables(); + } +} + +JNIEXPORT jboolean JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoEnv( + JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName, jstring jvalue) { + if (!g_config) { + return JNI_FALSE; + } + + auto var_name = Common::Android::GetJString(env, jvarName); + auto value = Common::Android::GetJString(env, jvalue); + + if (var_name.empty()) { + return JNI_FALSE; + } + + g_config->env_vars[var_name] = value; + + if (!ApplyEnvironmentVariable(var_name, value)) { + return JNI_FALSE; + } + + LOG_INFO(Frontend, "[Freedreno] Set {}={}", var_name, value); + return JNI_TRUE; +} + +JNIEXPORT jstring JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_getFreedrenoEnv( + JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName) { + if (!g_config) { + return env->NewStringUTF(""); + } + + auto var_name = Common::Android::GetJString(env, jvarName); + auto it = g_config->env_vars.find(var_name); + + if (it != g_config->env_vars.end()) { + return env->NewStringUTF(it->second.c_str()); + } + + return env->NewStringUTF(""); +} + +JNIEXPORT jboolean JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_isFreedrenoEnvSet( + JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName) { + if (!g_config) { + return JNI_FALSE; + } + + auto var_name = Common::Android::GetJString(env, jvarName); + auto it = g_config->env_vars.find(var_name); + + return (it != g_config->env_vars.end() && !it->second.empty()) ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT jboolean JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_clearFreedrenoEnv( + JNIEnv* env, [[maybe_unused]] jobject obj, jstring jvarName) { + if (!g_config) { + return JNI_FALSE; + } + + auto var_name = Common::Android::GetJString(env, jvarName); + auto it = g_config->env_vars.find(var_name); + + if (it != g_config->env_vars.end()) { + g_config->env_vars.erase(it); + unsetenv(var_name.c_str()); + LOG_INFO(Frontend, "[Freedreno] Cleared {}", var_name); + return JNI_TRUE; + } + + return JNI_FALSE; +} + +JNIEXPORT void JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_clearAllFreedrenoEnv( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { + if (!g_config) { + return; + } + + for (const auto& [key, value] : g_config->env_vars) { + unsetenv(key.c_str()); + } + + size_t count = g_config->env_vars.size(); + g_config->env_vars.clear(); + + if (count > 0) { + LOG_INFO(Frontend, "[Freedreno] Cleared all {} variables", count); + } +} + +JNIEXPORT jstring JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_getFreedrenoEnvSummary( + JNIEnv* env, [[maybe_unused]] jobject obj) { + if (!g_config || g_config->env_vars.empty()) { + return env->NewStringUTF(""); + } + + std::string summary; + for (const auto& [key, value] : g_config->env_vars) { + if (!summary.empty()) { + summary += ","; + } + summary += key + "=" + value; + } + + return env->NewStringUTF(summary.c_str()); +} + +JNIEXPORT void JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setCurrentProgramId( + JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) { + g_current_program_id = Common::Android::GetJString(env, jprogramId); +} + +JNIEXPORT jboolean JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_loadPerGameConfig( + JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) { + if (!g_config) { + return JNI_FALSE; + } + + auto program_id = Common::Android::GetJString(env, jprogramId); + if (program_id.empty()) { + return JNI_FALSE; + } + + // Clear current environment variables first + ClearAllEnvironmentVariables(); + g_current_program_id = program_id; + + // Try to load per-game config - do NOT fall back to global + // Per-game config should start empty if no config exists yet + std::string per_game_path = GetPerGameConfigPath(program_id); + if (LoadConfigFromFile(per_game_path)) { + LOG_INFO(Frontend, "[Freedreno] Loaded per-game config for {}", program_id); + LogActiveVariables(); + return JNI_TRUE; + } + + // No per-game config exists - start with empty config + LOG_INFO(Frontend, "[Freedreno] No per-game config for {}, starting empty", program_id); + return JNI_FALSE; +} + +JNIEXPORT jboolean JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_loadPerGameConfigWithGlobalFallback( + JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) { + if (!g_config) { + return JNI_FALSE; + } + + auto program_id = Common::Android::GetJString(env, jprogramId); + if (program_id.empty()) { + return JNI_FALSE; + } + + // Clear current environment variables first + ClearAllEnvironmentVariables(); + g_current_program_id = program_id; + + // Try to load per-game config first + std::string per_game_path = GetPerGameConfigPath(program_id); + if (LoadConfigFromFile(per_game_path)) { + LOG_INFO(Frontend, "[Freedreno] Loaded per-game config for {}", program_id); + LogActiveVariables(); + return JNI_TRUE; + } + + // Fall back to global config for emulation + std::string global_path = GetConfigPath(); + if (LoadConfigFromFile(global_path)) { + LOG_INFO(Frontend, "[Freedreno] No per-game config for {}, using global for emulation", program_id); + LogActiveVariables(); + } + + return JNI_FALSE; +} + +JNIEXPORT jboolean JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_savePerGameConfig( + JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) { + if (!g_config) { + return JNI_FALSE; + } + + auto program_id = Common::Android::GetJString(env, jprogramId); + if (program_id.empty()) { + return JNI_FALSE; + } + + EnsurePerGameConfigDir(); + std::string config_path = GetPerGameConfigPath(program_id); + + if (SaveConfigToFile(config_path)) { + LOG_INFO(Frontend, "[Freedreno] Saved per-game config for {}", program_id); + return JNI_TRUE; + } + + return JNI_FALSE; +} + +JNIEXPORT jboolean JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_hasPerGameConfig( + JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) { + auto program_id = Common::Android::GetJString(env, jprogramId); + if (program_id.empty()) { + return JNI_FALSE; + } + + std::string config_path = GetPerGameConfigPath(program_id); + FILE* file = fopen(config_path.c_str(), "r"); + if (file) { + fclose(file); + return JNI_TRUE; + } + return JNI_FALSE; +} + +JNIEXPORT jboolean JNICALL +Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_deletePerGameConfig( + JNIEnv* env, [[maybe_unused]] jobject obj, jstring jprogramId) { + auto program_id = Common::Android::GetJString(env, jprogramId); + if (program_id.empty()) { + return JNI_FALSE; + } + + std::string config_path = GetPerGameConfigPath(program_id); + if (remove(config_path.c_str()) == 0) { + LOG_INFO(Frontend, "[Freedreno] Deleted per-game config for {}", program_id); + return JNI_TRUE; + } + return JNI_FALSE; +} + +} // extern "C" diff --git a/src/android/app/src/main/res/layout/fragment_freedreno_settings.xml b/src/android/app/src/main/res/layout/fragment_freedreno_settings.xml new file mode 100644 index 0000000000..d0e10adc0d --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_freedreno_settings.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +