mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-09-04 11:23:12 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa8d2000f4 | |||
| 821765189a | |||
| f24b14e7cf |
@@ -84,6 +84,8 @@ option(ENABLE_WERROR "Enable -Werror diagnostics" ON)
|
|||||||
# Lossless Scaling frame generation. Only Android.
|
# Lossless Scaling frame generation. Only Android.
|
||||||
cmake_dependent_option(ENABLE_LSFG "Enable Lossless Scaling frame generation" ON "ANDROID" OFF)
|
cmake_dependent_option(ENABLE_LSFG "Enable Lossless Scaling frame generation" ON "ANDROID" OFF)
|
||||||
|
|
||||||
|
option(ENABLE_RESHADE "Enable ReShade FX post-processing effects" ON)
|
||||||
|
|
||||||
# non-linux bundled qt are static
|
# non-linux bundled qt are static
|
||||||
if (YUZU_USE_BUNDLED_QT AND (APPLE OR NOT UNIX))
|
if (YUZU_USE_BUNDLED_QT AND (APPLE OR NOT UNIX))
|
||||||
set(YUZU_STATIC_BUILD ON)
|
set(YUZU_STATIC_BUILD ON)
|
||||||
|
|||||||
@@ -246,6 +246,11 @@
|
|||||||
"repo": "stachenov/quazip",
|
"repo": "stachenov/quazip",
|
||||||
"version": "2e95c9001b"
|
"version": "2e95c9001b"
|
||||||
},
|
},
|
||||||
|
"reshade": {
|
||||||
|
"hash": "a1bd3fcf135fb6d018c1831ae45a8942d9777d0418b55e1189921e2ab775d1b53b0a9808924e09ef1c3e68ea11d69b3bcbebc7f4b59de14f6deec2dc24531d5e",
|
||||||
|
"repo": "crosire/reshade",
|
||||||
|
"version": "v6.7.3"
|
||||||
|
},
|
||||||
"sdl3": {
|
"sdl3": {
|
||||||
"hash": "df5a323af7ac366661a3c0e887969c72584d232f3cc211419d59b0487b620b6b2859d4549c9e8df002ee489290062e466fcfddf7edc0872a37b1f2845e81c0f3",
|
"hash": "df5a323af7ac366661a3c0e887969c72584d232f3cc211419d59b0487b620b6b2859d4549c9e8df002ee489290062e466fcfddf7edc0872a37b1f2845e81c0f3",
|
||||||
"min_version": "3.2.10",
|
"min_version": "3.2.10",
|
||||||
|
|||||||
Vendored
+62
@@ -0,0 +1,62 @@
|
|||||||
|
texture EdenBackBufferTex : COLOR;
|
||||||
|
sampler EdenBackBuffer { Texture = EdenBackBufferTex; };
|
||||||
|
|
||||||
|
uniform float Saturation <
|
||||||
|
ui_type = "slider";
|
||||||
|
ui_label = "Saturation";
|
||||||
|
ui_min = 0.0; ui_max = 2.0; ui_step = 0.01;
|
||||||
|
> = 1.0;
|
||||||
|
|
||||||
|
uniform float Brightness <
|
||||||
|
ui_type = "slider";
|
||||||
|
ui_label = "Brightness";
|
||||||
|
ui_min = 0.0; ui_max = 2.0; ui_step = 0.01;
|
||||||
|
> = 1.0;
|
||||||
|
|
||||||
|
uniform float Contrast <
|
||||||
|
ui_type = "slider";
|
||||||
|
ui_label = "Contrast";
|
||||||
|
ui_min = 0.0; ui_max = 2.0; ui_step = 0.01;
|
||||||
|
> = 1.0;
|
||||||
|
|
||||||
|
uniform float Gamma <
|
||||||
|
ui_type = "slider";
|
||||||
|
ui_label = "Gamma";
|
||||||
|
ui_min = 0.5; ui_max = 2.0; ui_step = 0.01;
|
||||||
|
> = 1.0;
|
||||||
|
|
||||||
|
void VS_Eden(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||||
|
{
|
||||||
|
uv = float2(0.0, 0.0);
|
||||||
|
if (id == 2)
|
||||||
|
{
|
||||||
|
uv.x = 2.0;
|
||||||
|
}
|
||||||
|
if (id == 1)
|
||||||
|
{
|
||||||
|
uv.y = 2.0;
|
||||||
|
}
|
||||||
|
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 PS_ColorGrade(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||||
|
{
|
||||||
|
float3 rgb = tex2D(EdenBackBuffer, uv).rgb;
|
||||||
|
|
||||||
|
float luma = dot(rgb, float3(0.2126, 0.7152, 0.0722));
|
||||||
|
rgb = lerp(float3(luma, luma, luma), rgb, Saturation);
|
||||||
|
rgb *= Brightness;
|
||||||
|
rgb = (rgb - 0.5) * Contrast + 0.5;
|
||||||
|
rgb = pow(max(rgb, 0.0), 1.0 / max(Gamma, 0.0001));
|
||||||
|
|
||||||
|
return float4(saturate(rgb), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
technique EdenColorGrade
|
||||||
|
{
|
||||||
|
pass
|
||||||
|
{
|
||||||
|
VertexShader = VS_Eden;
|
||||||
|
PixelShader = PS_ColorGrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+45
@@ -0,0 +1,45 @@
|
|||||||
|
texture EdenBackBufferTex : COLOR;
|
||||||
|
sampler EdenBackBuffer { Texture = EdenBackBufferTex; };
|
||||||
|
|
||||||
|
uniform float Amount <
|
||||||
|
ui_type = "slider";
|
||||||
|
ui_label = "Amount";
|
||||||
|
ui_min = 0.0; ui_max = 3.0; ui_step = 0.01;
|
||||||
|
> = 0.6;
|
||||||
|
|
||||||
|
void VS_Eden(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||||
|
{
|
||||||
|
uv = float2(0.0, 0.0);
|
||||||
|
if (id == 2)
|
||||||
|
{
|
||||||
|
uv.x = 2.0;
|
||||||
|
}
|
||||||
|
if (id == 1)
|
||||||
|
{
|
||||||
|
uv.y = 2.0;
|
||||||
|
}
|
||||||
|
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 PS_Sharpen(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||||
|
{
|
||||||
|
float2 texel = float2(BUFFER_RCP_WIDTH, BUFFER_RCP_HEIGHT);
|
||||||
|
|
||||||
|
float3 centre = tex2D(EdenBackBuffer, uv).rgb;
|
||||||
|
float3 blur = tex2D(EdenBackBuffer, uv + float2(-texel.x, 0.0)).rgb;
|
||||||
|
blur += tex2D(EdenBackBuffer, uv + float2(texel.x, 0.0)).rgb;
|
||||||
|
blur += tex2D(EdenBackBuffer, uv + float2(0.0, -texel.y)).rgb;
|
||||||
|
blur += tex2D(EdenBackBuffer, uv + float2(0.0, texel.y)).rgb;
|
||||||
|
blur *= 0.25;
|
||||||
|
|
||||||
|
return float4(centre + (centre - blur) * Amount, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
technique EdenSharpen
|
||||||
|
{
|
||||||
|
pass
|
||||||
|
{
|
||||||
|
VertexShader = VS_Eden;
|
||||||
|
PixelShader = PS_Sharpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+50
@@ -0,0 +1,50 @@
|
|||||||
|
texture EdenBackBufferTex : COLOR;
|
||||||
|
sampler EdenBackBuffer { Texture = EdenBackBufferTex; };
|
||||||
|
|
||||||
|
uniform float Strength <
|
||||||
|
ui_type = "slider";
|
||||||
|
ui_label = "Strength";
|
||||||
|
ui_tooltip = "How dark the corners become.";
|
||||||
|
ui_min = 0.0; ui_max = 2.0; ui_step = 0.01;
|
||||||
|
> = 0.6;
|
||||||
|
|
||||||
|
uniform float Aspect <
|
||||||
|
ui_type = "slider";
|
||||||
|
ui_label = "Aspect";
|
||||||
|
ui_min = 0.5; ui_max = 2.0; ui_step = 0.01;
|
||||||
|
> = 1.0;
|
||||||
|
|
||||||
|
void VS_Eden(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||||
|
{
|
||||||
|
uv = float2(0.0, 0.0);
|
||||||
|
if (id == 2)
|
||||||
|
{
|
||||||
|
uv.x = 2.0;
|
||||||
|
}
|
||||||
|
if (id == 1)
|
||||||
|
{
|
||||||
|
uv.y = 2.0;
|
||||||
|
}
|
||||||
|
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 PS_Vignette(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||||
|
{
|
||||||
|
float2 diff = uv - 0.5;
|
||||||
|
diff.x *= Aspect;
|
||||||
|
diff.y /= max(Aspect, 0.0001);
|
||||||
|
|
||||||
|
float falloff = 1.0 - min(1.0, Strength * dot(diff, diff) * 2.0);
|
||||||
|
float3 rgb = tex2D(EdenBackBuffer, uv).rgb;
|
||||||
|
|
||||||
|
return float4(rgb * falloff, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
technique EdenVignette
|
||||||
|
{
|
||||||
|
pass
|
||||||
|
{
|
||||||
|
VertexShader = VS_Eden;
|
||||||
|
PixelShader = PS_Vignette;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+33
@@ -195,6 +195,39 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# reshadefx
|
||||||
|
if (ENABLE_RESHADE)
|
||||||
|
AddJsonPackage(NAME reshade DOWNLOAD_ONLY)
|
||||||
|
|
||||||
|
set(RESHADEFX_SHIM_DIR ${CMAKE_CURRENT_BINARY_DIR}/reshadefx_shim)
|
||||||
|
file(WRITE ${RESHADEFX_SHIM_DIR}/spirv.hpp "#include <spirv/unified1/spirv.hpp>\n")
|
||||||
|
file(WRITE ${RESHADEFX_SHIM_DIR}/GLSL.std.450.h "#include <spirv/unified1/GLSL.std.450.h>\n")
|
||||||
|
|
||||||
|
add_library(reshadefx STATIC
|
||||||
|
${reshade_SOURCE_DIR}/source/effect_codegen_spirv.cpp
|
||||||
|
${reshade_SOURCE_DIR}/source/effect_expression.cpp
|
||||||
|
${reshade_SOURCE_DIR}/source/effect_lexer.cpp
|
||||||
|
${reshade_SOURCE_DIR}/source/effect_parser_exp.cpp
|
||||||
|
${reshade_SOURCE_DIR}/source/effect_parser_stmt.cpp
|
||||||
|
${reshade_SOURCE_DIR}/source/effect_preprocessor.cpp
|
||||||
|
${reshade_SOURCE_DIR}/source/effect_symbol_table.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(reshadefx SYSTEM PUBLIC ${reshade_SOURCE_DIR}/source)
|
||||||
|
target_include_directories(reshadefx PRIVATE ${RESHADEFX_SHIM_DIR})
|
||||||
|
target_link_libraries(reshadefx PUBLIC SPIRV-Headers::SPIRV-Headers)
|
||||||
|
|
||||||
|
if (NOT MSVC)
|
||||||
|
target_compile_options(reshadefx PRIVATE -w)
|
||||||
|
else()
|
||||||
|
target_compile_options(reshadefx PRIVATE /w)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT TARGET reshadefx::reshadefx)
|
||||||
|
add_library(reshadefx::reshadefx ALIAS reshadefx)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# Catch2
|
# Catch2
|
||||||
if (YUZU_TESTS OR DYNARMIC_TESTS)
|
if (YUZU_TESTS OR DYNARMIC_TESTS)
|
||||||
AddJsonPackage(catch2)
|
AddJsonPackage(catch2)
|
||||||
|
|||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FxUniformChoiceSetting(
|
||||||
|
index: Int,
|
||||||
|
uniform: NativePostProcessing.Uniform,
|
||||||
|
component: Int
|
||||||
|
) : FxUniformSetting(index, uniform, component), AbstractIntSetting {
|
||||||
|
override val defaultValue: Any
|
||||||
|
get() = uniform.defaultAt(component).roundToInt()
|
||||||
|
|
||||||
|
override fun getInt(needsGlobal: Boolean): Int = currentValue().roundToInt()
|
||||||
|
|
||||||
|
override fun setInt(value: Int) = commit(value.toFloat())
|
||||||
|
|
||||||
|
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
class FxUniformBooleanSetting(
|
||||||
|
index: Int,
|
||||||
|
uniform: NativePostProcessing.Uniform,
|
||||||
|
component: Int
|
||||||
|
) : FxUniformSetting(index, uniform, component), AbstractBooleanSetting {
|
||||||
|
override val defaultValue: Any
|
||||||
|
get() = uniform.defaultAt(component) != 0f
|
||||||
|
|
||||||
|
override fun getBoolean(needsGlobal: Boolean): Boolean = currentValue() != 0f
|
||||||
|
|
||||||
|
override fun setBoolean(value: Boolean) {
|
||||||
|
if (value) {
|
||||||
|
commit(1f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commit(0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValueAsString(needsGlobal: Boolean): String = getBoolean().toString()
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ object Settings {
|
|||||||
SECTION_SYSTEM(R.string.preferences_system),
|
SECTION_SYSTEM(R.string.preferences_system),
|
||||||
SECTION_RENDERER(R.string.preferences_graphics),
|
SECTION_RENDERER(R.string.preferences_graphics),
|
||||||
SECTION_FRAME_GEN(R.string.frame_gen),
|
SECTION_FRAME_GEN(R.string.frame_gen),
|
||||||
|
SECTION_POST_PROCESSING(R.string.post_processing),
|
||||||
SECTION_PERFORMANCE_STATS(R.string.stats_overlay_options),
|
SECTION_PERFORMANCE_STATS(R.string.stats_overlay_options),
|
||||||
SECTION_INPUT_OVERLAY(R.string.input_overlay_options),
|
SECTION_INPUT_OVERLAY(R.string.input_overlay_options),
|
||||||
SECTION_SOC_OVERLAY(R.string.soc_overlay_options),
|
SECTION_SOC_OVERLAY(R.string.soc_overlay_options),
|
||||||
|
|||||||
+1
@@ -13,6 +13,7 @@ enum class StringSetting(override val key: String) : AbstractStringSetting {
|
|||||||
DEVICE_NAME("device_name"),
|
DEVICE_NAME("device_name"),
|
||||||
LOG_FILTER("log_filter"),
|
LOG_FILTER("log_filter"),
|
||||||
PROGRAM_ARGS("program_args"),
|
PROGRAM_ARGS("program_args"),
|
||||||
|
POST_SHADER_CHAIN("post_shader_chain"),
|
||||||
|
|
||||||
WEB_TOKEN("eden_token"),
|
WEB_TOKEN("eden_token"),
|
||||||
WEB_USERNAME("eden_username")
|
WEB_USERNAME("eden_username")
|
||||||
|
|||||||
+218
@@ -18,6 +18,9 @@ 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.AbstractBooleanSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
|
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.BooleanSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.FxUniformBooleanSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.FxUniformChoiceSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.FxUniformSliderSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
|
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.IntSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
|
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
|
||||||
@@ -30,6 +33,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.*
|
|||||||
import org.yuzu.yuzu_emu.utils.InputHandler
|
import org.yuzu.yuzu_emu.utils.InputHandler
|
||||||
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
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.DirectoryInitialization
|
||||||
import org.yuzu.yuzu_emu.utils.FullscreenHelper
|
import org.yuzu.yuzu_emu.utils.FullscreenHelper
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
@@ -163,6 +167,7 @@ class SettingsFragmentPresenter(
|
|||||||
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
|
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
|
||||||
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
|
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
|
||||||
MenuTag.SECTION_FRAME_GEN -> addFrameGenSettings(sl)
|
MenuTag.SECTION_FRAME_GEN -> addFrameGenSettings(sl)
|
||||||
|
MenuTag.SECTION_POST_PROCESSING -> addPostProcessingSettings(sl)
|
||||||
MenuTag.SECTION_PERFORMANCE_STATS -> addPerformanceOverlaySettings(sl)
|
MenuTag.SECTION_PERFORMANCE_STATS -> addPerformanceOverlaySettings(sl)
|
||||||
MenuTag.SECTION_SOC_OVERLAY -> addSocOverlaySettings(sl)
|
MenuTag.SECTION_SOC_OVERLAY -> addSocOverlaySettings(sl)
|
||||||
MenuTag.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl)
|
MenuTag.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl)
|
||||||
@@ -190,6 +195,219 @@ class SettingsFragmentPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addPostProcessingSettings(sl: ArrayList<SettingsItem>) {
|
||||||
|
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(
|
||||||
|
titleId = R.string.post_processing_empty,
|
||||||
|
descriptionString = NativePostProcessing.getShaderDirectory(),
|
||||||
|
isRunnable = false
|
||||||
|
) {}
|
||||||
|
)
|
||||||
|
return@apply
|
||||||
|
}
|
||||||
|
|
||||||
|
val labels = 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.name)
|
||||||
|
} else {
|
||||||
|
labels.add(effect.name + " \u00b7 " + technique)
|
||||||
|
}
|
||||||
|
files.add(effect.file)
|
||||||
|
techniques.add(technique)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val chain = NativePostProcessing.chain()
|
||||||
|
for (index in chain.indices) {
|
||||||
|
val entry = chain[index]
|
||||||
|
val effect = usable.firstOrNull { it.file == entry.file }
|
||||||
|
|
||||||
|
var header = entry.file
|
||||||
|
if (effect != null) {
|
||||||
|
header = effect.name
|
||||||
|
}
|
||||||
|
add(HeaderSetting(titleString = header))
|
||||||
|
|
||||||
|
add(
|
||||||
|
IntSingleChoiceSetting(
|
||||||
|
buildSlotSelector(index, entry, files, techniques),
|
||||||
|
titleId = R.string.post_processing_effect,
|
||||||
|
choices = labels.toTypedArray(),
|
||||||
|
values = labels.indices.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (effect != null) {
|
||||||
|
for (uniform in effect.uniforms) {
|
||||||
|
addUniform(this, index, uniform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
add(
|
||||||
|
RunnableSetting(
|
||||||
|
titleId = R.string.post_processing_move_up,
|
||||||
|
isRunnable = true
|
||||||
|
) {
|
||||||
|
NativePostProcessing.move(index, -1)
|
||||||
|
NativePostProcessing.store()
|
||||||
|
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (index < chain.size - 1) {
|
||||||
|
add(
|
||||||
|
RunnableSetting(
|
||||||
|
titleId = R.string.post_processing_move_down,
|
||||||
|
isRunnable = true
|
||||||
|
) {
|
||||||
|
NativePostProcessing.move(index, 1)
|
||||||
|
NativePostProcessing.store()
|
||||||
|
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
add(
|
||||||
|
RunnableSetting(
|
||||||
|
titleId = R.string.post_processing_reset,
|
||||||
|
isRunnable = true
|
||||||
|
) {
|
||||||
|
NativePostProcessing.resetValues(index)
|
||||||
|
NativePostProcessing.store()
|
||||||
|
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
RunnableSetting(
|
||||||
|
titleId = R.string.post_processing_remove,
|
||||||
|
isRunnable = true
|
||||||
|
) {
|
||||||
|
NativePostProcessing.remove(index)
|
||||||
|
NativePostProcessing.store()
|
||||||
|
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
add(
|
||||||
|
RunnableSetting(
|
||||||
|
titleId = R.string.post_processing_add,
|
||||||
|
isRunnable = true
|
||||||
|
) {
|
||||||
|
NativePostProcessing.append(files[0], techniques[0])
|
||||||
|
NativePostProcessing.store()
|
||||||
|
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSlotSelector(
|
||||||
|
index: Int,
|
||||||
|
entry: NativePostProcessing.ChainEntry,
|
||||||
|
files: List<String>,
|
||||||
|
techniques: List<String>
|
||||||
|
): AbstractIntSetting = object : AbstractIntSetting {
|
||||||
|
override val key = "fx_slot_$index"
|
||||||
|
|
||||||
|
override fun getInt(needsGlobal: Boolean): Int {
|
||||||
|
for (i in files.indices) {
|
||||||
|
if (files[i] == entry.file && techniques[i] == entry.technique) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setInt(value: Int) {
|
||||||
|
NativePostProcessing.replace(index, files[value], techniques[value])
|
||||||
|
NativePostProcessing.store()
|
||||||
|
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val defaultValue = 0
|
||||||
|
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
|
||||||
|
override fun reset() {}
|
||||||
|
override val isRuntimeModifiable = true
|
||||||
|
override val pairedSettingKey = ""
|
||||||
|
override val isSwitchable = false
|
||||||
|
override val isSaveable = true
|
||||||
|
override var global: Boolean
|
||||||
|
get() = true
|
||||||
|
set(_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addUniform(
|
||||||
|
sl: ArrayList<SettingsItem>,
|
||||||
|
index: Int,
|
||||||
|
uniform: NativePostProcessing.Uniform
|
||||||
|
) {
|
||||||
|
if (uniform.uiType == NativePostProcessing.UI_CHECKBOX ||
|
||||||
|
uniform.kind == NativePostProcessing.KIND_BOOL
|
||||||
|
) {
|
||||||
|
sl.add(
|
||||||
|
SwitchSetting(
|
||||||
|
FxUniformBooleanSetting(index, uniform, 0),
|
||||||
|
titleString = uniform.label,
|
||||||
|
descriptionString = uniform.tooltip
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniform.items.isNotEmpty() &&
|
||||||
|
(uniform.uiType == NativePostProcessing.UI_COMBO ||
|
||||||
|
uniform.uiType == NativePostProcessing.UI_RADIO)
|
||||||
|
) {
|
||||||
|
sl.add(
|
||||||
|
IntSingleChoiceSetting(
|
||||||
|
FxUniformChoiceSetting(index, uniform, 0),
|
||||||
|
titleString = uniform.label,
|
||||||
|
descriptionString = uniform.tooltip,
|
||||||
|
choices = uniform.items.toTypedArray(),
|
||||||
|
values = uniform.items.indices.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (component in 0 until uniform.components) {
|
||||||
|
var title = uniform.label
|
||||||
|
if (uniform.components > 1) {
|
||||||
|
title = uniform.label + " [" + component + "]"
|
||||||
|
}
|
||||||
|
sl.add(
|
||||||
|
SliderSetting(
|
||||||
|
FxUniformSliderSetting(index, uniform, component),
|
||||||
|
titleString = title,
|
||||||
|
descriptionString = uniform.tooltip,
|
||||||
|
min = 0,
|
||||||
|
max = uniform.steps
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
|
private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
|
||||||
sl.apply {
|
sl.apply {
|
||||||
add(
|
add(
|
||||||
|
|||||||
@@ -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()) {
|
if (GpuDriverHelper.isAdrenoGpu()) {
|
||||||
add(
|
add(
|
||||||
|
|||||||
@@ -171,6 +171,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(
|
add(
|
||||||
HomeSetting(
|
HomeSetting(
|
||||||
R.string.lossless_scaling,
|
R.string.lossless_scaling,
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
// 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 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 defaultAt(component: Int): Float {
|
||||||
|
if (component < defaults.size) {
|
||||||
|
return defaults[component]
|
||||||
|
}
|
||||||
|
return 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Effect(
|
||||||
|
val file: String,
|
||||||
|
val name: 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)
|
||||||
|
|
||||||
|
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"),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ add_library(yuzu-android SHARED
|
|||||||
android_config.cpp
|
android_config.cpp
|
||||||
android_config.h
|
android_config.h
|
||||||
native_input.cpp
|
native_input.cpp
|
||||||
|
native_post_processing.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
|
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include "common/android/android_common.h"
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
#include "video_core/post_processing/fx_chain.h"
|
||||||
|
#include "video_core/post_processing/fx_effect.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
#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
|
||||||
|
for (const auto& effect : VideoCore::GetFxCatalog()) {
|
||||||
|
nlohmann::json entry;
|
||||||
|
entry["file"] = effect.file;
|
||||||
|
entry["name"] = effect.name;
|
||||||
|
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();
|
||||||
|
|
||||||
|
auto current = chain.GetValue(static_cast<size_t>(index), uniform);
|
||||||
|
current[static_cast<size_t>(component)] = value;
|
||||||
|
chain.SetValue(static_cast<size_t>(index), uniform, current);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_store(JNIEnv* env, jobject obj) {
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
VideoCore::FxChain::Instance().StoreToSettings();
|
||||||
|
#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
|
||||||
|
return Common::Android::ToJString(env, VideoCore::GetFxRootDirectory().string());
|
||||||
|
#else
|
||||||
|
return Common::Android::ToJString(env, "");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
@@ -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>
|
||||||
@@ -298,6 +298,18 @@
|
|||||||
<string name="gpu_driver_fetcher">GPU driver fetcher</string>
|
<string name="gpu_driver_fetcher">GPU driver fetcher</string>
|
||||||
<string name="gpu_driver_manager">GPU driver manager</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="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_effect">Effect</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">Frame generation</string>
|
||||||
<string name="frame_gen_per_game_description">Configure frame generation for this game</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>
|
<string name="frame_gen_description">Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled.</string>
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ add_library(
|
|||||||
param_package.h
|
param_package.h
|
||||||
parent_of_member.h
|
parent_of_member.h
|
||||||
point.h
|
point.h
|
||||||
|
quaternion.h
|
||||||
range_map.h
|
range_map.h
|
||||||
range_mutex.h
|
range_mutex.h
|
||||||
range_sets.h
|
range_sets.h
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#define LOSSLESS_DIR "lossless"
|
#define LOSSLESS_DIR "lossless"
|
||||||
#define NAND_DIR "nand"
|
#define NAND_DIR "nand"
|
||||||
#define PLAY_TIME_DIR "play_time"
|
#define PLAY_TIME_DIR "play_time"
|
||||||
|
#define POST_SHADER_DIR "post_shaders"
|
||||||
#define SCREENSHOTS_DIR "screenshots"
|
#define SCREENSHOTS_DIR "screenshots"
|
||||||
#define SDMC_DIR "sdmc"
|
#define SDMC_DIR "sdmc"
|
||||||
#define SHADER_DIR "shader"
|
#define SHADER_DIR "shader"
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ public:
|
|||||||
GenerateEdenPath(EdenPath::LosslessDir, eden_path / LOSSLESS_DIR);
|
GenerateEdenPath(EdenPath::LosslessDir, eden_path / LOSSLESS_DIR);
|
||||||
GenerateEdenPath(EdenPath::NANDDir, eden_path / NAND_DIR);
|
GenerateEdenPath(EdenPath::NANDDir, eden_path / NAND_DIR);
|
||||||
GenerateEdenPath(EdenPath::PlayTimeDir, eden_path / PLAY_TIME_DIR);
|
GenerateEdenPath(EdenPath::PlayTimeDir, eden_path / PLAY_TIME_DIR);
|
||||||
|
GenerateEdenPath(EdenPath::PostShaderDir, eden_path / POST_SHADER_DIR);
|
||||||
GenerateEdenPath(EdenPath::SaveDir, eden_path / NAND_DIR);
|
GenerateEdenPath(EdenPath::SaveDir, eden_path / NAND_DIR);
|
||||||
GenerateEdenPath(EdenPath::ScreenshotsDir, eden_path / SCREENSHOTS_DIR);
|
GenerateEdenPath(EdenPath::ScreenshotsDir, eden_path / SCREENSHOTS_DIR);
|
||||||
GenerateEdenPath(EdenPath::SDMCDir, eden_path / SDMC_DIR);
|
GenerateEdenPath(EdenPath::SDMCDir, eden_path / SDMC_DIR);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ enum class EdenPath {
|
|||||||
LosslessDir, // Where the user-supplied Lossless Scaling library is stored.
|
LosslessDir, // Where the user-supplied Lossless Scaling library is stored.
|
||||||
NANDDir, // Where the emulated NAND is stored.
|
NANDDir, // Where the emulated NAND is stored.
|
||||||
PlayTimeDir, // Where play time data is stored.
|
PlayTimeDir, // Where play time data is stored.
|
||||||
|
PostShaderDir, // Where user post-processing shaders are stored.
|
||||||
SaveDir, // Where save data is stored.
|
SaveDir, // Where save data is stored.
|
||||||
ScreenshotsDir, // Where yuzu screenshots are stored.
|
ScreenshotsDir, // Where yuzu screenshots are stored.
|
||||||
SDMCDir, // Where the emulated SDMC is stored.
|
SDMCDir, // Where the emulated SDMC is stored.
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/vector_math.h"
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class Quaternion {
|
||||||
|
public:
|
||||||
|
Vec3<T> xyz;
|
||||||
|
T w{};
|
||||||
|
|
||||||
|
[[nodiscard]] Quaternion<decltype(-T{})> Inverse() const {
|
||||||
|
return {-xyz, w};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Quaternion<decltype(T{} + T{})> operator+(const Quaternion& other) const {
|
||||||
|
return {xyz + other.xyz, w + other.w};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Quaternion<decltype(T{} - T{})> operator-(const Quaternion& other) const {
|
||||||
|
return {xyz - other.xyz, w - other.w};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Quaternion<decltype(T{} * T{} - T{} * T{})> operator*(
|
||||||
|
const Quaternion& other) const {
|
||||||
|
return {xyz * other.w + other.xyz * w + Cross(xyz, other.xyz),
|
||||||
|
w * other.w - Dot(xyz, other.xyz)};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Quaternion<T> Normalized() const {
|
||||||
|
T length = std::sqrt(xyz.Length2() + w * w);
|
||||||
|
return {xyz / length, w / length};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::array<decltype(-T{}), 16> ToMatrix() const {
|
||||||
|
const T x2 = xyz[0] * xyz[0];
|
||||||
|
const T y2 = xyz[1] * xyz[1];
|
||||||
|
const T z2 = xyz[2] * xyz[2];
|
||||||
|
|
||||||
|
const T xy = xyz[0] * xyz[1];
|
||||||
|
const T wz = w * xyz[2];
|
||||||
|
const T xz = xyz[0] * xyz[2];
|
||||||
|
const T wy = w * xyz[1];
|
||||||
|
const T yz = xyz[1] * xyz[2];
|
||||||
|
const T wx = w * xyz[0];
|
||||||
|
|
||||||
|
return {1.0f - 2.0f * (y2 + z2),
|
||||||
|
2.0f * (xy + wz),
|
||||||
|
2.0f * (xz - wy),
|
||||||
|
0.0f,
|
||||||
|
2.0f * (xy - wz),
|
||||||
|
1.0f - 2.0f * (x2 + z2),
|
||||||
|
2.0f * (yz + wx),
|
||||||
|
0.0f,
|
||||||
|
2.0f * (xz + wy),
|
||||||
|
2.0f * (yz - wx),
|
||||||
|
1.0f - 2.0f * (x2 + y2),
|
||||||
|
0.0f,
|
||||||
|
0.0f,
|
||||||
|
0.0f,
|
||||||
|
0.0f,
|
||||||
|
1.0f};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] auto QuaternionRotate(const Quaternion<T>& q, const Vec3<T>& v) {
|
||||||
|
return v + 2 * Cross(q.xyz, Cross(q.xyz, v) + v * q.w);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline Quaternion<float> MakeQuaternion(const Vec3<float>& axis, float angle) {
|
||||||
|
return {axis * std::sin(angle / 2), std::cos(angle / 2)};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common
|
||||||
@@ -388,6 +388,14 @@ struct Values {
|
|||||||
true,
|
true,
|
||||||
true};
|
true};
|
||||||
|
|
||||||
|
SwitchableSetting<std::string> post_shader_chain{linkage,
|
||||||
|
std::string(),
|
||||||
|
"post_shader_chain",
|
||||||
|
Category::Renderer,
|
||||||
|
Specialization::Default,
|
||||||
|
true,
|
||||||
|
true};
|
||||||
|
|
||||||
SwitchableSetting<bool> frame_gen{linkage, false, "frame_gen", Category::Renderer,
|
SwitchableSetting<bool> frame_gen{linkage, false, "frame_gen", Category::Renderer,
|
||||||
Specialization::Default, true, false};
|
Specialization::Default, true, false};
|
||||||
|
|
||||||
|
|||||||
+717
-93
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2014 Tony Wasserka
|
// SPDX-FileCopyrightText: 2014 Tony Wasserka
|
||||||
@@ -7,128 +7,752 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __ARM_NEON
|
||||||
|
#include <arm_neon.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
||||||
template <typename T, size_t N>
|
template <typename T>
|
||||||
class Vec {
|
class Vec2;
|
||||||
|
template <typename T>
|
||||||
|
class Vec3;
|
||||||
|
template <typename T>
|
||||||
|
class Vec4;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class Vec2 {
|
||||||
public:
|
public:
|
||||||
std::array<T, N> elems{};
|
T x{};
|
||||||
|
T y{};
|
||||||
|
|
||||||
constexpr Vec() = default;
|
constexpr Vec2() = default;
|
||||||
constexpr Vec(T e0) noexcept : elems{e0} {}
|
constexpr Vec2(const T& x_, const T& y_) : x(x_), y(y_) {}
|
||||||
constexpr Vec(T e0, T e1) noexcept : elems{e0, e1} {}
|
|
||||||
constexpr Vec(T e0, T e1, T e2) noexcept : elems{e0, e1, e2} {}
|
|
||||||
constexpr Vec(T e0, T e1, T e2, T e4) noexcept : elems{e0, e1, e2, e4} {}
|
|
||||||
//explicit constexpr Vec(const std::initializer_list<T> elems_) noexcept : elems{elems_} {}
|
|
||||||
|
|
||||||
[[nodiscard]] constexpr Vec<decltype(T{} + T{}), N> operator+(const Vec o) const noexcept {
|
template <typename T2>
|
||||||
Vec<decltype(T{} + T{}), N> r{};
|
[[nodiscard]] constexpr Vec2<T2> Cast() const {
|
||||||
for (size_t i = 0; i < N; ++i)
|
return Vec2<T2>(static_cast<T2>(x), static_cast<T2>(y));
|
||||||
r.elems[i] = elems[i] + o.elems[i];
|
|
||||||
return r;
|
|
||||||
}
|
}
|
||||||
constexpr Vec<T, N> operator+=(const Vec<T, N> o) noexcept { return *this = *this + o; }
|
|
||||||
|
|
||||||
[[nodiscard]] constexpr Vec<decltype(T{} - T{}), N> operator-(const Vec o) const noexcept {
|
[[nodiscard]] static constexpr Vec2 AssignToAll(const T& f) {
|
||||||
Vec<decltype(T{} - T{}), N> r{};
|
return Vec2{f, f};
|
||||||
for (size_t i = 0; i < N; ++i)
|
}
|
||||||
r.elems[i] = elems[i] - o.elems[i];
|
|
||||||
return r;
|
[[nodiscard]] constexpr Vec2<decltype(T{} + T{})> operator+(const Vec2& other) const {
|
||||||
|
return {x + other.x, y + other.y};
|
||||||
|
}
|
||||||
|
constexpr Vec2& operator+=(const Vec2& other) {
|
||||||
|
x += other.x;
|
||||||
|
y += other.y;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr Vec2<decltype(T{} - T{})> operator-(const Vec2& other) const {
|
||||||
|
return {x - other.x, y - other.y};
|
||||||
|
}
|
||||||
|
constexpr Vec2& operator-=(const Vec2& other) {
|
||||||
|
x -= other.x;
|
||||||
|
y -= other.y;
|
||||||
|
return *this;
|
||||||
}
|
}
|
||||||
constexpr Vec<T, N> operator-=(const Vec<T, N> o) noexcept { return *this = *this - o; }
|
|
||||||
|
|
||||||
template <typename U = T>
|
template <typename U = T>
|
||||||
[[nodiscard]] constexpr Vec<std::enable_if_t<std::is_signed_v<U>, U>, N> operator-() const noexcept {
|
[[nodiscard]] constexpr Vec2<std::enable_if_t<std::is_signed_v<U>, U>> operator-() const {
|
||||||
Vec<U, N> r{};
|
return {-x, -y};
|
||||||
for (size_t i = 0; i < N; ++i)
|
}
|
||||||
r.elems[i] = -elems[i];
|
[[nodiscard]] constexpr Vec2<decltype(T{} * T{})> operator*(const Vec2& other) const {
|
||||||
return r;
|
return {x * other.x, y * other.y};
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] constexpr Vec<decltype(T{} * T{}), N> operator*(const Vec o) const noexcept {
|
|
||||||
Vec<decltype(T{} * T{}), N> r{};
|
|
||||||
for (size_t i = 0; i < N; ++i)
|
|
||||||
r.elems[i] = elems[i] * o.elems[i];
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
template <typename V>
|
template <typename V>
|
||||||
[[nodiscard]] constexpr Vec<decltype(T{} * V{}), N> operator*(const V f) const noexcept {
|
[[nodiscard]] constexpr Vec2<decltype(T{} * V{})> operator*(const V& f) const {
|
||||||
using TV = decltype(T{} * V{});
|
using TV = decltype(T{} * V{});
|
||||||
using C = std::common_type_t<T, V>;
|
using C = std::common_type_t<T, V>;
|
||||||
Vec<TV, N> r{};
|
|
||||||
for (size_t i = 0; i < N; ++i)
|
return {
|
||||||
r.elems[i] = TV(C(elems[i]) * C(f));
|
static_cast<TV>(static_cast<C>(x) * static_cast<C>(f)),
|
||||||
return r;
|
static_cast<TV>(static_cast<C>(y) * static_cast<C>(f)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
template <typename V>
|
|
||||||
constexpr Vec<T, N> operator*=(const V f) noexcept { return *this = *this * f; }
|
|
||||||
|
|
||||||
template <typename V>
|
template <typename V>
|
||||||
[[nodiscard]] constexpr Vec<decltype(T{} / V{}), N> operator/(const V f) const noexcept {
|
constexpr Vec2& operator*=(const V& f) {
|
||||||
|
*this = *this * f;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename V>
|
||||||
|
[[nodiscard]] constexpr Vec2<decltype(T{} / V{})> operator/(const V& f) const {
|
||||||
using TV = decltype(T{} / V{});
|
using TV = decltype(T{} / V{});
|
||||||
using C = std::common_type_t<T, V>;
|
using C = std::common_type_t<T, V>;
|
||||||
Vec<TV, N> r{};
|
|
||||||
for (size_t i = 0; i < N; ++i)
|
|
||||||
r.elems[i] = TV(C(elems[i]) / C(f));
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
template <typename V>
|
|
||||||
constexpr Vec<T, N> operator/=(const V f) noexcept { return *this = *this / f; }
|
|
||||||
|
|
||||||
[[nodiscard]] constexpr T Length2() const noexcept {
|
|
||||||
T r{};
|
|
||||||
for (size_t i = 0; i < N; ++i)
|
|
||||||
r += elems[i] * elems[i];
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
// Only implemented for T=float
|
|
||||||
[[nodiscard]] T Length() const { return T(std::sqrt(float(Length2()))); }
|
|
||||||
[[nodiscard]] Vec<T, N> Normalized() const { return *this / Length(); }
|
|
||||||
[[nodiscard]] constexpr T& operator[](std::size_t i) noexcept { return elems[i]; }
|
|
||||||
[[nodiscard]] constexpr const T& operator[](std::size_t i) const noexcept { return elems[i]; }
|
|
||||||
|
|
||||||
[[nodiscard]] std::array<decltype(-T{}), 16> ToMatrix() const {
|
|
||||||
const T x2 = elems[0] * elems[0];
|
|
||||||
const T y2 = elems[1] * elems[1];
|
|
||||||
const T z2 = elems[2] * elems[2];
|
|
||||||
|
|
||||||
const T xy = elems[0] * elems[1];
|
|
||||||
const T wz = elems[3] * elems[2];
|
|
||||||
const T xz = elems[0] * elems[2];
|
|
||||||
const T wy = elems[3] * elems[1];
|
|
||||||
const T yz = elems[1] * elems[2];
|
|
||||||
const T wx = elems[3] * elems[0];
|
|
||||||
return {
|
return {
|
||||||
1.0f - 2.0f * (y2 + z2),
|
static_cast<TV>(static_cast<C>(x) / static_cast<C>(f)),
|
||||||
2.0f * (xy + wz),
|
static_cast<TV>(static_cast<C>(y) / static_cast<C>(f)),
|
||||||
2.0f * (xz - wy),
|
|
||||||
0.0f,
|
|
||||||
2.0f * (xy - wz),
|
|
||||||
1.0f - 2.0f * (x2 + z2),
|
|
||||||
2.0f * (yz + wx),
|
|
||||||
0.0f,
|
|
||||||
2.0f * (xz + wy),
|
|
||||||
2.0f * (yz - wx),
|
|
||||||
1.0f - 2.0f * (x2 + y2),
|
|
||||||
0.0f,
|
|
||||||
0.0f,
|
|
||||||
0.0f,
|
|
||||||
0.0f,
|
|
||||||
1.0f
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename V>
|
||||||
|
constexpr Vec2& operator/=(const V& f) {
|
||||||
|
*this = *this / f;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr T Length2() const {
|
||||||
|
return x * x + y * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only implemented for T=float
|
||||||
|
[[nodiscard]] float Length() const;
|
||||||
|
[[nodiscard]] float Normalize(); // returns the previous length, which is often useful
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr T& operator[](std::size_t i) {
|
||||||
|
return *((&x) + i);
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& operator[](std::size_t i) const {
|
||||||
|
return *((&x) + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetZero() {
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common aliases: UV (texel coordinates), ST (texture coordinates)
|
||||||
|
[[nodiscard]] constexpr T& u() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& v() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& s() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& t() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr const T& u() const {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& v() const {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& s() const {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& t() const {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// swizzlers - create a subvector of specific components
|
||||||
|
[[nodiscard]] constexpr Vec2 yx() const {
|
||||||
|
return Vec2(y, x);
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr Vec2 vu() const {
|
||||||
|
return Vec2(y, x);
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr Vec2 ts() const {
|
||||||
|
return Vec2(y, x);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename T, size_t N, typename V>
|
template <typename T, typename V>
|
||||||
[[nodiscard]] constexpr Vec<T, N> operator*(const V f, const Vec<T, N> v) noexcept {
|
[[nodiscard]] constexpr Vec2<T> operator*(const V& f, const Vec2<T>& vec) {
|
||||||
using C = std::common_type_t<T, V>;
|
using C = std::common_type_t<T, V>;
|
||||||
Vec<T, N> r{};
|
|
||||||
for (size_t i = 0; i < N; ++i)
|
return Vec2<T>(static_cast<T>(static_cast<C>(f) * static_cast<C>(vec.x)),
|
||||||
r.elems[i] = T(C(f) * C(v.elems[i]));
|
static_cast<T>(static_cast<C>(f) * static_cast<C>(vec.y)));
|
||||||
return r;
|
}
|
||||||
|
|
||||||
|
using Vec2f = Vec2<float>;
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline float Vec2<float>::Length() const {
|
||||||
|
return std::sqrt(x * x + y * y);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline float Vec2<float>::Normalize() {
|
||||||
|
float length = Length();
|
||||||
|
*this /= length;
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class Vec3 {
|
||||||
|
public:
|
||||||
|
T x{};
|
||||||
|
T y{};
|
||||||
|
T z{};
|
||||||
|
|
||||||
|
constexpr Vec3() = default;
|
||||||
|
constexpr Vec3(const T& x_, const T& y_, const T& z_) : x(x_), y(y_), z(z_) {}
|
||||||
|
|
||||||
|
template <typename T2>
|
||||||
|
[[nodiscard]] constexpr Vec3<T2> Cast() const {
|
||||||
|
return Vec3<T2>(static_cast<T2>(x), static_cast<T2>(y), static_cast<T2>(z));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static constexpr Vec3 AssignToAll(const T& f) {
|
||||||
|
return Vec3(f, f, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr Vec3<decltype(T{} + T{})> operator+(const Vec3& other) const {
|
||||||
|
return {x + other.x, y + other.y, z + other.z};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Vec3& operator+=(const Vec3& other) {
|
||||||
|
x += other.x;
|
||||||
|
y += other.y;
|
||||||
|
z += other.z;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr Vec3<decltype(T{} - T{})> operator-(const Vec3& other) const {
|
||||||
|
return {x - other.x, y - other.y, z - other.z};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Vec3& operator-=(const Vec3& other) {
|
||||||
|
x -= other.x;
|
||||||
|
y -= other.y;
|
||||||
|
z -= other.z;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename U = T>
|
||||||
|
[[nodiscard]] constexpr Vec3<std::enable_if_t<std::is_signed_v<U>, U>> operator-() const {
|
||||||
|
return {-x, -y, -z};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr Vec3<decltype(T{} * T{})> operator*(const Vec3& other) const {
|
||||||
|
return {x * other.x, y * other.y, z * other.z};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename V>
|
||||||
|
[[nodiscard]] constexpr Vec3<decltype(T{} * V{})> operator*(const V& f) const {
|
||||||
|
using TV = decltype(T{} * V{});
|
||||||
|
using C = std::common_type_t<T, V>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
static_cast<TV>(static_cast<C>(x) * static_cast<C>(f)),
|
||||||
|
static_cast<TV>(static_cast<C>(y) * static_cast<C>(f)),
|
||||||
|
static_cast<TV>(static_cast<C>(z) * static_cast<C>(f)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename V>
|
||||||
|
constexpr Vec3& operator*=(const V& f) {
|
||||||
|
*this = *this * f;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
template <typename V>
|
||||||
|
[[nodiscard]] constexpr Vec3<decltype(T{} / V{})> operator/(const V& f) const {
|
||||||
|
using TV = decltype(T{} / V{});
|
||||||
|
using C = std::common_type_t<T, V>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
static_cast<TV>(static_cast<C>(x) / static_cast<C>(f)),
|
||||||
|
static_cast<TV>(static_cast<C>(y) / static_cast<C>(f)),
|
||||||
|
static_cast<TV>(static_cast<C>(z) / static_cast<C>(f)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename V>
|
||||||
|
constexpr Vec3& operator/=(const V& f) {
|
||||||
|
*this = *this / f;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RotateFromOrigin(float roll, float pitch, float yaw) {
|
||||||
|
float temp = y;
|
||||||
|
y = std::cos(roll) * y - std::sin(roll) * z;
|
||||||
|
z = std::sin(roll) * temp + std::cos(roll) * z;
|
||||||
|
|
||||||
|
temp = x;
|
||||||
|
x = std::cos(pitch) * x + std::sin(pitch) * z;
|
||||||
|
z = -std::sin(pitch) * temp + std::cos(pitch) * z;
|
||||||
|
|
||||||
|
temp = x;
|
||||||
|
x = std::cos(yaw) * x - std::sin(yaw) * y;
|
||||||
|
y = std::sin(yaw) * temp + std::cos(yaw) * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr T Length2() const {
|
||||||
|
return x * x + y * y + z * z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only implemented for T=float
|
||||||
|
[[nodiscard]] float Length() const;
|
||||||
|
[[nodiscard]] Vec3 Normalized() const;
|
||||||
|
[[nodiscard]] float Normalize(); // returns the previous length, which is often useful
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr T& operator[](std::size_t i) {
|
||||||
|
return *((&x) + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr const T& operator[](std::size_t i) const {
|
||||||
|
return *((&x) + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetZero() {
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
z = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common aliases: UVW (texel coordinates), RGB (colors), STQ (texture coordinates)
|
||||||
|
[[nodiscard]] constexpr T& u() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& v() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& w() {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr T& r() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& g() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& b() {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr T& s() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& t() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& q() {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr const T& u() const {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& v() const {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& w() const {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr const T& r() const {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& g() const {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& b() const {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr const T& s() const {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& t() const {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& q() const {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// swizzlers - create a subvector of specific components
|
||||||
|
// e.g. Vec2 uv() { return Vec2(x,y); }
|
||||||
|
// _DEFINE_SWIZZLER2 defines a single such function, DEFINE_SWIZZLER2 defines all of them for all
|
||||||
|
// component names (x<->r) and permutations (xy<->yx)
|
||||||
|
#define _DEFINE_SWIZZLER2(a, b, name) \
|
||||||
|
[[nodiscard]] constexpr Vec2<T> name() const { return Vec2<T>(a, b); }
|
||||||
|
#define DEFINE_SWIZZLER2(a, b, a2, b2, a3, b3, a4, b4) \
|
||||||
|
_DEFINE_SWIZZLER2(a, b, a##b); \
|
||||||
|
_DEFINE_SWIZZLER2(a, b, a2##b2); \
|
||||||
|
_DEFINE_SWIZZLER2(a, b, a3##b3); \
|
||||||
|
_DEFINE_SWIZZLER2(a, b, a4##b4); \
|
||||||
|
_DEFINE_SWIZZLER2(b, a, b##a); \
|
||||||
|
_DEFINE_SWIZZLER2(b, a, b2##a2); \
|
||||||
|
_DEFINE_SWIZZLER2(b, a, b3##a3); \
|
||||||
|
_DEFINE_SWIZZLER2(b, a, b4##a4)
|
||||||
|
|
||||||
|
DEFINE_SWIZZLER2(x, y, r, g, u, v, s, t);
|
||||||
|
DEFINE_SWIZZLER2(x, z, r, b, u, w, s, q);
|
||||||
|
DEFINE_SWIZZLER2(y, z, g, b, v, w, t, q);
|
||||||
|
#undef DEFINE_SWIZZLER2
|
||||||
|
#undef _DEFINE_SWIZZLER2
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T, typename V>
|
||||||
|
[[nodiscard]] constexpr Vec3<T> operator*(const V& f, const Vec3<T>& vec) {
|
||||||
|
using C = std::common_type_t<T, V>;
|
||||||
|
|
||||||
|
return Vec3<T>(static_cast<T>(static_cast<C>(f) * static_cast<C>(vec.x)),
|
||||||
|
static_cast<T>(static_cast<C>(f) * static_cast<C>(vec.y)),
|
||||||
|
static_cast<T>(static_cast<C>(f) * static_cast<C>(vec.z)));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline float Vec3<float>::Length() const {
|
||||||
|
return std::sqrt(x * x + y * y + z * z);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline Vec3<float> Vec3<float>::Normalized() const {
|
||||||
|
return *this / Length();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline float Vec3<float>::Normalize() {
|
||||||
|
float length = Length();
|
||||||
|
*this /= length;
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
using Vec3f = Vec3<float>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class Vec4 {
|
||||||
|
public:
|
||||||
|
T x{};
|
||||||
|
T y{};
|
||||||
|
T z{};
|
||||||
|
T w{};
|
||||||
|
|
||||||
|
constexpr Vec4() = default;
|
||||||
|
constexpr Vec4(const T& x_, const T& y_, const T& z_, const T& w_)
|
||||||
|
: x(x_), y(y_), z(z_), w(w_) {}
|
||||||
|
|
||||||
|
template <typename T2>
|
||||||
|
[[nodiscard]] constexpr Vec4<T2> Cast() const {
|
||||||
|
return Vec4<T2>(static_cast<T2>(x), static_cast<T2>(y), static_cast<T2>(z),
|
||||||
|
static_cast<T2>(w));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static constexpr Vec4 AssignToAll(const T& f) {
|
||||||
|
return Vec4(f, f, f, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr Vec4<decltype(T{} + T{})> operator+(const Vec4& other) const {
|
||||||
|
return {x + other.x, y + other.y, z + other.z, w + other.w};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Vec4& operator+=(const Vec4& other) {
|
||||||
|
x += other.x;
|
||||||
|
y += other.y;
|
||||||
|
z += other.z;
|
||||||
|
w += other.w;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr Vec4<decltype(T{} - T{})> operator-(const Vec4& other) const {
|
||||||
|
return {x - other.x, y - other.y, z - other.z, w - other.w};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Vec4& operator-=(const Vec4& other) {
|
||||||
|
x -= other.x;
|
||||||
|
y -= other.y;
|
||||||
|
z -= other.z;
|
||||||
|
w -= other.w;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename U = T>
|
||||||
|
[[nodiscard]] constexpr Vec4<std::enable_if_t<std::is_signed_v<U>, U>> operator-() const {
|
||||||
|
return {-x, -y, -z, -w};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr Vec4<decltype(T{} * T{})> operator*(const Vec4& other) const {
|
||||||
|
return {x * other.x, y * other.y, z * other.z, w * other.w};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename V>
|
||||||
|
[[nodiscard]] constexpr Vec4<decltype(T{} * V{})> operator*(const V& f) const {
|
||||||
|
using TV = decltype(T{} * V{});
|
||||||
|
using C = std::common_type_t<T, V>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
static_cast<TV>(static_cast<C>(x) * static_cast<C>(f)),
|
||||||
|
static_cast<TV>(static_cast<C>(y) * static_cast<C>(f)),
|
||||||
|
static_cast<TV>(static_cast<C>(z) * static_cast<C>(f)),
|
||||||
|
static_cast<TV>(static_cast<C>(w) * static_cast<C>(f)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename V>
|
||||||
|
constexpr Vec4& operator*=(const V& f) {
|
||||||
|
*this = *this * f;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename V>
|
||||||
|
[[nodiscard]] constexpr Vec4<decltype(T{} / V{})> operator/(const V& f) const {
|
||||||
|
using TV = decltype(T{} / V{});
|
||||||
|
using C = std::common_type_t<T, V>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
static_cast<TV>(static_cast<C>(x) / static_cast<C>(f)),
|
||||||
|
static_cast<TV>(static_cast<C>(y) / static_cast<C>(f)),
|
||||||
|
static_cast<TV>(static_cast<C>(z) / static_cast<C>(f)),
|
||||||
|
static_cast<TV>(static_cast<C>(w) / static_cast<C>(f)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename V>
|
||||||
|
constexpr Vec4& operator/=(const V& f) {
|
||||||
|
*this = *this / f;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr T Length2() const {
|
||||||
|
return x * x + y * y + z * z + w * w;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr T& operator[](std::size_t i) {
|
||||||
|
return *((&x) + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr const T& operator[](std::size_t i) const {
|
||||||
|
return *((&x) + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetZero() {
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
z = 0;
|
||||||
|
w = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common alias: RGBA (colors)
|
||||||
|
[[nodiscard]] constexpr T& r() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& g() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& b() {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr T& a() {
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr const T& r() const {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& g() const {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& b() const {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr const T& a() const {
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swizzlers - Create a subvector of specific components
|
||||||
|
// e.g. Vec2 uv() { return Vec2(x,y); }
|
||||||
|
|
||||||
|
// _DEFINE_SWIZZLER2 defines a single such function
|
||||||
|
// DEFINE_SWIZZLER2_COMP1 defines one-component functions for all component names (x<->r)
|
||||||
|
// DEFINE_SWIZZLER2_COMP2 defines two component functions for all component names (x<->r) and
|
||||||
|
// permutations (xy<->yx)
|
||||||
|
#define _DEFINE_SWIZZLER2(a, b, name) \
|
||||||
|
[[nodiscard]] constexpr Vec2<T> name() const { return Vec2<T>(a, b); }
|
||||||
|
#define DEFINE_SWIZZLER2_COMP1(a, a2) \
|
||||||
|
_DEFINE_SWIZZLER2(a, a, a##a); \
|
||||||
|
_DEFINE_SWIZZLER2(a, a, a2##a2)
|
||||||
|
#define DEFINE_SWIZZLER2_COMP2(a, b, a2, b2) \
|
||||||
|
_DEFINE_SWIZZLER2(a, b, a##b); \
|
||||||
|
_DEFINE_SWIZZLER2(a, b, a2##b2); \
|
||||||
|
_DEFINE_SWIZZLER2(b, a, b##a); \
|
||||||
|
_DEFINE_SWIZZLER2(b, a, b2##a2)
|
||||||
|
|
||||||
|
DEFINE_SWIZZLER2_COMP2(x, y, r, g);
|
||||||
|
DEFINE_SWIZZLER2_COMP2(x, z, r, b);
|
||||||
|
DEFINE_SWIZZLER2_COMP2(x, w, r, a);
|
||||||
|
DEFINE_SWIZZLER2_COMP2(y, z, g, b);
|
||||||
|
DEFINE_SWIZZLER2_COMP2(y, w, g, a);
|
||||||
|
DEFINE_SWIZZLER2_COMP2(z, w, b, a);
|
||||||
|
DEFINE_SWIZZLER2_COMP1(x, r);
|
||||||
|
DEFINE_SWIZZLER2_COMP1(y, g);
|
||||||
|
DEFINE_SWIZZLER2_COMP1(z, b);
|
||||||
|
DEFINE_SWIZZLER2_COMP1(w, a);
|
||||||
|
#undef DEFINE_SWIZZLER2_COMP1
|
||||||
|
#undef DEFINE_SWIZZLER2_COMP2
|
||||||
|
#undef _DEFINE_SWIZZLER2
|
||||||
|
|
||||||
|
#define _DEFINE_SWIZZLER3(a, b, c, name) \
|
||||||
|
[[nodiscard]] constexpr Vec3<T> name() const { return Vec3<T>(a, b, c); }
|
||||||
|
#define DEFINE_SWIZZLER3_COMP1(a, a2) \
|
||||||
|
_DEFINE_SWIZZLER3(a, a, a, a##a##a); \
|
||||||
|
_DEFINE_SWIZZLER3(a, a, a, a2##a2##a2)
|
||||||
|
#define DEFINE_SWIZZLER3_COMP3(a, b, c, a2, b2, c2) \
|
||||||
|
_DEFINE_SWIZZLER3(a, b, c, a##b##c); \
|
||||||
|
_DEFINE_SWIZZLER3(a, c, b, a##c##b); \
|
||||||
|
_DEFINE_SWIZZLER3(b, a, c, b##a##c); \
|
||||||
|
_DEFINE_SWIZZLER3(b, c, a, b##c##a); \
|
||||||
|
_DEFINE_SWIZZLER3(c, a, b, c##a##b); \
|
||||||
|
_DEFINE_SWIZZLER3(c, b, a, c##b##a); \
|
||||||
|
_DEFINE_SWIZZLER3(a, b, c, a2##b2##c2); \
|
||||||
|
_DEFINE_SWIZZLER3(a, c, b, a2##c2##b2); \
|
||||||
|
_DEFINE_SWIZZLER3(b, a, c, b2##a2##c2); \
|
||||||
|
_DEFINE_SWIZZLER3(b, c, a, b2##c2##a2); \
|
||||||
|
_DEFINE_SWIZZLER3(c, a, b, c2##a2##b2); \
|
||||||
|
_DEFINE_SWIZZLER3(c, b, a, c2##b2##a2)
|
||||||
|
|
||||||
|
DEFINE_SWIZZLER3_COMP3(x, y, z, r, g, b);
|
||||||
|
DEFINE_SWIZZLER3_COMP3(x, y, w, r, g, a);
|
||||||
|
DEFINE_SWIZZLER3_COMP3(x, z, w, r, b, a);
|
||||||
|
DEFINE_SWIZZLER3_COMP3(y, z, w, g, b, a);
|
||||||
|
DEFINE_SWIZZLER3_COMP1(x, r);
|
||||||
|
DEFINE_SWIZZLER3_COMP1(y, g);
|
||||||
|
DEFINE_SWIZZLER3_COMP1(z, b);
|
||||||
|
DEFINE_SWIZZLER3_COMP1(w, a);
|
||||||
|
#undef DEFINE_SWIZZLER3_COMP1
|
||||||
|
#undef DEFINE_SWIZZLER3_COMP3
|
||||||
|
#undef _DEFINE_SWIZZLER3
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T, typename V>
|
||||||
|
[[nodiscard]] constexpr Vec4<decltype(V{} * T{})> operator*(const V& f, const Vec4<T>& vec) {
|
||||||
|
using TV = decltype(V{} * T{});
|
||||||
|
using C = std::common_type_t<T, V>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
static_cast<TV>(static_cast<C>(f) * static_cast<C>(vec.x)),
|
||||||
|
static_cast<TV>(static_cast<C>(f) * static_cast<C>(vec.y)),
|
||||||
|
static_cast<TV>(static_cast<C>(f) * static_cast<C>(vec.z)),
|
||||||
|
static_cast<TV>(static_cast<C>(f) * static_cast<C>(vec.w)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
using Vec4f = Vec4<float>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr decltype(T{} * T{} + T{} * T{}) Dot(const Vec2<T>& a, const Vec2<T>& b) {
|
||||||
|
return a.x * b.x + a.y * b.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr decltype(T{} * T{} + T{} * T{}) Dot(const Vec3<T>& a, const Vec3<T>& b) {
|
||||||
|
return a.x * b.x + a.y * b.y + a.z * b.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr decltype(T{} * T{} + T{} * T{}) Dot(const Vec4<T>& a, const Vec4<T>& b) {
|
||||||
|
return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
[[nodiscard]] inline float Dot(const Vec4<float>& a, const Vec4<float>& b) {
|
||||||
|
#ifdef __ARM_NEON
|
||||||
|
float32x4_t va = vld1q_f32(&a.x);
|
||||||
|
float32x4_t vb = vld1q_f32(&b.x);
|
||||||
|
float32x4_t result = vmulq_f32(va, vb);
|
||||||
|
#if defined(__aarch64__) // Use vaddvq_f32 in ARMv8 architectures
|
||||||
|
return vaddvq_f32(result);
|
||||||
|
#else // Use manual addition for older architectures
|
||||||
|
float32x2_t sum2 = vadd_f32(vget_high_f32(result), vget_low_f32(result));
|
||||||
|
return vget_lane_f32(vpadd_f32(sum2, sum2), 0);
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec3<decltype(T{} * T{} - T{} * T{})> Cross(const Vec3<T>& a,
|
||||||
|
const Vec3<T>& b) {
|
||||||
|
return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
|
||||||
|
}
|
||||||
|
|
||||||
|
// linear interpolation via float: 0.0=begin, 1.0=end
|
||||||
|
template <typename X>
|
||||||
|
[[nodiscard]] constexpr decltype(X{} * float{} + X{} * float{}) Lerp(const X& begin, const X& end,
|
||||||
|
const float t) {
|
||||||
|
return begin * (1.f - t) + end * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// linear interpolation via int: 0=begin, base=end
|
||||||
|
template <typename X, int base>
|
||||||
|
[[nodiscard]] constexpr decltype((X{} * int{} + X{} * int{}) / base) LerpInt(const X& begin,
|
||||||
|
const X& end,
|
||||||
|
const int t) {
|
||||||
|
return (begin * (base - t) + end * t) / base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bilinear interpolation. s is for interpolating x00-x01 and x10-x11, and t is for the second
|
||||||
|
// interpolation.
|
||||||
|
template <typename X>
|
||||||
|
[[nodiscard]] constexpr auto BilinearInterp(const X& x00, const X& x01, const X& x10, const X& x11,
|
||||||
|
const float s, const float t) {
|
||||||
|
auto y0 = Lerp(x00, x01, s);
|
||||||
|
auto y1 = Lerp(x10, x11, s);
|
||||||
|
return Lerp(y0, y1, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility vector factories
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec2<T> MakeVec(const T& x, const T& y) {
|
||||||
|
return Vec2<T>{x, y};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec3<T> MakeVec(const T& x, const T& y, const T& z) {
|
||||||
|
return Vec3<T>{x, y, z};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec4<T> MakeVec(const T& x, const T& y, const Vec2<T>& zw) {
|
||||||
|
return MakeVec(x, y, zw[0], zw[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec3<T> MakeVec(const Vec2<T>& xy, const T& z) {
|
||||||
|
return MakeVec(xy[0], xy[1], z);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec3<T> MakeVec(const T& x, const Vec2<T>& yz) {
|
||||||
|
return MakeVec(x, yz[0], yz[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec4<T> MakeVec(const T& x, const T& y, const T& z, const T& w) {
|
||||||
|
return Vec4<T>{x, y, z, w};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec4<T> MakeVec(const Vec2<T>& xy, const T& z, const T& w) {
|
||||||
|
return MakeVec(xy[0], xy[1], z, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec4<T> MakeVec(const T& x, const Vec2<T>& yz, const T& w) {
|
||||||
|
return MakeVec(x, yz[0], yz[1], w);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This has priority over "Vec2<Vec2<T>> MakeVec(const Vec2<T>& x, const Vec2<T>& y)".
|
||||||
|
// Even if someone wanted to use an odd object like Vec2<Vec2<T>>, the compiler would error
|
||||||
|
// out soon enough due to misuse of the returned structure.
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec4<T> MakeVec(const Vec2<T>& xy, const Vec2<T>& zw) {
|
||||||
|
return MakeVec(xy[0], xy[1], zw[0], zw[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec4<T> MakeVec(const Vec3<T>& xyz, const T& w) {
|
||||||
|
return MakeVec(xyz[0], xyz[1], xyz[2], w);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
[[nodiscard]] constexpr Vec4<T> MakeVec(const T& x, const Vec3<T>& yzw) {
|
||||||
|
return MakeVec(x, yzw[0], yzw[1], yzw[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -33,15 +30,15 @@ struct DeviceSettings {
|
|||||||
INSERT_PADDING_BYTES(0x20); // Reserved
|
INSERT_PADDING_BYTES(0x20); // Reserved
|
||||||
|
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAccelerationBias
|
// nn::settings::system::ConsoleSixAxisSensorAccelerationBias
|
||||||
Common::Vec<f32, 3> console_six_axis_sensor_acceleration_bias;
|
Common::Vec3<f32> console_six_axis_sensor_acceleration_bias;
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityBias
|
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityBias
|
||||||
Common::Vec<f32, 3> console_six_axis_sensor_angular_velocity_bias;
|
Common::Vec3<f32> console_six_axis_sensor_angular_velocity_bias;
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAccelerationGain
|
// nn::settings::system::ConsoleSixAxisSensorAccelerationGain
|
||||||
std::array<u8, 0x24> console_six_axis_sensor_acceleration_gain;
|
std::array<u8, 0x24> console_six_axis_sensor_acceleration_gain;
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityGain
|
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityGain
|
||||||
std::array<u8, 0x24> console_six_axis_sensor_angular_velocity_gain;
|
std::array<u8, 0x24> console_six_axis_sensor_angular_velocity_gain;
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityTimeBias
|
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityTimeBias
|
||||||
Common::Vec<f32, 3> console_six_axis_sensor_angular_velocity_time_bias;
|
Common::Vec3<f32> console_six_axis_sensor_angular_velocity_time_bias;
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAngularAcceleration
|
// nn::settings::system::ConsoleSixAxisSensorAngularAcceleration
|
||||||
std::array<u8, 0x24> console_six_axis_sensor_angular_acceleration;
|
std::array<u8, 0x24> console_six_axis_sensor_angular_acceleration;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
@@ -153,15 +153,15 @@ struct SystemSettings {
|
|||||||
INSERT_PADDING_BYTES(0x7FF8); // Reserved
|
INSERT_PADDING_BYTES(0x7FF8); // Reserved
|
||||||
|
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAccelerationBias
|
// nn::settings::system::ConsoleSixAxisSensorAccelerationBias
|
||||||
Common::Vec<f32, 3> console_six_axis_sensor_acceleration_bias;
|
Common::Vec3<f32> console_six_axis_sensor_acceleration_bias;
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityBias
|
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityBias
|
||||||
Common::Vec<f32, 3> console_six_axis_sensor_angular_velocity_bias;
|
Common::Vec3<f32> console_six_axis_sensor_angular_velocity_bias;
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAccelerationGain
|
// nn::settings::system::ConsoleSixAxisSensorAccelerationGain
|
||||||
std::array<u8, 0x24> console_six_axis_sensor_acceleration_gain;
|
std::array<u8, 0x24> console_six_axis_sensor_acceleration_gain;
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityGain
|
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityGain
|
||||||
std::array<u8, 0x24> console_six_axis_sensor_angular_velocity_gain;
|
std::array<u8, 0x24> console_six_axis_sensor_angular_velocity_gain;
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityTimeBias
|
// nn::settings::system::ConsoleSixAxisSensorAngularVelocityTimeBias
|
||||||
Common::Vec<f32, 3> console_six_axis_sensor_angular_velocity_time_bias;
|
Common::Vec3<f32> console_six_axis_sensor_angular_velocity_time_bias;
|
||||||
// nn::settings::system::ConsoleSixAxisSensorAngularAcceleration
|
// nn::settings::system::ConsoleSixAxisSensorAngularAcceleration
|
||||||
std::array<u8, 0x24> console_six_axis_sensor_angular_velocity_acceleration;
|
std::array<u8, 0x24> console_six_axis_sensor_angular_velocity_acceleration;
|
||||||
INSERT_PADDING_BYTES(0x70); // Reserved
|
INSERT_PADDING_BYTES(0x70); // Reserved
|
||||||
|
|||||||
@@ -170,12 +170,12 @@ void EmulatedConsole::SetMotion(const Common::Input::CallbackStatus& callback) {
|
|||||||
auto& emulated = console.motion_values.emulated;
|
auto& emulated = console.motion_values.emulated;
|
||||||
|
|
||||||
raw_status = TransformToMotion(callback);
|
raw_status = TransformToMotion(callback);
|
||||||
emulated.SetAcceleration(Common::Vec<f32, 3>{
|
emulated.SetAcceleration(Common::Vec3f{
|
||||||
raw_status.accel.x.value,
|
raw_status.accel.x.value,
|
||||||
raw_status.accel.y.value,
|
raw_status.accel.y.value,
|
||||||
raw_status.accel.z.value,
|
raw_status.accel.z.value,
|
||||||
});
|
});
|
||||||
emulated.SetGyroscope(Common::Vec<f32, 3>{
|
emulated.SetGyroscope(Common::Vec3f{
|
||||||
raw_status.gyro.x.value,
|
raw_status.gyro.x.value,
|
||||||
raw_status.gyro.y.value,
|
raw_status.gyro.y.value,
|
||||||
raw_status.gyro.z.value,
|
raw_status.gyro.z.value,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "common/input.h"
|
#include "common/input.h"
|
||||||
#include "common/param_package.h"
|
#include "common/param_package.h"
|
||||||
#include "common/point.h"
|
#include "common/point.h"
|
||||||
|
#include "common/quaternion.h"
|
||||||
#include "common/vector_math.h"
|
#include "common/vector_math.h"
|
||||||
#include "hid_core/frontend/motion_input.h"
|
#include "hid_core/frontend/motion_input.h"
|
||||||
#include "hid_core/hid_types.h"
|
#include "hid_core/hid_types.h"
|
||||||
@@ -42,12 +43,12 @@ using TouchValues = std::array<Common::Input::TouchStatus, MaxTouchDevices>;
|
|||||||
|
|
||||||
// Contains all motion related data that is used on the services
|
// Contains all motion related data that is used on the services
|
||||||
struct ConsoleMotion {
|
struct ConsoleMotion {
|
||||||
Common::Vec<f32, 3> accel{};
|
Common::Vec3f accel{};
|
||||||
Common::Vec<f32, 3> gyro{};
|
Common::Vec3f gyro{};
|
||||||
Common::Vec<f32, 3> rotation{};
|
Common::Vec3f rotation{};
|
||||||
std::array<Common::Vec<f32, 3>, 3> orientation{};
|
std::array<Common::Vec3f, 3> orientation{};
|
||||||
Common::Vec<f32, 4> quaternion{};
|
Common::Quaternion<f32> quaternion{};
|
||||||
Common::Vec<f32, 3> gyro_bias{};
|
Common::Vec3f gyro_bias{};
|
||||||
f32 verticalization_error{};
|
f32 verticalization_error{};
|
||||||
bool is_at_rest{};
|
bool is_at_rest{};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1051,12 +1051,12 @@ void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback
|
|||||||
auto& emulated = controller.motion_values[index].emulated;
|
auto& emulated = controller.motion_values[index].emulated;
|
||||||
|
|
||||||
raw_status = TransformToMotion(callback);
|
raw_status = TransformToMotion(callback);
|
||||||
emulated.SetAcceleration(Common::Vec<f32, 3>{
|
emulated.SetAcceleration(Common::Vec3f{
|
||||||
raw_status.accel.x.value,
|
raw_status.accel.x.value,
|
||||||
raw_status.accel.y.value,
|
raw_status.accel.y.value,
|
||||||
raw_status.accel.z.value,
|
raw_status.accel.z.value,
|
||||||
});
|
});
|
||||||
emulated.SetGyroscope(Common::Vec<f32, 3>{
|
emulated.SetGyroscope(Common::Vec3f{
|
||||||
raw_status.gyro.x.value,
|
raw_status.gyro.x.value,
|
||||||
raw_status.gyro.y.value,
|
raw_status.gyro.y.value,
|
||||||
raw_status.gyro.z.value,
|
raw_status.gyro.z.value,
|
||||||
|
|||||||
@@ -107,11 +107,11 @@ struct RingSensorForce {
|
|||||||
using NfcState = Common::Input::NfcStatus;
|
using NfcState = Common::Input::NfcStatus;
|
||||||
|
|
||||||
struct ControllerMotion {
|
struct ControllerMotion {
|
||||||
Common::Vec<f32, 3> accel{};
|
Common::Vec3f accel{};
|
||||||
Common::Vec<f32, 3> gyro{};
|
Common::Vec3f gyro{};
|
||||||
Common::Vec<f32, 3> rotation{};
|
Common::Vec3f rotation{};
|
||||||
Common::Vec<f32, 3> euler{};
|
Common::Vec3f euler{};
|
||||||
std::array<Common::Vec<f32, 3>, 3> orientation{};
|
std::array<Common::Vec3f, 3> orientation{};
|
||||||
bool is_at_rest{};
|
bool is_at_rest{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,19 +26,20 @@ void MotionInput::SetPID(f32 new_kp, f32 new_ki, f32 new_kd) {
|
|||||||
kd = new_kd;
|
kd = new_kd;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MotionInput::SetAcceleration(const Common::Vec<f32, 3>& acceleration) {
|
void MotionInput::SetAcceleration(const Common::Vec3f& acceleration) {
|
||||||
accel = acceleration;
|
accel = acceleration;
|
||||||
accel[0] = std::clamp(accel[0], -AccelMaxValue, AccelMaxValue);
|
|
||||||
accel[1] = std::clamp(accel[1], -AccelMaxValue, AccelMaxValue);
|
accel.x = std::clamp(accel.x, -AccelMaxValue, AccelMaxValue);
|
||||||
accel[2] = std::clamp(accel[2], -AccelMaxValue, AccelMaxValue);
|
accel.y = std::clamp(accel.y, -AccelMaxValue, AccelMaxValue);
|
||||||
|
accel.z = std::clamp(accel.z, -AccelMaxValue, AccelMaxValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MotionInput::SetGyroscope(const Common::Vec<f32, 3>& gyroscope) {
|
void MotionInput::SetGyroscope(const Common::Vec3f& gyroscope) {
|
||||||
gyro = gyroscope - gyro_bias;
|
gyro = gyroscope - gyro_bias;
|
||||||
|
|
||||||
gyro[0] = std::clamp(gyro[0], -GyroMaxValue, GyroMaxValue);
|
gyro.x = std::clamp(gyro.x, -GyroMaxValue, GyroMaxValue);
|
||||||
gyro[1] = std::clamp(gyro[1], -GyroMaxValue, GyroMaxValue);
|
gyro.y = std::clamp(gyro.y, -GyroMaxValue, GyroMaxValue);
|
||||||
gyro[2] = std::clamp(gyro[2], -GyroMaxValue, GyroMaxValue);
|
gyro.z = std::clamp(gyro.z, -GyroMaxValue, GyroMaxValue);
|
||||||
|
|
||||||
// Auto adjust gyro_bias to minimize drift
|
// Auto adjust gyro_bias to minimize drift
|
||||||
if (!IsMoving(IsAtRestRelaxed)) {
|
if (!IsMoving(IsAtRestRelaxed)) {
|
||||||
@@ -58,25 +59,25 @@ void MotionInput::SetGyroscope(const Common::Vec<f32, 3>& gyroscope) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MotionInput::SetQuaternion(const Common::Vec<f32, 4>& quaternion) {
|
void MotionInput::SetQuaternion(const Common::Quaternion<f32>& quaternion) {
|
||||||
quat = quaternion;
|
quat = quaternion;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MotionInput::SetEulerAngles(const Common::Vec<f32, 3>& euler_angles) {
|
void MotionInput::SetEulerAngles(const Common::Vec3f& euler_angles) {
|
||||||
const float cr = std::cos(euler_angles[0] * 0.5f);
|
const float cr = std::cos(euler_angles.x * 0.5f);
|
||||||
const float sr = std::sin(euler_angles[0] * 0.5f);
|
const float sr = std::sin(euler_angles.x * 0.5f);
|
||||||
const float cp = std::cos(euler_angles[1] * 0.5f);
|
const float cp = std::cos(euler_angles.y * 0.5f);
|
||||||
const float sp = std::sin(euler_angles[1] * 0.5f);
|
const float sp = std::sin(euler_angles.y * 0.5f);
|
||||||
const float cy = std::cos(euler_angles[2] * 0.5f);
|
const float cy = std::cos(euler_angles.z * 0.5f);
|
||||||
const float sy = std::sin(euler_angles[2] * 0.5f);
|
const float sy = std::sin(euler_angles.z * 0.5f);
|
||||||
|
|
||||||
quat[3] = cr * cp * cy + sr * sp * sy;
|
quat.w = cr * cp * cy + sr * sp * sy;
|
||||||
quat[0] = sr * cp * cy - cr * sp * sy;
|
quat.xyz.x = sr * cp * cy - cr * sp * sy;
|
||||||
quat[1] = cr * sp * cy + sr * cp * sy;
|
quat.xyz.y = cr * sp * cy + sr * cp * sy;
|
||||||
quat[2] = cr * cp * sy - sr * sp * cy;
|
quat.xyz.z = cr * cp * sy - sr * sp * cy;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MotionInput::SetGyroBias(const Common::Vec<f32, 3>& bias) {
|
void MotionInput::SetGyroBias(const Common::Vec3f& bias) {
|
||||||
gyro_bias = bias;
|
gyro_bias = bias;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ void MotionInput::ResetRotations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MotionInput::ResetQuaternion() {
|
void MotionInput::ResetQuaternion() {
|
||||||
quat = Common::Vec<f32, 4>{0.0f, 0.0f, -1.0f, 0.0f};
|
quat = {{0.0f, 0.0f, -1.0f}, 0.0f};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MotionInput::IsMoving(f32 sensitivity) const {
|
bool MotionInput::IsMoving(f32 sensitivity) const {
|
||||||
@@ -136,10 +137,10 @@ void MotionInput::UpdateOrientation(u64 elapsed_time) {
|
|||||||
ResetOrientation();
|
ResetOrientation();
|
||||||
}
|
}
|
||||||
// Short name local variable for readability
|
// Short name local variable for readability
|
||||||
f32 q1 = quat[3];
|
f32 q1 = quat.w;
|
||||||
f32 q2 = quat[0];
|
f32 q2 = quat.xyz[0];
|
||||||
f32 q3 = quat[1];
|
f32 q3 = quat.xyz[1];
|
||||||
f32 q4 = quat[2];
|
f32 q4 = quat.xyz[2];
|
||||||
const auto sample_period = static_cast<f32>(elapsed_time) / 1000000.0f;
|
const auto sample_period = static_cast<f32>(elapsed_time) / 1000000.0f;
|
||||||
|
|
||||||
// Ignore invalid elapsed time
|
// Ignore invalid elapsed time
|
||||||
@@ -149,23 +150,23 @@ void MotionInput::UpdateOrientation(u64 elapsed_time) {
|
|||||||
|
|
||||||
const auto normal_accel = accel.Normalized();
|
const auto normal_accel = accel.Normalized();
|
||||||
auto rad_gyro = gyro * std::numbers::pi_v<float> * 2.f;
|
auto rad_gyro = gyro * std::numbers::pi_v<float> * 2.f;
|
||||||
const f32 swap = rad_gyro[0];
|
const f32 swap = rad_gyro.x;
|
||||||
rad_gyro[0] = rad_gyro[1];
|
rad_gyro.x = rad_gyro.y;
|
||||||
rad_gyro[1] = -swap;
|
rad_gyro.y = -swap;
|
||||||
rad_gyro[2] = -rad_gyro[2];
|
rad_gyro.z = -rad_gyro.z;
|
||||||
|
|
||||||
// Clear gyro values if there is no gyro present
|
// Clear gyro values if there is no gyro present
|
||||||
if (only_accelerometer) {
|
if (only_accelerometer) {
|
||||||
rad_gyro[0] = 0;
|
rad_gyro.x = 0;
|
||||||
rad_gyro[1] = 0;
|
rad_gyro.y = 0;
|
||||||
rad_gyro[2] = 0;
|
rad_gyro.z = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore drift correction if acceleration is not reliable
|
// Ignore drift correction if acceleration is not reliable
|
||||||
if (accel.Length() >= 0.75f && accel.Length() <= 1.25f) {
|
if (accel.Length() >= 0.75f && accel.Length() <= 1.25f) {
|
||||||
const f32 ax = -normal_accel[0];
|
const f32 ax = -normal_accel.x;
|
||||||
const f32 ay = normal_accel[1];
|
const f32 ay = normal_accel.y;
|
||||||
const f32 az = -normal_accel[2];
|
const f32 az = -normal_accel.z;
|
||||||
|
|
||||||
// Estimated direction of gravity
|
// Estimated direction of gravity
|
||||||
const f32 vx = 2.0f * (q2 * q4 - q1 * q3);
|
const f32 vx = 2.0f * (q2 * q4 - q1 * q3);
|
||||||
@@ -173,7 +174,7 @@ void MotionInput::UpdateOrientation(u64 elapsed_time) {
|
|||||||
const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4;
|
const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4;
|
||||||
|
|
||||||
// Error is cross product between estimated direction and measured direction of gravity
|
// Error is cross product between estimated direction and measured direction of gravity
|
||||||
const Common::Vec<f32, 3> new_real_error{
|
const Common::Vec3f new_real_error = {
|
||||||
az * vx - ax * vz,
|
az * vx - ax * vz,
|
||||||
ay * vz - az * vy,
|
ay * vz - az * vy,
|
||||||
ax * vy - ay * vx,
|
ax * vy - ay * vx,
|
||||||
@@ -201,16 +202,16 @@ void MotionInput::UpdateOrientation(u64 elapsed_time) {
|
|||||||
rad_gyro += 10.0f * kd * derivative_error;
|
rad_gyro += 10.0f * kd * derivative_error;
|
||||||
|
|
||||||
// Emulate gyro values for games that need them
|
// Emulate gyro values for games that need them
|
||||||
gyro[0] = -rad_gyro[1];
|
gyro.x = -rad_gyro.y;
|
||||||
gyro[1] = rad_gyro[0];
|
gyro.y = rad_gyro.x;
|
||||||
gyro[2] = -rad_gyro[2];
|
gyro.z = -rad_gyro.z;
|
||||||
UpdateRotation(elapsed_time);
|
UpdateRotation(elapsed_time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const f32 gx = rad_gyro[1];
|
const f32 gx = rad_gyro.y;
|
||||||
const f32 gy = rad_gyro[0];
|
const f32 gy = rad_gyro.x;
|
||||||
const f32 gz = rad_gyro[2];
|
const f32 gz = rad_gyro.z;
|
||||||
|
|
||||||
// Integrate rate of change of quaternion
|
// Integrate rate of change of quaternion
|
||||||
const f32 pa = q2;
|
const f32 pa = q2;
|
||||||
@@ -221,58 +222,57 @@ void MotionInput::UpdateOrientation(u64 elapsed_time) {
|
|||||||
q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period);
|
q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period);
|
||||||
q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period);
|
q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period);
|
||||||
|
|
||||||
quat[3] = q1;
|
quat.w = q1;
|
||||||
quat[0] = q2;
|
quat.xyz[0] = q2;
|
||||||
quat[1] = q3;
|
quat.xyz[1] = q3;
|
||||||
quat[2] = q4;
|
quat.xyz[2] = q4;
|
||||||
quat = quat.Normalized();
|
quat = quat.Normalized();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::array<Common::Vec<f32, 3>, 3> MotionInput::GetOrientation() const {
|
std::array<Common::Vec3f, 3> MotionInput::GetOrientation() const {
|
||||||
const Common::Vec<f32, 4> quad{
|
const Common::Quaternion<float> quad{
|
||||||
-quat[1],
|
.xyz = {-quat.xyz[1], -quat.xyz[0], -quat.w},
|
||||||
-quat[0],
|
.w = -quat.xyz[2],
|
||||||
-quat[3],
|
|
||||||
-quat[2],
|
|
||||||
};
|
};
|
||||||
const std::array<f32, 16> matrix4x4 = quad.ToMatrix();
|
const std::array<float, 16> matrix4x4 = quad.ToMatrix();
|
||||||
return {Common::Vec<f32, 3>(matrix4x4[0], matrix4x4[1], -matrix4x4[2]),
|
|
||||||
Common::Vec<f32, 3>(matrix4x4[4], matrix4x4[5], -matrix4x4[6]),
|
return {Common::Vec3f(matrix4x4[0], matrix4x4[1], -matrix4x4[2]),
|
||||||
Common::Vec<f32, 3>(-matrix4x4[8], -matrix4x4[9], matrix4x4[10])};
|
Common::Vec3f(matrix4x4[4], matrix4x4[5], -matrix4x4[6]),
|
||||||
|
Common::Vec3f(-matrix4x4[8], -matrix4x4[9], matrix4x4[10])};
|
||||||
}
|
}
|
||||||
|
|
||||||
Common::Vec<f32, 3> MotionInput::GetAcceleration() const {
|
Common::Vec3f MotionInput::GetAcceleration() const {
|
||||||
return accel;
|
return accel;
|
||||||
}
|
}
|
||||||
|
|
||||||
Common::Vec<f32, 3> MotionInput::GetGyroscope() const {
|
Common::Vec3f MotionInput::GetGyroscope() const {
|
||||||
return gyro;
|
return gyro;
|
||||||
}
|
}
|
||||||
|
|
||||||
Common::Vec<f32, 3> MotionInput::GetGyroBias() const {
|
Common::Vec3f MotionInput::GetGyroBias() const {
|
||||||
return gyro_bias;
|
return gyro_bias;
|
||||||
}
|
}
|
||||||
|
|
||||||
Common::Vec<f32, 4> MotionInput::GetQuaternion() const {
|
Common::Quaternion<f32> MotionInput::GetQuaternion() const {
|
||||||
return quat;
|
return quat;
|
||||||
}
|
}
|
||||||
|
|
||||||
Common::Vec<f32, 3> MotionInput::GetRotations() const {
|
Common::Vec3f MotionInput::GetRotations() const {
|
||||||
return rotations;
|
return rotations;
|
||||||
}
|
}
|
||||||
|
|
||||||
Common::Vec<f32, 3> MotionInput::GetEulerAngles() const {
|
Common::Vec3f MotionInput::GetEulerAngles() const {
|
||||||
// roll (x-axis rotation)
|
// roll (x-axis rotation)
|
||||||
const float sinr_cosp = 2 * (quat[3] * quat[0] + quat[1] * quat[2]);
|
const float sinr_cosp = 2 * (quat.w * quat.xyz.x + quat.xyz.y * quat.xyz.z);
|
||||||
const float cosr_cosp = 1 - 2 * (quat[0] * quat[0] + quat[1] * quat[1]);
|
const float cosr_cosp = 1 - 2 * (quat.xyz.x * quat.xyz.x + quat.xyz.y * quat.xyz.y);
|
||||||
|
|
||||||
// pitch (y-axis rotation)
|
// pitch (y-axis rotation)
|
||||||
const float sinp = std::sqrt(1 + 2 * (quat[3] * quat[1] - quat[0] * quat[2]));
|
const float sinp = std::sqrt(1 + 2 * (quat.w * quat.xyz.y - quat.xyz.x * quat.xyz.z));
|
||||||
const float cosp = std::sqrt(1 - 2 * (quat[3] * quat[1] - quat[0] * quat[2]));
|
const float cosp = std::sqrt(1 - 2 * (quat.w * quat.xyz.y - quat.xyz.x * quat.xyz.z));
|
||||||
|
|
||||||
// yaw (z-axis rotation)
|
// yaw (z-axis rotation)
|
||||||
const float siny_cosp = 2 * (quat[3] * quat[2] + quat[0] * quat[1]);
|
const float siny_cosp = 2 * (quat.w * quat.xyz.z + quat.xyz.x * quat.xyz.y);
|
||||||
const float cosy_cosp = 1 - 2 * (quat[1] * quat[1] + quat[2] * quat[2]);
|
const float cosy_cosp = 1 - 2 * (quat.xyz.y * quat.xyz.y + quat.xyz.z * quat.xyz.z);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
std::atan2(sinr_cosp, cosr_cosp),
|
std::atan2(sinr_cosp, cosr_cosp),
|
||||||
@@ -285,13 +285,13 @@ void MotionInput::ResetOrientation() {
|
|||||||
if (!reset_enabled || only_accelerometer) {
|
if (!reset_enabled || only_accelerometer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!IsMoving(IsAtRestRelaxed) && accel[2] <= -0.9f) {
|
if (!IsMoving(IsAtRestRelaxed) && accel.z <= -0.9f) {
|
||||||
++reset_counter;
|
++reset_counter;
|
||||||
if (reset_counter > 900) {
|
if (reset_counter > 900) {
|
||||||
quat[3] = 0;
|
quat.w = 0;
|
||||||
quat[0] = 0;
|
quat.xyz[0] = 0;
|
||||||
quat[1] = 0;
|
quat.xyz[1] = 0;
|
||||||
quat[2] = -1;
|
quat.xyz[2] = -1;
|
||||||
SetOrientationFromAccelerometer();
|
SetOrientationFromAccelerometer();
|
||||||
integral_error = {};
|
integral_error = {};
|
||||||
reset_counter = 0;
|
reset_counter = 0;
|
||||||
@@ -309,15 +309,15 @@ void MotionInput::SetOrientationFromAccelerometer() {
|
|||||||
|
|
||||||
while (!IsCalibrated(0.01f) && ++iterations < 100) {
|
while (!IsCalibrated(0.01f) && ++iterations < 100) {
|
||||||
// Short name local variable for readability
|
// Short name local variable for readability
|
||||||
f32 q1 = quat[3];
|
f32 q1 = quat.w;
|
||||||
f32 q2 = quat[0];
|
f32 q2 = quat.xyz[0];
|
||||||
f32 q3 = quat[1];
|
f32 q3 = quat.xyz[1];
|
||||||
f32 q4 = quat[2];
|
f32 q4 = quat.xyz[2];
|
||||||
|
|
||||||
Common::Vec<f32, 3> rad_gyro;
|
Common::Vec3f rad_gyro;
|
||||||
const f32 ax = -normal_accel[0];
|
const f32 ax = -normal_accel.x;
|
||||||
const f32 ay = normal_accel[1];
|
const f32 ay = normal_accel.y;
|
||||||
const f32 az = -normal_accel[2];
|
const f32 az = -normal_accel.z;
|
||||||
|
|
||||||
// Estimated direction of gravity
|
// Estimated direction of gravity
|
||||||
const f32 vx = 2.0f * (q2 * q4 - q1 * q3);
|
const f32 vx = 2.0f * (q2 * q4 - q1 * q3);
|
||||||
@@ -325,7 +325,7 @@ void MotionInput::SetOrientationFromAccelerometer() {
|
|||||||
const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4;
|
const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4;
|
||||||
|
|
||||||
// Error is cross product between estimated direction and measured direction of gravity
|
// Error is cross product between estimated direction and measured direction of gravity
|
||||||
const Common::Vec<f32, 3> new_real_error = {
|
const Common::Vec3f new_real_error = {
|
||||||
az * vx - ax * vz,
|
az * vx - ax * vz,
|
||||||
ay * vz - az * vy,
|
ay * vz - az * vy,
|
||||||
ax * vy - ay * vx,
|
ax * vy - ay * vx,
|
||||||
@@ -338,9 +338,9 @@ void MotionInput::SetOrientationFromAccelerometer() {
|
|||||||
rad_gyro += 5.0f * ki * integral_error;
|
rad_gyro += 5.0f * ki * integral_error;
|
||||||
rad_gyro += 10.0f * kd * derivative_error;
|
rad_gyro += 10.0f * kd * derivative_error;
|
||||||
|
|
||||||
const f32 gx = rad_gyro[1];
|
const f32 gx = rad_gyro.y;
|
||||||
const f32 gy = rad_gyro[0];
|
const f32 gy = rad_gyro.x;
|
||||||
const f32 gz = rad_gyro[2];
|
const f32 gz = rad_gyro.z;
|
||||||
|
|
||||||
// Integrate rate of change of quaternion
|
// Integrate rate of change of quaternion
|
||||||
const f32 pa = q2;
|
const f32 pa = q2;
|
||||||
@@ -351,10 +351,10 @@ void MotionInput::SetOrientationFromAccelerometer() {
|
|||||||
q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period);
|
q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period);
|
||||||
q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period);
|
q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period);
|
||||||
|
|
||||||
quat[3] = q1;
|
quat.w = q1;
|
||||||
quat[0] = q2;
|
quat.xyz[0] = q2;
|
||||||
quat[1] = q3;
|
quat.xyz[1] = q3;
|
||||||
quat[2] = q4;
|
quat.xyz[2] = q4;
|
||||||
quat = quat.Normalized();
|
quat = quat.Normalized();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
#include "common/quaternion.h"
|
||||||
#include "common/vector_math.h"
|
#include "common/vector_math.h"
|
||||||
|
|
||||||
namespace Core::HID {
|
namespace Core::HID {
|
||||||
@@ -36,11 +34,11 @@ public:
|
|||||||
MotionInput& operator=(MotionInput&&) = default;
|
MotionInput& operator=(MotionInput&&) = default;
|
||||||
|
|
||||||
void SetPID(f32 new_kp, f32 new_ki, f32 new_kd);
|
void SetPID(f32 new_kp, f32 new_ki, f32 new_kd);
|
||||||
void SetAcceleration(const Common::Vec<f32, 3>& acceleration);
|
void SetAcceleration(const Common::Vec3f& acceleration);
|
||||||
void SetGyroscope(const Common::Vec<f32, 3>& gyroscope);
|
void SetGyroscope(const Common::Vec3f& gyroscope);
|
||||||
void SetQuaternion(const Common::Vec<f32, 4>& quaternion);
|
void SetQuaternion(const Common::Quaternion<f32>& quaternion);
|
||||||
void SetEulerAngles(const Common::Vec<f32, 3>& euler_angles);
|
void SetEulerAngles(const Common::Vec3f& euler_angles);
|
||||||
void SetGyroBias(const Common::Vec<f32, 3>& bias);
|
void SetGyroBias(const Common::Vec3f& bias);
|
||||||
void SetGyroThreshold(f32 threshold);
|
void SetGyroThreshold(f32 threshold);
|
||||||
|
|
||||||
/// Applies a modifier on top of the normal gyro threshold
|
/// Applies a modifier on top of the normal gyro threshold
|
||||||
@@ -55,13 +53,13 @@ public:
|
|||||||
|
|
||||||
void Calibrate();
|
void Calibrate();
|
||||||
|
|
||||||
[[nodiscard]] std::array<Common::Vec<f32, 3>, 3> GetOrientation() const;
|
[[nodiscard]] std::array<Common::Vec3f, 3> GetOrientation() const;
|
||||||
[[nodiscard]] Common::Vec<f32, 3> GetAcceleration() const;
|
[[nodiscard]] Common::Vec3f GetAcceleration() const;
|
||||||
[[nodiscard]] Common::Vec<f32, 3> GetGyroscope() const;
|
[[nodiscard]] Common::Vec3f GetGyroscope() const;
|
||||||
[[nodiscard]] Common::Vec<f32, 3> GetGyroBias() const;
|
[[nodiscard]] Common::Vec3f GetGyroBias() const;
|
||||||
[[nodiscard]] Common::Vec<f32, 3> GetRotations() const;
|
[[nodiscard]] Common::Vec3f GetRotations() const;
|
||||||
[[nodiscard]] Common::Vec<f32, 4> GetQuaternion() const;
|
[[nodiscard]] Common::Quaternion<f32> GetQuaternion() const;
|
||||||
[[nodiscard]] Common::Vec<f32, 3> GetEulerAngles() const;
|
[[nodiscard]] Common::Vec3f GetEulerAngles() const;
|
||||||
|
|
||||||
[[nodiscard]] bool IsMoving(f32 sensitivity) const;
|
[[nodiscard]] bool IsMoving(f32 sensitivity) const;
|
||||||
[[nodiscard]] bool IsCalibrated(f32 sensitivity) const;
|
[[nodiscard]] bool IsCalibrated(f32 sensitivity) const;
|
||||||
@@ -77,24 +75,24 @@ private:
|
|||||||
f32 kd;
|
f32 kd;
|
||||||
|
|
||||||
// PID errors
|
// PID errors
|
||||||
Common::Vec<f32, 3> real_error;
|
Common::Vec3f real_error;
|
||||||
Common::Vec<f32, 3> integral_error;
|
Common::Vec3f integral_error;
|
||||||
Common::Vec<f32, 3> derivative_error;
|
Common::Vec3f derivative_error;
|
||||||
|
|
||||||
// Quaternion containing the device orientation
|
// Quaternion containing the device orientation
|
||||||
Common::Vec<f32, 4> quat;
|
Common::Quaternion<f32> quat;
|
||||||
|
|
||||||
// Number of full rotations in each axis
|
// Number of full rotations in each axis
|
||||||
Common::Vec<f32, 3> rotations;
|
Common::Vec3f rotations;
|
||||||
|
|
||||||
// Acceleration vector measurement in G force
|
// Acceleration vector measurement in G force
|
||||||
Common::Vec<f32, 3> accel;
|
Common::Vec3f accel;
|
||||||
|
|
||||||
// Gyroscope vector measurement in radians/s.
|
// Gyroscope vector measurement in radians/s.
|
||||||
Common::Vec<f32, 3> gyro;
|
Common::Vec3f gyro;
|
||||||
|
|
||||||
// Vector to be subtracted from gyro measurements
|
// Vector to be subtracted from gyro measurements
|
||||||
Common::Vec<f32, 3> gyro_bias;
|
Common::Vec3f gyro_bias;
|
||||||
|
|
||||||
// Minimum gyro amplitude to detect if the device is moving
|
// Minimum gyro amplitude to detect if the device is moving
|
||||||
f32 gyro_threshold = 0.0f;
|
f32 gyro_threshold = 0.0f;
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -605,10 +602,10 @@ static_assert(sizeof(SixAxisSensorAttribute) == 4, "SixAxisSensorAttribute is an
|
|||||||
struct SixAxisSensorState {
|
struct SixAxisSensorState {
|
||||||
s64 delta_time{};
|
s64 delta_time{};
|
||||||
s64 sampling_number{};
|
s64 sampling_number{};
|
||||||
Common::Vec<f32, 3> accel{};
|
Common::Vec3f accel{};
|
||||||
Common::Vec<f32, 3> gyro{};
|
Common::Vec3f gyro{};
|
||||||
Common::Vec<f32, 3> rotation{};
|
Common::Vec3f rotation{};
|
||||||
std::array<Common::Vec<f32, 3>, 3> orientation{};
|
std::array<Common::Vec3f, 3> orientation{};
|
||||||
SixAxisSensorAttribute attribute{};
|
SixAxisSensorAttribute attribute{};
|
||||||
INSERT_PADDING_BYTES(4); // Reserved
|
INSERT_PADDING_BYTES(4); // Reserved
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
@@ -196,7 +196,7 @@ struct ConsoleSixAxisSensorSharedMemoryFormat {
|
|||||||
bool is_seven_six_axis_sensor_at_rest{};
|
bool is_seven_six_axis_sensor_at_rest{};
|
||||||
INSERT_PADDING_BYTES(3); // padding
|
INSERT_PADDING_BYTES(3); // padding
|
||||||
f32 verticalization_error{};
|
f32 verticalization_error{};
|
||||||
Common::Vec<f32, 3> gyro_bias{};
|
Common::Vec3f gyro_bias{};
|
||||||
INSERT_PADDING_BYTES(4); // padding
|
INSERT_PADDING_BYTES(4); // padding
|
||||||
};
|
};
|
||||||
static_assert(sizeof(ConsoleSixAxisSensorSharedMemoryFormat) == 0x20,
|
static_assert(sizeof(ConsoleSixAxisSensorSharedMemoryFormat) == 0x20,
|
||||||
|
|||||||
@@ -46,11 +46,14 @@ void SevenSixAxis::OnUpdate(const Core::Timing::CoreTiming& core_timing) {
|
|||||||
next_seven_sixaxis_state.accel = motion_status.accel;
|
next_seven_sixaxis_state.accel = motion_status.accel;
|
||||||
next_seven_sixaxis_state.gyro = motion_status.gyro;
|
next_seven_sixaxis_state.gyro = motion_status.gyro;
|
||||||
next_seven_sixaxis_state.quaternion = {
|
next_seven_sixaxis_state.quaternion = {
|
||||||
motion_status.quaternion[1],
|
{
|
||||||
motion_status.quaternion[0],
|
motion_status.quaternion.xyz.y,
|
||||||
-motion_status.quaternion[3],
|
motion_status.quaternion.xyz.x,
|
||||||
-motion_status.quaternion[2],
|
-motion_status.quaternion.w,
|
||||||
|
},
|
||||||
|
-motion_status.quaternion.xyz.z,
|
||||||
};
|
};
|
||||||
|
|
||||||
seven_sixaxis_lifo.WriteNextEntry(next_seven_sixaxis_state);
|
seven_sixaxis_lifo.WriteNextEntry(next_seven_sixaxis_state);
|
||||||
transfer_memory_owner->GetMemory().WriteBlock(transfer_memory, &seven_sixaxis_lifo,
|
transfer_memory_owner->GetMemory().WriteBlock(transfer_memory, &seven_sixaxis_lifo,
|
||||||
sizeof(seven_sixaxis_lifo));
|
sizeof(seven_sixaxis_lifo));
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/vector_math.h"
|
#include "common/quaternion.h"
|
||||||
#include "common/typed_address.h"
|
#include "common/typed_address.h"
|
||||||
#include "hid_core/resources/controller_base.h"
|
#include "hid_core/resources/controller_base.h"
|
||||||
#include "hid_core/resources/ring_lifo.h"
|
#include "hid_core/resources/ring_lifo.h"
|
||||||
@@ -51,9 +51,9 @@ private:
|
|||||||
u64 timestamp{};
|
u64 timestamp{};
|
||||||
u64 sampling_number{};
|
u64 sampling_number{};
|
||||||
u64 unknown{};
|
u64 unknown{};
|
||||||
Common::Vec<f32, 3> accel{};
|
Common::Vec3f accel{};
|
||||||
Common::Vec<f32, 3> gyro{};
|
Common::Vec3f gyro{};
|
||||||
Common::Vec<f32, 4> quaternion{};
|
Common::Quaternion<f32> quaternion{};
|
||||||
};
|
};
|
||||||
static_assert(sizeof(SevenSixAxisState) == 0x48, "SevenSixAxisState is an invalid size");
|
static_assert(sizeof(SevenSixAxisState) == 0x48, "SevenSixAxisState is an invalid size");
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
@@ -96,9 +93,9 @@ void SixAxis::OnUpdate(const Core::Timing::CoreTiming& core_timing) {
|
|||||||
.accel = {0, 0, -1.0f},
|
.accel = {0, 0, -1.0f},
|
||||||
.orientation =
|
.orientation =
|
||||||
{
|
{
|
||||||
Common::Vec<f32, 3>{1.0f, 0, 0},
|
Common::Vec3f{1.0f, 0, 0},
|
||||||
Common::Vec<f32, 3>{0, 1.0f, 0},
|
Common::Vec3f{0, 1.0f, 0},
|
||||||
Common::Vec<f32, 3>{0, 0, 1.0f},
|
Common::Vec3f{0, 0, 1.0f},
|
||||||
},
|
},
|
||||||
.attribute = {1},
|
.attribute = {1},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ void Mouse::UpdateStickInput() {
|
|||||||
last_mouse_change *= maximum_stick_range;
|
last_mouse_change *= maximum_stick_range;
|
||||||
}
|
}
|
||||||
|
|
||||||
SetAxis(identifier, mouse_axis_x, last_mouse_change[0]);
|
SetAxis(identifier, mouse_axis_x, last_mouse_change.x);
|
||||||
SetAxis(identifier, mouse_axis_y, -last_mouse_change[1]);
|
SetAxis(identifier, mouse_axis_y, -last_mouse_change.y);
|
||||||
|
|
||||||
// Decay input over time
|
// Decay input over time
|
||||||
const float clamped_length = (std::min)(1.0f, length);
|
const float clamped_length = (std::min)(1.0f, length);
|
||||||
@@ -104,20 +104,20 @@ void Mouse::UpdateMotionInput() {
|
|||||||
const float sensitivity =
|
const float sensitivity =
|
||||||
IsMousePanningEnabled() ? default_motion_panning_sensitivity : default_motion_sensitivity;
|
IsMousePanningEnabled() ? default_motion_panning_sensitivity : default_motion_sensitivity;
|
||||||
|
|
||||||
const float rotation_velocity = std::sqrt(last_motion_change[0] * last_motion_change[0] +
|
const float rotation_velocity = std::sqrt(last_motion_change.x * last_motion_change.x +
|
||||||
last_motion_change[1] * last_motion_change[1]);
|
last_motion_change.y * last_motion_change.y);
|
||||||
|
|
||||||
// Clamp rotation speed
|
// Clamp rotation speed
|
||||||
if (rotation_velocity > maximum_rotation_speed / sensitivity) {
|
if (rotation_velocity > maximum_rotation_speed / sensitivity) {
|
||||||
const float multiplier = maximum_rotation_speed / rotation_velocity / sensitivity;
|
const float multiplier = maximum_rotation_speed / rotation_velocity / sensitivity;
|
||||||
last_motion_change[0] = last_motion_change[0] * multiplier;
|
last_motion_change.x = last_motion_change.x * multiplier;
|
||||||
last_motion_change[1] = last_motion_change[1] * multiplier;
|
last_motion_change.y = last_motion_change.y * multiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BasicMotion motion_data{
|
const BasicMotion motion_data{
|
||||||
.gyro_x = last_motion_change[0] * sensitivity,
|
.gyro_x = last_motion_change.x * sensitivity,
|
||||||
.gyro_y = last_motion_change[1] * sensitivity,
|
.gyro_y = last_motion_change.y * sensitivity,
|
||||||
.gyro_z = last_motion_change[2] * sensitivity,
|
.gyro_z = last_motion_change.z * sensitivity,
|
||||||
.accel_x = 0,
|
.accel_x = 0,
|
||||||
.accel_y = 0,
|
.accel_y = 0,
|
||||||
.accel_z = 0,
|
.accel_z = 0,
|
||||||
@@ -125,46 +125,53 @@ void Mouse::UpdateMotionInput() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (IsMousePanningEnabled()) {
|
if (IsMousePanningEnabled()) {
|
||||||
last_motion_change[0] = 0;
|
last_motion_change.x = 0;
|
||||||
last_motion_change[1] = 0;
|
last_motion_change.y = 0;
|
||||||
}
|
}
|
||||||
last_motion_change[2] = 0;
|
last_motion_change.z = 0;
|
||||||
|
|
||||||
SetMotion(motion_identifier, 0, motion_data);
|
SetMotion(motion_identifier, 0, motion_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mouse::Move(int x, int y, int center_x, int center_y) {
|
void Mouse::Move(int x, int y, int center_x, int center_y) {
|
||||||
if (IsMousePanningEnabled()) {
|
if (IsMousePanningEnabled()) {
|
||||||
auto const mouse_change_int = Common::Vec<int, 2>(x, y) - Common::Vec<int, 2>(center_x, center_y);
|
const auto mouse_change =
|
||||||
auto const mouse_change = Common::Vec<float, 2>(float(mouse_change_int[0]), float(mouse_change_int[1]));
|
(Common::MakeVec(x, y) - Common::MakeVec(center_x, center_y)).Cast<float>();
|
||||||
auto const x_sensitivity = Settings::values.mouse_panning_x_sensitivity.GetValue() * default_panning_sensitivity;
|
const float x_sensitivity =
|
||||||
auto const y_sensitivity = Settings::values.mouse_panning_y_sensitivity.GetValue() * default_panning_sensitivity;
|
Settings::values.mouse_panning_x_sensitivity.GetValue() * default_panning_sensitivity;
|
||||||
auto const deadzone_cw = Settings::values.mouse_panning_deadzone_counterweight.GetValue() * default_deadzone_counterweight;
|
const float y_sensitivity =
|
||||||
last_motion_change += {-mouse_change[1] * x_sensitivity, -mouse_change[0] * y_sensitivity, 0};
|
Settings::values.mouse_panning_y_sensitivity.GetValue() * default_panning_sensitivity;
|
||||||
last_mouse_change[0] += mouse_change[0] * x_sensitivity;
|
const float deadzone_counterweight =
|
||||||
last_mouse_change[1] += mouse_change[1] * y_sensitivity;
|
Settings::values.mouse_panning_deadzone_counterweight.GetValue() *
|
||||||
// Bind the mouse change to [0 <= deadzone_cw <= 1.0]
|
default_deadzone_counterweight;
|
||||||
|
|
||||||
|
last_motion_change += {-mouse_change.y * x_sensitivity, -mouse_change.x * y_sensitivity, 0};
|
||||||
|
last_mouse_change.x += mouse_change.x * x_sensitivity;
|
||||||
|
last_mouse_change.y += mouse_change.y * y_sensitivity;
|
||||||
|
|
||||||
|
// Bind the mouse change to [0 <= deadzone_counterweight <= 1.0]
|
||||||
const float length = last_mouse_change.Length();
|
const float length = last_mouse_change.Length();
|
||||||
if (length < deadzone_cw && length != 0.0f) {
|
if (length < deadzone_counterweight && length != 0.0f) {
|
||||||
last_mouse_change /= length;
|
last_mouse_change /= length;
|
||||||
last_mouse_change *= deadzone_cw;
|
last_mouse_change *= deadzone_counterweight;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (button_pressed) {
|
if (button_pressed) {
|
||||||
const auto mouse_move = Common::Vec<int, 2>(x, y) - mouse_origin;
|
const auto mouse_move = Common::MakeVec<int>(x, y) - mouse_origin;
|
||||||
const float x_sensitivity =
|
const float x_sensitivity =
|
||||||
Settings::values.mouse_panning_x_sensitivity.GetValue() * default_stick_sensitivity;
|
Settings::values.mouse_panning_x_sensitivity.GetValue() * default_stick_sensitivity;
|
||||||
const float y_sensitivity =
|
const float y_sensitivity =
|
||||||
Settings::values.mouse_panning_y_sensitivity.GetValue() * default_stick_sensitivity;
|
Settings::values.mouse_panning_y_sensitivity.GetValue() * default_stick_sensitivity;
|
||||||
SetAxis(identifier, mouse_axis_x, float(mouse_move[0]) * x_sensitivity);
|
SetAxis(identifier, mouse_axis_x, static_cast<float>(mouse_move.x) * x_sensitivity);
|
||||||
SetAxis(identifier, mouse_axis_y, float(-mouse_move[1]) * y_sensitivity);
|
SetAxis(identifier, mouse_axis_y, static_cast<float>(-mouse_move.y) * y_sensitivity);
|
||||||
|
|
||||||
last_motion_change = {
|
last_motion_change = {
|
||||||
float(-mouse_move[1]) * x_sensitivity,
|
static_cast<float>(-mouse_move.y) * x_sensitivity,
|
||||||
float(-mouse_move[0]) * y_sensitivity,
|
static_cast<float>(-mouse_move.x) * y_sensitivity,
|
||||||
last_motion_change[2],
|
last_motion_change.z,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,18 +220,18 @@ void Mouse::ReleaseButton(MouseButton button) {
|
|||||||
SetAxis(identifier, mouse_axis_y, 0);
|
SetAxis(identifier, mouse_axis_y, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
last_motion_change[0] = 0;
|
last_motion_change.x = 0;
|
||||||
last_motion_change[1] = 0;
|
last_motion_change.y = 0;
|
||||||
|
|
||||||
button_pressed = false;
|
button_pressed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mouse::MouseWheelChange(int x, int y) {
|
void Mouse::MouseWheelChange(int x, int y) {
|
||||||
wheel_position[0] += x;
|
wheel_position.x += x;
|
||||||
wheel_position[1] += y;
|
wheel_position.y += y;
|
||||||
last_motion_change[2] += static_cast<f32>(y);
|
last_motion_change.z += static_cast<f32>(y);
|
||||||
SetAxis(identifier, wheel_axis_x, static_cast<f32>(wheel_position[0]));
|
SetAxis(identifier, wheel_axis_x, static_cast<f32>(wheel_position.x));
|
||||||
SetAxis(identifier, wheel_axis_y, static_cast<f32>(wheel_position[1]));
|
SetAxis(identifier, wheel_axis_y, static_cast<f32>(wheel_position.y));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mouse::ReleaseAllButtons() {
|
void Mouse::ReleaseAllButtons() {
|
||||||
|
|||||||
@@ -107,11 +107,11 @@ private:
|
|||||||
|
|
||||||
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
|
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
|
||||||
|
|
||||||
Common::Vec<int, 2> mouse_origin;
|
Common::Vec2<int> mouse_origin;
|
||||||
Common::Vec<int, 2> last_mouse_position;
|
Common::Vec2<int> last_mouse_position;
|
||||||
Common::Vec<float, 2> last_mouse_change;
|
Common::Vec2<float> last_mouse_change;
|
||||||
Common::Vec<float, 3> last_motion_change;
|
Common::Vec3<float> last_motion_change;
|
||||||
Common::Vec<int, 2> wheel_position;
|
Common::Vec2<int> wheel_position;
|
||||||
bool button_pressed = false;
|
bool button_pressed = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
|
|||||||
INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"),
|
INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"),
|
||||||
tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA "
|
tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA "
|
||||||
"can produce a more stable picture in lower resolutions."));
|
"can produce a more stable picture in lower resolutions."));
|
||||||
|
INSERT(Settings, post_shader_chain, QString(), QString());
|
||||||
INSERT(Settings, fullscreen_mode, tr("Fullscreen Mode:"),
|
INSERT(Settings, fullscreen_mode, tr("Fullscreen Mode:"),
|
||||||
tr("The method used to render the window in fullscreen.\nBorderless offers the best "
|
tr("The method used to render the window in fullscreen.\nBorderless offers the best "
|
||||||
"compatibility with the on-screen keyboard that some games request for "
|
"compatibility with the on-screen keyboard that some games request for "
|
||||||
|
|||||||
@@ -299,6 +299,21 @@ if (ENABLE_LSFG)
|
|||||||
target_compile_definitions(video_core PUBLIC HAS_LSFG)
|
target_compile_definitions(video_core PUBLIC HAS_LSFG)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (ENABLE_RESHADE)
|
||||||
|
target_sources(video_core PRIVATE
|
||||||
|
post_processing/fx_chain.cpp
|
||||||
|
post_processing/fx_chain.h
|
||||||
|
post_processing/fx_compile.cpp
|
||||||
|
post_processing/fx_compile.h
|
||||||
|
post_processing/fx_effect.cpp
|
||||||
|
post_processing/fx_effect.h
|
||||||
|
renderer_vulkan/present/post_process.cpp
|
||||||
|
renderer_vulkan/present/post_process.h
|
||||||
|
)
|
||||||
|
target_link_libraries(video_core PRIVATE reshadefx::reshadefx)
|
||||||
|
target_compile_definitions(video_core PUBLIC HAS_RESHADE)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (ENABLE_OPENGL)
|
if (ENABLE_OPENGL)
|
||||||
target_sources(video_core PRIVATE
|
target_sources(video_core PRIVATE
|
||||||
renderer_opengl/present/filters.cpp
|
renderer_opengl/present/filters.cpp
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
#include "common/settings.h"
|
||||||
|
#include "video_core/post_processing/fx_chain.h"
|
||||||
|
#include "video_core/post_processing/fx_effect.h"
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::vector<std::string_view> Split(std::string_view value, char separator) {
|
||||||
|
std::vector<std::string_view> out;
|
||||||
|
size_t start = 0;
|
||||||
|
while (start <= value.size()) {
|
||||||
|
size_t end = value.find(separator, start);
|
||||||
|
if (end == std::string_view::npos) {
|
||||||
|
end = value.size();
|
||||||
|
}
|
||||||
|
out.push_back(value.substr(start, end - start));
|
||||||
|
start = end + 1;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsSerializableName(std::string_view value) {
|
||||||
|
return value.find_first_of(";|,=") == std::string_view::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
std::vector<FxChainEntry> ParseFxChain(std::string_view value) {
|
||||||
|
std::vector<FxChainEntry> parsed;
|
||||||
|
|
||||||
|
for (const std::string_view record : Split(value, ';')) {
|
||||||
|
if (record.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto fields = Split(record, '|');
|
||||||
|
if (fields.size() < 2 || fields[0].empty() || fields[1].empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FxChainEntry entry;
|
||||||
|
entry.file = std::string(fields[0]);
|
||||||
|
entry.technique = std::string(fields[1]);
|
||||||
|
|
||||||
|
if (fields.size() >= 3) {
|
||||||
|
for (const std::string_view assignment : Split(fields[2], ',')) {
|
||||||
|
const size_t equals = assignment.find('=');
|
||||||
|
if (equals == std::string_view::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string name(assignment.substr(0, equals));
|
||||||
|
if (name.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<f32, 4> components{};
|
||||||
|
size_t index = 0;
|
||||||
|
for (const std::string_view piece : Split(assignment.substr(equals + 1), '/')) {
|
||||||
|
if (index >= components.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const std::string text(piece);
|
||||||
|
if (!text.empty()) {
|
||||||
|
components[index] = std::strtof(text.c_str(), nullptr);
|
||||||
|
}
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
entry.values.emplace(name, components);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.push_back(std::move(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string SerializeFxChain(std::span<const FxChainEntry> entries) {
|
||||||
|
std::string out;
|
||||||
|
|
||||||
|
for (const auto& entry : entries) {
|
||||||
|
if (!IsSerializableName(entry.file) || !IsSerializableName(entry.technique)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!out.empty()) {
|
||||||
|
out += ';';
|
||||||
|
}
|
||||||
|
out += entry.file;
|
||||||
|
out += '|';
|
||||||
|
out += entry.technique;
|
||||||
|
out += '|';
|
||||||
|
|
||||||
|
bool first = true;
|
||||||
|
for (const auto& [name, value] : entry.values) {
|
||||||
|
if (!IsSerializableName(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!first) {
|
||||||
|
out += ',';
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
out += name;
|
||||||
|
out += '=';
|
||||||
|
for (size_t i = 0; i < value.size(); ++i) {
|
||||||
|
if (i > 0) {
|
||||||
|
out += '/';
|
||||||
|
}
|
||||||
|
out += fmt::format("{}", value[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
FxChain& FxChain::Instance() {
|
||||||
|
static FxChain instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
FxChainSnapshot FxChain::Snapshot() const {
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
return FxChainSnapshot{
|
||||||
|
.entries = entries,
|
||||||
|
.generation = generation.load(std::memory_order_relaxed),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<FxChainEntry> FxChain::Entries() const {
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t FxChain::Size() const {
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
return entries.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FxChain::Append(std::string_view file, std::string_view technique) {
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
FxChainEntry entry;
|
||||||
|
entry.file = std::string(file);
|
||||||
|
entry.technique = std::string(technique);
|
||||||
|
entries.push_back(std::move(entry));
|
||||||
|
}
|
||||||
|
generation.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FxChain::Replace(size_t index, std::string_view file, std::string_view technique) {
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
if (index >= entries.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entries[index].file == file && entries[index].technique == technique) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FxChainEntry entry;
|
||||||
|
entry.file = std::string(file);
|
||||||
|
entry.technique = std::string(technique);
|
||||||
|
entries[index] = std::move(entry);
|
||||||
|
}
|
||||||
|
generation.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FxChain::Remove(size_t index) {
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
if (index >= entries.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entries.erase(entries.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
|
}
|
||||||
|
generation.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FxChain::Move(size_t index, int delta) {
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
if (index >= entries.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::ptrdiff_t target = static_cast<std::ptrdiff_t>(index) + delta;
|
||||||
|
if (target < 0 || target >= static_cast<std::ptrdiff_t>(entries.size())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::swap(entries[index], entries[static_cast<size_t>(target)]);
|
||||||
|
}
|
||||||
|
generation.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FxChain::Clear() {
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
entries.clear();
|
||||||
|
}
|
||||||
|
generation.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FxChain::SetValue(size_t index, std::string_view uniform, const std::array<f32, 4>& value) {
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
if (index >= entries.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entries[index].values[std::string(uniform)] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<f32, 4> FxChain::GetValue(size_t index, std::string_view uniform) const {
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
if (index >= entries.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto it = entries[index].values.find(std::string(uniform));
|
||||||
|
if (it == entries[index].values.end()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FxChain::HasValue(size_t index, std::string_view uniform) const {
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
if (index >= entries.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entries[index].values.contains(std::string(uniform));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FxChain::ResetValues(size_t index) {
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
if (index >= entries.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entries[index].values.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FxChain::LoadFromSettings() {
|
||||||
|
auto loaded = ParseFxChain(Settings::values.post_shader_chain.GetValue());
|
||||||
|
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
entries = std::move(loaded);
|
||||||
|
generation.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FxChain::StoreToSettings() const {
|
||||||
|
std::string serialized;
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
serialized = SerializeFxChain(entries);
|
||||||
|
}
|
||||||
|
Settings::values.post_shader_chain.SetValue(serialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FxChain::DropUnknownEntries() {
|
||||||
|
bool changed = false;
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
const auto removed = std::remove_if(entries.begin(), entries.end(), [](const FxChainEntry& entry) {
|
||||||
|
const FxEffectDesc* effect = FindFxEffect(entry.file);
|
||||||
|
if (effect == nullptr || !effect->Valid()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return std::find(effect->techniques.begin(), effect->techniques.end(),
|
||||||
|
entry.technique) == effect->techniques.end();
|
||||||
|
});
|
||||||
|
if (removed != entries.end()) {
|
||||||
|
entries.erase(removed, entries.end());
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
generation.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
struct FxChainEntry {
|
||||||
|
std::string file;
|
||||||
|
std::string technique;
|
||||||
|
std::map<std::string, std::array<f32, 4>> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FxChainSnapshot {
|
||||||
|
std::vector<FxChainEntry> entries;
|
||||||
|
u64 generation{};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<FxChainEntry> ParseFxChain(std::string_view value);
|
||||||
|
|
||||||
|
std::string SerializeFxChain(std::span<const FxChainEntry> entries);
|
||||||
|
|
||||||
|
class FxChain {
|
||||||
|
public:
|
||||||
|
static FxChain& Instance();
|
||||||
|
|
||||||
|
FxChainSnapshot Snapshot() const;
|
||||||
|
|
||||||
|
std::vector<FxChainEntry> Entries() const;
|
||||||
|
|
||||||
|
size_t Size() const;
|
||||||
|
|
||||||
|
void Append(std::string_view file, std::string_view technique);
|
||||||
|
|
||||||
|
void Replace(size_t index, std::string_view file, std::string_view technique);
|
||||||
|
|
||||||
|
void Remove(size_t index);
|
||||||
|
|
||||||
|
void Move(size_t index, int delta);
|
||||||
|
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
void SetValue(size_t index, std::string_view uniform, const std::array<f32, 4>& value);
|
||||||
|
|
||||||
|
std::array<f32, 4> GetValue(size_t index, std::string_view uniform) const;
|
||||||
|
|
||||||
|
bool HasValue(size_t index, std::string_view uniform) const;
|
||||||
|
|
||||||
|
void ResetValues(size_t index);
|
||||||
|
|
||||||
|
void LoadFromSettings();
|
||||||
|
|
||||||
|
void StoreToSettings() const;
|
||||||
|
|
||||||
|
void DropUnknownEntries();
|
||||||
|
|
||||||
|
private:
|
||||||
|
FxChain() = default;
|
||||||
|
|
||||||
|
mutable std::mutex mutex;
|
||||||
|
std::vector<FxChainEntry> entries;
|
||||||
|
std::atomic<u64> generation{1};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
#include "effect_codegen.hpp"
|
||||||
|
#include "effect_parser.hpp"
|
||||||
|
#include "effect_preprocessor.hpp"
|
||||||
|
|
||||||
|
#include "common/fs/fs.h"
|
||||||
|
#include "common/fs/fs_util.h"
|
||||||
|
#include "video_core/post_processing/fx_compile.h"
|
||||||
|
#include "video_core/post_processing/fx_effect.h"
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
FxCompileResult CompileFxEffect(const std::filesystem::path& path, u32 width, u32 height,
|
||||||
|
u32 color_bit_depth) {
|
||||||
|
FxCompileResult result;
|
||||||
|
|
||||||
|
if (!Common::FS::Exists(path)) {
|
||||||
|
result.error = "Effect file not found: " + Common::FS::PathToUTF8String(path);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
reshadefx::preprocessor preprocessor;
|
||||||
|
preprocessor.add_macro_definition("__RESHADE__", "50000");
|
||||||
|
preprocessor.add_macro_definition("__RESHADE_PERFORMANCE_MODE__", "1");
|
||||||
|
preprocessor.add_macro_definition("__RENDERER__", "0x20000");
|
||||||
|
preprocessor.add_macro_definition("__VENDOR__", "0");
|
||||||
|
preprocessor.add_macro_definition("__DEVICE__", "0");
|
||||||
|
preprocessor.add_macro_definition("__APPLICATION__", "0");
|
||||||
|
preprocessor.add_macro_definition("BUFFER_WIDTH", std::to_string(width));
|
||||||
|
preprocessor.add_macro_definition("BUFFER_HEIGHT", std::to_string(height));
|
||||||
|
preprocessor.add_macro_definition("BUFFER_RCP_WIDTH", "(1.0 / BUFFER_WIDTH)");
|
||||||
|
preprocessor.add_macro_definition("BUFFER_RCP_HEIGHT", "(1.0 / BUFFER_HEIGHT)");
|
||||||
|
preprocessor.add_macro_definition("BUFFER_COLOR_DEPTH", std::to_string(color_bit_depth));
|
||||||
|
preprocessor.add_macro_definition("BUFFER_COLOR_BIT_DEPTH", std::to_string(color_bit_depth));
|
||||||
|
|
||||||
|
for (const auto& include : GetFxIncludePaths(path)) {
|
||||||
|
preprocessor.add_include_path(include);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preprocessor.append_file(path)) {
|
||||||
|
result.error = preprocessor.errors();
|
||||||
|
if (result.error.empty()) {
|
||||||
|
result.error = "Failed to preprocess " + Common::FS::PathToUTF8String(path);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<reshadefx::codegen> backend(
|
||||||
|
reshadefx::create_codegen_spirv(true, false, false, false, false));
|
||||||
|
|
||||||
|
reshadefx::parser parser;
|
||||||
|
if (!parser.parse(preprocessor.output(), backend.get())) {
|
||||||
|
result.error = parser.errors();
|
||||||
|
if (result.error.empty()) {
|
||||||
|
result.error = "Failed to parse " + Common::FS::PathToUTF8String(path);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.module = backend->module();
|
||||||
|
|
||||||
|
std::set<std::string> wanted;
|
||||||
|
for (const auto& technique : result.module.techniques) {
|
||||||
|
for (const auto& pass : technique.passes) {
|
||||||
|
if (!pass.vs_entry_point.empty()) {
|
||||||
|
wanted.insert(pass.vs_entry_point);
|
||||||
|
}
|
||||||
|
if (!pass.ps_entry_point.empty()) {
|
||||||
|
wanted.insert(pass.ps_entry_point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& name : wanted) {
|
||||||
|
std::string binary;
|
||||||
|
std::string assembly;
|
||||||
|
std::string errors;
|
||||||
|
if (!backend->assemble_code_for_entry_point(name, binary, assembly, errors)) {
|
||||||
|
result.error = "Failed to assemble entry point '" + name + "': " + errors;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (binary.size() % sizeof(u32) != 0) {
|
||||||
|
result.error = "Entry point '" + name + "' produced a malformed SPIR-V module";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u32> words(binary.size() / sizeof(u32));
|
||||||
|
std::memcpy(words.data(), binary.data(), binary.size());
|
||||||
|
result.entry_points.emplace(name, std::move(words));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.entry_points.empty()) {
|
||||||
|
result.error = "Effect declares no usable entry points";
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "effect_module.hpp"
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
struct FxCompileResult {
|
||||||
|
reshadefx::effect_module module;
|
||||||
|
std::map<std::string, std::vector<u32>> entry_points;
|
||||||
|
std::string error;
|
||||||
|
|
||||||
|
bool Succeeded() const {
|
||||||
|
return error.empty() && !entry_points.empty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FxCompileResult CompileFxEffect(const std::filesystem::path& path, u32 width, u32 height,
|
||||||
|
u32 color_bit_depth);
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "common/fs/fs.h"
|
||||||
|
#include "common/fs/fs_util.h"
|
||||||
|
#include "common/fs/path_util.h"
|
||||||
|
#include "common/logging.h"
|
||||||
|
#include "video_core/post_processing/fx_compile.h"
|
||||||
|
#include "video_core/post_processing/fx_effect.h"
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr u32 CATALOG_PROBE_WIDTH = 1280;
|
||||||
|
constexpr u32 CATALOG_PROBE_HEIGHT = 720;
|
||||||
|
constexpr u32 CATALOG_PROBE_DEPTH = 8;
|
||||||
|
|
||||||
|
std::vector<FxEffectDesc> catalog;
|
||||||
|
|
||||||
|
const reshadefx::annotation* FindAnnotation(const std::vector<reshadefx::annotation>& annotations,
|
||||||
|
std::string_view name) {
|
||||||
|
const auto it = std::find_if(annotations.begin(), annotations.end(),
|
||||||
|
[&](const reshadefx::annotation& a) { return a.name == name; });
|
||||||
|
if (it == annotations.end()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &*it;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AnnotationString(const std::vector<reshadefx::annotation>& annotations,
|
||||||
|
std::string_view name) {
|
||||||
|
const reshadefx::annotation* a = FindAnnotation(annotations, name);
|
||||||
|
if (a == nullptr) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
return a->value.string_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AnnotationFloat(const std::vector<reshadefx::annotation>& annotations, std::string_view name,
|
||||||
|
f32& out) {
|
||||||
|
const reshadefx::annotation* a = FindAnnotation(annotations, name);
|
||||||
|
if (a == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (a->type.is_floating_point()) {
|
||||||
|
out = a->value.as_float[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (a->type.is_integral()) {
|
||||||
|
out = static_cast<f32>(a->value.as_int[0]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FxUiType ParseUiType(std::string_view value) {
|
||||||
|
if (value == "slider") {
|
||||||
|
return FxUiType::Slider;
|
||||||
|
}
|
||||||
|
if (value == "drag") {
|
||||||
|
return FxUiType::Drag;
|
||||||
|
}
|
||||||
|
if (value == "combo") {
|
||||||
|
return FxUiType::Combo;
|
||||||
|
}
|
||||||
|
if (value == "radio") {
|
||||||
|
return FxUiType::Radio;
|
||||||
|
}
|
||||||
|
if (value == "check" || value == "checkbox") {
|
||||||
|
return FxUiType::CheckBox;
|
||||||
|
}
|
||||||
|
if (value == "color") {
|
||||||
|
return FxUiType::Color;
|
||||||
|
}
|
||||||
|
if (value == "input") {
|
||||||
|
return FxUiType::InputBox;
|
||||||
|
}
|
||||||
|
return FxUiType::Hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> SplitItems(const std::string& items) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
std::string current;
|
||||||
|
for (const char c : items) {
|
||||||
|
if (c == '\0') {
|
||||||
|
out.push_back(current);
|
||||||
|
current.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current += c;
|
||||||
|
}
|
||||||
|
if (!current.empty()) {
|
||||||
|
out.push_back(current);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
FxUniformDesc DescribeUniform(const reshadefx::uniform& info) {
|
||||||
|
FxUniformDesc desc;
|
||||||
|
desc.name = info.name;
|
||||||
|
desc.components = std::min<u32>(info.type.components(), 4);
|
||||||
|
|
||||||
|
if (info.type.is_boolean()) {
|
||||||
|
desc.kind = FxUniformKind::Boolean;
|
||||||
|
} else if (info.type.is_integral()) {
|
||||||
|
desc.kind = FxUniformKind::Integer;
|
||||||
|
} else {
|
||||||
|
desc.kind = FxUniformKind::Floating;
|
||||||
|
}
|
||||||
|
|
||||||
|
desc.label = AnnotationString(info.annotations, "ui_label");
|
||||||
|
if (desc.label.empty()) {
|
||||||
|
desc.label = info.name;
|
||||||
|
}
|
||||||
|
desc.tooltip = AnnotationString(info.annotations, "ui_tooltip");
|
||||||
|
desc.category = AnnotationString(info.annotations, "ui_category");
|
||||||
|
desc.ui_type = ParseUiType(AnnotationString(info.annotations, "ui_type"));
|
||||||
|
desc.items = SplitItems(AnnotationString(info.annotations, "ui_items"));
|
||||||
|
|
||||||
|
if (desc.kind == FxUniformKind::Boolean) {
|
||||||
|
desc.ui_min = 0.0f;
|
||||||
|
desc.ui_max = 1.0f;
|
||||||
|
desc.ui_step = 1.0f;
|
||||||
|
} else if (desc.kind == FxUniformKind::Integer) {
|
||||||
|
desc.ui_min = 0.0f;
|
||||||
|
desc.ui_max = 100.0f;
|
||||||
|
desc.ui_step = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void(AnnotationFloat(info.annotations, "ui_min", desc.ui_min));
|
||||||
|
void(AnnotationFloat(info.annotations, "ui_max", desc.ui_max));
|
||||||
|
void(AnnotationFloat(info.annotations, "ui_step", desc.ui_step));
|
||||||
|
|
||||||
|
if (desc.ui_step <= 0.0f) {
|
||||||
|
desc.ui_step = 0.01f;
|
||||||
|
if (desc.kind != FxUniformKind::Floating) {
|
||||||
|
desc.ui_step = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (desc.ui_max < desc.ui_min) {
|
||||||
|
std::swap(desc.ui_min, desc.ui_max);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.has_initializer_value) {
|
||||||
|
for (u32 i = 0; i < desc.components; ++i) {
|
||||||
|
if (desc.kind == FxUniformKind::Floating) {
|
||||||
|
desc.default_value[i] = info.initializer_value.as_float[i];
|
||||||
|
} else {
|
||||||
|
desc.default_value[i] = static_cast<f32>(info.initializer_value.as_int[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
FxEffectDesc DescribeEffect(const std::filesystem::path& path, const std::filesystem::path& root) {
|
||||||
|
FxEffectDesc desc;
|
||||||
|
desc.file = Common::FS::PathToUTF8String(std::filesystem::relative(path, root));
|
||||||
|
desc.name = Common::FS::PathToUTF8String(path.stem());
|
||||||
|
|
||||||
|
const auto compiled =
|
||||||
|
CompileFxEffect(path, CATALOG_PROBE_WIDTH, CATALOG_PROBE_HEIGHT, CATALOG_PROBE_DEPTH);
|
||||||
|
if (!compiled.Succeeded()) {
|
||||||
|
desc.error = compiled.error;
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& technique : compiled.module.techniques) {
|
||||||
|
desc.techniques.push_back(technique.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& uniform : compiled.module.uniforms) {
|
||||||
|
FxUniformDesc uniform_desc = DescribeUniform(uniform);
|
||||||
|
if (uniform_desc.ui_type == FxUiType::Hidden) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
desc.uniforms.push_back(std::move(uniform_desc));
|
||||||
|
}
|
||||||
|
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
std::filesystem::path GetFxRootDirectory() {
|
||||||
|
return Common::FS::GetEdenPath(Common::FS::EdenPath::PostShaderDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> GetFxIncludePaths(const std::filesystem::path& effect_path) {
|
||||||
|
const auto root = GetFxRootDirectory();
|
||||||
|
std::vector<std::filesystem::path> paths;
|
||||||
|
paths.push_back(effect_path.parent_path());
|
||||||
|
paths.push_back(root);
|
||||||
|
paths.push_back(root / "Shaders");
|
||||||
|
|
||||||
|
const auto last = std::unique(paths.begin(), paths.end());
|
||||||
|
paths.erase(last, paths.end());
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path ResolveFxTexturePath(const std::filesystem::path& effect_path,
|
||||||
|
std::string_view source) {
|
||||||
|
const auto root = GetFxRootDirectory();
|
||||||
|
const std::filesystem::path name{source};
|
||||||
|
|
||||||
|
const std::array candidates{
|
||||||
|
effect_path.parent_path() / name,
|
||||||
|
root / "Textures" / name,
|
||||||
|
root / name,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& candidate : candidates) {
|
||||||
|
if (Common::FS::Exists(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::filesystem::path();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReloadFxCatalog() {
|
||||||
|
catalog.clear();
|
||||||
|
|
||||||
|
const auto root = GetFxRootDirectory();
|
||||||
|
if (!Common::FS::Exists(root)) {
|
||||||
|
void(Common::FS::CreateDirs(root));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> effect_files;
|
||||||
|
Common::FS::IterateDirEntriesRecursively(
|
||||||
|
root,
|
||||||
|
[&](const std::filesystem::directory_entry& entry) {
|
||||||
|
if (entry.path().extension() == ".fx") {
|
||||||
|
effect_files.push_back(entry.path());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
Common::FS::DirEntryFilter::File);
|
||||||
|
|
||||||
|
std::sort(effect_files.begin(), effect_files.end());
|
||||||
|
|
||||||
|
for (const auto& file : effect_files) {
|
||||||
|
FxEffectDesc desc = DescribeEffect(file, root);
|
||||||
|
if (!desc.error.empty()) {
|
||||||
|
LOG_WARNING(Render, "Post-processing effect '{}' failed to compile:\n{}", desc.file,
|
||||||
|
desc.error);
|
||||||
|
}
|
||||||
|
catalog.push_back(std::move(desc));
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t usable = std::count_if(catalog.begin(), catalog.end(),
|
||||||
|
[](const FxEffectDesc& d) { return d.Valid(); });
|
||||||
|
LOG_INFO(Render, "Loaded {} post-processing effects ({} usable)", catalog.size(), usable);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<FxEffectDesc>& GetFxCatalog() {
|
||||||
|
return catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FxEffectDesc* FindFxEffect(std::string_view file) {
|
||||||
|
const auto it = std::find_if(catalog.begin(), catalog.end(),
|
||||||
|
[&](const FxEffectDesc& d) { return d.file == file; });
|
||||||
|
if (it == catalog.end()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &*it;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FxUniformDesc* FindFxUniform(const FxEffectDesc& effect, std::string_view name) {
|
||||||
|
const auto it = std::find_if(effect.uniforms.begin(), effect.uniforms.end(),
|
||||||
|
[&](const FxUniformDesc& u) { return u.name == name; });
|
||||||
|
if (it == effect.uniforms.end()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &*it;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
enum class FxUniformKind {
|
||||||
|
Boolean,
|
||||||
|
Integer,
|
||||||
|
Floating,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class FxUiType {
|
||||||
|
Hidden,
|
||||||
|
Slider,
|
||||||
|
Drag,
|
||||||
|
Combo,
|
||||||
|
Radio,
|
||||||
|
CheckBox,
|
||||||
|
Color,
|
||||||
|
InputBox,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FxUniformDesc {
|
||||||
|
std::string name;
|
||||||
|
std::string label;
|
||||||
|
std::string tooltip;
|
||||||
|
std::string category;
|
||||||
|
FxUniformKind kind{FxUniformKind::Floating};
|
||||||
|
u32 components{1};
|
||||||
|
FxUiType ui_type{FxUiType::Hidden};
|
||||||
|
f32 ui_min{0.0f};
|
||||||
|
f32 ui_max{1.0f};
|
||||||
|
f32 ui_step{0.01f};
|
||||||
|
std::vector<std::string> items;
|
||||||
|
std::array<f32, 4> default_value{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FxEffectDesc {
|
||||||
|
std::string file;
|
||||||
|
std::string name;
|
||||||
|
std::vector<std::string> techniques;
|
||||||
|
std::vector<FxUniformDesc> uniforms;
|
||||||
|
std::string error;
|
||||||
|
|
||||||
|
bool Valid() const {
|
||||||
|
return error.empty() && !techniques.empty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::filesystem::path GetFxRootDirectory();
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> GetFxIncludePaths(const std::filesystem::path& effect_path);
|
||||||
|
|
||||||
|
std::filesystem::path ResolveFxTexturePath(const std::filesystem::path& effect_path,
|
||||||
|
std::string_view source);
|
||||||
|
|
||||||
|
void ReloadFxCatalog();
|
||||||
|
|
||||||
|
const std::vector<FxEffectDesc>& GetFxCatalog();
|
||||||
|
|
||||||
|
const FxEffectDesc* FindFxEffect(std::string_view file);
|
||||||
|
|
||||||
|
const FxUniformDesc* FindFxUniform(const FxEffectDesc& effect, std::string_view name);
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
||||||
@@ -18,6 +18,10 @@
|
|||||||
#include "video_core/renderer_vulkan/present/sgsr.h"
|
#include "video_core/renderer_vulkan/present/sgsr.h"
|
||||||
#include "video_core/renderer_vulkan/present/fxaa.h"
|
#include "video_core/renderer_vulkan/present/fxaa.h"
|
||||||
#include "video_core/renderer_vulkan/present/layer.h"
|
#include "video_core/renderer_vulkan/present/layer.h"
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
#include "video_core/post_processing/fx_chain.h"
|
||||||
|
#include "video_core/renderer_vulkan/present/post_process.h"
|
||||||
|
#endif
|
||||||
#include "video_core/renderer_vulkan/present/present_push_constants.h"
|
#include "video_core/renderer_vulkan/present/present_push_constants.h"
|
||||||
#include "video_core/renderer_vulkan/present/smaa.h"
|
#include "video_core/renderer_vulkan/present/smaa.h"
|
||||||
#include "video_core/renderer_vulkan/present/util.h"
|
#include "video_core/renderer_vulkan/present/util.h"
|
||||||
@@ -93,6 +97,9 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c
|
|||||||
|
|
||||||
RefreshResources(device, framebuffer);
|
RefreshResources(device, framebuffer);
|
||||||
SetAntiAliasPass(device);
|
SetAntiAliasPass(device);
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
SetPostProcessPass(device);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Finish any pending renderpass
|
// Finish any pending renderpass
|
||||||
scheduler.RequestOutsideRenderPassOperationContext();
|
scheduler.RequestOutsideRenderPassOperationContext();
|
||||||
@@ -115,6 +122,12 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c
|
|||||||
smaa->Draw(device, scheduler, image_index, &source_image, &source_image_view);
|
smaa->Draw(device, scheduler, image_index, &source_image, &source_image_view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
if (post_process.has_value()) {
|
||||||
|
post_process->Draw(device, scheduler, image_index, &source_image, &source_image_view);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
auto crop_rect = Tegra::NormalizeCrop(framebuffer, texture_width, texture_height);
|
auto crop_rect = Tegra::NormalizeCrop(framebuffer, texture_width, texture_height);
|
||||||
const VkExtent2D render_extent{
|
const VkExtent2D render_extent{
|
||||||
.width = scaled_width,
|
.width = scaled_width,
|
||||||
@@ -214,6 +227,40 @@ void Layer::SetAntiAliasPass(const Device& device) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
void Layer::SetPostProcessPass(const Device& device) {
|
||||||
|
const VkExtent2D render_area{
|
||||||
|
.width = Settings::values.resolution_info.ScaleUp(raw_width),
|
||||||
|
.height = Settings::values.resolution_info.ScaleUp(raw_height),
|
||||||
|
};
|
||||||
|
|
||||||
|
const u64 generation = VideoCore::FxChain::Instance().Snapshot().generation;
|
||||||
|
|
||||||
|
if (post_process_generation == generation && post_process_extent.width == render_area.width &&
|
||||||
|
post_process_extent.height == render_area.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const u64 tick : resource_ticks) {
|
||||||
|
scheduler.Wait(tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
post_process_generation = generation;
|
||||||
|
post_process_extent = render_area;
|
||||||
|
post_process.reset();
|
||||||
|
|
||||||
|
if (VideoCore::FxChain::Instance().Size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
post_process.emplace(device, memory_allocator, scheduler, image_count, render_area);
|
||||||
|
|
||||||
|
if (post_process->Empty()) {
|
||||||
|
post_process.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void Layer::ReleaseRawImages() {
|
void Layer::ReleaseRawImages() {
|
||||||
for (const u64 tick : resource_ticks) {
|
for (const u64 tick : resource_ticks) {
|
||||||
scheduler.Wait(tick);
|
scheduler.Wait(tick);
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
#include "video_core/renderer_vulkan/present/fsr.h"
|
#include "video_core/renderer_vulkan/present/fsr.h"
|
||||||
#include "video_core/renderer_vulkan/present/sgsr.h"
|
#include "video_core/renderer_vulkan/present/sgsr.h"
|
||||||
#include "video_core/renderer_vulkan/present/fxaa.h"
|
#include "video_core/renderer_vulkan/present/fxaa.h"
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
#include "video_core/renderer_vulkan/present/post_process.h"
|
||||||
|
#endif
|
||||||
#include "video_core/renderer_vulkan/present/smaa.h"
|
#include "video_core/renderer_vulkan/present/smaa.h"
|
||||||
|
|
||||||
namespace Layout {
|
namespace Layout {
|
||||||
@@ -66,6 +69,9 @@ private:
|
|||||||
|
|
||||||
void RefreshResources(const Device& device, const Tegra::FramebufferConfig& framebuffer);
|
void RefreshResources(const Device& device, const Tegra::FramebufferConfig& framebuffer);
|
||||||
void SetAntiAliasPass(const Device& device);
|
void SetAntiAliasPass(const Device& device);
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
void SetPostProcessPass(const Device& device);
|
||||||
|
#endif
|
||||||
void ReleaseRawImages();
|
void ReleaseRawImages();
|
||||||
|
|
||||||
u64 CalculateBufferSize(const Tegra::FramebufferConfig& framebuffer) const;
|
u64 CalculateBufferSize(const Tegra::FramebufferConfig& framebuffer) const;
|
||||||
@@ -95,6 +101,11 @@ private:
|
|||||||
Settings::AntiAliasing anti_alias_setting{};
|
Settings::AntiAliasing anti_alias_setting{};
|
||||||
std::variant<std::monostate, FXAA, SMAA> anti_alias{};
|
std::variant<std::monostate, FXAA, SMAA> anti_alias{};
|
||||||
std::variant<std::monostate, SGSR, FSR> sr_filter{};
|
std::variant<std::monostate, SGSR, FSR> sr_filter{};
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
std::optional<PostProcessChain> post_process{};
|
||||||
|
u64 post_process_generation{};
|
||||||
|
VkExtent2D post_process_extent{};
|
||||||
|
#endif
|
||||||
std::vector<u64> resource_ticks{};
|
std::vector<u64> resource_ticks{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,898 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
#include "common/fs/fs.h"
|
||||||
|
#include "common/fs/fs_util.h"
|
||||||
|
#include "common/logging.h"
|
||||||
|
#include "video_core/post_processing/fx_chain.h"
|
||||||
|
#include "video_core/post_processing/fx_compile.h"
|
||||||
|
#include "video_core/post_processing/fx_effect.h"
|
||||||
|
#include "video_core/renderer_vulkan/present/post_process.h"
|
||||||
|
#include "video_core/renderer_vulkan/present/util.h"
|
||||||
|
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||||
|
#include "video_core/vulkan_common/vulkan_device.h"
|
||||||
|
|
||||||
|
namespace Vulkan {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr VkFormat BACKBUFFER_FORMAT = VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||||
|
constexpr size_t NO_TEXTURE = ~size_t{0};
|
||||||
|
|
||||||
|
VkFormat ToVkFormat(reshadefx::texture_format format) {
|
||||||
|
switch (format) {
|
||||||
|
case reshadefx::texture_format::r8:
|
||||||
|
return VK_FORMAT_R8_UNORM;
|
||||||
|
case reshadefx::texture_format::r16f:
|
||||||
|
return VK_FORMAT_R16_SFLOAT;
|
||||||
|
case reshadefx::texture_format::r32f:
|
||||||
|
return VK_FORMAT_R32_SFLOAT;
|
||||||
|
case reshadefx::texture_format::rg8:
|
||||||
|
return VK_FORMAT_R8G8_UNORM;
|
||||||
|
case reshadefx::texture_format::rg16:
|
||||||
|
return VK_FORMAT_R16G16_UNORM;
|
||||||
|
case reshadefx::texture_format::rg16f:
|
||||||
|
return VK_FORMAT_R16G16_SFLOAT;
|
||||||
|
case reshadefx::texture_format::rg32f:
|
||||||
|
return VK_FORMAT_R32G32_SFLOAT;
|
||||||
|
case reshadefx::texture_format::rgba8:
|
||||||
|
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||||
|
case reshadefx::texture_format::rgba16:
|
||||||
|
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||||
|
case reshadefx::texture_format::rgba16f:
|
||||||
|
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||||
|
case reshadefx::texture_format::rgba32f:
|
||||||
|
return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||||
|
case reshadefx::texture_format::rgb10a2:
|
||||||
|
return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||||
|
default:
|
||||||
|
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VkSamplerAddressMode ToAddressMode(reshadefx::texture_address_mode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case reshadefx::texture_address_mode::wrap:
|
||||||
|
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
|
||||||
|
case reshadefx::texture_address_mode::mirror:
|
||||||
|
return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
|
||||||
|
case reshadefx::texture_address_mode::border:
|
||||||
|
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
|
||||||
|
case reshadefx::texture_address_mode::clamp:
|
||||||
|
default:
|
||||||
|
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VkBlendFactor ToBlendFactor(reshadefx::blend_factor func) {
|
||||||
|
switch (func) {
|
||||||
|
case reshadefx::blend_factor::zero:
|
||||||
|
return VK_BLEND_FACTOR_ZERO;
|
||||||
|
case reshadefx::blend_factor::source_color:
|
||||||
|
return VK_BLEND_FACTOR_SRC_COLOR;
|
||||||
|
case reshadefx::blend_factor::source_alpha:
|
||||||
|
return VK_BLEND_FACTOR_SRC_ALPHA;
|
||||||
|
case reshadefx::blend_factor::one_minus_source_color:
|
||||||
|
return VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR;
|
||||||
|
case reshadefx::blend_factor::one_minus_source_alpha:
|
||||||
|
return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
||||||
|
case reshadefx::blend_factor::dest_color:
|
||||||
|
return VK_BLEND_FACTOR_DST_COLOR;
|
||||||
|
case reshadefx::blend_factor::dest_alpha:
|
||||||
|
return VK_BLEND_FACTOR_DST_ALPHA;
|
||||||
|
case reshadefx::blend_factor::one_minus_dest_color:
|
||||||
|
return VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR;
|
||||||
|
case reshadefx::blend_factor::one_minus_dest_alpha:
|
||||||
|
return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA;
|
||||||
|
case reshadefx::blend_factor::one:
|
||||||
|
default:
|
||||||
|
return VK_BLEND_FACTOR_ONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VkBlendOp ToBlendOp(reshadefx::blend_op op) {
|
||||||
|
switch (op) {
|
||||||
|
case reshadefx::blend_op::subtract:
|
||||||
|
return VK_BLEND_OP_SUBTRACT;
|
||||||
|
case reshadefx::blend_op::reverse_subtract:
|
||||||
|
return VK_BLEND_OP_REVERSE_SUBTRACT;
|
||||||
|
case reshadefx::blend_op::min:
|
||||||
|
return VK_BLEND_OP_MIN;
|
||||||
|
case reshadefx::blend_op::max:
|
||||||
|
return VK_BLEND_OP_MAX;
|
||||||
|
case reshadefx::blend_op::add:
|
||||||
|
default:
|
||||||
|
return VK_BLEND_OP_ADD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VkPrimitiveTopology ToTopology(reshadefx::primitive_topology topology) {
|
||||||
|
switch (topology) {
|
||||||
|
case reshadefx::primitive_topology::point_list:
|
||||||
|
return VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
|
||||||
|
case reshadefx::primitive_topology::line_list:
|
||||||
|
return VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
|
||||||
|
case reshadefx::primitive_topology::line_strip:
|
||||||
|
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
|
||||||
|
case reshadefx::primitive_topology::triangle_strip:
|
||||||
|
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
|
||||||
|
case reshadefx::primitive_topology::triangle_list:
|
||||||
|
default:
|
||||||
|
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vk::RenderPass CreateFxRenderPass(const Device& device, VkFormat format, bool clear) {
|
||||||
|
VkAttachmentLoadOp load_op = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||||
|
VkImageLayout initial_layout = VK_IMAGE_LAYOUT_GENERAL;
|
||||||
|
if (clear) {
|
||||||
|
load_op = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||||
|
initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VkAttachmentDescription attachment{
|
||||||
|
.flags = 0,
|
||||||
|
.format = format,
|
||||||
|
.samples = VK_SAMPLE_COUNT_1_BIT,
|
||||||
|
.loadOp = load_op,
|
||||||
|
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
|
||||||
|
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
|
||||||
|
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
|
||||||
|
.initialLayout = initial_layout,
|
||||||
|
.finalLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const VkAttachmentReference reference{
|
||||||
|
.attachment = 0,
|
||||||
|
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const VkSubpassDescription subpass{
|
||||||
|
.flags = 0,
|
||||||
|
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
|
.inputAttachmentCount = 0,
|
||||||
|
.pInputAttachments = nullptr,
|
||||||
|
.colorAttachmentCount = 1,
|
||||||
|
.pColorAttachments = &reference,
|
||||||
|
.pResolveAttachments = nullptr,
|
||||||
|
.pDepthStencilAttachment = nullptr,
|
||||||
|
.preserveAttachmentCount = 0,
|
||||||
|
.pPreserveAttachments = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
return device.GetLogical().CreateRenderPass(VkRenderPassCreateInfo{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.attachmentCount = 1,
|
||||||
|
.pAttachments = &attachment,
|
||||||
|
.subpassCount = 1,
|
||||||
|
.pSubpasses = &subpass,
|
||||||
|
.dependencyCount = 0,
|
||||||
|
.pDependencies = nullptr,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
vk::Pipeline CreateFxPipeline(const Device& device, vk::RenderPass& renderpass,
|
||||||
|
vk::PipelineLayout& layout, VkShaderModule vertex_shader,
|
||||||
|
VkShaderModule fragment_shader,
|
||||||
|
const reshadefx::pass& pass) {
|
||||||
|
const std::array<VkPipelineShaderStageCreateInfo, 2> stages{{
|
||||||
|
{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.stage = VK_SHADER_STAGE_VERTEX_BIT,
|
||||||
|
.module = vertex_shader,
|
||||||
|
.pName = pass.vs_entry_point.c_str(),
|
||||||
|
.pSpecializationInfo = nullptr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.stage = VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||||
|
.module = fragment_shader,
|
||||||
|
.pName = pass.ps_entry_point.c_str(),
|
||||||
|
.pSpecializationInfo = nullptr,
|
||||||
|
},
|
||||||
|
}};
|
||||||
|
|
||||||
|
constexpr VkPipelineVertexInputStateCreateInfo vertex_input{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.vertexBindingDescriptionCount = 0,
|
||||||
|
.pVertexBindingDescriptions = nullptr,
|
||||||
|
.vertexAttributeDescriptionCount = 0,
|
||||||
|
.pVertexAttributeDescriptions = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
const VkPipelineInputAssemblyStateCreateInfo input_assembly{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.topology = ToTopology(pass.topology),
|
||||||
|
.primitiveRestartEnable = VK_FALSE,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr VkPipelineViewportStateCreateInfo viewport_state{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.viewportCount = 1,
|
||||||
|
.pViewports = nullptr,
|
||||||
|
.scissorCount = 1,
|
||||||
|
.pScissors = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr VkPipelineRasterizationStateCreateInfo rasterization{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.depthClampEnable = VK_FALSE,
|
||||||
|
.rasterizerDiscardEnable = VK_FALSE,
|
||||||
|
.polygonMode = VK_POLYGON_MODE_FILL,
|
||||||
|
.cullMode = VK_CULL_MODE_NONE,
|
||||||
|
.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE,
|
||||||
|
.depthBiasEnable = VK_FALSE,
|
||||||
|
.depthBiasConstantFactor = 0.0f,
|
||||||
|
.depthBiasClamp = 0.0f,
|
||||||
|
.depthBiasSlopeFactor = 0.0f,
|
||||||
|
.lineWidth = 1.0f,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr VkPipelineMultisampleStateCreateInfo multisampling{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT,
|
||||||
|
.sampleShadingEnable = VK_FALSE,
|
||||||
|
.minSampleShading = 0.0f,
|
||||||
|
.pSampleMask = nullptr,
|
||||||
|
.alphaToCoverageEnable = VK_FALSE,
|
||||||
|
.alphaToOneEnable = VK_FALSE,
|
||||||
|
};
|
||||||
|
|
||||||
|
VkBool32 blend_enable = VK_FALSE;
|
||||||
|
if (pass.blend_enable[0]) {
|
||||||
|
blend_enable = VK_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VkPipelineColorBlendAttachmentState blending{
|
||||||
|
.blendEnable = blend_enable,
|
||||||
|
.srcColorBlendFactor = ToBlendFactor(pass.source_color_blend_factor[0]),
|
||||||
|
.dstColorBlendFactor = ToBlendFactor(pass.dest_color_blend_factor[0]),
|
||||||
|
.colorBlendOp = ToBlendOp(pass.color_blend_op[0]),
|
||||||
|
.srcAlphaBlendFactor = ToBlendFactor(pass.source_alpha_blend_factor[0]),
|
||||||
|
.dstAlphaBlendFactor = ToBlendFactor(pass.dest_alpha_blend_factor[0]),
|
||||||
|
.alphaBlendOp = ToBlendOp(pass.alpha_blend_op[0]),
|
||||||
|
.colorWriteMask = static_cast<VkColorComponentFlags>(pass.render_target_write_mask[0] & 0xF),
|
||||||
|
};
|
||||||
|
|
||||||
|
const VkPipelineColorBlendStateCreateInfo color_blend{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.logicOpEnable = VK_FALSE,
|
||||||
|
.logicOp = VK_LOGIC_OP_COPY,
|
||||||
|
.attachmentCount = 1,
|
||||||
|
.pAttachments = &blending,
|
||||||
|
.blendConstants = {0.0f, 0.0f, 0.0f, 0.0f},
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr std::array dynamic_states{
|
||||||
|
VK_DYNAMIC_STATE_VIEWPORT,
|
||||||
|
VK_DYNAMIC_STATE_SCISSOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
const VkPipelineDynamicStateCreateInfo dynamic_state{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.dynamicStateCount = static_cast<u32>(dynamic_states.size()),
|
||||||
|
.pDynamicStates = dynamic_states.data(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return device.GetLogical().CreateGraphicsPipeline(VkGraphicsPipelineCreateInfo{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.stageCount = static_cast<u32>(stages.size()),
|
||||||
|
.pStages = stages.data(),
|
||||||
|
.pVertexInputState = &vertex_input,
|
||||||
|
.pInputAssemblyState = &input_assembly,
|
||||||
|
.pTessellationState = nullptr,
|
||||||
|
.pViewportState = &viewport_state,
|
||||||
|
.pRasterizationState = &rasterization,
|
||||||
|
.pMultisampleState = &multisampling,
|
||||||
|
.pDepthStencilState = nullptr,
|
||||||
|
.pColorBlendState = &color_blend,
|
||||||
|
.pDynamicState = &dynamic_state,
|
||||||
|
.layout = *layout,
|
||||||
|
.renderPass = *renderpass,
|
||||||
|
.subpass = 0,
|
||||||
|
.basePipelineHandle = nullptr,
|
||||||
|
.basePipelineIndex = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
PostProcessChain::PostProcessChain(const Device& device, MemoryAllocator& allocator,
|
||||||
|
Scheduler& scheduler, size_t image_count, VkExtent2D extent)
|
||||||
|
: m_extent(extent)
|
||||||
|
, m_image_count(u32(image_count))
|
||||||
|
{
|
||||||
|
m_start = std::chrono::steady_clock::now();
|
||||||
|
m_previous = m_start;
|
||||||
|
|
||||||
|
CreatePingPongImages(device, allocator);
|
||||||
|
|
||||||
|
m_fallback_sampler = CreateWrappedSampler(device);
|
||||||
|
m_fallback_image = CreateWrappedImage(allocator, VkExtent2D{1, 1}, VK_FORMAT_R8G8B8A8_UNORM);
|
||||||
|
m_fallback_view = CreateWrappedImageView(device, m_fallback_image, VK_FORMAT_R8G8B8A8_UNORM);
|
||||||
|
|
||||||
|
if (!BuildEffects(device, allocator, scheduler)) {
|
||||||
|
m_effects.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PostProcessChain::~PostProcessChain() = default;
|
||||||
|
|
||||||
|
bool PostProcessChain::Empty() const {
|
||||||
|
return m_effects.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostProcessChain::CreatePingPongImages(const Device& device, MemoryAllocator& allocator) {
|
||||||
|
m_frames.resize(m_image_count);
|
||||||
|
for (auto& frame : m_frames) {
|
||||||
|
for (size_t i = 0; i < frame.images.size(); ++i) {
|
||||||
|
frame.images[i] = CreateWrappedImage(allocator, m_extent, BACKBUFFER_FORMAT);
|
||||||
|
frame.views[i] = CreateWrappedImageView(device, frame.images[i], BACKBUFFER_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PostProcessChain::BuildEffects(const Device& device, MemoryAllocator& allocator,
|
||||||
|
Scheduler& scheduler) {
|
||||||
|
const auto snapshot = VideoCore::FxChain::Instance().Snapshot();
|
||||||
|
if (snapshot.entries.empty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto root = VideoCore::GetFxRootDirectory();
|
||||||
|
|
||||||
|
for (size_t entry_index = 0; entry_index < snapshot.entries.size(); ++entry_index) {
|
||||||
|
const auto& entry = snapshot.entries[entry_index];
|
||||||
|
const auto path = root / entry.file;
|
||||||
|
|
||||||
|
const auto compiled = VideoCore::CompileFxEffect(path, m_extent.width, m_extent.height, 8);
|
||||||
|
if (!compiled.Succeeded()) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Post-processing effect '{}' failed to compile:\n{}",
|
||||||
|
entry.file, compiled.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& module = compiled.module;
|
||||||
|
|
||||||
|
const auto technique = std::find_if(
|
||||||
|
module.techniques.begin(), module.techniques.end(),
|
||||||
|
[&](const reshadefx::technique& t) { return t.name == entry.technique; });
|
||||||
|
if (technique == module.techniques.end()) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Effect '{}' has no technique '{}'", entry.file,
|
||||||
|
entry.technique);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Effect effect;
|
||||||
|
effect.entry_index = entry_index;
|
||||||
|
effect.file = entry.file;
|
||||||
|
effect.uniform_size = module.total_uniform_size;
|
||||||
|
for (const auto& [name, words] : compiled.entry_points) {
|
||||||
|
effect.shaders.emplace(name, CreateWrappedShaderModule(device, words));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& texture : module.textures) {
|
||||||
|
Texture out;
|
||||||
|
out.name = texture.unique_name;
|
||||||
|
out.extent = VkExtent2D{texture.width, texture.height};
|
||||||
|
out.format = ToVkFormat(texture.format);
|
||||||
|
|
||||||
|
if (texture.semantic == "COLOR") {
|
||||||
|
out.is_backbuffer = true;
|
||||||
|
effect.textures.push_back(std::move(out));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (texture.semantic == "DEPTH") {
|
||||||
|
effect.textures.push_back(std::move(out));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.image = CreateWrappedImage(allocator, out.extent, out.format);
|
||||||
|
out.view = CreateWrappedImageView(device, out.image, out.format);
|
||||||
|
effect.textures.push_back(std::move(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& sampler : module.samplers) {
|
||||||
|
Sampler out;
|
||||||
|
out.texture_index = NO_TEXTURE;
|
||||||
|
for (size_t i = 0; i < effect.textures.size(); ++i) {
|
||||||
|
if (effect.textures[i].name == sampler.texture_name) {
|
||||||
|
out.texture_index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VkFilter mag_filter = VK_FILTER_LINEAR;
|
||||||
|
VkFilter min_filter = VK_FILTER_LINEAR;
|
||||||
|
VkSamplerMipmapMode mip_mode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
||||||
|
const u32 filter = static_cast<u32>(sampler.filter);
|
||||||
|
if ((filter & 0x10) == 0) {
|
||||||
|
min_filter = VK_FILTER_NEAREST;
|
||||||
|
}
|
||||||
|
if ((filter & 0x04) == 0) {
|
||||||
|
mag_filter = VK_FILTER_NEAREST;
|
||||||
|
}
|
||||||
|
if ((filter & 0x01) == 0) {
|
||||||
|
mip_mode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.sampler = device.GetLogical().CreateSampler(VkSamplerCreateInfo{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.magFilter = mag_filter,
|
||||||
|
.minFilter = min_filter,
|
||||||
|
.mipmapMode = mip_mode,
|
||||||
|
.addressModeU = ToAddressMode(sampler.address_u),
|
||||||
|
.addressModeV = ToAddressMode(sampler.address_v),
|
||||||
|
.addressModeW = ToAddressMode(sampler.address_w),
|
||||||
|
.mipLodBias = sampler.lod_bias,
|
||||||
|
.anisotropyEnable = VK_FALSE,
|
||||||
|
.maxAnisotropy = 1.0f,
|
||||||
|
.compareEnable = VK_FALSE,
|
||||||
|
.compareOp = VK_COMPARE_OP_NEVER,
|
||||||
|
.minLod = sampler.min_lod,
|
||||||
|
.maxLod = sampler.max_lod,
|
||||||
|
.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
|
||||||
|
.unnormalizedCoordinates = VK_FALSE,
|
||||||
|
});
|
||||||
|
|
||||||
|
effect.samplers.push_back(std::move(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& uniform : module.uniforms) {
|
||||||
|
UniformWrite write;
|
||||||
|
write.name = uniform.name;
|
||||||
|
write.offset = uniform.offset;
|
||||||
|
write.components = std::min<u32>(uniform.type.components(), 4);
|
||||||
|
write.kind = UniformKind::Floating;
|
||||||
|
if (uniform.type.is_boolean()) {
|
||||||
|
write.kind = UniformKind::Boolean;
|
||||||
|
} else if (uniform.type.is_integral()) {
|
||||||
|
write.kind = UniformKind::Integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& annotation : uniform.annotations) {
|
||||||
|
if (annotation.name != "source") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string& source = annotation.value.string_data;
|
||||||
|
if (source == "frametime") {
|
||||||
|
write.source = UniformSource::FrameTime;
|
||||||
|
} else if (source == "framecount") {
|
||||||
|
write.source = UniformSource::FrameCount;
|
||||||
|
} else if (source == "timer") {
|
||||||
|
write.source = UniformSource::Timer;
|
||||||
|
} else if (source == "random") {
|
||||||
|
write.source = UniformSource::Random;
|
||||||
|
} else if (source == "pingpong") {
|
||||||
|
write.source = UniformSource::PingPong;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniform.has_initializer_value) {
|
||||||
|
for (u32 i = 0; i < write.components; ++i) {
|
||||||
|
if (write.kind == UniformKind::Floating) {
|
||||||
|
write.fallback[i] = uniform.initializer_value.as_float[i];
|
||||||
|
} else {
|
||||||
|
write.fallback[i] = static_cast<f32>(uniform.initializer_value.as_int[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto override = entry.values.find(uniform.name);
|
||||||
|
if (override != entry.values.end()) {
|
||||||
|
write.fallback = override->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
write.args = {0.0f, 1.0f, 1.0f, 0.0f};
|
||||||
|
for (const auto& annotation : uniform.annotations) {
|
||||||
|
if (annotation.name == "min" && annotation.type.is_floating_point()) {
|
||||||
|
write.args[0] = annotation.value.as_float[0];
|
||||||
|
}
|
||||||
|
if (annotation.name == "max" && annotation.type.is_floating_point()) {
|
||||||
|
write.args[1] = annotation.value.as_float[0];
|
||||||
|
}
|
||||||
|
if (annotation.name == "step" && annotation.type.is_floating_point()) {
|
||||||
|
write.args[2] = annotation.value.as_float[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write.state = write.args[0];
|
||||||
|
|
||||||
|
effect.uniforms.push_back(std::move(write));
|
||||||
|
}
|
||||||
|
|
||||||
|
effect.uniform_layout = CreateWrappedDescriptorSetLayout(
|
||||||
|
device, std::array{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER},
|
||||||
|
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
|
||||||
|
|
||||||
|
for (const auto& pass : technique->passes) {
|
||||||
|
Pass out;
|
||||||
|
out.num_vertices = pass.num_vertices;
|
||||||
|
out.clear = pass.clear_render_targets != 0;
|
||||||
|
out.target_texture = NO_TEXTURE;
|
||||||
|
out.extent = m_extent;
|
||||||
|
|
||||||
|
const std::string& target = pass.render_target_names[0];
|
||||||
|
if (target.empty()) {
|
||||||
|
out.writes_backbuffer = true;
|
||||||
|
} else {
|
||||||
|
for (size_t i = 0; i < effect.textures.size(); ++i) {
|
||||||
|
if (effect.textures[i].name == target) {
|
||||||
|
out.target_texture = i;
|
||||||
|
out.extent = effect.textures[i].extent;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (out.target_texture == NO_TEXTURE) {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Effect '{}' pass targets unknown texture '{}'",
|
||||||
|
entry.file, target);
|
||||||
|
out.writes_backbuffer = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pass.viewport_width != 0 && pass.viewport_height != 0) {
|
||||||
|
out.extent = VkExtent2D{pass.viewport_width, pass.viewport_height};
|
||||||
|
}
|
||||||
|
|
||||||
|
VkFormat target_format = BACKBUFFER_FORMAT;
|
||||||
|
if (!out.writes_backbuffer) {
|
||||||
|
target_format = effect.textures[out.target_texture].format;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 binding_count = 0;
|
||||||
|
for (const auto& binding : pass.sampler_bindings) {
|
||||||
|
SamplerBinding entry_binding;
|
||||||
|
entry_binding.binding = binding.entry_point_binding;
|
||||||
|
entry_binding.sampler_index = binding.index;
|
||||||
|
out.sampler_bindings.push_back(entry_binding);
|
||||||
|
binding_count = std::max(binding_count, binding.entry_point_binding + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<VkDescriptorType> sampler_types(
|
||||||
|
std::max<size_t>(binding_count, 1), VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||||
|
out.sampler_layout = CreateWrappedDescriptorSetLayout(
|
||||||
|
device, sampler_types, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
|
||||||
|
|
||||||
|
const std::array set_layouts{*effect.uniform_layout, *out.sampler_layout};
|
||||||
|
out.pipeline_layout =
|
||||||
|
device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.setLayoutCount = static_cast<u32>(set_layouts.size()),
|
||||||
|
.pSetLayouts = set_layouts.data(),
|
||||||
|
.pushConstantRangeCount = 0,
|
||||||
|
.pPushConstantRanges = nullptr,
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto vertex_shader = effect.shaders.find(pass.vs_entry_point);
|
||||||
|
const auto fragment_shader = effect.shaders.find(pass.ps_entry_point);
|
||||||
|
if (vertex_shader == effect.shaders.end() ||
|
||||||
|
fragment_shader == effect.shaders.end()) {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Effect '{}' pass references a missing entry point",
|
||||||
|
entry.file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.renderpass = CreateFxRenderPass(device, target_format, out.clear);
|
||||||
|
out.pipeline = CreateFxPipeline(device, out.renderpass, out.pipeline_layout,
|
||||||
|
*vertex_shader->second, *fragment_shader->second, pass);
|
||||||
|
|
||||||
|
if (out.writes_backbuffer) {
|
||||||
|
out.backbuffer_slot = static_cast<u32>(effect.backbuffer_pass_count % 2);
|
||||||
|
++effect.backbuffer_pass_count;
|
||||||
|
for (u32 image = 0; image < m_image_count; ++image) {
|
||||||
|
for (size_t slot = 0; slot < 2; ++slot) {
|
||||||
|
out.framebuffers.push_back(CreateWrappedFramebuffer(
|
||||||
|
device, out.renderpass, m_frames[image].views[slot], out.extent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.framebuffers.push_back(
|
||||||
|
CreateWrappedFramebuffer(device, out.renderpass,
|
||||||
|
effect.textures[out.target_texture].view, out.extent));
|
||||||
|
}
|
||||||
|
|
||||||
|
effect.passes.push_back(std::move(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effect.passes.empty()) {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Effect '{}' technique '{}' has no passes", entry.file,
|
||||||
|
entry.technique);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u32 buffer_size = std::max<u32>(effect.uniform_size, 4);
|
||||||
|
for (u32 i = 0; i < m_image_count; ++i) {
|
||||||
|
effect.uniform_buffers.push_back(
|
||||||
|
CreateWrappedBuffer(allocator, buffer_size, MemoryUsage::Upload));
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t sampler_descriptor_count = 0;
|
||||||
|
size_t sampler_set_count = 0;
|
||||||
|
for (const auto& pass : effect.passes) {
|
||||||
|
sampler_descriptor_count +=
|
||||||
|
m_image_count * std::max<size_t>(pass.sampler_bindings.size(), 1);
|
||||||
|
sampler_set_count += m_image_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
effect.descriptor_pool = CreateWrappedDescriptorPool(
|
||||||
|
device, m_image_count + sampler_descriptor_count, m_image_count + sampler_set_count,
|
||||||
|
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER});
|
||||||
|
|
||||||
|
const std::vector<VkDescriptorSetLayout> uniform_layouts(m_image_count,
|
||||||
|
*effect.uniform_layout);
|
||||||
|
effect.uniform_sets = CreateWrappedDescriptorSets(effect.descriptor_pool, uniform_layouts);
|
||||||
|
|
||||||
|
for (auto& pass : effect.passes) {
|
||||||
|
const std::vector<VkDescriptorSetLayout> layouts(m_image_count, *pass.sampler_layout);
|
||||||
|
pass.sampler_sets = CreateWrappedDescriptorSets(effect.descriptor_pool, layouts);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_effects.push_back(std::move(effect));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostProcessChain::PrepareImages(const Device& device, Scheduler& scheduler) {
|
||||||
|
if (m_images_ready) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler.Record([this](vk::CommandBuffer cmdbuf) {
|
||||||
|
ClearColorImage(cmdbuf, *m_fallback_image);
|
||||||
|
for (auto& frame : m_frames) {
|
||||||
|
for (auto& image : frame.images) {
|
||||||
|
ClearColorImage(cmdbuf, *image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto& effect : m_effects) {
|
||||||
|
for (auto& texture : effect.textures) {
|
||||||
|
if (texture.image) {
|
||||||
|
ClearColorImage(cmdbuf, *texture.image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
scheduler.Finish();
|
||||||
|
|
||||||
|
m_images_ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostProcessChain::UpdateUniforms(Effect& effect, size_t image_index, f32 delta_seconds) {
|
||||||
|
if (effect.uniform_size == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static thread_local std::mt19937 rng{std::random_device{}()};
|
||||||
|
|
||||||
|
std::vector<u8> staging(effect.uniform_size, 0);
|
||||||
|
const f32 elapsed =
|
||||||
|
std::chrono::duration<f32>(std::chrono::steady_clock::now() - m_start).count();
|
||||||
|
|
||||||
|
for (auto& uniform : effect.uniforms) {
|
||||||
|
std::array<f32, 4> value = uniform.fallback;
|
||||||
|
|
||||||
|
switch (uniform.source) {
|
||||||
|
case UniformSource::FrameTime:
|
||||||
|
value[0] = delta_seconds * 1000.0f;
|
||||||
|
break;
|
||||||
|
case UniformSource::FrameCount:
|
||||||
|
value[0] = static_cast<f32>(m_frame_count);
|
||||||
|
break;
|
||||||
|
case UniformSource::Timer:
|
||||||
|
value[0] = elapsed * 1000.0f;
|
||||||
|
break;
|
||||||
|
case UniformSource::Random: {
|
||||||
|
const int low = static_cast<int>(uniform.args[0]);
|
||||||
|
int high = static_cast<int>(uniform.args[1]);
|
||||||
|
if (high <= low) {
|
||||||
|
high = low + 1;
|
||||||
|
}
|
||||||
|
std::uniform_int_distribution<int> dist(low, high);
|
||||||
|
value[0] = static_cast<f32>(dist(rng));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UniformSource::PingPong: {
|
||||||
|
const f32 min_value = uniform.args[0];
|
||||||
|
f32 max_value = uniform.args[1];
|
||||||
|
if (max_value <= min_value) {
|
||||||
|
max_value = min_value + 1.0f;
|
||||||
|
}
|
||||||
|
f32 step = uniform.args[2];
|
||||||
|
if (step == 0.0f) {
|
||||||
|
step = 1.0f;
|
||||||
|
}
|
||||||
|
uniform.state += uniform.direction * step * delta_seconds;
|
||||||
|
if (uniform.state >= max_value) {
|
||||||
|
uniform.state = max_value;
|
||||||
|
uniform.direction = -1.0f;
|
||||||
|
}
|
||||||
|
if (uniform.state <= min_value) {
|
||||||
|
uniform.state = min_value;
|
||||||
|
uniform.direction = 1.0f;
|
||||||
|
}
|
||||||
|
value[0] = uniform.state;
|
||||||
|
value[1] = uniform.direction;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UniformSource::Value:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (u32 i = 0; i < uniform.components; ++i) {
|
||||||
|
const size_t offset = uniform.offset + i * sizeof(u32);
|
||||||
|
if (offset + sizeof(u32) > staging.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (uniform.kind == UniformKind::Floating) {
|
||||||
|
const f32 element = value[i];
|
||||||
|
std::memcpy(staging.data() + offset, &element, sizeof(f32));
|
||||||
|
} else {
|
||||||
|
const s32 element = static_cast<s32>(value[i]);
|
||||||
|
std::memcpy(staging.data() + offset, &element, sizeof(s32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::span<u8> mapped = effect.uniform_buffers[image_index].Mapped();
|
||||||
|
if (mapped.size() >= staging.size()) {
|
||||||
|
std::memcpy(mapped.data(), staging.data(), staging.size());
|
||||||
|
effect.uniform_buffers[image_index].Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostProcessChain::UpdateDescriptors(const Device& device, Effect& effect, Pass& pass,
|
||||||
|
size_t image_index, VkImageView backbuffer_view) {
|
||||||
|
std::vector<VkDescriptorImageInfo> image_infos;
|
||||||
|
std::vector<VkWriteDescriptorSet> writes;
|
||||||
|
image_infos.reserve(pass.sampler_bindings.size() + 1);
|
||||||
|
|
||||||
|
const VkDescriptorSet sampler_set = pass.sampler_sets[image_index];
|
||||||
|
|
||||||
|
for (const auto& binding : pass.sampler_bindings) {
|
||||||
|
VkImageView view = *m_fallback_view;
|
||||||
|
VkSampler handle = *m_fallback_sampler;
|
||||||
|
|
||||||
|
if (binding.sampler_index < effect.samplers.size()) {
|
||||||
|
const Sampler& sampler = effect.samplers[binding.sampler_index];
|
||||||
|
if (sampler.sampler) {
|
||||||
|
handle = *sampler.sampler;
|
||||||
|
}
|
||||||
|
if (sampler.texture_index != NO_TEXTURE) {
|
||||||
|
const Texture& texture = effect.textures[sampler.texture_index];
|
||||||
|
if (texture.is_backbuffer) {
|
||||||
|
view = backbuffer_view;
|
||||||
|
} else if (texture.view) {
|
||||||
|
view = *texture.view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writes.push_back(
|
||||||
|
CreateWriteDescriptorSet(image_infos, handle, view, sampler_set, binding.binding));
|
||||||
|
}
|
||||||
|
|
||||||
|
const VkDescriptorBufferInfo buffer_info{
|
||||||
|
.buffer = *effect.uniform_buffers[image_index],
|
||||||
|
.offset = 0,
|
||||||
|
.range = VK_WHOLE_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
writes.push_back(VkWriteDescriptorSet{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.dstSet = effect.uniform_sets[image_index],
|
||||||
|
.dstBinding = 0,
|
||||||
|
.dstArrayElement = 0,
|
||||||
|
.descriptorCount = 1,
|
||||||
|
.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
|
||||||
|
.pImageInfo = nullptr,
|
||||||
|
.pBufferInfo = &buffer_info,
|
||||||
|
.pTexelBufferView = nullptr,
|
||||||
|
});
|
||||||
|
|
||||||
|
device.GetLogical().UpdateDescriptorSets(writes, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostProcessChain::Draw(const Device& device, Scheduler& scheduler, size_t image_index,
|
||||||
|
VkImage* inout_image, VkImageView* inout_image_view) {
|
||||||
|
if (m_effects.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrepareImages(device, scheduler);
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
const f32 delta_seconds = std::chrono::duration<f32>(now - m_previous).count();
|
||||||
|
m_previous = now;
|
||||||
|
++m_frame_count;
|
||||||
|
|
||||||
|
FrameImages& frame = m_frames[image_index];
|
||||||
|
VkImage current_image = *inout_image;
|
||||||
|
VkImageView current_view = *inout_image_view;
|
||||||
|
u32 slot = 0;
|
||||||
|
|
||||||
|
for (auto& effect : m_effects) {
|
||||||
|
UpdateUniforms(effect, image_index, delta_seconds);
|
||||||
|
|
||||||
|
for (size_t pass_index = 0; pass_index < effect.passes.size(); ++pass_index) {
|
||||||
|
Pass& pass = effect.passes[pass_index];
|
||||||
|
|
||||||
|
UpdateDescriptors(device, effect, pass, image_index, current_view);
|
||||||
|
|
||||||
|
VkFramebuffer framebuffer{};
|
||||||
|
VkImage target_image{};
|
||||||
|
if (pass.writes_backbuffer) {
|
||||||
|
const u32 target_slot = (slot + 1) % 2;
|
||||||
|
framebuffer = *pass.framebuffers[image_index * 2 + target_slot];
|
||||||
|
target_image = *frame.images[target_slot];
|
||||||
|
} else {
|
||||||
|
framebuffer = *pass.framebuffers[0];
|
||||||
|
target_image = *effect.textures[pass.target_texture].image;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VkImage source_image = current_image;
|
||||||
|
const VkRenderPass renderpass = *pass.renderpass;
|
||||||
|
const VkPipeline pipeline = *pass.pipeline;
|
||||||
|
const VkPipelineLayout layout = *pass.pipeline_layout;
|
||||||
|
const VkDescriptorSet uniform_set = effect.uniform_sets[image_index];
|
||||||
|
const VkDescriptorSet sampler_set = pass.sampler_sets[image_index];
|
||||||
|
const VkExtent2D extent = pass.extent;
|
||||||
|
const u32 vertices = pass.num_vertices;
|
||||||
|
|
||||||
|
scheduler.RequestOutsideRenderPassOperationContext();
|
||||||
|
scheduler.Record([=](vk::CommandBuffer cmdbuf) {
|
||||||
|
TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL);
|
||||||
|
TransitionImageLayout(cmdbuf, target_image, VK_IMAGE_LAYOUT_GENERAL);
|
||||||
|
BeginRenderPass(cmdbuf, renderpass, framebuffer, extent);
|
||||||
|
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||||
|
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0,
|
||||||
|
std::array{uniform_set, sampler_set}, {});
|
||||||
|
cmdbuf.Draw(vertices, 1, 0, 0);
|
||||||
|
cmdbuf.EndRenderPass();
|
||||||
|
TransitionImageLayout(cmdbuf, target_image, VK_IMAGE_LAYOUT_GENERAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pass.writes_backbuffer) {
|
||||||
|
slot = (slot + 1) % 2;
|
||||||
|
current_image = *frame.images[slot];
|
||||||
|
current_view = *frame.views[slot];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*inout_image = current_image;
|
||||||
|
*inout_image_view = current_view;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Vulkan
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
||||||
|
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||||
|
|
||||||
|
namespace Vulkan {
|
||||||
|
|
||||||
|
class Device;
|
||||||
|
class Scheduler;
|
||||||
|
|
||||||
|
class PostProcessChain {
|
||||||
|
public:
|
||||||
|
explicit PostProcessChain(const Device& device, MemoryAllocator& allocator, Scheduler& scheduler,
|
||||||
|
size_t image_count, VkExtent2D extent);
|
||||||
|
~PostProcessChain();
|
||||||
|
|
||||||
|
void Draw(const Device& device, Scheduler& scheduler, size_t image_index, VkImage* inout_image,
|
||||||
|
VkImageView* inout_image_view);
|
||||||
|
|
||||||
|
bool Empty() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class UniformKind : u32 {
|
||||||
|
Boolean,
|
||||||
|
Integer,
|
||||||
|
Floating,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class UniformSource : u32 {
|
||||||
|
Value,
|
||||||
|
FrameTime,
|
||||||
|
FrameCount,
|
||||||
|
Timer,
|
||||||
|
Random,
|
||||||
|
PingPong,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UniformWrite {
|
||||||
|
std::string name;
|
||||||
|
u32 offset{};
|
||||||
|
u32 components{};
|
||||||
|
UniformKind kind{UniformKind::Floating};
|
||||||
|
UniformSource source{UniformSource::Value};
|
||||||
|
std::array<f32, 4> fallback{};
|
||||||
|
std::array<f32, 4> args{};
|
||||||
|
f32 state{};
|
||||||
|
f32 direction{1.0f};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Texture {
|
||||||
|
std::string name;
|
||||||
|
vk::Image image{};
|
||||||
|
vk::ImageView view{};
|
||||||
|
VkExtent2D extent{};
|
||||||
|
VkFormat format{};
|
||||||
|
bool is_backbuffer{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Sampler {
|
||||||
|
vk::Sampler sampler{};
|
||||||
|
size_t texture_index{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SamplerBinding {
|
||||||
|
u32 binding{};
|
||||||
|
size_t sampler_index{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Pass {
|
||||||
|
vk::RenderPass renderpass{};
|
||||||
|
vk::Pipeline pipeline{};
|
||||||
|
vk::DescriptorSetLayout sampler_layout{};
|
||||||
|
vk::PipelineLayout pipeline_layout{};
|
||||||
|
vk::DescriptorSets sampler_sets{};
|
||||||
|
std::vector<SamplerBinding> sampler_bindings{};
|
||||||
|
std::vector<vk::Framebuffer> framebuffers{};
|
||||||
|
size_t target_texture{};
|
||||||
|
VkExtent2D extent{};
|
||||||
|
u32 num_vertices{3};
|
||||||
|
bool clear{};
|
||||||
|
bool writes_backbuffer{};
|
||||||
|
u32 backbuffer_slot{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Effect {
|
||||||
|
size_t entry_index{};
|
||||||
|
std::string file{};
|
||||||
|
std::map<std::string, vk::ShaderModule> shaders{};
|
||||||
|
std::vector<Texture> textures{};
|
||||||
|
std::vector<Sampler> samplers{};
|
||||||
|
std::vector<Pass> passes{};
|
||||||
|
std::vector<UniformWrite> uniforms{};
|
||||||
|
u32 uniform_size{};
|
||||||
|
std::vector<vk::Buffer> uniform_buffers{};
|
||||||
|
vk::DescriptorSetLayout uniform_layout{};
|
||||||
|
vk::DescriptorPool descriptor_pool{};
|
||||||
|
vk::DescriptorSets uniform_sets{};
|
||||||
|
size_t backbuffer_pass_count{};
|
||||||
|
u32 backbuffer_slots{1};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FrameImages {
|
||||||
|
std::array<vk::Image, 2> images{};
|
||||||
|
std::array<vk::ImageView, 2> views{};
|
||||||
|
};
|
||||||
|
|
||||||
|
bool BuildEffects(const Device& device, MemoryAllocator& allocator, Scheduler& scheduler);
|
||||||
|
void CreatePingPongImages(const Device& device, MemoryAllocator& allocator);
|
||||||
|
void PrepareImages(const Device& device, Scheduler& scheduler);
|
||||||
|
void UpdateUniforms(Effect& effect, size_t image_index, f32 delta_seconds);
|
||||||
|
void UpdateDescriptors(const Device& device, Effect& effect, Pass& pass, size_t image_index,
|
||||||
|
VkImageView backbuffer_view);
|
||||||
|
|
||||||
|
const VkExtent2D m_extent;
|
||||||
|
const u32 m_image_count;
|
||||||
|
|
||||||
|
std::vector<Effect> m_effects{};
|
||||||
|
std::vector<FrameImages> m_frames{};
|
||||||
|
|
||||||
|
vk::Sampler m_fallback_sampler{};
|
||||||
|
vk::Image m_fallback_image{};
|
||||||
|
vk::ImageView m_fallback_view{};
|
||||||
|
|
||||||
|
std::chrono::steady_clock::time_point m_start{};
|
||||||
|
std::chrono::steady_clock::time_point m_previous{};
|
||||||
|
u64 m_frame_count{};
|
||||||
|
bool m_images_ready{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Vulkan
|
||||||
@@ -24,6 +24,10 @@
|
|||||||
#include "video_core/gpu.h"
|
#include "video_core/gpu.h"
|
||||||
#include "video_core/present.h"
|
#include "video_core/present.h"
|
||||||
#include "video_core/renderer_vulkan/present/util.h"
|
#include "video_core/renderer_vulkan/present/util.h"
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
#include "video_core/post_processing/fx_chain.h"
|
||||||
|
#include "video_core/post_processing/fx_effect.h"
|
||||||
|
#endif
|
||||||
#include "video_core/renderer_vulkan/renderer_vulkan.h"
|
#include "video_core/renderer_vulkan/renderer_vulkan.h"
|
||||||
#include "video_core/renderer_vulkan/vk_blit_screen.h"
|
#include "video_core/renderer_vulkan/vk_blit_screen.h"
|
||||||
#include "video_core/renderer_vulkan/vk_rasterizer.h"
|
#include "video_core/renderer_vulkan/vk_rasterizer.h"
|
||||||
@@ -183,6 +187,12 @@ try
|
|||||||
scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); });
|
scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
VideoCore::ReloadFxCatalog();
|
||||||
|
VideoCore::FxChain::Instance().LoadFromSettings();
|
||||||
|
VideoCore::FxChain::Instance().DropUnknownEntries();
|
||||||
|
#endif
|
||||||
|
|
||||||
Report();
|
Report();
|
||||||
} catch (const vk::Exception& exception) {
|
} catch (const vk::Exception& exception) {
|
||||||
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
|
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
|
||||||
|
|||||||
@@ -250,6 +250,12 @@ if (YUZU_CRASH_DUMPS)
|
|||||||
target_compile_definitions(yuzu PRIVATE YUZU_CRASH_DUMPS)
|
target_compile_definitions(yuzu PRIVATE YUZU_CRASH_DUMPS)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (ENABLE_RESHADE)
|
||||||
|
target_sources(yuzu PRIVATE
|
||||||
|
configuration/configure_post_processing.cpp
|
||||||
|
configuration/configure_post_processing.h)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (CXX_CLANG)
|
if (CXX_CLANG)
|
||||||
target_compile_definitions(yuzu PRIVATE
|
target_compile_definitions(yuzu PRIVATE
|
||||||
$<$<VERSION_LESS:$<CXX_COMPILER_VERSION>,15>:CANNOT_EXPLICITLY_INSTANTIATE>)
|
$<$<VERSION_LESS:$<CXX_COMPILER_VERSION>,15>:CANNOT_EXPLICITLY_INSTANTIATE>)
|
||||||
|
|||||||
@@ -2936,10 +2936,10 @@ void PlayerControlPreview::DrawArrow(QPainter& p, const QPointF center, const Di
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw motion functions
|
// Draw motion functions
|
||||||
void PlayerControlPreview::Draw3dCube(QPainter& p, QPointF center, const Common::Vec<f32, 3>& euler,
|
void PlayerControlPreview::Draw3dCube(QPainter& p, QPointF center, const Common::Vec3f& euler,
|
||||||
float size) {
|
float size) {
|
||||||
std::array<Common::Vec<f32, 3>, 8> cube{
|
std::array<Common::Vec3f, 8> cube{
|
||||||
Common::Vec<f32, 3>{-0.7f, -1, -0.5f},
|
Common::Vec3f{-0.7f, -1, -0.5f},
|
||||||
{-0.7f, 1, -0.5f},
|
{-0.7f, 1, -0.5f},
|
||||||
{0.7f, 1, -0.5f},
|
{0.7f, 1, -0.5f},
|
||||||
{0.7f, -1, -0.5f},
|
{0.7f, -1, -0.5f},
|
||||||
@@ -2949,38 +2949,30 @@ void PlayerControlPreview::Draw3dCube(QPainter& p, QPointF center, const Common:
|
|||||||
{0.7f, -1, 0.5f},
|
{0.7f, -1, 0.5f},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (Common::Vec<f32, 3>& point : cube) {
|
for (Common::Vec3f& point : cube) {
|
||||||
float temp = point[1];
|
point.RotateFromOrigin(euler.x, euler.y, euler.z);
|
||||||
point[1] = std::cos(euler[0]) * point[1] - std::sin(euler[0]) * point[2];
|
|
||||||
point[2] = std::sin(euler[0]) * temp + std::cos(euler[0]) * point[2];
|
|
||||||
temp = point[0];
|
|
||||||
point[0] = std::cos(euler[1]) * point[0] + std::sin(euler[1]) * point[2];
|
|
||||||
point[2] = -std::sin(euler[1]) * temp + std::cos(euler[1]) * point[2];
|
|
||||||
temp = point[0];
|
|
||||||
point[0] = std::cos(euler[2]) * point[0] - std::sin(euler[2]) * point[1];
|
|
||||||
point[1] = std::sin(euler[2]) * temp + std::cos(euler[2]) * point[1];
|
|
||||||
point *= size;
|
point *= size;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::array<QPointF, 4> front_face{
|
const std::array<QPointF, 4> front_face{
|
||||||
center + QPointF{cube[0][0], cube[0][1]},
|
center + QPointF{cube[0].x, cube[0].y},
|
||||||
center + QPointF{cube[1][0], cube[1][1]},
|
center + QPointF{cube[1].x, cube[1].y},
|
||||||
center + QPointF{cube[2][0], cube[2][1]},
|
center + QPointF{cube[2].x, cube[2].y},
|
||||||
center + QPointF{cube[3][0], cube[3][1]},
|
center + QPointF{cube[3].x, cube[3].y},
|
||||||
};
|
};
|
||||||
const std::array<QPointF, 4> back_face{
|
const std::array<QPointF, 4> back_face{
|
||||||
center + QPointF{cube[4][0], cube[4][1]},
|
center + QPointF{cube[4].x, cube[4].y},
|
||||||
center + QPointF{cube[5][0], cube[5][1]},
|
center + QPointF{cube[5].x, cube[5].y},
|
||||||
center + QPointF{cube[6][0], cube[6][1]},
|
center + QPointF{cube[6].x, cube[6].y},
|
||||||
center + QPointF{cube[7][0], cube[7][1]},
|
center + QPointF{cube[7].x, cube[7].y},
|
||||||
};
|
};
|
||||||
|
|
||||||
DrawPolygon(p, front_face);
|
DrawPolygon(p, front_face);
|
||||||
DrawPolygon(p, back_face);
|
DrawPolygon(p, back_face);
|
||||||
p.drawLine(center + QPointF{cube[0][0], cube[0][1]}, center + QPointF{cube[4][0], cube[4][1]});
|
p.drawLine(center + QPointF{cube[0].x, cube[0].y}, center + QPointF{cube[4].x, cube[4].y});
|
||||||
p.drawLine(center + QPointF{cube[1][0], cube[1][1]}, center + QPointF{cube[5][0], cube[5][1]});
|
p.drawLine(center + QPointF{cube[1].x, cube[1].y}, center + QPointF{cube[5].x, cube[5].y});
|
||||||
p.drawLine(center + QPointF{cube[2][0], cube[2][1]}, center + QPointF{cube[6][0], cube[6][1]});
|
p.drawLine(center + QPointF{cube[2].x, cube[2].y}, center + QPointF{cube[6].x, cube[6].y});
|
||||||
p.drawLine(center + QPointF{cube[3][0], cube[3][1]}, center + QPointF{cube[7][0], cube[7][1]});
|
p.drawLine(center + QPointF{cube[3].x, cube[3].y}, center + QPointF{cube[7].x, cube[7].y});
|
||||||
}
|
}
|
||||||
|
|
||||||
template <size_t N>
|
template <size_t N>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
@@ -198,7 +198,7 @@ private:
|
|||||||
void DrawArrow(QPainter& p, QPointF center, Direction direction, float size);
|
void DrawArrow(QPainter& p, QPointF center, Direction direction, float size);
|
||||||
|
|
||||||
// Draw motion functions
|
// Draw motion functions
|
||||||
void Draw3dCube(QPainter& p, QPointF center, const Common::Vec<f32, 3>& euler, float size);
|
void Draw3dCube(QPainter& p, QPointF center, const Common::Vec3f& euler, float size);
|
||||||
|
|
||||||
// Draw primitive types
|
// Draw primitive types
|
||||||
template <size_t N>
|
template <size_t N>
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
|
#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"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::array<float, 4> CurrentValue(int index, const VideoCore::FxUniformDesc& uniform) {
|
||||||
|
auto& chain = VideoCore::FxChain::Instance();
|
||||||
|
if (chain.HasValue(static_cast<size_t>(index), uniform.name)) {
|
||||||
|
return chain.GetValue(static_cast<size_t>(index), uniform.name);
|
||||||
|
}
|
||||||
|
return uniform.default_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SliderSteps(const VideoCore::FxUniformDesc& uniform) {
|
||||||
|
const float span = uniform.ui_max - uniform.ui_min;
|
||||||
|
const int steps = static_cast<int>(std::lround(span / uniform.ui_step));
|
||||||
|
if (steps < 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FormatValue(const VideoCore::FxUniformDesc& uniform, float value) {
|
||||||
|
if (uniform.kind == VideoCore::FxUniformKind::Floating) {
|
||||||
|
return QString::number(value, 'f', 3);
|
||||||
|
}
|
||||||
|
return QString::number(static_cast<int>(std::lround(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SlotLabel(const VideoCore::FxEffectDesc& effect, const std::string& technique) {
|
||||||
|
const QString name = QString::fromStdString(effect.name);
|
||||||
|
if (effect.techniques.size() == 1) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
return name + QStringLiteral(" · ") + QString::fromStdString(technique);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
ConfigurePostProcessing::ConfigurePostProcessing(QWidget* parent) : QDialog(parent) {
|
||||||
|
setWindowTitle(tr("Post-Processing Effects"));
|
||||||
|
setMinimumWidth(560);
|
||||||
|
setMinimumHeight(460);
|
||||||
|
|
||||||
|
auto* root = new QVBoxLayout(this);
|
||||||
|
|
||||||
|
auto* description = new QLabel(
|
||||||
|
tr("ReShade FX effects are loaded from the post_shaders folder in the Eden data "
|
||||||
|
"directory. Changes apply immediately while a game is running."),
|
||||||
|
this);
|
||||||
|
description->setWordWrap(true);
|
||||||
|
root->addWidget(description);
|
||||||
|
|
||||||
|
auto* scroll = new QScrollArea(this);
|
||||||
|
scroll->setWidgetResizable(true);
|
||||||
|
slots_container = new QWidget(scroll);
|
||||||
|
slots_layout = new QVBoxLayout(slots_container);
|
||||||
|
slots_layout->setAlignment(Qt::AlignTop);
|
||||||
|
scroll->setWidget(slots_container);
|
||||||
|
root->addWidget(scroll, 1);
|
||||||
|
|
||||||
|
auto* actions = new QHBoxLayout();
|
||||||
|
|
||||||
|
auto* add_button = new QPushButton(tr("Add Effect"), this);
|
||||||
|
connect(add_button, &QPushButton::clicked, this, [this]() {
|
||||||
|
for (const auto& effect : VideoCore::GetFxCatalog()) {
|
||||||
|
if (!effect.Valid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
VideoCore::FxChain::Instance().Append(effect.file, effect.techniques.front());
|
||||||
|
ApplyStructuralChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
|
||||||
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
|
||||||
|
root->addWidget(buttons);
|
||||||
|
|
||||||
|
RebuildRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurePostProcessing::~ConfigurePostProcessing() = default;
|
||||||
|
|
||||||
|
void ConfigurePostProcessing::ApplyStructuralChange() {
|
||||||
|
VideoCore::FxChain::Instance().StoreToSettings();
|
||||||
|
RebuildRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigurePostProcessing::PopulateEffectCombo(QComboBox* combo,
|
||||||
|
const VideoCore::FxChainEntry& entry) const {
|
||||||
|
combo->clear();
|
||||||
|
int selected = -1;
|
||||||
|
|
||||||
|
for (const auto& effect : VideoCore::GetFxCatalog()) {
|
||||||
|
if (!effect.Valid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const auto& technique : effect.techniques) {
|
||||||
|
const QString key = QString::fromStdString(effect.file + "|" + technique);
|
||||||
|
combo->addItem(SlotLabel(effect, technique), key);
|
||||||
|
if (effect.file == entry.file && technique == entry.technique) {
|
||||||
|
selected = combo->count() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected >= 0) {
|
||||||
|
combo->setCurrentIndex(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigurePostProcessing::BuildUniformWidget(QWidget* parent, QVBoxLayout* layout, int index,
|
||||||
|
const VideoCore::FxUniformDesc& uniform) {
|
||||||
|
const auto value = CurrentValue(index, uniform);
|
||||||
|
const QString label = QString::fromStdString(uniform.label);
|
||||||
|
|
||||||
|
if (uniform.ui_type == VideoCore::FxUiType::CheckBox) {
|
||||||
|
auto* box = new QCheckBox(label, parent);
|
||||||
|
box->setChecked(value[0] != 0.0f);
|
||||||
|
if (!uniform.tooltip.empty()) {
|
||||||
|
box->setToolTip(QString::fromStdString(uniform.tooltip));
|
||||||
|
}
|
||||||
|
const std::string name = uniform.name;
|
||||||
|
connect(box, &QCheckBox::toggled, this, [index, name](bool checked) {
|
||||||
|
std::array<float, 4> next{};
|
||||||
|
if (checked) {
|
||||||
|
next[0] = 1.0f;
|
||||||
|
}
|
||||||
|
VideoCore::FxChain::Instance().SetValue(static_cast<size_t>(index), name, next);
|
||||||
|
VideoCore::FxChain::Instance().StoreToSettings();
|
||||||
|
});
|
||||||
|
layout->addWidget(box);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniform.ui_type == VideoCore::FxUiType::Combo ||
|
||||||
|
uniform.ui_type == VideoCore::FxUiType::Radio) {
|
||||||
|
auto* row = new QHBoxLayout();
|
||||||
|
row->addWidget(new QLabel(label, parent));
|
||||||
|
auto* combo = new QComboBox(parent);
|
||||||
|
for (size_t i = 0; i < uniform.items.size(); ++i) {
|
||||||
|
combo->addItem(QString::fromStdString(uniform.items[i]), static_cast<int>(i));
|
||||||
|
}
|
||||||
|
if (combo->count() == 0) {
|
||||||
|
combo->addItem(tr("Enabled"), 1);
|
||||||
|
combo->addItem(tr("Disabled"), 0);
|
||||||
|
}
|
||||||
|
const int current = static_cast<int>(std::lround(value[0]));
|
||||||
|
if (current >= 0 && current < combo->count()) {
|
||||||
|
combo->setCurrentIndex(current);
|
||||||
|
}
|
||||||
|
if (!uniform.tooltip.empty()) {
|
||||||
|
combo->setToolTip(QString::fromStdString(uniform.tooltip));
|
||||||
|
}
|
||||||
|
const std::string name = uniform.name;
|
||||||
|
connect(combo, &QComboBox::currentIndexChanged, this, [index, name](int selected) {
|
||||||
|
std::array<float, 4> next{};
|
||||||
|
next[0] = static_cast<float>(selected);
|
||||||
|
VideoCore::FxChain::Instance().SetValue(static_cast<size_t>(index), name, next);
|
||||||
|
VideoCore::FxChain::Instance().StoreToSettings();
|
||||||
|
});
|
||||||
|
row->addWidget(combo, 1);
|
||||||
|
layout->addLayout(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* grid = new QGridLayout();
|
||||||
|
for (unsigned component = 0; component < uniform.components; ++component) {
|
||||||
|
QString component_label = label;
|
||||||
|
if (uniform.components > 1) {
|
||||||
|
component_label = label + QStringLiteral(" [%1]").arg(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* name_label = new QLabel(component_label, parent);
|
||||||
|
auto* value_label = new QLabel(parent);
|
||||||
|
value_label->setMinimumWidth(64);
|
||||||
|
value_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
|
value_label->setText(FormatValue(uniform, value[component]));
|
||||||
|
|
||||||
|
auto* slider = new QSlider(Qt::Horizontal, parent);
|
||||||
|
slider->setMinimum(0);
|
||||||
|
slider->setMaximum(SliderSteps(uniform));
|
||||||
|
slider->setValue(
|
||||||
|
static_cast<int>(std::lround((value[component] - uniform.ui_min) / uniform.ui_step)));
|
||||||
|
if (!uniform.tooltip.empty()) {
|
||||||
|
slider->setToolTip(QString::fromStdString(uniform.tooltip));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string name = uniform.name;
|
||||||
|
const auto desc = uniform;
|
||||||
|
connect(slider, &QSlider::valueChanged, this,
|
||||||
|
[index, name, desc, component, value_label](int steps) {
|
||||||
|
auto next = CurrentValue(index, desc);
|
||||||
|
next[component] = desc.ui_min + static_cast<float>(steps) * desc.ui_step;
|
||||||
|
VideoCore::FxChain::Instance().SetValue(static_cast<size_t>(index), name, next);
|
||||||
|
value_label->setText(FormatValue(desc, next[component]));
|
||||||
|
});
|
||||||
|
connect(slider, &QSlider::sliderReleased, this,
|
||||||
|
[]() { VideoCore::FxChain::Instance().StoreToSettings(); });
|
||||||
|
|
||||||
|
grid->addWidget(name_label, static_cast<int>(component), 0);
|
||||||
|
grid->addWidget(slider, static_cast<int>(component), 1);
|
||||||
|
grid->addWidget(value_label, static_cast<int>(component), 2);
|
||||||
|
}
|
||||||
|
layout->addLayout(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget* ConfigurePostProcessing::BuildSlot(int index, const VideoCore::FxChainEntry& entry) {
|
||||||
|
auto* group = new QGroupBox(slots_container);
|
||||||
|
auto* layout = new QVBoxLayout(group);
|
||||||
|
|
||||||
|
auto* header = new QHBoxLayout();
|
||||||
|
|
||||||
|
auto* combo = new QComboBox(group);
|
||||||
|
PopulateEffectCombo(combo, entry);
|
||||||
|
connect(combo, &QComboBox::currentIndexChanged, this, [this, index, combo](int) {
|
||||||
|
const QString key = combo->currentData().toString();
|
||||||
|
const qsizetype separator = key.indexOf(QLatin1Char('|'));
|
||||||
|
if (separator < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
VideoCore::FxChain::Instance().Replace(static_cast<size_t>(index),
|
||||||
|
key.left(separator).toStdString(),
|
||||||
|
key.mid(separator + 1).toStdString());
|
||||||
|
ApplyStructuralChange();
|
||||||
|
});
|
||||||
|
header->addWidget(combo, 1);
|
||||||
|
|
||||||
|
auto* up_button = new QToolButton(group);
|
||||||
|
up_button->setText(QStringLiteral("▲"));
|
||||||
|
up_button->setEnabled(index > 0);
|
||||||
|
connect(up_button, &QToolButton::clicked, this, [this, index]() {
|
||||||
|
VideoCore::FxChain::Instance().Move(static_cast<size_t>(index), -1);
|
||||||
|
ApplyStructuralChange();
|
||||||
|
});
|
||||||
|
header->addWidget(up_button);
|
||||||
|
|
||||||
|
auto* down_button = new QToolButton(group);
|
||||||
|
down_button->setText(QStringLiteral("▼"));
|
||||||
|
down_button->setEnabled(static_cast<size_t>(index) + 1 < VideoCore::FxChain::Instance().Size());
|
||||||
|
connect(down_button, &QToolButton::clicked, this, [this, index]() {
|
||||||
|
VideoCore::FxChain::Instance().Move(static_cast<size_t>(index), 1);
|
||||||
|
ApplyStructuralChange();
|
||||||
|
});
|
||||||
|
header->addWidget(down_button);
|
||||||
|
|
||||||
|
auto* reset_button = new QToolButton(group);
|
||||||
|
reset_button->setText(QStringLiteral("⟲"));
|
||||||
|
reset_button->setToolTip(tr("Reset to defaults"));
|
||||||
|
connect(reset_button, &QToolButton::clicked, this, [this, index]() {
|
||||||
|
VideoCore::FxChain::Instance().ResetValues(static_cast<size_t>(index));
|
||||||
|
ApplyStructuralChange();
|
||||||
|
});
|
||||||
|
header->addWidget(reset_button);
|
||||||
|
|
||||||
|
auto* remove_button = new QToolButton(group);
|
||||||
|
remove_button->setText(QStringLiteral("✕"));
|
||||||
|
connect(remove_button, &QToolButton::clicked, this, [this, index]() {
|
||||||
|
VideoCore::FxChain::Instance().Remove(static_cast<size_t>(index));
|
||||||
|
ApplyStructuralChange();
|
||||||
|
});
|
||||||
|
header->addWidget(remove_button);
|
||||||
|
|
||||||
|
layout->addLayout(header);
|
||||||
|
|
||||||
|
const VideoCore::FxEffectDesc* effect = VideoCore::FindFxEffect(entry.file);
|
||||||
|
if (effect == nullptr) {
|
||||||
|
auto* missing =
|
||||||
|
new QLabel(tr("Effect '%1' was not found.").arg(QString::fromStdString(entry.file)),
|
||||||
|
group);
|
||||||
|
missing->setWordWrap(true);
|
||||||
|
layout->addWidget(missing);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!effect->error.empty()) {
|
||||||
|
auto* failed = new QLabel(
|
||||||
|
tr("Effect failed to compile:\n%1").arg(QString::fromStdString(effect->error)), group);
|
||||||
|
failed->setWordWrap(true);
|
||||||
|
layout->addWidget(failed);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string current_category;
|
||||||
|
for (const auto& uniform : effect->uniforms) {
|
||||||
|
if (uniform.category != current_category) {
|
||||||
|
current_category = uniform.category;
|
||||||
|
if (!current_category.empty()) {
|
||||||
|
auto* category = new QLabel(QString::fromStdString(current_category), group);
|
||||||
|
category->setStyleSheet(QStringLiteral("font-weight: bold;"));
|
||||||
|
layout->addWidget(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BuildUniformWidget(group, layout, index, uniform);
|
||||||
|
}
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigurePostProcessing::RebuildRows() {
|
||||||
|
QLayoutItem* item = nullptr;
|
||||||
|
while ((item = slots_layout->takeAt(0)) != nullptr) {
|
||||||
|
if (item->widget() != nullptr) {
|
||||||
|
item->widget()->deleteLater();
|
||||||
|
}
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_usable = false;
|
||||||
|
for (const auto& effect : VideoCore::GetFxCatalog()) {
|
||||||
|
if (effect.Valid()) {
|
||||||
|
has_usable = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!has_usable) {
|
||||||
|
auto* empty = new QLabel(
|
||||||
|
tr("No usable ReShade FX effects were found. Place .fx files in the post_shaders "
|
||||||
|
"folder."),
|
||||||
|
slots_container);
|
||||||
|
empty->setWordWrap(true);
|
||||||
|
slots_layout->addWidget(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto entries = VideoCore::FxChain::Instance().Entries();
|
||||||
|
for (size_t i = 0; i < entries.size(); ++i) {
|
||||||
|
slots_layout->addWidget(BuildSlot(static_cast<int>(i), entries[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
|
class QComboBox;
|
||||||
|
class QVBoxLayout;
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
struct FxChainEntry;
|
||||||
|
struct FxEffectDesc;
|
||||||
|
struct FxUniformDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigurePostProcessing : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ConfigurePostProcessing(QWidget* parent = nullptr);
|
||||||
|
~ConfigurePostProcessing() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RebuildRows();
|
||||||
|
QWidget* BuildSlot(int index, const VideoCore::FxChainEntry& entry);
|
||||||
|
void BuildUniformWidget(QWidget* parent, QVBoxLayout* layout, int index,
|
||||||
|
const VideoCore::FxUniformDesc& uniform);
|
||||||
|
void PopulateEffectCombo(QComboBox* combo, const VideoCore::FxChainEntry& entry) const;
|
||||||
|
void ApplyStructuralChange();
|
||||||
|
|
||||||
|
QVBoxLayout* slots_layout{};
|
||||||
|
QWidget* slots_container{};
|
||||||
|
};
|
||||||
@@ -148,6 +148,7 @@
|
|||||||
<addaction name="action_Show_Filter_Bar"/>
|
<addaction name="action_Show_Filter_Bar"/>
|
||||||
<addaction name="action_Show_Status_Bar"/>
|
<addaction name="action_Show_Status_Bar"/>
|
||||||
<addaction name="action_Show_Performance_Overlay"/>
|
<addaction name="action_Show_Performance_Overlay"/>
|
||||||
|
<addaction name="action_Post_Processing_Shaders"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="menu_Reset_Window_Size"/>
|
<addaction name="menu_Reset_Window_Size"/>
|
||||||
<addaction name="menu_View_Debugging"/>
|
<addaction name="menu_View_Debugging"/>
|
||||||
@@ -610,6 +611,11 @@
|
|||||||
<string>Show &Performance Overlay</string>
|
<string>Show &Performance Overlay</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_Post_Processing_Shaders">
|
||||||
|
<property name="text">
|
||||||
|
<string>Post-Processing &Shaders...</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
<action name="action_Carousel_View">
|
<action name="action_Carousel_View">
|
||||||
<property name="checkable">
|
<property name="checkable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
#include "common/settings_enums.h"
|
#include "common/settings_enums.h"
|
||||||
#include "frontend_common/settings_generator.h"
|
#include "frontend_common/settings_generator.h"
|
||||||
#include "render/performance_overlay.h"
|
#include "render/performance_overlay.h"
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
#include "configuration/configure_post_processing.h"
|
||||||
|
#endif
|
||||||
#include "updater/update_dialog.h"
|
#include "updater/update_dialog.h"
|
||||||
|
|
||||||
#include "common/fs/ryujinx_compat.h"
|
#include "common/fs/ryujinx_compat.h"
|
||||||
@@ -1518,6 +1521,11 @@ void MainWindow::ConnectMenuEvents() {
|
|||||||
connect_menu(ui->action_Show_Filter_Bar, &MainWindow::OnToggleFilterBar);
|
connect_menu(ui->action_Show_Filter_Bar, &MainWindow::OnToggleFilterBar);
|
||||||
connect_menu(ui->action_Show_Status_Bar, &MainWindow::OnToggleStatusBar);
|
connect_menu(ui->action_Show_Status_Bar, &MainWindow::OnToggleStatusBar);
|
||||||
connect_menu(ui->action_Show_Performance_Overlay, &MainWindow::OnTogglePerfOverlay);
|
connect_menu(ui->action_Show_Performance_Overlay, &MainWindow::OnTogglePerfOverlay);
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
connect_menu(ui->action_Post_Processing_Shaders, &MainWindow::OnPostProcessingShaders);
|
||||||
|
#else
|
||||||
|
ui->action_Post_Processing_Shaders->setVisible(false);
|
||||||
|
#endif
|
||||||
|
|
||||||
connect_menu(ui->action_Reset_Window_Size_720, &MainWindow::ResetWindowSize720);
|
connect_menu(ui->action_Reset_Window_Size_720, &MainWindow::ResetWindowSize720);
|
||||||
connect_menu(ui->action_Reset_Window_Size_900, &MainWindow::ResetWindowSize900);
|
connect_menu(ui->action_Reset_Window_Size_900, &MainWindow::ResetWindowSize900);
|
||||||
@@ -3900,6 +3908,22 @@ void MainWindow::OnTogglePerfOverlay() {
|
|||||||
perf_overlay->setVisible(ui->action_Show_Performance_Overlay->isChecked());
|
perf_overlay->setVisible(ui->action_Show_Performance_Overlay->isChecked());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
void MainWindow::OnPostProcessingShaders() {
|
||||||
|
if (post_processing_dialog == nullptr) {
|
||||||
|
post_processing_dialog = new ConfigurePostProcessing(this);
|
||||||
|
connect(post_processing_dialog, &QDialog::finished, post_processing_dialog, [this]() {
|
||||||
|
post_processing_dialog->deleteLater();
|
||||||
|
post_processing_dialog = nullptr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
post_processing_dialog->show();
|
||||||
|
post_processing_dialog->raise();
|
||||||
|
post_processing_dialog->activateWindow();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void MainWindow::OnGameListRefresh() {
|
void MainWindow::OnGameListRefresh() {
|
||||||
// Resets metadata cache and reloads
|
// Resets metadata cache and reloads
|
||||||
QtCommon::Game::ResetMetadata(false);
|
QtCommon::Game::ResetMetadata(false);
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ class QSlider;
|
|||||||
class QHBoxLayout;
|
class QHBoxLayout;
|
||||||
class WaitTreeWidget;
|
class WaitTreeWidget;
|
||||||
class PerformanceOverlay;
|
class PerformanceOverlay;
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
class ConfigurePostProcessing;
|
||||||
|
#endif
|
||||||
enum class GameListOpenTarget;
|
enum class GameListOpenTarget;
|
||||||
enum class DumpRomFSTarget;
|
enum class DumpRomFSTarget;
|
||||||
class GameListPlaceholder;
|
class GameListPlaceholder;
|
||||||
@@ -392,6 +395,9 @@ private slots:
|
|||||||
void OnToggleFilterBar();
|
void OnToggleFilterBar();
|
||||||
void OnToggleStatusBar();
|
void OnToggleStatusBar();
|
||||||
void OnTogglePerfOverlay();
|
void OnTogglePerfOverlay();
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
void OnPostProcessingShaders();
|
||||||
|
#endif
|
||||||
void OnGameListRefresh();
|
void OnGameListRefresh();
|
||||||
void InitializeHotkeys();
|
void InitializeHotkeys();
|
||||||
void ToggleFullscreen();
|
void ToggleFullscreen();
|
||||||
@@ -496,6 +502,9 @@ private:
|
|||||||
QTimer shutdown_timer;
|
QTimer shutdown_timer;
|
||||||
OverlayDialog* shutdown_dialog{};
|
OverlayDialog* shutdown_dialog{};
|
||||||
PerformanceOverlay* perf_overlay = nullptr;
|
PerformanceOverlay* perf_overlay = nullptr;
|
||||||
|
#ifdef HAS_RESHADE
|
||||||
|
ConfigurePostProcessing* post_processing_dialog = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
GameListPlaceholder* game_list_placeholder = nullptr;
|
GameListPlaceholder* game_list_placeholder = nullptr;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user