Adjustment on the design of UI + new shaders

This commit is contained in:
CamilleLaVey
2026-09-07 17:58:46 -04:00
parent fcaea7d502
commit 7eda109134
29 changed files with 774 additions and 63 deletions
@@ -18,6 +18,7 @@ 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
@@ -232,6 +233,250 @@ 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,
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)
}
}
slider.setOnTouchListener { _, event ->
val drawer = emulationFragment.view?.findViewById<DrawerLayout>(R.id.drawer_layout)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
drawer?.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
drawer?.requestDisallowInterceptTouchEvent(false)
}
}
false
}
container.addView(itemView)
}
fun addPostProcessing(container: ViewGroup, onStructureChanged: () -> Unit) {
val usable = NativePostProcessing.catalog().filter { it.valid }
if (usable.isEmpty()) {
return
}
val labels = mutableListOf(
YuzuApplication.appContext.getString(R.string.post_processing_none)
)
val files = mutableListOf("")
val techniques = mutableListOf("")
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()
for (index in 0..chain.size) {
var selected = 0
var effect: NativePostProcessing.Effect? = null
if (index < chain.size) {
val entry = chain[index]
effect = usable.firstOrNull { it.file == entry.file }
for (i in files.indices) {
if (files[i] == entry.file && techniques[i] == entry.technique) {
selected = i
}
}
}
var title = YuzuApplication.appContext.getString(R.string.post_processing_add)
if (index < chain.size) {
title = YuzuApplication.appContext.getString(R.string.post_processing_effect)
}
addChoice(title, container, labels, selected) { picked ->
applyEffectPick(index, picked, files, techniques, chain.size)
onStructureChanged()
}
if (effect != null) {
for (uniform in effect.uniforms) {
addUniformSliders(container, index, uniform)
}
}
}
}
private fun applyEffectPick(
index: Int,
picked: Int,
files: List<String>,
techniques: List<String>,
chainSize: Int
) {
if (index >= chainSize) {
if (picked > 0) {
NativePostProcessing.append(files[picked], techniques[picked])
NativePostProcessing.persist()
}
return
}
if (picked == 0) {
NativePostProcessing.remove(index)
NativePostProcessing.persist()
return
}
NativePostProcessing.replace(index, files[picked], techniques[picked])
NativePostProcessing.persist()
}
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 -> describeUniform(uniform, position) }
) { position ->
NativePostProcessing.setValue(
index,
uniform.name,
component,
uniform.min + position * uniform.step
)
NativePostProcessing.persist()
}
}
}
private fun describeUniform(
uniform: NativePostProcessing.Uniform,
position: Int
): String {
val value = uniform.min + position * uniform.step
if (uniform.kind == NativePostProcessing.KIND_FLOAT) {
return String.format("%.3f", value)
}
return Math.round(value).toString()
}
fun addDivider(container: ViewGroup) {
val inflater = LayoutInflater.from(emulationFragment.requireContext())
val dividerView = inflater.inflate(R.layout.item_quick_settings_divider, container, false)
@@ -199,17 +199,6 @@ class SettingsFragmentPresenter(
val usable = NativePostProcessing.catalog().filter { it.valid }
sl.apply {
add(
RunnableSetting(
titleId = R.string.post_processing_reload,
descriptionId = R.string.post_processing_reload_description,
isRunnable = true
) {
NativePostProcessing.reload()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
)
if (usable.isEmpty()) {
add(
RunnableSetting(
@@ -227,9 +216,9 @@ class SettingsFragmentPresenter(
for (effect in usable) {
for (technique in effect.techniques) {
if (effect.techniques.size == 1) {
labels.add(effect.name)
labels.add(effect.label)
} else {
labels.add(effect.name + " \u00b7 " + technique)
labels.add(effect.label + " \u00b7 " + technique)
}
files.add(effect.file)
techniques.add(technique)
@@ -242,8 +231,10 @@ class SettingsFragmentPresenter(
val effect = usable.firstOrNull { it.file == entry.file }
var header = entry.file
var summary = ""
if (effect != null) {
header = effect.name
header = effect.label
summary = effect.description
}
add(HeaderSetting(titleString = header))
@@ -251,6 +242,7 @@ class SettingsFragmentPresenter(
IntSingleChoiceSetting(
buildSlotSelector(index, entry, files, techniques),
titleId = R.string.post_processing_effect,
descriptionString = summary,
choices = labels.toTypedArray(),
values = labels.indices.toList().toTypedArray()
)
@@ -1194,6 +1194,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
R.array.rendererAntiAliasingNames,
R.array.rendererAntiAliasingValues
)
quickSettings.addPostProcessing(container) {
addQuickSettings()
}
}
}
@@ -47,7 +47,6 @@ object NativePostProcessing {
NativeConfig.saveGlobalConfig()
}
external fun reload()
external fun getShaderDirectory(): String
@@ -89,6 +88,8 @@ object NativePostProcessing {
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>
@@ -108,6 +109,8 @@ object NativePostProcessing {
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()
@@ -65,10 +65,14 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getCatalogJson(JNIEnv
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;
@@ -199,13 +203,6 @@ void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_store(JNIEnv* env, jobje
#endif
}
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_reload(JNIEnv* env, jobject obj) {
#ifdef HAS_RESHADE
VideoCore::ReloadFxCatalog();
VideoCore::FxChain::Instance().DropUnknownEntries();
#endif
}
jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getShaderDirectory(JNIEnv* env,
jobject obj) {
#ifdef HAS_RESHADE
@@ -302,13 +302,12 @@
<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_effect">Effect</string>
<string name="post_processing_none">None</string>
<string name="post_processing_add">Add effect</string>
<string name="post_processing_remove">Remove</string>
<string name="post_processing_move_up">Move up</string>
<string name="post_processing_move_down">Move down</string>
<string name="post_processing_reset">Reset to defaults</string>
<string name="post_processing_reload">Reload from disk</string>
<string name="post_processing_reload_description">Rescan the shader folder for .fx files</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>
+1 -4
View File
@@ -94,10 +94,7 @@ std::string SerializeFxChain(std::span<const FxChainEntry> entries) {
if (!out.empty()) {
out += ';';
}
out += entry.file;
out += '|';
out += entry.technique;
out += '|';
out += fmt::format("{}|{}|", entry.file, entry.technique);
bool first = true;
for (const auto& [name, value] : entry.values) {
@@ -176,6 +176,15 @@ FxEffectDesc DescribeEffect(const std::filesystem::path& path, const std::filesy
desc.techniques.push_back(technique.name);
}
if (!compiled.module.techniques.empty()) {
const auto& annotations = compiled.module.techniques.front().annotations;
desc.label = AnnotationString(annotations, "ui_label");
desc.description = AnnotationString(annotations, "ui_tooltip");
}
if (desc.label.empty()) {
desc.label = desc.name;
}
for (const auto& uniform : compiled.module.uniforms) {
FxUniformDesc uniform_desc = DescribeUniform(uniform);
if (uniform_desc.ui_type == FxUiType::Hidden) {
@@ -48,6 +48,8 @@ struct FxUniformDesc {
struct FxEffectDesc {
std::string file;
std::string name;
std::string label;
std::string description;
std::vector<std::string> techniques;
std::vector<FxUniformDesc> uniforms;
std::string error;
@@ -7,7 +7,6 @@
#include <QCheckBox>
#include <QComboBox>
#include <QDesktopServices>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QGroupBox>
@@ -17,11 +16,8 @@
#include <QScrollArea>
#include <QSlider>
#include <QToolButton>
#include <QUrl>
#include <QVBoxLayout>
#include "common/fs/fs.h"
#include "common/fs/fs_util.h"
#include "video_core/post_processing/fx_chain.h"
#include "video_core/post_processing/fx_effect.h"
#include "yuzu/configuration/configure_post_processing.h"
@@ -53,7 +49,7 @@ QString FormatValue(const VideoCore::FxUniformDesc& uniform, float value) {
}
QString SlotLabel(const VideoCore::FxEffectDesc& effect, const std::string& technique) {
const QString name = QString::fromStdString(effect.name);
const QString name = QString::fromStdString(effect.label);
if (effect.techniques.size() == 1) {
return name;
}
@@ -99,23 +95,6 @@ ConfigurePostProcessing::ConfigurePostProcessing(QWidget* parent) : QDialog(pare
});
actions->addWidget(add_button);
auto* reload_button = new QPushButton(tr("Reload From Disk"), this);
connect(reload_button, &QPushButton::clicked, this, [this]() {
VideoCore::ReloadFxCatalog();
VideoCore::FxChain::Instance().DropUnknownEntries();
ApplyStructuralChange();
});
actions->addWidget(reload_button);
auto* open_button = new QPushButton(tr("Open Folder"), this);
connect(open_button, &QPushButton::clicked, this, []() {
const auto path = VideoCore::GetFxRootDirectory();
void(Common::FS::CreateDirs(path));
QDesktopServices::openUrl(
QUrl::fromLocalFile(QString::fromStdString(Common::FS::PathToUTF8String(path))));
});
actions->addWidget(open_button);
actions->addStretch();
root->addLayout(actions);
@@ -147,6 +126,8 @@ void ConfigurePostProcessing::PopulateEffectCombo(QComboBox* combo,
for (const auto& technique : effect.techniques) {
const QString key = QString::fromStdString(effect.file + "|" + technique);
combo->addItem(SlotLabel(effect, technique), key);
combo->setItemData(combo->count() - 1, QString::fromStdString(effect.description),
Qt::ToolTipRole);
if (effect.file == entry.file && technique == entry.technique) {
selected = combo->count() - 1;
}