From d1df35a720e4bc73b61058c6e69f672a40d43d8d Mon Sep 17 00:00:00 2001 From: CamilleLaVey Date: Wed, 9 Sep 2026 04:00:45 -0400 Subject: [PATCH] More adjustments to UI --- .../yuzu/yuzu_emu/dialogs/QuickSettings.kt | 96 +++++--- .../settings/model/FxPresetNameSetting.kt | 36 +++ .../settings/model/FxUniformSetting.kt | 8 +- .../settings/model/view/FxPresetSetting.kt | 14 ++ .../settings/model/view/FxToolbarSetting.kt | 19 ++ .../settings/model/view/SettingsItem.kt | 2 + .../features/settings/ui/SettingsAdapter.kt | 18 +- .../settings/ui/SettingsFragmentPresenter.kt | 215 ++++++++++-------- .../ui/viewholder/FxPresetViewHolder.kt | 35 +++ .../ui/viewholder/FxToolbarViewHolder.kt | 57 +++++ .../yuzu_emu/fragments/EmulationFragment.kt | 31 +++ .../yuzu_emu/utils/NativePostProcessing.kt | 10 +- .../app/src/main/jni/native_config.cpp | 14 ++ .../src/main/jni/native_post_processing.cpp | 36 +++ .../res/layout/item_quick_settings_add.xml | 29 ++- .../layout/list_item_setting_fx_preset.xml | 65 ++++++ .../layout/list_item_setting_fx_toolbar.xml | 55 +++++ .../app/src/main/res/values/strings.xml | 10 +- src/video_core/post_processing/fx_chain.cpp | 36 ++- src/video_core/post_processing/fx_chain.h | 9 +- 20 files changed, 643 insertions(+), 152 deletions(-) create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FxPresetNameSetting.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxPresetSetting.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxToolbarSetting.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxPresetViewHolder.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxToolbarViewHolder.kt create mode 100644 src/android/app/src/main/res/layout/list_item_setting_fx_preset.xml create mode 100644 src/android/app/src/main/res/layout/list_item_setting_fx_toolbar.xml diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt index b0ca1c3bb5..071739cc55 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt @@ -298,6 +298,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) { steps: Int, selectedStep: Int, describe: (Int) -> String, + onCommitted: (Int) -> Unit, onChanged: (Int) -> Unit ) { val inflater = LayoutInflater.from(emulationFragment.requireContext()) @@ -327,14 +328,20 @@ class QuickSettings(val emulationFragment: EmulationFragment) { } } + var pressedValue = slider.value + slider.setOnTouchListener { _, event -> val drawer = emulationFragment.view?.findViewById(R.id.drawer_layout) when (event.action) { MotionEvent.ACTION_DOWN -> { drawer?.requestDisallowInterceptTouchEvent(true) + pressedValue = slider.value } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { drawer?.requestDisallowInterceptTouchEvent(false) + if (slider.value != pressedValue) { + onCommitted(slider.value.toInt()) + } } } false @@ -389,6 +396,8 @@ class QuickSettings(val emulationFragment: EmulationFragment) { fun addEffectPicker( container: ViewGroup, choices: List, + hasEffects: Boolean, + onRemoveAll: () -> Unit, onPicked: (Int) -> Unit ) { val context = emulationFragment.requireContext() @@ -398,6 +407,10 @@ class QuickSettings(val emulationFragment: EmulationFragment) { val button = itemView.findViewById( R.id.add_button ) + val removeButton = + itemView.findViewById( + R.id.remove_all_button + ) val choiceGroup = itemView.findViewById(R.id.add_choices) choices.forEachIndexed { index, name -> @@ -413,22 +426,27 @@ class QuickSettings(val emulationFragment: EmulationFragment) { choiceGroup.addView(radioButton) } - val closedBackground = MaterialColors.getColor( - button, - com.google.android.material.R.attr.colorSecondaryContainer - ) - val closedForeground = MaterialColors.getColor( - button, - com.google.android.material.R.attr.colorOnSecondaryContainer - ) - val openBackground = MaterialColors.getColor( - button, + var closedLabel = R.string.post_processing_add + if (hasEffects) { + closedLabel = R.string.post_processing_open_list + removeButton.visibility = View.VISIBLE + } + button.setText(closedLabel) + + val removeBackground = MaterialColors.getColor( + removeButton, com.google.android.material.R.attr.colorErrorContainer ) - val openForeground = MaterialColors.getColor( - button, + val removeForeground = MaterialColors.getColor( + removeButton, com.google.android.material.R.attr.colorOnErrorContainer ) + removeButton.backgroundTintList = ColorStateList.valueOf(removeBackground) + removeButton.setTextColor(removeForeground) + removeButton.iconTint = ColorStateList.valueOf(removeForeground) + removeButton.setOnClickListener { + onRemoveAll() + } val slide = context.resources.displayMetrics.density * 24.0f @@ -451,11 +469,8 @@ class QuickSettings(val emulationFragment: EmulationFragment) { } .start() - button.text = context.getString(R.string.post_processing_hide_effects) + button.setText(R.string.post_processing_close_list) button.setIconResource(R.drawable.ic_clear) - button.backgroundTintList = ColorStateList.valueOf(openBackground) - button.setTextColor(openForeground) - button.iconTint = ColorStateList.valueOf(openForeground) } else { choiceGroup.animate() .alpha(0.0f) @@ -466,11 +481,8 @@ class QuickSettings(val emulationFragment: EmulationFragment) { } .start() - button.text = context.getString(R.string.post_processing_add) + button.setText(closedLabel) button.setIconResource(R.drawable.ic_add) - button.backgroundTintList = ColorStateList.valueOf(closedBackground) - button.setTextColor(closedForeground) - button.iconTint = ColorStateList.valueOf(closedForeground) } } @@ -494,8 +506,9 @@ class QuickSettings(val emulationFragment: EmulationFragment) { switchView.isChecked = NativePostProcessing.isEnabled() switchView.setOnCheckedChangeListener { _, checked -> - NativePostProcessing.setEnabled(checked) - saveSettings() + emulationFragment.editPostProcessing { + NativePostProcessing.setEnabled(checked) + } } container.addView(itemView) @@ -510,12 +523,21 @@ class QuickSettings(val emulationFragment: EmulationFragment) { val preset = NativePostProcessing.getActivePreset() if (preset.isNotEmpty()) { addDivider(container) - var summary = - YuzuApplication.appContext.getString(R.string.post_processing_preset_locked) + + var summary = "" + val described = NativePostProcessing.presets().firstOrNull { it.name == preset } + if (described != null) { + summary = described.description + } + if (summary.isEmpty()) { + summary = + YuzuApplication.appContext.getString(R.string.post_processing_preset_locked) + } if (NativePostProcessing.isPresetModified()) { summary = YuzuApplication.appContext.getString(R.string.post_processing_preset_modified) } + addPresetBand(container, preset, summary) return } @@ -553,8 +575,9 @@ class QuickSettings(val emulationFragment: EmulationFragment) { } val body = addShaderCard(title, summary, container) { - NativePostProcessing.remove(index) - NativePostProcessing.persistFor(emulationFragment.shouldUseCustom) + emulationFragment.editPostProcessing { + NativePostProcessing.remove(index) + } onStructureChanged() } @@ -565,9 +588,20 @@ class QuickSettings(val emulationFragment: EmulationFragment) { } } - addEffectPicker(container, labels) { picked -> - NativePostProcessing.append(files[picked], techniques[picked]) - NativePostProcessing.persistFor(emulationFragment.shouldUseCustom) + addEffectPicker( + container, + labels, + chain.isNotEmpty(), + { + emulationFragment.editPostProcessing { + NativePostProcessing.clearChain() + } + onStructureChanged() + } + ) { picked -> + emulationFragment.editPostProcessing { + NativePostProcessing.append(files[picked], techniques[picked]) + } onStructureChanged() } } @@ -600,7 +634,8 @@ class QuickSettings(val emulationFragment: EmulationFragment) { container, steps, step, - { position -> describeUniform(uniform, position) } + { position -> describeUniform(uniform, position) }, + { emulationFragment.persistPostProcessing() } ) { position -> NativePostProcessing.setValue( index, @@ -608,7 +643,6 @@ class QuickSettings(val emulationFragment: EmulationFragment) { component, uniform.min + position * uniform.step ) - NativePostProcessing.persistFor(emulationFragment.shouldUseCustom) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FxPresetNameSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FxPresetNameSetting.kt new file mode 100644 index 0000000000..496cb5390a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FxPresetNameSetting.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model + +class FxPresetNameSetting(private val onNamed: (String) -> Unit) : AbstractStringSetting { + override val key: String + get() = "fx_preset_name" + + override val defaultValue: Any + get() = "" + + override val isRuntimeModifiable: Boolean + get() = true + + override val pairedSettingKey: String + get() = "" + + override val isSwitchable: Boolean + get() = false + + override val isSaveable: Boolean + get() = true + + override var global: Boolean + get() = true + set(_) {} + + override fun getString(needsGlobal: Boolean): String = "" + + override fun setString(value: String) = onNamed(value) + + override fun getValueAsString(needsGlobal: Boolean): String = "" + + override fun reset() {} +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FxUniformSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FxUniformSetting.kt index 398e54b4fe..a9a932c796 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FxUniformSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FxUniformSetting.kt @@ -39,7 +39,7 @@ abstract class FxUniformSetting( protected fun commit(value: Float) { NativePostProcessing.setValue(index, uniform.name, component, value) - NativePostProcessing.persist() + NativePostProcessing.store() } override fun reset() = commit(uniform.defaultAt(component)) @@ -92,11 +92,11 @@ class FxUniformBooleanSetting( override fun getBoolean(needsGlobal: Boolean): Boolean = currentValue() != 0f override fun setBoolean(value: Boolean) { + var scalar = 0f if (value) { - commit(1f) - return + scalar = 1f } - commit(0f) + commit(scalar) } override fun getValueAsString(needsGlobal: Boolean): String = getBoolean().toString() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxPresetSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxPresetSetting.kt new file mode 100644 index 0000000000..9873b1c396 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxPresetSetting.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +class FxPresetSetting( + titleString: String, + descriptionString: String = "", + val deletable: Boolean = false, + val onApply: () -> Unit, + val onDelete: () -> Unit = {} +) : SettingsItem(emptySetting, 0, titleString, 0, descriptionString) { + override val type = TYPE_FX_PRESET +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxToolbarSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxToolbarSetting.kt new file mode 100644 index 0000000000..e62b0b914b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxToolbarSetting.kt @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes + +class FxToolbarSetting( + @StringRes val addLabelId: Int, + val listOpen: Boolean, + val presetLabel: String, + val hasEffects: Boolean, + val createPreset: StringInputSetting, + val onAdd: () -> Unit, + val onPresets: () -> Unit, + val onRemoveAll: () -> Unit +) : SettingsItem(emptySetting, 0, "", 0, "") { + override val type = TYPE_FX_TOOLBAR +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index b2b49c34e7..d5b3ddaffb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -145,6 +145,8 @@ abstract class SettingsItem( const val TYPE_PATH = 14 const val TYPE_GPU_UNSWIZZLE = 15 const val TYPE_CARD = 16 + const val TYPE_FX_TOOLBAR = 17 + const val TYPE_FX_PRESET = 18 const val FASTMEM_COMBINED = "fastmem_combined" const val GPU_UNSWIZZLE_COMBINED = "gpu_unswizzle_combined" 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 ed4fb37569..5a33a11aea 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 @@ -24,6 +24,8 @@ import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.SettingsNavigationDirections import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingCardBinding +import org.yuzu.yuzu_emu.databinding.ListItemSettingFxPresetBinding +import org.yuzu.yuzu_emu.databinding.ListItemSettingFxToolbarBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding @@ -108,7 +110,21 @@ class SettingsAdapter( } SettingsItem.TYPE_CARD -> { - CardViewHolder(ListItemSettingCardBinding.inflate(inflater), this) + CardViewHolder(ListItemSettingCardBinding.inflate(inflater, parent, false), this) + } + + SettingsItem.TYPE_FX_TOOLBAR -> { + FxToolbarViewHolder( + ListItemSettingFxToolbarBinding.inflate(inflater, parent, false), + this + ) + } + + SettingsItem.TYPE_FX_PRESET -> { + FxPresetViewHolder( + ListItemSettingFxPresetBinding.inflate(inflater, parent, false), + this + ) } else -> { 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 01577fb30e..daf0af1456 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 @@ -18,6 +18,7 @@ import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting +import org.yuzu.yuzu_emu.features.settings.model.FxPresetNameSetting import org.yuzu.yuzu_emu.features.settings.model.FxUniformBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.FxUniformChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.FxUniformSliderSetting @@ -54,6 +55,8 @@ class SettingsFragmentPresenter( private var presetPickerOpen = false + private var postProcessingSynced = false + private val context get() = YuzuApplication.appContext // Extension for altering settings list based on each setting's properties @@ -202,6 +205,11 @@ class SettingsFragmentPresenter( } private fun addPostProcessingSettings(sl: ArrayList) { + if (!postProcessingSynced) { + postProcessingSynced = true + NativePostProcessing.reload() + } + val usable = NativePostProcessing.catalog().filter { it.valid } sl.apply { @@ -216,68 +224,6 @@ class SettingsFragmentPresenter( return@apply } - val active = NativePostProcessing.getActivePreset() - var presetSummary = context.getString(R.string.post_processing_preset_none) - if (active.isNotEmpty()) { - presetSummary = active - if (NativePostProcessing.isPresetModified()) { - presetSummary = context.getString(R.string.post_processing_preset_modified) - } - } - - add( - CardSetting( - titleId = R.string.post_processing_preset, - descriptionString = presetSummary, - expanded = presetPickerOpen - ) { - presetPickerOpen = !presetPickerOpen - settingsViewModel.setReloadListAndNotifyDataset(true) - } - ) - - if (presetPickerOpen) { - add( - RunnableSetting( - titleId = R.string.post_processing_preset_none, - isRunnable = true - ) { - NativePostProcessing.clearPreset() - presetPickerOpen = false - settingsViewModel.setReloadListAndNotifyDataset(true) - } - ) - for (preset in NativePostProcessing.presets()) { - add( - RunnableSetting( - titleString = preset.name, - descriptionString = preset.description, - isRunnable = true - ) { - NativePostProcessing.applyPreset(preset.name) - NativePostProcessing.store() - presetPickerOpen = false - expandedShaderSlots.clear() - settingsViewModel.setReloadListAndNotifyDataset(true) - } - ) - } - } - - if (active.isNotEmpty()) { - add( - RunnableSetting( - titleId = R.string.post_processing_preset_reset, - isRunnable = true - ) { - NativePostProcessing.applyPreset(active) - NativePostProcessing.store() - expandedShaderSlots.clear() - settingsViewModel.setReloadListAndNotifyDataset(true) - } - ) - } - val labels = mutableListOf() val summaries = mutableListOf() val files = mutableListOf() @@ -296,6 +242,124 @@ class SettingsFragmentPresenter( } val chain = NativePostProcessing.chain() + val active = NativePostProcessing.getActivePreset() + + var addLabel = R.string.post_processing_add + if (chain.isNotEmpty()) { + addLabel = R.string.post_processing_open_list + } + if (shaderPickerOpen) { + addLabel = R.string.post_processing_close_list + } + + var presetLabel = context.getString(R.string.post_processing_presets) + if (active.isNotEmpty()) { + presetLabel = active + } + + val createPreset = StringInputSetting( + setting = FxPresetNameSetting { name -> + NativePostProcessing.savePreset(name, "") + settingsViewModel.setReloadListAndNotifyDataset(true) + }, + titleId = R.string.post_processing_preset_new, + descriptionId = R.string.post_processing_preset_new_description, + validator = { it != null && it.isNotBlank() && !it.contains('=') }, + errorId = R.string.post_processing_preset_name_invalid + ) + + add( + FxToolbarSetting( + addLabelId = addLabel, + listOpen = shaderPickerOpen, + presetLabel = presetLabel, + hasEffects = chain.isNotEmpty(), + createPreset = createPreset, + onAdd = { + shaderPickerOpen = !shaderPickerOpen + presetPickerOpen = false + settingsViewModel.setReloadListAndNotifyDataset(true) + }, + onPresets = { + presetPickerOpen = !presetPickerOpen + shaderPickerOpen = false + settingsViewModel.setReloadListAndNotifyDataset(true) + }, + onRemoveAll = { + NativePostProcessing.clearChain() + NativePostProcessing.clearPreset() + NativePostProcessing.store() + expandedShaderSlots.clear() + shaderPickerOpen = false + settingsViewModel.setReloadListAndNotifyDataset(true) + } + ) + ) + + if (shaderPickerOpen) { + for (choice in labels.indices) { + add( + RunnableSetting( + titleString = labels[choice], + descriptionString = summaries[choice], + isRunnable = true + ) { + NativePostProcessing.append(files[choice], techniques[choice]) + NativePostProcessing.store() + shaderPickerOpen = false + settingsViewModel.setReloadListAndNotifyDataset(true) + } + ) + } + } + + if (presetPickerOpen) { + add( + FxPresetSetting( + titleString = context.getString(R.string.post_processing_preset_none), + onApply = { + NativePostProcessing.clearPreset() + presetPickerOpen = false + settingsViewModel.setReloadListAndNotifyDataset(true) + } + ) + ) + for (preset in NativePostProcessing.presets()) { + add( + FxPresetSetting( + titleString = preset.name, + descriptionString = preset.description, + deletable = !preset.bundled, + onApply = { + NativePostProcessing.applyPreset(preset.name) + NativePostProcessing.store() + presetPickerOpen = false + expandedShaderSlots.clear() + settingsViewModel.setReloadListAndNotifyDataset(true) + }, + onDelete = { + NativePostProcessing.deletePreset(preset.name) + settingsViewModel.setReloadListAndNotifyDataset(true) + } + ) + ) + } + } + + if (active.isNotEmpty()) { + add( + RunnableSetting( + titleId = R.string.post_processing_preset_reset, + isRunnable = true + ) { + NativePostProcessing.applyPreset(active) + NativePostProcessing.store() + expandedShaderSlots.clear() + settingsViewModel.setReloadListAndNotifyDataset(true) + } + ) + } + for (index in chain.indices) { val entry = chain[index] val effect = usable.firstOrNull { it.file == entry.file } @@ -385,33 +449,6 @@ class SettingsFragmentPresenter( } ) } - - add( - CardSetting( - titleId = R.string.post_processing_add, - expanded = shaderPickerOpen - ) { - shaderPickerOpen = !shaderPickerOpen - settingsViewModel.setReloadListAndNotifyDataset(true) - } - ) - - if (shaderPickerOpen) { - for (choice in labels.indices) { - add( - RunnableSetting( - titleString = labels[choice], - descriptionString = summaries[choice], - isRunnable = true - ) { - NativePostProcessing.append(files[choice], techniques[choice]) - NativePostProcessing.store() - shaderPickerOpen = false - settingsViewModel.setReloadListAndNotifyDataset(true) - } - ) - } - } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxPresetViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxPresetViewHolder.kt new file mode 100644 index 0000000000..d8634c1002 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxPresetViewHolder.kt @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui.viewholder + +import android.view.View +import org.yuzu.yuzu_emu.databinding.ListItemSettingFxPresetBinding +import org.yuzu.yuzu_emu.features.settings.model.view.FxPresetSetting +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible + +class FxPresetViewHolder( + val binding: ListItemSettingFxPresetBinding, + adapter: SettingsAdapter +) : SettingViewHolder(binding.root, adapter) { + private lateinit var setting: FxPresetSetting + + override fun bind(item: SettingsItem) { + setting = item as FxPresetSetting + + binding.presetName.text = item.title + binding.presetDescription.text = item.description + binding.presetDescription.setVisible(item.description.isNotEmpty()) + + binding.presetRow.setOnClickListener { setting.onApply.invoke() } + + binding.presetDelete.setVisible(setting.deletable) + binding.presetDelete.setOnClickListener { setting.onDelete.invoke() } + } + + override fun onClick(clicked: View) {} + + override fun onLongClick(clicked: View): Boolean = true +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxToolbarViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxToolbarViewHolder.kt new file mode 100644 index 0000000000..21bad70040 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxToolbarViewHolder.kt @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui.viewholder + +import android.content.res.ColorStateList +import android.view.View +import com.google.android.material.color.MaterialColors +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.ListItemSettingFxToolbarBinding +import org.yuzu.yuzu_emu.features.settings.model.view.FxToolbarSetting +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter + +class FxToolbarViewHolder( + val binding: ListItemSettingFxToolbarBinding, + adapter: SettingsAdapter +) : SettingViewHolder(binding.root, adapter) { + private lateinit var setting: FxToolbarSetting + + override fun bind(item: SettingsItem) { + setting = item as FxToolbarSetting + + binding.fxAdd.setText(setting.addLabelId) + var addIcon = R.drawable.ic_add + if (setting.listOpen) { + addIcon = R.drawable.ic_clear + } + binding.fxAdd.setIconResource(addIcon) + binding.fxAdd.setOnClickListener { setting.onAdd.invoke() } + + binding.fxPresets.text = setting.presetLabel + binding.fxPresets.setOnClickListener { setting.onPresets.invoke() } + + binding.fxCreatePreset.isEnabled = setting.hasEffects + binding.fxCreatePreset.setOnClickListener { + adapter.onStringInputClick(setting.createPreset, bindingAdapterPosition) + } + + val removeBackground = MaterialColors.getColor( + binding.fxRemoveAll, + com.google.android.material.R.attr.colorErrorContainer + ) + val removeForeground = MaterialColors.getColor( + binding.fxRemoveAll, + com.google.android.material.R.attr.colorOnErrorContainer + ) + binding.fxRemoveAll.backgroundTintList = ColorStateList.valueOf(removeBackground) + binding.fxRemoveAll.iconTint = ColorStateList.valueOf(removeForeground) + binding.fxRemoveAll.isEnabled = setting.hasEffects + binding.fxRemoveAll.setOnClickListener { setting.onRemoveAll.invoke() } + } + + override fun onClick(clicked: View) {} + + override fun onLongClick(clicked: View): Boolean = true +} 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 1955981761..3f1f3da2ba 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 @@ -94,6 +94,7 @@ import org.yuzu.yuzu_emu.utils.InputHandler 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.NativePostProcessing import org.yuzu.yuzu_emu.utils.ViewUtils import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.collect @@ -883,6 +884,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { if (shouldUseCustom) { SettingsFile.loadCustomConfig(game!!) } + refreshPostProcessing() + addQuickSettings() } } @@ -1087,6 +1090,34 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } + private fun withPerGameConfig(create: Boolean, action: () -> Unit) { + val target = game + var owned = false + if (target != null && !NativeConfig.isPerGameConfigLoaded()) { + if (create || SettingsFile.getCustomSettingsFile(target).exists()) { + SettingsFile.loadCustomConfig(target) + owned = true + } + } + action() + if (owned) { + NativeConfig.unloadPerGameConfig() + } + } + + fun refreshPostProcessing() = withPerGameConfig(false) { + NativePostProcessing.reload() + } + + fun persistPostProcessing() = withPerGameConfig(true) { + NativePostProcessing.persist() + } + + fun editPostProcessing(action: () -> Unit) = withPerGameConfig(true) { + action() + NativePostProcessing.persist() + } + private fun addQuickSettings() { binding.quickSettingsSheet.apply { val container = binding.quickSettingsSheet.findViewById(R.id.quick_settings_container) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt index b43fa9b4a7..6ced845a57 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt @@ -42,6 +42,10 @@ object NativePostProcessing { external fun store() + external fun reload() + + external fun clearChain() + external fun getPresetsJson(): String external fun getActivePreset(): String @@ -63,11 +67,7 @@ object NativePostProcessing { external fun getPresetDirectory(): String fun persist() { - store() - NativeConfig.saveGlobalConfig() - } - - fun persistFor(perGame: Boolean) { + val perGame = NativeConfig.isPerGameConfigLoaded() store() if (perGame) { NativeConfig.savePerGameConfig() diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 09a79ce4d7..1b8e343986 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp @@ -15,10 +15,22 @@ #include "frontend_common/config.h" #include "frontend_common/settings_generator.h" #include "native.h" +#ifdef HAS_RESHADE +#include "video_core/post_processing/fx_chain.h" +#endif std::unique_ptr global_config; std::unique_ptr per_game_config; +#ifdef HAS_RESHADE +static void ResetFxChainToGlobal() { + VideoCore::UseGlobalFxSettings(); + VideoCore::FxChain::Instance().LoadFromSettings(); +} +#else +static void ResetFxChainToGlobal() {} +#endif + template Settings::Setting* getSetting(JNIEnv* env, jstring jkey) { auto key = Common::Android::GetJString(env, jkey); @@ -39,6 +51,7 @@ extern "C" { void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) { global_config = std::make_unique(); FrontendCommon::GenerateSettings(); + ResetFxChainToGlobal(); } void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) { @@ -47,6 +60,7 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadGlobalConfig(JNIEnv* env, jobject obj) { global_config->AndroidConfig::ReloadAllValues(); + ResetFxChainToGlobal(); } void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveGlobalConfig(JNIEnv* env, jobject obj) { diff --git a/src/android/app/src/main/jni/native_post_processing.cpp b/src/android/app/src/main/jni/native_post_processing.cpp index 71d5227eb1..7fb04e7f9f 100644 --- a/src/android/app/src/main/jni/native_post_processing.cpp +++ b/src/android/app/src/main/jni/native_post_processing.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include +#include #include #include @@ -9,15 +10,29 @@ #include "common/android/android_common.h" #ifdef HAS_RESHADE +#include "android_config.h" #include "video_core/post_processing/fx_chain.h" #include "common/settings.h" #include "video_core/post_processing/fx_effect.h" #include "video_core/post_processing/fx_preset.h" + +extern std::unique_ptr per_game_config; #endif namespace { #ifdef HAS_RESHADE +bool EditingPerGame() { + return per_game_config != nullptr; +} + +void BeginFxEdit() { + if (!EditingPerGame()) { + return; + } + VideoCore::UsePerGameFxSettings(); +} + nlohmann::json SerializeUniform(const VideoCore::FxUniformDesc& uniform) { nlohmann::json out; out["name"] = uniform.name; @@ -201,10 +216,26 @@ void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_setValue(JNIEnv* env, jo void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_store(JNIEnv* env, jobject obj) { #ifdef HAS_RESHADE + BeginFxEdit(); VideoCore::FxChain::Instance().StoreToSettings(); #endif } +void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_reload(JNIEnv* env, jobject obj) { +#ifdef HAS_RESHADE + if (!EditingPerGame()) { + VideoCore::UseGlobalFxSettings(); + } + VideoCore::FxChain::Instance().LoadFromSettings(); +#endif +} + +void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_clearChain(JNIEnv* env, jobject obj) { +#ifdef HAS_RESHADE + VideoCore::FxChain::Instance().Clear(); +#endif +} + jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getShaderDirectory(JNIEnv* env, jobject obj) { #ifdef HAS_RESHADE @@ -252,6 +283,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_isPresetModified(JNI jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_applyPreset(JNIEnv* env, jobject obj, jstring jname) { #ifdef HAS_RESHADE + BeginFxEdit(); return static_cast( VideoCore::ApplyFxPreset(Common::Android::GetJString(env, jname))); #else @@ -263,6 +295,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_savePreset(JNIEnv* e jstring jname, jstring jdescription) { #ifdef HAS_RESHADE + BeginFxEdit(); return static_cast( VideoCore::SaveFxPreset(Common::Android::GetJString(env, jname), Common::Android::GetJString(env, jdescription))); @@ -274,6 +307,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_savePreset(JNIEnv* e jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_deletePreset(JNIEnv* env, jobject obj, jstring jname) { #ifdef HAS_RESHADE + BeginFxEdit(); return static_cast( VideoCore::DeleteFxPreset(Common::Android::GetJString(env, jname))); #else @@ -283,6 +317,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_deletePreset(JNIEnv* void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_clearPreset(JNIEnv* env, jobject obj) { #ifdef HAS_RESHADE + BeginFxEdit(); VideoCore::SetActiveFxPreset(std::string_view()); #endif } @@ -298,6 +333,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_isEnabled(JNIEnv* en void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_setEnabled(JNIEnv* env, jobject obj, jboolean enabled) { #ifdef HAS_RESHADE + BeginFxEdit(); Settings::values.post_shader_enabled.SetValue(enabled != JNI_FALSE); #endif } diff --git a/src/android/app/src/main/res/layout/item_quick_settings_add.xml b/src/android/app/src/main/res/layout/item_quick_settings_add.xml index 66b89cf576..dbafb03abd 100644 --- a/src/android/app/src/main/res/layout/item_quick_settings_add.xml +++ b/src/android/app/src/main/res/layout/item_quick_settings_add.xml @@ -5,16 +5,35 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout/list_item_setting_fx_toolbar.xml b/src/android/app/src/main/res/layout/list_item_setting_fx_toolbar.xml new file mode 100644 index 0000000000..a3fb09e9c0 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting_fx_toolbar.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 635e9f28d9..3ff186293b 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -303,11 +303,17 @@ Configure the effect chain for this game Add effect Remove - Remove effects + Open list + Close list + Remove effects Preset active. Edit it in Post-Processing Effects before starting a game. Preset active, with your own changes. - Preset + Presets + New preset + Saves the effects you have loaded, with their current values, as a preset you can pick later. + Give the preset a name without an equals sign. No preset + Delete preset Reset preset values Move up Move down diff --git a/src/video_core/post_processing/fx_chain.cpp b/src/video_core/post_processing/fx_chain.cpp index 88d0da7091..882fdaf726 100644 --- a/src/video_core/post_processing/fx_chain.cpp +++ b/src/video_core/post_processing/fx_chain.cpp @@ -119,6 +119,26 @@ std::string SerializeFxChain(std::span entries) { return out; } +void UseGlobalFxSettings() { + Settings::values.post_shader_chain.SetGlobal(true); + Settings::values.post_shader_preset.SetGlobal(true); + Settings::values.post_shader_enabled.SetGlobal(true); +} + +void UsePerGameFxSettings() { + const std::string chain = Settings::values.post_shader_chain.GetValue(); + const std::string preset = Settings::values.post_shader_preset.GetValue(); + const bool enabled = Settings::values.post_shader_enabled.GetValue(); + + Settings::values.post_shader_chain.SetGlobal(false); + Settings::values.post_shader_preset.SetGlobal(false); + Settings::values.post_shader_enabled.SetGlobal(false); + + Settings::values.post_shader_chain.SetValue(chain); + Settings::values.post_shader_preset.SetValue(preset); + Settings::values.post_shader_enabled.SetValue(enabled); +} + FxChain& FxChain::Instance() { static FxChain instance; return instance; @@ -260,19 +280,11 @@ void FxChain::LoadFromSettings() { auto parsed = ParseFxChain(Settings::values.post_shader_chain.GetValue()); std::scoped_lock lock{mutex}; - entries = std::move(parsed); - loaded = true; - generation.fetch_add(1, std::memory_order_relaxed); -} - -void FxChain::EnsureLoadedFromSettings() { - { - std::scoped_lock lock{mutex}; - if (loaded) { - return; - } + if (entries == parsed) { + return; } - LoadFromSettings(); + entries = std::move(parsed); + generation.fetch_add(1, std::memory_order_relaxed); } void FxChain::StoreToSettings() const { diff --git a/src/video_core/post_processing/fx_chain.h b/src/video_core/post_processing/fx_chain.h index ce12e65a79..066f7e428d 100644 --- a/src/video_core/post_processing/fx_chain.h +++ b/src/video_core/post_processing/fx_chain.h @@ -20,6 +20,8 @@ struct FxChainEntry { std::string file; std::string technique; std::map> values; + + bool operator==(const FxChainEntry&) const = default; }; struct FxChainSnapshot { @@ -31,6 +33,10 @@ std::vector ParseFxChain(std::string_view value); std::string SerializeFxChain(std::span entries); +void UseGlobalFxSettings(); + +void UsePerGameFxSettings(); + class FxChain { public: static FxChain& Instance(); @@ -65,8 +71,6 @@ public: void LoadFromSettings(); - void EnsureLoadedFromSettings(); - void StoreToSettings() const; void DropUnknownEntries(); @@ -76,7 +80,6 @@ private: mutable std::mutex mutex; std::vector entries; - bool loaded{}; std::atomic generation{1}; };