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