mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-09-09 13:36:35 +00:00
Adjustment on the design of UI + new shaders
This commit is contained in:
@@ -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)
|
||||
|
||||
+6
-14
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user