diff options
Diffstat (limited to 'src/android/app')
35 files changed, 2271 insertions, 0 deletions
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 7ba7772bd7..1136923fd0 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 @@ -3,6 +3,8 @@ package org.yuzu.yuzu_emu.dialogs +import android.content.res.ColorStateList +import android.graphics.Rect import android.view.LayoutInflater import android.view.MotionEvent import android.view.View @@ -18,11 +20,28 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.fragments.EmulationFragment import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.NativePostProcessing import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractShortSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting class QuickSettings(val emulationFragment: EmulationFragment) { + private val expandedShaders = mutableSetOf<Int>() + + private fun forgetShaderSlot(index: Int) { + val shifted = mutableSetOf<Int>() + for (slot in expandedShaders) { + if (slot < index) { + shifted.add(slot) + } + if (slot > index) { + shifted.add(slot - 1) + } + } + expandedShaders.clear() + expandedShaders.addAll(shifted) + } + private fun saveSettings() { if (emulationFragment.shouldUseCustom) { NativeConfig.savePerGameConfig() @@ -232,6 +251,428 @@ class QuickSettings(val emulationFragment: EmulationFragment) { container.addView(itemView) } + fun addChoice( + title: String, + container: ViewGroup, + choices: List<String>, + selectedIndex: Int, + onSelected: (Int) -> Unit + ) { + val inflater = LayoutInflater.from(emulationFragment.requireContext()) + val itemView = inflater.inflate(R.layout.item_quick_settings_menu, container, false) + val headerView = itemView.findViewById<ViewGroup>(R.id.setting_header) + val titleView = itemView.findViewById<TextView>(R.id.setting_title) + val valueView = itemView.findViewById<TextView>(R.id.setting_value) + val expandIcon = itemView.findViewById<android.widget.ImageView>(R.id.expand_icon) + val radioGroup = itemView.findViewById<RadioGroup>(R.id.radio_group) + + titleView.text = title + + var current = "" + if (selectedIndex in choices.indices) { + current = choices[selectedIndex] + } + valueView.text = current + headerView.visibility = View.VISIBLE + + var isExpanded = false + choices.forEachIndexed { index, name -> + val radioButton = com.google.android.material.radiobutton.MaterialRadioButton( + emulationFragment.requireContext() + ) + radioButton.text = name + radioButton.id = View.generateViewId() + radioButton.isChecked = index == selectedIndex + radioButton.setPadding(16, 8, 16, 8) + + radioButton.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + valueView.text = name + onSelected(index) + } + } + radioGroup.addView(radioButton) + } + + headerView.setOnClickListener { + isExpanded = !isExpanded + if (isExpanded) { + radioGroup.visibility = View.VISIBLE + expandIcon.animate().rotation(180f).setDuration(200).start() + } else { + radioGroup.visibility = View.GONE + expandIcon.animate().rotation(0f).setDuration(200).start() + } + } + + container.addView(itemView) + } + + fun addStepSlider( + title: String, + container: ViewGroup, + steps: Int, + selectedStep: Int, + describe: (Int) -> String, + onCommitted: (Int) -> Unit, + onChanged: (Int) -> Unit + ) { + val inflater = LayoutInflater.from(emulationFragment.requireContext()) + val itemView = inflater.inflate(R.layout.item_quick_settings_menu, container, false) + + val sliderContainer = itemView.findViewById<ViewGroup>(R.id.slider_container) + val titleView = itemView.findViewById<TextView>(R.id.slider_title) + val valueDisplay = itemView.findViewById<TextView>(R.id.slider_value_display) + val slider = itemView.findViewById<com.google.android.material.slider.Slider>( + R.id.setting_slider + ) + + titleView.text = title + sliderContainer.visibility = View.VISIBLE + + slider.valueFrom = 0f + slider.valueTo = steps.toFloat() + slider.stepSize = 1f + slider.value = selectedStep.toFloat().coerceIn(0f, steps.toFloat()) + valueDisplay.text = describe(slider.value.toInt()) + + slider.addOnChangeListener { _, value, fromUser -> + if (fromUser) { + val step = value.toInt() + onChanged(step) + valueDisplay.text = describe(step) + } + } + + var pressedValue = slider.value + + slider.setOnTouchListener { _, event -> + val drawer = emulationFragment.view?.findViewById<DrawerLayout>(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 + } + + container.addView(itemView) + } + + fun addShaderCard( + index: Int, + title: String, + summary: String, + container: ViewGroup, + onRemove: () -> Unit + ): ViewGroup { + val inflater = LayoutInflater.from(emulationFragment.requireContext()) + val itemView = inflater.inflate(R.layout.item_quick_settings_shader, container, false) + + val headerView = itemView.findViewById<ViewGroup>(R.id.shader_header) + val titleView = itemView.findViewById<TextView>(R.id.shader_title) + val summaryView = itemView.findViewById<TextView>(R.id.shader_summary) + val removeView = itemView.findViewById<android.widget.ImageView>(R.id.shader_remove) + val expandIcon = itemView.findViewById<android.widget.ImageView>(R.id.shader_expand) + val bodyView = itemView.findViewById<ViewGroup>(R.id.shader_body) + + titleView.text = title + if (summary.isEmpty()) { + summaryView.visibility = View.GONE + } else { + summaryView.text = summary + } + + var isExpanded = expandedShaders.contains(index) + if (isExpanded) { + bodyView.visibility = View.VISIBLE + expandIcon.rotation = 180f + } + + headerView.setOnClickListener { + isExpanded = !isExpanded + if (isExpanded) { + expandedShaders.add(index) + bodyView.visibility = View.VISIBLE + expandIcon.animate().rotation(180f).setDuration(200).start() + } else { + expandedShaders.remove(index) + bodyView.visibility = View.GONE + expandIcon.animate().rotation(0f).setDuration(200).start() + } + } + + removeView.setOnClickListener { + onRemove() + } + + container.addView(itemView) + return bodyView + } + + fun addEffectPicker( + container: ViewGroup, + choices: List<String>, + hasEffects: Boolean, + onRemoveAll: () -> Unit, + onPicked: (Int) -> Unit + ) { + val context = emulationFragment.requireContext() + val inflater = LayoutInflater.from(context) + val itemView = inflater.inflate(R.layout.item_quick_settings_add, container, false) + + val button = itemView.findViewById<com.google.android.material.button.MaterialButton>( + R.id.add_button + ) + val removeButton = + itemView.findViewById<com.google.android.material.button.MaterialButton>( + R.id.remove_all_button + ) + val choiceGroup = itemView.findViewById<RadioGroup>(R.id.add_choices) + + choices.forEachIndexed { index, name -> + val radioButton = com.google.android.material.radiobutton.MaterialRadioButton(context) + radioButton.text = name + radioButton.id = View.generateViewId() + radioButton.setPadding(16, 8, 16, 8) + radioButton.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + onPicked(index) + } + } + choiceGroup.addView(radioButton) + } + + 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 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 + + var isOpen = false + button.setOnClickListener { + isOpen = !isOpen + if (isOpen) { + choiceGroup.alpha = 0.0f + choiceGroup.translationY = slide + choiceGroup.visibility = View.VISIBLE + choiceGroup.animate() + .alpha(1.0f) + .translationY(0.0f) + .setDuration(220) + .withEndAction { + choiceGroup.requestRectangleOnScreen( + Rect(0, 0, choiceGroup.width, choiceGroup.height), + false + ) + } + .start() + + button.setText(R.string.post_processing_close_list) + button.setIconResource(R.drawable.ic_clear) + } else { + choiceGroup.animate() + .alpha(0.0f) + .translationY(slide) + .setDuration(160) + .withEndAction { + choiceGroup.visibility = View.GONE + } + .start() + + button.setText(closedLabel) + button.setIconResource(R.drawable.ic_add) + } + } + + container.addView(itemView) + } + + fun addPresetBand(container: ViewGroup, name: String, summary: String) { + val inflater = LayoutInflater.from(emulationFragment.requireContext()) + val itemView = inflater.inflate(R.layout.item_quick_settings_preset, container, false) + + val titleView = itemView.findViewById<TextView>(R.id.preset_title) + val summaryView = itemView.findViewById<TextView>(R.id.preset_summary) + val switchView = itemView.findViewById<MaterialSwitch>(R.id.preset_switch) + + titleView.text = name + if (summary.isEmpty()) { + summaryView.visibility = View.GONE + } else { + summaryView.text = summary + } + + switchView.isChecked = NativePostProcessing.isEnabled() + switchView.setOnCheckedChangeListener { _, checked -> + emulationFragment.editPostProcessing { + NativePostProcessing.setEnabled(checked) + } + } + + container.addView(itemView) + } + + fun addPostProcessing(container: ViewGroup, onStructureChanged: () -> Unit) { + val usable = NativePostProcessing.catalog().filter { it.valid } + if (usable.isEmpty()) { + return + } + + val preset = NativePostProcessing.getActivePreset() + if (preset.isNotEmpty()) { + addDivider(container) + + 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 = summary + "\n" + + YuzuApplication.appContext.getString(R.string.post_processing_preset_modified) + } + + addPresetBand(container, preset, summary) + return + } + + val labels = mutableListOf<String>() + val files = mutableListOf<String>() + val techniques = mutableListOf<String>() + + for (effect in usable) { + for (technique in effect.techniques) { + var label = effect.label + if (effect.techniques.size > 1) { + label = effect.label + " \u00b7 " + technique + } + labels.add(label) + files.add(effect.file) + techniques.add(technique) + } + } + + addDivider(container) + + val chain = NativePostProcessing.chain() + chain.forEachIndexed { index, entry -> + val effect = usable.firstOrNull { it.file == entry.file } + + var title = entry.file + var summary = "" + if (effect != null) { + title = effect.label + if (effect.techniques.size > 1) { + title = effect.label + " \u00b7 " + entry.technique + } + summary = effect.description + } + + val body = addShaderCard(index, title, summary, container) { + emulationFragment.editPostProcessing { + NativePostProcessing.remove(index) + } + forgetShaderSlot(index) + onStructureChanged() + } + + if (effect != null) { + for (uniform in effect.uniforms) { + addUniformSliders(body, index, uniform) + } + } + } + + addEffectPicker( + container, + labels, + chain.isNotEmpty(), + { + emulationFragment.editPostProcessing { + NativePostProcessing.clearChain() + } + expandedShaders.clear() + onStructureChanged() + } + ) { picked -> + emulationFragment.editPostProcessing { + NativePostProcessing.append(files[picked], techniques[picked]) + } + onStructureChanged() + } + } + + private fun addUniformSliders( + container: ViewGroup, + index: Int, + uniform: NativePostProcessing.Uniform + ) { + if (uniform.uiType == NativePostProcessing.UI_HIDDEN) { + return + } + + for (component in 0 until uniform.components) { + var title = uniform.label + if (uniform.components > 1) { + title = uniform.label + " [" + component + "]" + } + + var value = uniform.defaultAt(component) + if (NativePostProcessing.hasValue(index, uniform.name)) { + value = NativePostProcessing.getValue(index, uniform.name, component) + } + + val steps = uniform.steps + val step = Math.round((value - uniform.min) / uniform.step) + + addStepSlider( + title, + container, + steps, + step, + { position -> uniform.describe(position) }, + { emulationFragment.persistPostProcessing() } + ) { position -> + NativePostProcessing.setValue( + index, + uniform.name, + component, + uniform.min + position * uniform.step + ) + } + } + } + fun addDivider(container: ViewGroup) { val inflater = LayoutInflater.from(emulationFragment.requireContext()) val dividerView = inflater.inflate(R.layout.item_quick_settings_divider, container, false) 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 new file mode 100644 index 0000000000..23c7837e12 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FxUniformSetting.kt @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model + +import org.yuzu.yuzu_emu.utils.NativePostProcessing +import kotlin.math.roundToInt + +abstract class FxUniformSetting( + protected val index: Int, + protected val uniform: NativePostProcessing.Uniform, + protected val component: Int +) : AbstractSetting { + override val key: String + get() = "fx_${index}_${uniform.name}_$component" + + 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(_) {} + + protected fun currentValue(): Float { + if (NativePostProcessing.hasValue(index, uniform.name)) { + return NativePostProcessing.getValue(index, uniform.name, component) + } + return uniform.defaultAt(component) + } + + protected fun commit(value: Float) { + NativePostProcessing.setValue(index, uniform.name, component, value) + NativePostProcessing.store() + } + + override fun reset() = commit(uniform.defaultAt(component)) +} + +class FxUniformSliderSetting( + index: Int, + uniform: NativePostProcessing.Uniform, + component: Int +) : FxUniformSetting(index, uniform, component), AbstractIntSetting { + override val defaultValue: Any + get() = ((uniform.defaultAt(component) - uniform.min) / uniform.step).roundToInt() + + override fun getInt(needsGlobal: Boolean): Int = + ((currentValue() - uniform.min) / uniform.step).roundToInt() + + override fun setInt(value: Int) = commit(uniform.min + value * uniform.step) + + override fun getValueAsString(needsGlobal: Boolean): String { + if (uniform.kind == NativePostProcessing.KIND_FLOAT) { + return String.format("%.3f", currentValue()) + } + return currentValue().roundToInt().toString() + } +} 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 fc4df4952d..23c5ceb88b 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 @@ -12,6 +12,7 @@ object Settings { SECTION_SYSTEM(R.string.preferences_system), SECTION_RENDERER(R.string.preferences_graphics), SECTION_FRAME_GEN(R.string.frame_gen), + SECTION_POST_PROCESSING(R.string.post_processing), SECTION_PERFORMANCE_STATS(R.string.stats_overlay_options), SECTION_INPUT_OVERLAY(R.string.input_overlay_options), SECTION_SOC_OVERLAY(R.string.soc_overlay_options), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt index 43fb1fe0fe..e3ccf9e998 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt @@ -13,6 +13,7 @@ enum class StringSetting(override val key: String) : AbstractStringSetting { DEVICE_NAME("device_name"), LOG_FILTER("log_filter"), PROGRAM_ARGS("program_args"), + POST_SHADER_CHAIN("post_shader_chain"), WEB_TOKEN("eden_token"), WEB_USERNAME("eden_username") diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxButtonSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxButtonSetting.kt new file mode 100644 index 0000000000..3fc999e267 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxButtonSetting.kt @@ -0,0 +1,13 @@ +// 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 FxButtonSetting( + @StringRes titleId: Int, + val onClick: () -> Unit +) : SettingsItem(emptySetting, titleId, "", 0, "") { + override val type = TYPE_FX_BUTTON +} 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/FxShaderCardSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxShaderCardSetting.kt new file mode 100644 index 0000000000..e54ac3b5a0 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/FxShaderCardSetting.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 org.yuzu.yuzu_emu.utils.NativePostProcessing + +class FxShaderCardSetting( + titleString: String, + descriptionString: String, + val index: Int, + val expanded: Boolean, + val uniforms: List<NativePostProcessing.Uniform>, + val onToggle: () -> Unit, + val onRemove: () -> Unit, + val onReset: () -> Unit +) : SettingsItem(emptySetting, 0, titleString, 0, descriptionString) { + override val type = TYPE_FX_SHADER +} 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 30d784267a..ab2ac7eb42 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 @@ -144,6 +144,10 @@ abstract class SettingsItem( const val TYPE_LAUNCHABLE = 13 const val TYPE_PATH = 14 const val TYPE_GPU_UNSWIZZLE = 15 + const val TYPE_FX_TOOLBAR = 16 + const val TYPE_FX_PRESET = 17 + const val TYPE_FX_SHADER = 18 + const val TYPE_FX_BUTTON = 19 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 d7311eded5..8d47db94e0 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 @@ -23,6 +23,10 @@ import com.google.android.material.timepicker.TimeFormat 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.ListItemSettingFxButtonBinding +import org.yuzu.yuzu_emu.databinding.ListItemSettingFxPresetBinding +import org.yuzu.yuzu_emu.databinding.ListItemSettingFxShaderBinding +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 @@ -106,6 +110,34 @@ class SettingsAdapter( GpuUnswizzleViewHolder(ListItemSettingBinding.inflate(inflater), this) } + SettingsItem.TYPE_FX_TOOLBAR -> { + FxToolbarViewHolder( + ListItemSettingFxToolbarBinding.inflate(inflater, parent, false), + this + ) + } + + SettingsItem.TYPE_FX_PRESET -> { + FxPresetViewHolder( + ListItemSettingFxPresetBinding.inflate(inflater, parent, false), + this + ) + } + + SettingsItem.TYPE_FX_SHADER -> { + FxShaderCardViewHolder( + ListItemSettingFxShaderBinding.inflate(inflater, parent, false), + this + ) + } + + SettingsItem.TYPE_FX_BUTTON -> { + FxButtonViewHolder( + ListItemSettingFxButtonBinding.inflate(inflater, parent, false), + this + ) + } + else -> { HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this) } 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 bdb10c3146..89e0084df0 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.ByteSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.LongSetting @@ -30,6 +31,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.utils.InputHandler import org.yuzu.yuzu_emu.utils.LosslessScalingHelper import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.NativePostProcessing import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.FullscreenHelper import androidx.core.content.edit @@ -44,6 +46,14 @@ class SettingsFragmentPresenter( ) { private var settingsList = ArrayList<SettingsItem>() + private val expandedShaderSlots = mutableSetOf<Int>() + + private var shaderPickerOpen = false + + 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 @@ -163,6 +173,7 @@ class SettingsFragmentPresenter( MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) MenuTag.SECTION_FRAME_GEN -> addFrameGenSettings(sl) + MenuTag.SECTION_POST_PROCESSING -> addPostProcessingSettings(sl) MenuTag.SECTION_PERFORMANCE_STATS -> addPerformanceOverlaySettings(sl) MenuTag.SECTION_SOC_OVERLAY -> addSocOverlaySettings(sl) MenuTag.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl) @@ -190,6 +201,209 @@ class SettingsFragmentPresenter( } } + private fun addPostProcessingSettings(sl: ArrayList<SettingsItem>) { + if (!postProcessingSynced) { + postProcessingSynced = true + NativePostProcessing.reload() + } + + val usable = NativePostProcessing.catalog().filter { it.valid } + + sl.apply { + if (usable.isEmpty()) { + add( + RunnableSetting( + titleId = R.string.post_processing_empty, + descriptionString = NativePostProcessing.getShaderDirectory(), + isRunnable = false + ) {} + ) + return@apply + } + + val labels = mutableListOf<String>() + val summaries = mutableListOf<String>() + val files = mutableListOf<String>() + val techniques = mutableListOf<String>() + for (effect in usable) { + for (technique in effect.techniques) { + if (effect.techniques.size == 1) { + labels.add(effect.label) + } else { + labels.add(effect.label + " \u00b7 " + technique) + } + summaries.add(effect.description) + files.add(effect.file) + techniques.add(technique) + } + } + + 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( + FxButtonSetting(titleId = R.string.post_processing_preset_reset) { + 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 } + + var header = entry.file + var summary = "" + var uniforms = emptyList<NativePostProcessing.Uniform>() + if (effect != null) { + header = effect.label + if (effect.techniques.size > 1) { + header = effect.label + " \u00b7 " + entry.technique + } + summary = effect.description + uniforms = effect.uniforms + } + + val isOpen = expandedShaderSlots.contains(index) + + add( + FxShaderCardSetting( + titleString = header, + descriptionString = summary, + index = index, + expanded = isOpen, + uniforms = uniforms, + onToggle = { + if (isOpen) { + expandedShaderSlots.remove(index) + } else { + expandedShaderSlots.add(index) + } + settingsViewModel.setReloadListAndNotifyDataset(true) + }, + onRemove = { + NativePostProcessing.remove(index) + NativePostProcessing.store() + expandedShaderSlots.clear() + settingsViewModel.setReloadListAndNotifyDataset(true) + }, + onReset = { + NativePostProcessing.resetValues(index) + NativePostProcessing.store() + settingsViewModel.setReloadListAndNotifyDataset(true) + } + ) + ) + } + } + } + private fun addConfigSettings(sl: ArrayList<SettingsItem>) { sl.apply { add( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxButtonViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxButtonViewHolder.kt new file mode 100644 index 0000000000..914f96085a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxButtonViewHolder.kt @@ -0,0 +1,28 @@ +// 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.ListItemSettingFxButtonBinding +import org.yuzu.yuzu_emu.features.settings.model.view.FxButtonSetting +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter + +class FxButtonViewHolder( + val binding: ListItemSettingFxButtonBinding, + adapter: SettingsAdapter +) : SettingViewHolder(binding.root, adapter) { + private lateinit var setting: FxButtonSetting + + override fun bind(item: SettingsItem) { + setting = item as FxButtonSetting + + binding.fxButton.text = item.title + binding.fxButton.setOnClickListener { setting.onClick.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/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/FxShaderCardViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxShaderCardViewHolder.kt new file mode 100644 index 0000000000..fc72f04225 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/FxShaderCardViewHolder.kt @@ -0,0 +1,100 @@ +// 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.LayoutInflater +import android.view.View +import org.yuzu.yuzu_emu.databinding.ItemSettingFxActionsBinding +import org.yuzu.yuzu_emu.databinding.ItemSettingFxSliderBinding +import org.yuzu.yuzu_emu.databinding.ListItemSettingFxShaderBinding +import org.yuzu.yuzu_emu.features.settings.model.FxUniformSliderSetting +import org.yuzu.yuzu_emu.features.settings.model.view.FxShaderCardSetting +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.NativePostProcessing +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible + +class FxShaderCardViewHolder( + val binding: ListItemSettingFxShaderBinding, + adapter: SettingsAdapter +) : SettingViewHolder(binding.root, adapter) { + private lateinit var setting: FxShaderCardSetting + + override fun bind(item: SettingsItem) { + setting = item as FxShaderCardSetting + + binding.shaderTitle.text = item.title + binding.shaderSummary.text = item.description + binding.shaderSummary.setVisible(item.description.isNotEmpty()) + + var rotation = 0f + if (setting.expanded) { + rotation = 180f + } + binding.shaderExpand.rotation = rotation + + binding.shaderHeader.setOnClickListener { setting.onToggle.invoke() } + binding.shaderRemove.setOnClickListener { setting.onRemove.invoke() } + + binding.shaderBody.removeAllViews() + binding.shaderBody.setVisible(setting.expanded) + if (!setting.expanded) { + return + } + + val inflater = LayoutInflater.from(binding.root.context) + for (uniform in setting.uniforms) { + if (uniform.uiType == NativePostProcessing.UI_HIDDEN) { + continue + } + for (component in 0 until uniform.components) { + addSlider(inflater, uniform, component) + } + } + addActions(inflater) + } + + private fun addSlider( + inflater: LayoutInflater, + uniform: NativePostProcessing.Uniform, + component: Int + ) { + val row = ItemSettingFxSliderBinding.inflate(inflater, binding.shaderBody, false) + val value = FxUniformSliderSetting(setting.index, uniform, component) + + var title = uniform.label + if (uniform.components > 1) { + title = uniform.label + " [" + component + "]" + } + row.fxSliderTitle.text = title + + val steps = uniform.steps.toFloat() + row.fxSlider.valueFrom = 0f + row.fxSlider.valueTo = steps + row.fxSlider.stepSize = 1f + row.fxSlider.value = value.getInt(false).toFloat().coerceIn(0f, steps) + row.fxSliderValue.text = uniform.describe(row.fxSlider.value.toInt()) + + row.fxSlider.addOnChangeListener { _, position, fromUser -> + if (fromUser) { + value.setInt(position.toInt()) + row.fxSliderValue.text = uniform.describe(position.toInt()) + } + } + + binding.shaderBody.addView(row.root) + } + + private fun addActions(inflater: LayoutInflater) { + val row = ItemSettingFxActionsBinding.inflate(inflater, binding.shaderBody, false) + + row.fxReset.setOnClickListener { setting.onReset.invoke() } + + binding.shaderBody.addView(row.root) + } + + 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 016ac57356..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<ViewGroup>(R.id.quick_settings_container) @@ -1194,6 +1225,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { R.array.rendererAntiAliasingNames, R.array.rendererAntiAliasingValues ) + + quickSettings.addPostProcessing(container) { + addQuickSettings() + } } } 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 a2d6563e50..d2505c5f61 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 @@ -383,6 +383,20 @@ class GamePropertiesFragment : Fragment() { } ) ) + add( + SubmenuProperty( + R.string.post_processing, + R.string.post_processing_per_game_description, + R.drawable.ic_post_processing, + action = { + val action = HomeNavigationDirections.actionGlobalSettingsActivity( + args.game, + Settings.MenuTag.SECTION_POST_PROCESSING + ) + binding.root.findNavController().navigate(action) + } + ) + ) if (GpuDriverHelper.isAdrenoGpu()) { add( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index bd4d8465f3..2b4cfc5ca8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -173,6 +173,20 @@ class HomeSettingsFragment : Fragment() { } add( HomeSetting( + R.string.post_processing, + R.string.post_processing_description, + R.drawable.ic_post_processing, + { + val action = HomeNavigationDirections.actionGlobalSettingsActivity( + null, + Settings.MenuTag.SECTION_POST_PROCESSING + ) + binding.root.findNavController().navigate(action) + } + ) + ) + add( + HomeSetting( R.string.lossless_scaling, R.string.lossless_scaling_description, R.drawable.ic_duck, 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 new file mode 100644 index 0000000000..2c04900064 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.utils + +import org.json.JSONArray +import org.json.JSONObject + +object NativePostProcessing { + const val KIND_BOOL = 0 + const val KIND_INT = 1 + const val KIND_FLOAT = 2 + + const val UI_HIDDEN = 0 + const val UI_SLIDER = 1 + const val UI_DRAG = 2 + const val UI_COMBO = 3 + const val UI_RADIO = 4 + const val UI_CHECKBOX = 5 + const val UI_COLOR = 6 + const val UI_INPUT_BOX = 7 + + external fun getCatalogJson(): String + + external fun getChainJson(): String + + external fun append(file: String, technique: String) + + external fun replace(index: Int, file: String, technique: String) + + external fun remove(index: Int) + + external fun move(index: Int, delta: Int) + + external fun resetValues(index: Int) + + external fun getValue(index: Int, uniform: String, component: Int): Float + + external fun hasValue(index: Int, uniform: String): Boolean + + external fun setValue(index: Int, uniform: String, component: Int, value: Float) + + external fun store() + + external fun reload() + + external fun clearChain() + + external fun getPresetsJson(): String + + external fun getActivePreset(): String + + external fun isPresetModified(): Boolean + + external fun applyPreset(name: String): Boolean + + external fun savePreset(name: String, description: String): Boolean + + external fun deletePreset(name: String): Boolean + + external fun clearPreset() + + external fun isEnabled(): Boolean + + external fun setEnabled(enabled: Boolean) + + external fun getPresetDirectory(): String + + fun persist() { + val perGame = NativeConfig.isPerGameConfigLoaded() + store() + if (perGame) { + NativeConfig.savePerGameConfig() + } else { + NativeConfig.saveGlobalConfig() + } + } + + + external fun getShaderDirectory(): String + + data class Uniform( + val name: String, + val label: String, + val tooltip: String, + val category: String, + val kind: Int, + val uiType: Int, + val components: Int, + val min: Float, + val max: Float, + val step: Float, + val items: List<String>, + val defaults: List<Float> + ) { + val steps: Int + get() { + val span = max - min + if (step <= 0f) { + return 1 + } + val count = Math.round(span / step) + if (count < 1) { + return 1 + } + return count + } + + fun describe(position: Int): String { + val value = min + position * step + if (kind == NativePostProcessing.KIND_FLOAT) { + return String.format("%.3f", value) + } + return Math.round(value).toString() + } + + fun defaultAt(component: Int): Float { + if (component < defaults.size) { + return defaults[component] + } + return 0f + } + } + + data class Effect( + val file: String, + val name: String, + val label: String, + val description: String, + val error: String, + val techniques: List<String>, + val uniforms: List<Uniform> + ) { + val valid: Boolean + get() = error.isEmpty() && techniques.isNotEmpty() + } + + data class ChainEntry(val file: String, val technique: String) + + data class Preset( + val name: String, + val description: String, + val bundled: Boolean + ) + + fun presets(): List<Preset> { + val out = mutableListOf<Preset>() + val array = JSONArray(getPresetsJson()) + for (i in 0 until array.length()) { + val obj = array.getJSONObject(i) + out.add( + Preset( + name = obj.optString("name"), + description = obj.optString("description"), + bundled = obj.optBoolean("bundled") + ) + ) + } + return out + } + + fun catalog(): List<Effect> { + val out = mutableListOf<Effect>() + val array = JSONArray(getCatalogJson()) + for (i in 0 until array.length()) { + val obj = array.getJSONObject(i) + out.add( + Effect( + file = obj.optString("file"), + name = obj.optString("name"), + label = obj.optString("label"), + description = obj.optString("description"), + error = obj.optString("error"), + techniques = obj.optJSONArray("techniques").toStringList(), + uniforms = obj.optJSONArray("uniforms").toUniformList() + ) + ) + } + return out + } + + fun chain(): List<ChainEntry> { + val out = mutableListOf<ChainEntry>() + val array = JSONArray(getChainJson()) + for (i in 0 until array.length()) { + val obj = array.getJSONObject(i) + out.add(ChainEntry(obj.optString("file"), obj.optString("technique"))) + } + return out + } + + fun findEffect(file: String): Effect? = catalog().firstOrNull { it.file == file } + + private fun JSONArray?.toStringList(): List<String> { + if (this == null) { + return emptyList() + } + val out = mutableListOf<String>() + for (i in 0 until length()) { + out.add(optString(i)) + } + return out + } + + private fun JSONArray?.toFloatList(): List<Float> { + if (this == null) { + return emptyList() + } + val out = mutableListOf<Float>() + for (i in 0 until length()) { + out.add(optDouble(i, 0.0).toFloat()) + } + return out + } + + private fun JSONArray?.toUniformList(): List<Uniform> { + if (this == null) { + return emptyList() + } + val out = mutableListOf<Uniform>() + for (i in 0 until length()) { + val obj: JSONObject = optJSONObject(i) ?: continue + out.add( + Uniform( + name = obj.optString("name"), + label = obj.optString("label"), + tooltip = obj.optString("tooltip"), + category = obj.optString("category"), + kind = obj.optInt("kind", KIND_FLOAT), + uiType = obj.optInt("uiType", UI_HIDDEN), + components = obj.optInt("components", 1), + min = obj.optDouble("min", 0.0).toFloat(), + max = obj.optDouble("max", 1.0).toFloat(), + step = obj.optDouble("step", 0.01).toFloat(), + items = obj.optJSONArray("items").toStringList(), + defaults = obj.optJSONArray("defaults").toFloatList() + ) + ) + } + return out + } +} diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index 86c05d7c0d..337d9fea1b 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -17,6 +17,7 @@ add_library(yuzu-android SHARED android_config.cpp android_config.h native_input.cpp + native_post_processing.cpp ) set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) 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<AndroidConfig> global_config; std::unique_ptr<AndroidConfig> per_game_config; +#ifdef HAS_RESHADE +static void ResetFxChainToGlobal() { + VideoCore::UseGlobalFxSettings(); + VideoCore::FxChain::Instance().LoadFromSettings(); +} +#else +static void ResetFxChainToGlobal() {} +#endif + template <typename T> Settings::Setting<T>* 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<AndroidConfig>(); 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 new file mode 100644 index 0000000000..7fb04e7f9f --- /dev/null +++ b/src/android/app/src/main/jni/native_post_processing.cpp @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <array> +#include <memory> +#include <string> + +#include <jni.h> +#include <nlohmann/json.hpp> + +#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<AndroidConfig> 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; + out["label"] = uniform.label; + out["tooltip"] = uniform.tooltip; + out["category"] = uniform.category; + out["kind"] = static_cast<int>(uniform.kind); + out["uiType"] = static_cast<int>(uniform.ui_type); + out["components"] = uniform.components; + out["min"] = uniform.ui_min; + out["max"] = uniform.ui_max; + out["step"] = uniform.ui_step; + out["items"] = uniform.items; + + nlohmann::json defaults = nlohmann::json::array(); + for (u32 i = 0; i < uniform.components; ++i) { + defaults.push_back(uniform.default_value[i]); + } + out["defaults"] = defaults; + + return out; +} + +std::array<f32, 4> DefaultValueOf(size_t index, const std::string& uniform) { + const auto entries = VideoCore::FxChain::Instance().Entries(); + if (index >= entries.size()) { + return {}; + } + const VideoCore::FxEffectDesc* effect = VideoCore::FindFxEffect(entries[index].file); + if (effect == nullptr) { + return {}; + } + const VideoCore::FxUniformDesc* desc = VideoCore::FindFxUniform(*effect, uniform); + if (desc == nullptr) { + return {}; + } + return desc->default_value; +} +#endif + +} // Anonymous namespace + +extern "C" { + +jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getCatalogJson(JNIEnv* env, + jobject obj) { + nlohmann::json out = nlohmann::json::array(); + +#ifdef HAS_RESHADE + VideoCore::FxChain::Instance().DropUnknownEntries(); + + for (const auto& effect : VideoCore::GetFxCatalog()) { + nlohmann::json entry; + entry["file"] = effect.file; + entry["name"] = effect.name; + entry["label"] = effect.label; + entry["description"] = effect.description; + entry["error"] = effect.error; + entry["techniques"] = effect.techniques; + + nlohmann::json uniforms = nlohmann::json::array(); + for (const auto& uniform : effect.uniforms) { + uniforms.push_back(SerializeUniform(uniform)); + } + entry["uniforms"] = uniforms; + + out.push_back(entry); + } +#endif + + return Common::Android::ToJString(env, out.dump()); +} + +jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getChainJson(JNIEnv* env, jobject obj) { + nlohmann::json out = nlohmann::json::array(); + +#ifdef HAS_RESHADE + for (const auto& entry : VideoCore::FxChain::Instance().Entries()) { + nlohmann::json item; + item["file"] = entry.file; + item["technique"] = entry.technique; + + nlohmann::json values = nlohmann::json::object(); + for (const auto& [name, value] : entry.values) { + values[name] = {value[0], value[1], value[2], value[3]}; + } + item["values"] = values; + + out.push_back(item); + } +#endif + + return Common::Android::ToJString(env, out.dump()); +} + +void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_append(JNIEnv* env, jobject obj, + jstring jfile, jstring jtechnique) { +#ifdef HAS_RESHADE + VideoCore::FxChain::Instance().Append(Common::Android::GetJString(env, jfile), + Common::Android::GetJString(env, jtechnique)); +#endif +} + +void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_replace(JNIEnv* env, jobject obj, + jint index, jstring jfile, + jstring jtechnique) { +#ifdef HAS_RESHADE + VideoCore::FxChain::Instance().Replace(static_cast<size_t>(index), + Common::Android::GetJString(env, jfile), + Common::Android::GetJString(env, jtechnique)); +#endif +} + +void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_remove(JNIEnv* env, jobject obj, + jint index) { +#ifdef HAS_RESHADE + VideoCore::FxChain::Instance().Remove(static_cast<size_t>(index)); +#endif +} + +void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_move(JNIEnv* env, jobject obj, jint index, + jint delta) { +#ifdef HAS_RESHADE + VideoCore::FxChain::Instance().Move(static_cast<size_t>(index), delta); +#endif +} + +void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_resetValues(JNIEnv* env, jobject obj, + jint index) { +#ifdef HAS_RESHADE + VideoCore::FxChain::Instance().ResetValues(static_cast<size_t>(index)); +#endif +} + +jfloat Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getValue(JNIEnv* env, jobject obj, + jint index, jstring juniform, + jint component) { +#ifdef HAS_RESHADE + if (component < 0 || component >= 4) { + return 0.0f; + } + const auto value = VideoCore::FxChain::Instance().GetValue( + static_cast<size_t>(index), Common::Android::GetJString(env, juniform)); + return value[static_cast<size_t>(component)]; +#else + return 0.0f; +#endif +} + +jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_hasValue(JNIEnv* env, jobject obj, + jint index, + jstring juniform) { +#ifdef HAS_RESHADE + return static_cast<jboolean>(VideoCore::FxChain::Instance().HasValue( + static_cast<size_t>(index), Common::Android::GetJString(env, juniform))); +#else + return static_cast<jboolean>(false); +#endif +} + +void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_setValue(JNIEnv* env, jobject obj, + jint index, jstring juniform, + jint component, jfloat value) { +#ifdef HAS_RESHADE + if (component < 0 || component >= 4) { + return; + } + const std::string uniform = Common::Android::GetJString(env, juniform); + auto& chain = VideoCore::FxChain::Instance(); + const auto slot = static_cast<size_t>(index); + + auto current = chain.GetValue(slot, uniform); + if (!chain.HasValue(slot, uniform)) { + current = DefaultValueOf(slot, uniform); + } + + current[static_cast<size_t>(component)] = value; + chain.SetValue(slot, uniform, current); +#endif +} + +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 + return Common::Android::ToJString(env, VideoCore::GetFxRootDirectory().string()); +#else + return Common::Android::ToJString(env, ""); +#endif +} + +jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getPresetsJson(JNIEnv* env, + jobject obj) { + nlohmann::json out = nlohmann::json::array(); +#ifdef HAS_RESHADE + VideoCore::ReloadFxPresetCatalog(); + + for (const auto& preset : VideoCore::GetFxPresetCatalog()) { + nlohmann::json entry; + entry["name"] = preset.name; + entry["description"] = preset.description; + entry["bundled"] = preset.bundled; + out.push_back(entry); + } +#endif + return Common::Android::ToJString(env, out.dump()); +} + +jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getActivePreset(JNIEnv* env, + jobject obj) { +#ifdef HAS_RESHADE + return Common::Android::ToJString(env, VideoCore::GetActiveFxPreset()); +#else + return Common::Android::ToJString(env, ""); +#endif +} + +jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_isPresetModified(JNIEnv* env, + jobject obj) { +#ifdef HAS_RESHADE + return static_cast<jboolean>(VideoCore::IsActiveFxPresetModified()); +#else + return static_cast<jboolean>(false); +#endif +} + +jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_applyPreset(JNIEnv* env, jobject obj, + jstring jname) { +#ifdef HAS_RESHADE + BeginFxEdit(); + return static_cast<jboolean>( + VideoCore::ApplyFxPreset(Common::Android::GetJString(env, jname))); +#else + return static_cast<jboolean>(false); +#endif +} + +jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_savePreset(JNIEnv* env, jobject obj, + jstring jname, + jstring jdescription) { +#ifdef HAS_RESHADE + BeginFxEdit(); + return static_cast<jboolean>( + VideoCore::SaveFxPreset(Common::Android::GetJString(env, jname), + Common::Android::GetJString(env, jdescription))); +#else + return static_cast<jboolean>(false); +#endif +} + +jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_deletePreset(JNIEnv* env, jobject obj, + jstring jname) { +#ifdef HAS_RESHADE + BeginFxEdit(); + return static_cast<jboolean>( + VideoCore::DeleteFxPreset(Common::Android::GetJString(env, jname))); +#else + return static_cast<jboolean>(false); +#endif +} + +void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_clearPreset(JNIEnv* env, jobject obj) { +#ifdef HAS_RESHADE + BeginFxEdit(); + VideoCore::SetActiveFxPreset(std::string_view()); +#endif +} + +jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_isEnabled(JNIEnv* env, jobject obj) { +#ifdef HAS_RESHADE + return static_cast<jboolean>(Settings::values.post_shader_enabled.GetValue()); +#else + return static_cast<jboolean>(false); +#endif +} + +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 +} + +jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getPresetDirectory(JNIEnv* env, + jobject obj) { +#ifdef HAS_RESHADE + return Common::Android::ToJString(env, VideoCore::GetFxPresetDirectory().string()); +#else + return Common::Android::ToJString(env, ""); +#endif +} + +} // extern "C" diff --git a/src/android/app/src/main/res/drawable/ic_post_processing.xml b/src/android/app/src/main/res/drawable/ic_post_processing.xml new file mode 100644 index 0000000000..06ce58d40e --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_post_processing.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?attr/colorControlNormal" + android:fillType="evenOdd" + android:pathData="M8.5,3 L19.4,3 Q21,3 21,4.6 L21,16.5 L19.4,16.5 L19.4,4.6 L8.5,4.6 Z M5,7 L15,7 Q17,7 17,9 L17,19 Q17,21 15,21 L5,21 Q3,21 3,19 L3,9 Q3,7 5,7 Z M5.2,8.6 L14.8,8.6 Q15.4,8.6 15.4,9.2 L15.4,18.8 Q15.4,19.4 14.8,19.4 L5.2,19.4 Q4.6,19.4 4.6,18.8 L4.6,9.2 Q4.6,8.6 5.2,8.6 Z M10,10.9 A3.1,3.1 0 0 1 10,17.1 Z"/> +</vector> diff --git a/src/android/app/src/main/res/drawable/shader_card_background.xml b/src/android/app/src/main/res/drawable/shader_card_background.xml new file mode 100644 index 0000000000..40e840695a --- /dev/null +++ b/src/android/app/src/main/res/drawable/shader_card_background.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + + <solid android:color="?attr/colorSurfaceVariant" /> + + <corners android:radius="16dp" /> + + <stroke + android:width="1dp" + android:color="?attr/colorOutline" /> + +</shape> 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 new file mode 100644 index 0000000000..1af62f917b --- /dev/null +++ b/src/android/app/src/main/res/layout/item_quick_settings_add.xml @@ -0,0 +1,58 @@ +<?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="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="4dp" + android:baselineAligned="false" + android:gravity="center_vertical" + android:orientation="horizontal"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/add_button" + style="@style/Widget.Material3.Button.TonalButton" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:minHeight="48dp" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:text="@string/post_processing_add" + app:icon="@drawable/ic_add" + app:iconPadding="6dp" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/remove_all_button" + style="@style/Widget.Material3.Button.TonalButton" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_marginStart="8dp" + android:layout_weight="1" + android:minHeight="48dp" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:text="@string/post_processing_remove_all" + android:visibility="gone" + app:icon="@drawable/ic_delete" + app:iconPadding="6dp" /> + + </LinearLayout> + + <RadioGroup + android:id="@+id/add_choices" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="24dp" + android:paddingEnd="24dp" + android:paddingTop="4dp" + android:paddingBottom="8dp" + android:visibility="gone" /> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/item_quick_settings_preset.xml b/src/android/app/src/main/res/layout/item_quick_settings_preset.xml new file mode 100644 index 0000000000..935ce4d45d --- /dev/null +++ b/src/android/app/src/main/res/layout/item_quick_settings_preset.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" + android:background="@drawable/shader_card_background" + android:orientation="horizontal" + android:paddingStart="20dp" + android:paddingEnd="16dp" + android:paddingTop="16dp" + android:paddingBottom="16dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:orientation="vertical"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/preset_title" + style="@style/TextAppearance.Material3.TitleMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/preset_summary" + style="@style/TextAppearance.Material3.BodySmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + </LinearLayout> + + <com.google.android.material.materialswitch.MaterialSwitch + android:id="@+id/preset_switch" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="16dp" + android:minHeight="48dp" /> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/item_quick_settings_shader.xml b/src/android/app/src/main/res/layout/item_quick_settings_shader.xml new file mode 100644 index 0000000000..dac3bb4de8 --- /dev/null +++ b/src/android/app/src/main/res/layout/item_quick_settings_shader.xml @@ -0,0 +1,82 @@ +<?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="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" + android:background="@drawable/shader_card_background" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/shader_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingStart="16dp" + android:paddingEnd="8dp" + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:orientation="vertical"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/shader_title" + style="@style/TextAppearance.Material3.TitleSmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/shader_summary" + style="@style/TextAppearance.Material3.BodySmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="2dp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + </LinearLayout> + + <ImageView + android:id="@+id/shader_remove" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_gravity="center_vertical" + android:padding="8dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:clickable="true" + android:focusable="true" + android:src="@drawable/ic_delete" + android:contentDescription="@string/post_processing_remove" + app:tint="?attr/colorOnSurfaceVariant" /> + + <ImageView + android:id="@+id/shader_expand" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_gravity="center_vertical" + android:layout_marginStart="4dp" + android:src="@drawable/ic_dropdown_arrow" + android:contentDescription="" + app:tint="?attr/colorPrimary" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/shader_body" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="8dp" + android:visibility="gone" /> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/item_setting_fx_actions.xml b/src/android/app/src/main/res/layout/item_setting_fx_actions.xml new file mode 100644 index 0000000000..c7df946416 --- /dev/null +++ b/src/android/app/src/main/res/layout/item_setting_fx_actions.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingStart="20dp" + android:paddingEnd="20dp" + android:paddingTop="8dp" + android:paddingBottom="8dp"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/fx_reset" + style="@style/Widget.Material3.Button.TonalButton" + android:layout_width="match_parent" + android:layout_height="48dp" + android:ellipsize="end" + android:maxLines="1" + android:text="@string/post_processing_reset" /> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/item_setting_fx_slider.xml b/src/android/app/src/main/res/layout/item_setting_fx_slider.xml new file mode 100644 index 0000000000..1e2989195f --- /dev/null +++ b/src/android/app/src/main/res/layout/item_setting_fx_slider.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="20dp" + android:paddingEnd="20dp" + android:paddingTop="8dp" + android:paddingBottom="4dp"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/fx_slider_title" + style="@style/TextAppearance.Material3.TitleSmall" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/fx_slider_value" + style="@style/TextAppearance.Material3.BodySmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + </LinearLayout> + + <com.google.android.material.slider.Slider + android:id="@+id/fx_slider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:stepSize="1" + android:valueFrom="0" + android:valueTo="100" /> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/list_item_setting_fx_button.xml b/src/android/app/src/main/res/layout/list_item_setting_fx_button.xml new file mode 100644 index 0000000000..8d207888cb --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting_fx_button.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" + android:orientation="horizontal"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/fx_button" + style="@style/Widget.Material3.Button.TonalButton" + android:layout_width="match_parent" + android:layout_height="48dp" + android:ellipsize="end" + android:maxLines="1" /> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/list_item_setting_fx_preset.xml b/src/android/app/src/main/res/layout/list_item_setting_fx_preset.xml new file mode 100644 index 0000000000..a212f67f4b --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting_fx_preset.xml @@ -0,0 +1,65 @@ +<?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="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" + android:background="@drawable/shader_card_background" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/preset_row" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:orientation="horizontal" + android:paddingStart="16dp" + android:paddingEnd="8dp" + android:paddingTop="12dp" + android:paddingBottom="12dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:orientation="vertical"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/preset_name" + style="@style/TextAppearance.Material3.TitleSmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/preset_description" + style="@style/TextAppearance.Material3.BodySmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="2dp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + </LinearLayout> + + <ImageView + android:id="@+id/preset_delete" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_gravity="center_vertical" + android:background="?attr/selectableItemBackgroundBorderless" + android:clickable="true" + android:contentDescription="@string/post_processing_preset_delete" + android:focusable="true" + android:padding="8dp" + android:src="@drawable/ic_delete" + android:visibility="gone" + app:tint="?attr/colorOnSurfaceVariant" /> + + </LinearLayout> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/list_item_setting_fx_shader.xml b/src/android/app/src/main/res/layout/list_item_setting_fx_shader.xml new file mode 100644 index 0000000000..e6f7b12eb4 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting_fx_shader.xml @@ -0,0 +1,82 @@ +<?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="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" + android:background="@drawable/shader_card_background" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/shader_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:orientation="horizontal" + android:paddingStart="20dp" + android:paddingEnd="8dp" + android:paddingTop="14dp" + android:paddingBottom="14dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:orientation="vertical"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/shader_title" + style="@style/TextAppearance.Material3.TitleMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/shader_summary" + style="@style/TextAppearance.Material3.BodySmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + </LinearLayout> + + <ImageView + android:id="@+id/shader_remove" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_gravity="center_vertical" + android:background="?attr/selectableItemBackgroundBorderless" + android:clickable="true" + android:contentDescription="@string/post_processing_remove" + android:focusable="true" + android:padding="8dp" + android:src="@drawable/ic_delete" + app:tint="?attr/colorOnSurfaceVariant" /> + + <ImageView + android:id="@+id/shader_expand" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_gravity="center_vertical" + android:layout_marginStart="4dp" + android:contentDescription="" + android:src="@drawable/ic_dropdown_arrow" + app:tint="?attr/colorPrimary" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/shader_body" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="8dp" + android:visibility="gone" /> + +</LinearLayout> 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..074c5a0662 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting_fx_toolbar.xml @@ -0,0 +1,64 @@ +<?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="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" + android:baselineAligned="false" + android:gravity="center_vertical" + android:orientation="horizontal"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/fx_add" + style="@style/Widget.Material3.Button.TonalButton" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:ellipsize="end" + android:maxLines="1" + android:minHeight="48dp" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:text="@string/post_processing_add" + app:icon="@drawable/ic_add" + app:iconPadding="6dp" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/fx_presets" + style="@style/Widget.Material3.Button.TonalButton" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_marginStart="8dp" + android:layout_weight="1.4" + android:ellipsize="end" + android:maxLines="1" + android:minHeight="48dp" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:text="@string/post_processing_presets" + app:icon="@drawable/ic_dropdown_arrow" + app:iconGravity="textEnd" + app:iconPadding="6dp" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/fx_create_preset" + style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="8dp" + android:contentDescription="@string/post_processing_preset_new" + app:icon="@drawable/ic_add" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/fx_remove_all" + style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="8dp" + android:contentDescription="@string/post_processing_remove_all" + app:icon="@drawable/ic_delete" /> + +</LinearLayout> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index f073000380..a9200bbca9 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -298,6 +298,25 @@ <string name="gpu_driver_fetcher">GPU driver fetcher</string> <string name="gpu_driver_manager">GPU driver manager</string> <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string> + <string name="post_processing">Post-Processing Effects</string> + <string name="post_processing_description">ReShade FX effects applied after rendering</string> + <string name="post_processing_per_game_description">Configure the effect chain for this game</string> + <string name="post_processing_add">Add effect</string> + <string name="post_processing_remove">Remove</string> + <string name="post_processing_open_list">Open list</string> + <string name="post_processing_close_list">Close list</string> + <string name="post_processing_remove_all">Remove effects</string> + <string name="post_processing_preset_locked">Preset active. Edit it in Post-Processing Effects before starting a game.</string> + <string name="post_processing_preset_modified">Values changed from the original preset.</string> + <string name="post_processing_presets">Presets</string> + <string name="post_processing_preset_new">New preset</string> + <string name="post_processing_preset_new_description">Saves the effects you have loaded, with their current values, as a preset you can pick later.</string> + <string name="post_processing_preset_name_invalid">Give the preset a name without an equals sign.</string> + <string name="post_processing_preset_none">No preset</string> + <string name="post_processing_preset_delete">Delete preset</string> + <string name="post_processing_preset_reset">Reset preset values</string> + <string name="post_processing_reset">Reset to default</string> + <string name="post_processing_empty">No effects found. Place .fx files in this folder:</string> <string name="frame_gen">Frame generation</string> <string name="frame_gen_per_game_description">Configure frame generation for this game</string> <string name="frame_gen_description">Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled.</string> |
