mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-09-04 11:23:12 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa8d2000f4 | |||
| 821765189a | |||
| f24b14e7cf | |||
| 1dcc574591 | |||
| 5c20f244c9 |
+3
-1
@@ -84,6 +84,8 @@ option(ENABLE_WERROR "Enable -Werror diagnostics" ON)
|
||||
# Lossless Scaling frame generation. Only Android.
|
||||
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
|
||||
if (YUZU_USE_BUNDLED_QT AND (APPLE OR NOT UNIX))
|
||||
set(YUZU_STATIC_BUILD ON)
|
||||
@@ -238,7 +240,7 @@ option(YUZU_USE_BUNDLED_SIRIT "Download bundled sirit" ${BUNDLED_SIRIT_DEFAULT})
|
||||
# FreeBSD 15+ has libusb, versions below should disable it
|
||||
cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "WIN32 OR LINUX OR FREEBSD OR APPLE" OFF)
|
||||
|
||||
cmake_dependent_option(ENABLE_OPENGL "Enable OpenGL" ON "NOT (WIN32 AND ARCHITECTURE_arm64) AND NOT APPLE" OFF)
|
||||
cmake_dependent_option(ENABLE_OPENGL "Enable OpenGL" ON "NOT (WIN32 AND ARCHITECTURE_arm64) AND NOT APPLE AND NOT ANDROID" OFF)
|
||||
mark_as_advanced(FORCE ENABLE_OPENGL)
|
||||
|
||||
option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON)
|
||||
|
||||
@@ -246,6 +246,11 @@
|
||||
"repo": "stachenov/quazip",
|
||||
"version": "2e95c9001b"
|
||||
},
|
||||
"reshade": {
|
||||
"hash": "a1bd3fcf135fb6d018c1831ae45a8942d9777d0418b55e1189921e2ab775d1b53b0a9808924e09ef1c3e68ea11d69b3bcbebc7f4b59de14f6deec2dc24531d5e",
|
||||
"repo": "crosire/reshade",
|
||||
"version": "v6.7.3"
|
||||
},
|
||||
"sdl3": {
|
||||
"hash": "df5a323af7ac366661a3c0e887969c72584d232f3cc211419d59b0487b620b6b2859d4549c9e8df002ee489290062e466fcfddf7edc0872a37b1f2845e81c0f3",
|
||||
"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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -76,7 +76,7 @@ The following options are desktop only.
|
||||
|
||||
- `ENABLE_LIBUSB` (ON) Enable the use of the libusb input backend (HIGHLY RECOMMENDED)
|
||||
- `ENABLE_OPENGL` (ON) Enable the OpenGL graphics backend
|
||||
- Unavailable on Windows/ARM64
|
||||
- Unavailable on Windows/ARM64 and on Android
|
||||
- You probably shouldn't turn this off.
|
||||
|
||||
### Qt
|
||||
|
||||
Vendored
+33
@@ -195,6 +195,39 @@ else()
|
||||
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
|
||||
if (YUZU_TESTS OR DYNARMIC_TESTS)
|
||||
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_RENDERER(R.string.preferences_graphics),
|
||||
SECTION_FRAME_GEN(R.string.frame_gen),
|
||||
SECTION_POST_PROCESSING(R.string.post_processing),
|
||||
SECTION_PERFORMANCE_STATS(R.string.stats_overlay_options),
|
||||
SECTION_INPUT_OVERLAY(R.string.input_overlay_options),
|
||||
SECTION_SOC_OVERLAY(R.string.soc_overlay_options),
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ enum class StringSetting(override val key: String) : AbstractStringSetting {
|
||||
DEVICE_NAME("device_name"),
|
||||
LOG_FILTER("log_filter"),
|
||||
PROGRAM_ARGS("program_args"),
|
||||
POST_SHADER_CHAIN("post_shader_chain"),
|
||||
|
||||
WEB_TOKEN("eden_token"),
|
||||
WEB_USERNAME("eden_username")
|
||||
|
||||
+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.AbstractIntSetting
|
||||
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.IntSetting
|
||||
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.LosslessScalingHelper
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import org.yuzu.yuzu_emu.utils.NativePostProcessing
|
||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||
import org.yuzu.yuzu_emu.utils.FullscreenHelper
|
||||
import androidx.core.content.edit
|
||||
@@ -163,6 +167,7 @@ class SettingsFragmentPresenter(
|
||||
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
|
||||
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
|
||||
MenuTag.SECTION_FRAME_GEN -> addFrameGenSettings(sl)
|
||||
MenuTag.SECTION_POST_PROCESSING -> addPostProcessingSettings(sl)
|
||||
MenuTag.SECTION_PERFORMANCE_STATS -> addPerformanceOverlaySettings(sl)
|
||||
MenuTag.SECTION_SOC_OVERLAY -> addSocOverlaySettings(sl)
|
||||
MenuTag.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl)
|
||||
@@ -190,6 +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>) {
|
||||
sl.apply {
|
||||
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()) {
|
||||
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(
|
||||
HomeSetting(
|
||||
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.h
|
||||
native_input.cpp
|
||||
native_post_processing.cpp
|
||||
)
|
||||
|
||||
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_manager">GPU driver manager</string>
|
||||
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
|
||||
<string name="post_processing">Post-Processing Effects</string>
|
||||
<string name="post_processing_description">ReShade FX effects applied after rendering</string>
|
||||
<string name="post_processing_per_game_description">Configure the effect chain for this game</string>
|
||||
<string name="post_processing_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_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>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#define LOSSLESS_DIR "lossless"
|
||||
#define NAND_DIR "nand"
|
||||
#define PLAY_TIME_DIR "play_time"
|
||||
#define POST_SHADER_DIR "post_shaders"
|
||||
#define SCREENSHOTS_DIR "screenshots"
|
||||
#define SDMC_DIR "sdmc"
|
||||
#define SHADER_DIR "shader"
|
||||
|
||||
@@ -160,6 +160,7 @@ public:
|
||||
GenerateEdenPath(EdenPath::LosslessDir, eden_path / LOSSLESS_DIR);
|
||||
GenerateEdenPath(EdenPath::NANDDir, eden_path / NAND_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::ScreenshotsDir, eden_path / SCREENSHOTS_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.
|
||||
NANDDir, // Where the emulated NAND is stored.
|
||||
PlayTimeDir, // Where play time data is stored.
|
||||
PostShaderDir, // Where user post-processing shaders are stored.
|
||||
SaveDir, // Where save data is stored.
|
||||
ScreenshotsDir, // Where yuzu screenshots are stored.
|
||||
SDMCDir, // Where the emulated SDMC is stored.
|
||||
|
||||
@@ -388,6 +388,14 @@ struct Values {
|
||||
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,
|
||||
Specialization::Default, true, false};
|
||||
|
||||
|
||||
@@ -358,7 +358,7 @@ private:
|
||||
|
||||
ConnectionState(boost::asio::ip::tcp::socket&& client_socket_, async_pipe signal_pipe_, Kernel::KernelCore& kernel)
|
||||
: client_socket{std::move(client_socket_)}
|
||||
, signal_pipe{signal_pipe_}
|
||||
, signal_pipe{std::move(signal_pipe_)}
|
||||
, active_thread{kernel, nullptr}
|
||||
{}
|
||||
|
||||
|
||||
@@ -1021,6 +1021,9 @@ Result KProcess::Run(KernelCore& kernel, s32 priority, size_t stack_size) {
|
||||
|
||||
// Suspend for debug, if we should.
|
||||
if (kernel.System().DebuggerEnabled()) {
|
||||
LOG_INFO(Debug_GDBStub,
|
||||
"GDB stub enabled; suspending guest process until a debugger continues execution on port {}",
|
||||
Settings::values.gdbstub_port.GetValue());
|
||||
main_thread->RequestSuspend(kernel, SuspendType::Debug);
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
|
||||
INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"),
|
||||
tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA "
|
||||
"can produce a more stable picture in lower resolutions."));
|
||||
INSERT(Settings, post_shader_chain, QString(), QString());
|
||||
INSERT(Settings, fullscreen_mode, tr("Fullscreen Mode:"),
|
||||
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 "
|
||||
|
||||
@@ -299,6 +299,21 @@ if (ENABLE_LSFG)
|
||||
target_compile_definitions(video_core PUBLIC HAS_LSFG)
|
||||
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)
|
||||
target_sources(video_core PRIVATE
|
||||
renderer_opengl/present/filters.cpp
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace VideoCommon {
|
||||
|
||||
enum class BufferFlagBits {
|
||||
Picked = 1 << 0,
|
||||
CachedWrites = 1 << 1,
|
||||
PreemtiveDownload = 1 << 2,
|
||||
};
|
||||
DECLARE_ENUM_FLAG_OPERATORS(BufferFlagBits)
|
||||
|
||||
@@ -56,6 +58,15 @@ public:
|
||||
flags |= BufferFlagBits::Picked;
|
||||
}
|
||||
|
||||
void MarkPreemtiveDownload() noexcept {
|
||||
flags |= BufferFlagBits::PreemtiveDownload;
|
||||
}
|
||||
|
||||
/// Unmark buffer as picked
|
||||
void Unpick() noexcept {
|
||||
flags &= ~BufferFlagBits::Picked;
|
||||
}
|
||||
|
||||
/// Increases the likeliness of this being a stream buffer
|
||||
void IncreaseStreamScore(int score) noexcept {
|
||||
stream_score += score;
|
||||
@@ -76,6 +87,15 @@ public:
|
||||
return True(flags & BufferFlagBits::Picked);
|
||||
}
|
||||
|
||||
/// Returns true when the buffer has pending cached writes
|
||||
[[nodiscard]] bool HasCachedWrites() const noexcept {
|
||||
return True(flags & BufferFlagBits::CachedWrites);
|
||||
}
|
||||
|
||||
bool IsPreemtiveDownload() const noexcept {
|
||||
return True(flags & BufferFlagBits::PreemtiveDownload);
|
||||
}
|
||||
|
||||
/// Returns the base CPU address of the buffer
|
||||
[[nodiscard]] VAddr CpuAddr() const noexcept {
|
||||
return cpu_addr;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
|
||||
@@ -122,6 +121,25 @@ void BufferCache<P>::WriteMemory(DAddr device_addr, u64 size) {
|
||||
memory_tracker.MarkRegionAsCpuModified(device_addr, size);
|
||||
}
|
||||
|
||||
template <class P>
|
||||
void BufferCache<P>::CachedWriteMemory(DAddr device_addr, u64 size) {
|
||||
const bool is_dirty = IsRegionRegistered(device_addr, size);
|
||||
if (!is_dirty) {
|
||||
return;
|
||||
}
|
||||
DAddr aligned_start = Common::AlignDown(device_addr, DEVICE_PAGESIZE);
|
||||
DAddr aligned_end = Common::AlignUp(device_addr + size, DEVICE_PAGESIZE);
|
||||
if (!IsRegionGpuModified(aligned_start, aligned_end - aligned_start)) {
|
||||
WriteMemory(device_addr, size);
|
||||
return;
|
||||
}
|
||||
|
||||
tmp_buffer.resize_destructive(size);
|
||||
device_memory.ReadBlockUnsafe(device_addr, tmp_buffer.data(), size);
|
||||
|
||||
InlineMemoryImplementation(device_addr, size, tmp_buffer);
|
||||
}
|
||||
|
||||
template <class P>
|
||||
bool BufferCache<P>::OnCPUWrite(DAddr device_addr, u64 size) {
|
||||
const bool is_dirty = IsRegionRegistered(device_addr, size);
|
||||
@@ -404,7 +422,7 @@ void BufferCache<P>::UnbindGraphicsStorageBuffers(size_t stage) {
|
||||
}
|
||||
|
||||
template <class P>
|
||||
void BufferCache<P>::BindGraphicsStorageBuffer(size_t stage, size_t ssbo_index, u32 cbuf_index,
|
||||
bool BufferCache<P>::BindGraphicsStorageBuffer(size_t stage, size_t ssbo_index, u32 cbuf_index,
|
||||
u32 cbuf_offset, bool is_written) {
|
||||
const bool already_enabled =
|
||||
((channel_state->enabled_storage_buffers[stage] >> ssbo_index) & 1U) != 0;
|
||||
@@ -415,7 +433,7 @@ void BufferCache<P>::BindGraphicsStorageBuffer(size_t stage, size_t ssbo_index,
|
||||
LOG_WARNING(HW_GPU,
|
||||
"Skipping graphics storage buffer {} due to driver limit {}",
|
||||
ssbo_index, max_bindings);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,6 +449,7 @@ void BufferCache<P>::BindGraphicsStorageBuffer(size_t stage, size_t ssbo_index,
|
||||
const GPUVAddr ssbo_addr = cbufs.const_buffers[cbuf_index].address + cbuf_offset;
|
||||
channel_state->storage_buffers[stage][ssbo_index] =
|
||||
StorageBufferBinding(ssbo_addr, cbuf_index, is_written);
|
||||
return (channel_state->storage_buffers[stage][ssbo_index].buffer_id != NULL_BUFFER_ID);
|
||||
}
|
||||
|
||||
template <class P>
|
||||
@@ -743,6 +762,16 @@ void BufferCache<P>::BindHostIndexBuffer() {
|
||||
}
|
||||
}
|
||||
|
||||
template <class P>
|
||||
void BufferCache<P>::BindHostVertexBuffer(u32 index, Buffer& buffer, u32 offset, u32 size,
|
||||
u32 stride) {
|
||||
if constexpr (IS_OPENGL) {
|
||||
runtime.BindVertexBuffer(index, buffer, offset, size, stride);
|
||||
} else {
|
||||
runtime.BindVertexBuffer(index, buffer.Handle(), offset, size, stride);
|
||||
}
|
||||
}
|
||||
|
||||
template <class P>
|
||||
Binding& BufferCache<P>::VertexBufferSlot(u32 index) {
|
||||
ASSERT(index < NUM_VERTEX_BUFFERS);
|
||||
@@ -1168,7 +1197,7 @@ void BufferCache<P>::DoUpdateGraphicsBuffers(bool is_indexed) {
|
||||
if (is_indexed) {
|
||||
UpdateIndexBuffer();
|
||||
}
|
||||
UpdateVertexBuffers(is_indexed);
|
||||
UpdateVertexBuffers();
|
||||
UpdateTransformFeedbackBuffers();
|
||||
for (size_t stage = 0; stage < NUM_STAGES; ++stage) {
|
||||
UpdateUniformBuffers(stage);
|
||||
@@ -1222,14 +1251,9 @@ void BufferCache<P>::UpdateIndexBuffer() {
|
||||
const GPUVAddr gpu_addr_begin = index_buffer_ref.StartAddress();
|
||||
const GPUVAddr gpu_addr_end = index_buffer_ref.EndAddress();
|
||||
const std::optional<DAddr> device_addr = gpu_memory->GpuToCpuAddress(gpu_addr_begin);
|
||||
u64 address_size = 0;
|
||||
if (gpu_addr_end > gpu_addr_begin) {
|
||||
address_size = (std::min)(gpu_addr_end - gpu_addr_begin,
|
||||
u64{(std::numeric_limits<u32>::max)()});
|
||||
}
|
||||
const u64 draw_size = (u64{index_buffer_ref.count} + u64{index_buffer_ref.first}) *
|
||||
u64{index_buffer_ref.FormatSizeInBytes()};
|
||||
const u32 size = static_cast<u32>((std::min)(address_size, draw_size));
|
||||
const u32 address_size = static_cast<u32>(gpu_addr_end - gpu_addr_begin);
|
||||
const u32 draw_size = (index_buffer_ref.count + index_buffer_ref.first) * u32(index_buffer_ref.FormatSizeInBytes());
|
||||
const u32 size = (std::min)(address_size, draw_size);
|
||||
if (size == 0 || !device_addr) {
|
||||
channel_state->index_buffer = NULL_BINDING;
|
||||
return;
|
||||
@@ -1242,142 +1266,20 @@ void BufferCache<P>::UpdateIndexBuffer() {
|
||||
}
|
||||
|
||||
template <class P>
|
||||
u64 BufferCache<P>::DrawMaxIndex() {
|
||||
if (max_index_scanned) {
|
||||
return cached_max_index;
|
||||
}
|
||||
max_index_scanned = true;
|
||||
cached_max_index = 0;
|
||||
const auto& index_buffer_ref = maxwell3d->draw_manager.draw_state.index_buffer;
|
||||
const u32 count = index_buffer_ref.count;
|
||||
if (count == 0) {
|
||||
return 0;
|
||||
}
|
||||
index_scan_buffer.resize_destructive(count);
|
||||
gpu_memory->ReadBlockUnsafe(index_buffer_ref.IndexStart(), index_scan_buffer.data(),
|
||||
size_t{count} * sizeof(u32));
|
||||
u32 restart_index = (std::numeric_limits<u32>::max)();
|
||||
if (maxwell3d->regs.primitive_restart.enabled != 0) {
|
||||
restart_index = maxwell3d->regs.primitive_restart.index;
|
||||
}
|
||||
u32 max_index = 0;
|
||||
for (u32 i = 0; i < count; ++i) {
|
||||
const u32 value = index_scan_buffer[i];
|
||||
if (value == restart_index) {
|
||||
continue;
|
||||
}
|
||||
max_index = (std::max)(max_index, value);
|
||||
}
|
||||
cached_max_index = max_index;
|
||||
return cached_max_index;
|
||||
}
|
||||
|
||||
template <class P>
|
||||
u64 BufferCache<P>::StreamAttributeExtent(u32 index) {
|
||||
using VertexAttribute = typename Maxwell::VertexAttribute;
|
||||
if (stream_extents_valid) {
|
||||
return stream_extents[index];
|
||||
}
|
||||
stream_extents_valid = true;
|
||||
stream_extents.fill(0);
|
||||
for (size_t i = 0; i < Maxwell::NumVertexAttributes; ++i) {
|
||||
const auto& attribute = maxwell3d->regs.vertex_attrib_format[i];
|
||||
if (attribute.constant != 0 || attribute.size == VertexAttribute::Size::Invalid) {
|
||||
continue;
|
||||
}
|
||||
const u32 buffer = attribute.buffer.Value();
|
||||
if (buffer >= NUM_VERTEX_BUFFERS) {
|
||||
continue;
|
||||
}
|
||||
const u64 end = static_cast<u64>(attribute.offset.Value()) +
|
||||
static_cast<u64>(attribute.SizeInBytes());
|
||||
stream_extents[buffer] = (std::max)(stream_extents[buffer], end);
|
||||
}
|
||||
return stream_extents[index];
|
||||
}
|
||||
|
||||
template <class P>
|
||||
u64 BufferCache<P>::DrawVertexBound(u32 index, bool is_indexed) {
|
||||
const auto& array = maxwell3d->regs.vertex_streams[index];
|
||||
if (array.enable == 0) {
|
||||
return 0;
|
||||
}
|
||||
const u64 extent = StreamAttributeExtent(index);
|
||||
if (extent == 0) {
|
||||
return 0;
|
||||
}
|
||||
const u64 stride = static_cast<u64>(array.stride);
|
||||
if (stride == 0) {
|
||||
return extent;
|
||||
}
|
||||
const auto& draw_state = maxwell3d->draw_manager.draw_state;
|
||||
u64 elements = 0;
|
||||
if (maxwell3d->regs.vertex_stream_instances.IsInstancingEnabled(index)) {
|
||||
if (draw_instance_count == 0) {
|
||||
return 0;
|
||||
}
|
||||
const u64 base_instance = static_cast<u64>(draw_state.base_instance);
|
||||
elements = base_instance + 1;
|
||||
if (array.frequency != 0) {
|
||||
elements = (base_instance + static_cast<u64>(draw_instance_count) - 1) /
|
||||
static_cast<u64>(array.frequency) +
|
||||
1;
|
||||
}
|
||||
} else if (!is_indexed) {
|
||||
elements = static_cast<u64>(draw_state.vertex_buffer.first) +
|
||||
static_cast<u64>(draw_state.vertex_buffer.count);
|
||||
} else {
|
||||
const auto format = draw_state.index_buffer.format;
|
||||
u64 max_index = 0xFF;
|
||||
if (format == Maxwell::IndexFormat::UnsignedShort) {
|
||||
max_index = 0xFFFF;
|
||||
} else if (format != Maxwell::IndexFormat::UnsignedByte) {
|
||||
const auto& limit = maxwell3d->regs.vertex_stream_limits[index];
|
||||
const GPUVAddr gpu_addr_begin = array.Address();
|
||||
const GPUVAddr gpu_addr_end = limit.Address() + 1;
|
||||
if (gpu_addr_end <= gpu_addr_begin) {
|
||||
return 0;
|
||||
}
|
||||
const bool walks = gpu_addr_end - gpu_addr_begin >= IMPLAUSIBLE_VERTEX_SIZE ||
|
||||
!gpu_memory->IsWithinGPUAddressRange(gpu_addr_end);
|
||||
if (!walks) {
|
||||
return 0;
|
||||
}
|
||||
max_index = DrawMaxIndex();
|
||||
}
|
||||
elements = static_cast<u64>(draw_state.base_index) + max_index + 1;
|
||||
}
|
||||
if (elements == 0) {
|
||||
return extent;
|
||||
}
|
||||
return (elements - 1) * stride + extent;
|
||||
}
|
||||
|
||||
template <class P>
|
||||
void BufferCache<P>::UpdateVertexBuffers(bool is_indexed) {
|
||||
void BufferCache<P>::UpdateVertexBuffers() {
|
||||
auto& flags = maxwell3d->dirty.flags;
|
||||
max_index_scanned = false;
|
||||
stream_extents_valid = false;
|
||||
for (u32 index = 0; index < NUM_VERTEX_BUFFERS; ++index) {
|
||||
const u64 bound = DrawVertexBound(index, is_indexed);
|
||||
if (bound <= last_draw_bounds[index]) {
|
||||
continue;
|
||||
}
|
||||
flags[Dirty::VertexBuffer0 + index] = true;
|
||||
flags[Dirty::VertexBuffers] = true;
|
||||
}
|
||||
if (!maxwell3d->dirty.flags[Dirty::VertexBuffers]) {
|
||||
return;
|
||||
}
|
||||
flags[Dirty::VertexBuffers] = false;
|
||||
|
||||
for (u32 index = 0; index < NUM_VERTEX_BUFFERS; ++index) {
|
||||
UpdateVertexBuffer(index, is_indexed);
|
||||
UpdateVertexBuffer(index);
|
||||
}
|
||||
}
|
||||
|
||||
template <class P>
|
||||
void BufferCache<P>::UpdateVertexBuffer(u32 index, bool is_indexed) {
|
||||
void BufferCache<P>::UpdateVertexBuffer(u32 index) {
|
||||
if (!maxwell3d->dirty.flags[Dirty::VertexBuffer0 + index]) {
|
||||
return;
|
||||
}
|
||||
@@ -1386,31 +1288,15 @@ void BufferCache<P>::UpdateVertexBuffer(u32 index, bool is_indexed) {
|
||||
const GPUVAddr gpu_addr_begin = array.Address();
|
||||
const GPUVAddr gpu_addr_end = limit.Address() + 1;
|
||||
const std::optional<DAddr> device_addr = gpu_memory->GpuToCpuAddress(gpu_addr_begin);
|
||||
if (array.enable == 0 || !device_addr || gpu_addr_end <= gpu_addr_begin) {
|
||||
const u32 address_size = static_cast<u32>(gpu_addr_end - gpu_addr_begin);
|
||||
u32 size = address_size; // TODO: Analyze stride and number of vertices
|
||||
if (array.enable == 0 || size == 0 || !device_addr) {
|
||||
channel_state->vertex_buffers[index] = NULL_BINDING;
|
||||
UpdateVertexBufferSlot(index, NULL_BINDING);
|
||||
return;
|
||||
}
|
||||
// TODO: Analyze stride and number of vertices
|
||||
constexpr u64 implausible_size = IMPLAUSIBLE_VERTEX_SIZE;
|
||||
u64 address_size = gpu_addr_end - gpu_addr_begin;
|
||||
if (address_size > u64{(std::numeric_limits<u32>::max)()}) {
|
||||
address_size = implausible_size;
|
||||
}
|
||||
const u64 draw_bound = DrawVertexBound(index, is_indexed);
|
||||
last_draw_bounds[index] = (std::numeric_limits<u64>::max)();
|
||||
if (draw_bound != 0) {
|
||||
last_draw_bounds[index] = draw_bound;
|
||||
address_size = (std::min)(address_size, draw_bound);
|
||||
}
|
||||
if (!gpu_memory->IsWithinGPUAddressRange(gpu_addr_end) || address_size >= implausible_size) {
|
||||
address_size = gpu_memory->MaxContinuousRange(gpu_addr_begin, address_size);
|
||||
}
|
||||
const u32 size = static_cast<u32>(address_size);
|
||||
if (size == 0) {
|
||||
channel_state->vertex_buffers[index] = NULL_BINDING;
|
||||
UpdateVertexBufferSlot(index, NULL_BINDING);
|
||||
return;
|
||||
if (!gpu_memory->IsWithinGPUAddressRange(gpu_addr_end) || size >= 64_MiB) {
|
||||
size = static_cast<u32>(gpu_memory->MaxContinuousRange(gpu_addr_begin, size));
|
||||
}
|
||||
const BufferId buffer_id = FindBuffer(*device_addr, size);
|
||||
const Binding binding{
|
||||
@@ -1692,10 +1578,9 @@ template <class P>
|
||||
BufferId BufferCache<P>::CreateBuffer(DAddr device_addr, u32 wanted_size) {
|
||||
DAddr device_addr_end = Common::AlignUp(device_addr + wanted_size, CACHING_PAGESIZE);
|
||||
device_addr = Common::AlignDown(device_addr, CACHING_PAGESIZE);
|
||||
constexpr u64 max_buffer_size = u64{(std::numeric_limits<u32>::max)()};
|
||||
wanted_size = static_cast<u32>((std::min)(device_addr_end - device_addr, max_buffer_size));
|
||||
wanted_size = static_cast<u32>(device_addr_end - device_addr);
|
||||
const OverlapResult overlap = ResolveOverlaps(device_addr, wanted_size);
|
||||
const u32 size = static_cast<u32>((std::min)(overlap.end - overlap.begin, max_buffer_size));
|
||||
const u32 size = static_cast<u32>(overlap.end - overlap.begin);
|
||||
const BufferId new_buffer_id = slot_buffers.insert(runtime, overlap.begin, size);
|
||||
auto& new_buffer = slot_buffers[new_buffer_id];
|
||||
const size_t size_bytes = new_buffer.SizeBytes();
|
||||
|
||||
@@ -51,7 +51,6 @@ constexpr u32 NUM_VERTEX_BUFFERS = 16;
|
||||
#else
|
||||
constexpr u32 NUM_VERTEX_BUFFERS = 32;
|
||||
#endif
|
||||
constexpr u64 IMPLAUSIBLE_VERTEX_SIZE = 64_MiB;
|
||||
constexpr u32 NUM_TRANSFORM_FEEDBACK_BUFFERS = 4;
|
||||
constexpr u32 NUM_GRAPHICS_UNIFORM_BUFFERS = 18;
|
||||
constexpr u32 NUM_COMPUTE_UNIFORM_BUFFERS = 8;
|
||||
@@ -218,6 +217,8 @@ public:
|
||||
|
||||
void WriteMemory(DAddr device_addr, u64 size);
|
||||
|
||||
void CachedWriteMemory(DAddr device_addr, u64 size);
|
||||
|
||||
bool OnCPUWrite(DAddr device_addr, u64 size);
|
||||
|
||||
void DownloadMemory(DAddr device_addr, u64 size);
|
||||
@@ -247,7 +248,7 @@ public:
|
||||
|
||||
void UnbindGraphicsStorageBuffers(size_t stage);
|
||||
|
||||
void BindGraphicsStorageBuffer(size_t stage, size_t ssbo_index, u32 cbuf_index, u32 cbuf_offset,
|
||||
bool BindGraphicsStorageBuffer(size_t stage, size_t ssbo_index, u32 cbuf_index, u32 cbuf_offset,
|
||||
bool is_written);
|
||||
|
||||
void UnbindGraphicsTextureBuffers(size_t stage);
|
||||
@@ -308,10 +309,6 @@ public:
|
||||
current_draw_indirect = current_draw_indirect_;
|
||||
}
|
||||
|
||||
void SetDrawInstanceCount(u32 draw_instance_count_) {
|
||||
draw_instance_count = draw_instance_count_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::pair<Buffer*, u32> GetDrawIndirectCount();
|
||||
|
||||
[[nodiscard]] std::pair<Buffer*, u32> GetDrawIndirectBuffer();
|
||||
@@ -379,6 +376,8 @@ private:
|
||||
|
||||
void BindHostTransformFeedbackBuffers();
|
||||
|
||||
void BindHostVertexBuffer(u32 index, Buffer& buffer, u32 offset, u32 size, u32 stride);
|
||||
|
||||
void BindHostComputeUniformBuffers();
|
||||
|
||||
void BindHostComputeStorageBuffers();
|
||||
@@ -391,15 +390,9 @@ private:
|
||||
|
||||
void UpdateIndexBuffer();
|
||||
|
||||
void UpdateVertexBuffers(bool is_indexed);
|
||||
void UpdateVertexBuffers();
|
||||
|
||||
void UpdateVertexBuffer(u32 index, bool is_indexed);
|
||||
|
||||
[[nodiscard]] u64 DrawVertexBound(u32 index, bool is_indexed);
|
||||
|
||||
[[nodiscard]] u64 DrawMaxIndex();
|
||||
|
||||
[[nodiscard]] u64 StreamAttributeExtent(u32 index);
|
||||
void UpdateVertexBuffer(u32 index);
|
||||
|
||||
void UpdateDrawIndirect();
|
||||
|
||||
@@ -491,14 +484,6 @@ private:
|
||||
|
||||
const Tegra::Engines::Maxwell3D::DrawManager::IndirectParams* current_draw_indirect{};
|
||||
|
||||
u32 draw_instance_count = 0;
|
||||
std::array<u64, NUM_VERTEX_BUFFERS> last_draw_bounds{};
|
||||
Common::ScratchBuffer<u32> index_scan_buffer;
|
||||
u64 cached_max_index = 0;
|
||||
bool max_index_scanned = false;
|
||||
std::array<u64, NUM_VERTEX_BUFFERS> stream_extents{};
|
||||
bool stream_extents_valid = false;
|
||||
|
||||
u32 last_index_count = 0;
|
||||
|
||||
u32 enabled_vertex_buffers_mask = 0;
|
||||
|
||||
@@ -206,40 +206,6 @@ foreach(VARIANT IN ITEMS ${SHADER_TYPE_VARIANTS})
|
||||
set(SHADER_HEADERS ${SHADER_HEADERS} ${VARIANT_HEADER_FILE})
|
||||
endforeach()
|
||||
|
||||
set(SHADER_DEFINE_VARIANTS
|
||||
"block_linear_unswizzle_2d.comp|nonarrow|HAS_EXTENDED_TYPES=0"
|
||||
"pitch_unswizzle.comp|nonarrow|HAS_EXTENDED_TYPES=0"
|
||||
"block_linear_unswizzle_3d.comp|nonarrow|HAS_EXTENDED_TYPES=0"
|
||||
)
|
||||
|
||||
foreach(VARIANT IN ITEMS ${SHADER_DEFINE_VARIANTS})
|
||||
string(REPLACE "|" ";" VARIANT_PARTS ${VARIANT})
|
||||
list(GET VARIANT_PARTS 0 VARIANT_FILENAME)
|
||||
list(GET VARIANT_PARTS 1 VARIANT_SUFFIX)
|
||||
list(GET VARIANT_PARTS 2 VARIANT_DEFINE)
|
||||
|
||||
set(VARIANT_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/${VARIANT_FILENAME})
|
||||
get_filename_component(VARIANT_STEM ${VARIANT_FILENAME} NAME_WE)
|
||||
get_filename_component(VARIANT_EXT ${VARIANT_FILENAME} EXT)
|
||||
string(REPLACE "." "" VARIANT_EXT ${VARIANT_EXT})
|
||||
set(VARIANT_NAME ${VARIANT_STEM}_${VARIANT_SUFFIX}_${VARIANT_EXT})
|
||||
|
||||
string(TOUPPER ${VARIANT_NAME}_SPV VARIANT_VARIABLE_NAME)
|
||||
set(VARIANT_HEADER_FILE ${SHADER_DIR}/${VARIANT_NAME}_spv.h)
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
${VARIANT_HEADER_FILE}
|
||||
COMMAND
|
||||
${GLSLANGVALIDATOR} -V ${QUIET_FLAG} -I"${FIDELITYFX_INCLUDE_DIR}" ${GLSL_FLAGS}
|
||||
-D${VARIANT_DEFINE}
|
||||
--variable-name ${VARIANT_VARIABLE_NAME} -o ${VARIANT_HEADER_FILE} ${VARIANT_SOURCE}
|
||||
--target-env ${SPIR_V_VERSION}
|
||||
MAIN_DEPENDENCY
|
||||
${VARIANT_SOURCE}
|
||||
)
|
||||
set(SHADER_HEADERS ${SHADER_HEADERS} ${VARIANT_HEADER_FILE})
|
||||
endforeach()
|
||||
|
||||
foreach(FILEPATH IN ITEMS ${FIDELITYFX_FILES})
|
||||
get_filename_component(FILENAME ${FILEPATH} NAME)
|
||||
string(REPLACE "." "_" HEADER_NAME ${FILENAME})
|
||||
|
||||
@@ -5,13 +5,9 @@
|
||||
|
||||
#ifdef VULKAN
|
||||
|
||||
#ifndef HAS_EXTENDED_TYPES
|
||||
#define HAS_EXTENDED_TYPES 1
|
||||
#endif
|
||||
#if HAS_EXTENDED_TYPES
|
||||
#extension GL_EXT_shader_16bit_storage : require
|
||||
#extension GL_EXT_shader_8bit_storage : require
|
||||
#endif
|
||||
#define HAS_EXTENDED_TYPES 1
|
||||
#define BEGIN_PUSH_CONSTANTS layout(push_constant) uniform PushConstants {
|
||||
#define END_PUSH_CONSTANTS };
|
||||
#define UNIFORM(n)
|
||||
|
||||
@@ -5,13 +5,9 @@
|
||||
|
||||
#ifdef VULKAN
|
||||
|
||||
#ifndef HAS_EXTENDED_TYPES
|
||||
#define HAS_EXTENDED_TYPES 1
|
||||
#endif
|
||||
#if HAS_EXTENDED_TYPES
|
||||
#extension GL_EXT_shader_16bit_storage : require
|
||||
#extension GL_EXT_shader_8bit_storage : require
|
||||
#endif
|
||||
#define HAS_EXTENDED_TYPES 1
|
||||
#define BEGIN_PUSH_CONSTANTS layout(push_constant) uniform PushConstants {
|
||||
#define END_PUSH_CONSTANTS };
|
||||
#define UNIFORM(n)
|
||||
|
||||
@@ -5,13 +5,9 @@
|
||||
|
||||
#ifdef VULKAN
|
||||
|
||||
#ifndef HAS_EXTENDED_TYPES
|
||||
#define HAS_EXTENDED_TYPES 1
|
||||
#endif
|
||||
#if HAS_EXTENDED_TYPES
|
||||
#extension GL_EXT_shader_16bit_storage : require
|
||||
#extension GL_EXT_shader_8bit_storage : require
|
||||
#endif
|
||||
#define HAS_EXTENDED_TYPES 1
|
||||
#define BEGIN_PUSH_CONSTANTS layout(push_constant) uniform PushConstants {
|
||||
#define END_PUSH_CONSTANTS };
|
||||
#define UNIFORM(n)
|
||||
|
||||
@@ -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
|
||||
@@ -226,6 +226,22 @@ void BufferCacheRuntime::BindIndexBuffer(Buffer& buffer, u32 offset, u32 size) {
|
||||
}
|
||||
}
|
||||
|
||||
void BufferCacheRuntime::BindVertexBuffer(u32 index, Buffer& buffer, u32 offset, u32 size,
|
||||
u32 stride) {
|
||||
if (index >= max_attributes) {
|
||||
return;
|
||||
}
|
||||
if (has_unified_vertex_buffers) {
|
||||
buffer.MakeResident(GL_READ_ONLY);
|
||||
glBindVertexBuffer(index, 0, 0, static_cast<GLsizei>(stride));
|
||||
glBufferAddressRangeNV(GL_VERTEX_ATTRIB_ARRAY_ADDRESS_NV, index,
|
||||
buffer.HostGpuAddr() + offset, static_cast<GLsizeiptr>(size));
|
||||
} else {
|
||||
glBindVertexBuffer(index, buffer.Handle(), static_cast<GLintptr>(offset),
|
||||
static_cast<GLsizei>(stride));
|
||||
}
|
||||
}
|
||||
|
||||
void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
|
||||
// TODO: Should HostBindings provide the correct runtime types to avoid these transforms?
|
||||
std::array<GLuint, 32> buffer_handles;
|
||||
|
||||
@@ -99,6 +99,8 @@ public:
|
||||
|
||||
void BindIndexBuffer(Buffer& buffer, u32 offset, u32 size);
|
||||
|
||||
void BindVertexBuffer(u32 index, Buffer& buffer, u32 offset, u32 size, u32 stride);
|
||||
|
||||
void BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings);
|
||||
|
||||
void BindUniformBuffer(size_t stage, u32 binding_index, Buffer& buffer, u32 offset, u32 size);
|
||||
|
||||
@@ -259,7 +259,6 @@ void RasterizerOpenGL::PrepareDraw(bool is_indexed, Func&& draw_func) {
|
||||
}
|
||||
|
||||
void RasterizerOpenGL::Draw(bool is_indexed, u32 instance_count) {
|
||||
buffer_cache.SetDrawInstanceCount(instance_count);
|
||||
PrepareDraw(is_indexed, [this, is_indexed, instance_count](GLenum primitive_mode) {
|
||||
const auto& draw_state = maxwell3d->draw_manager.draw_state;
|
||||
const GLuint base_instance = GLuint(draw_state.base_instance);
|
||||
@@ -305,7 +304,6 @@ void RasterizerOpenGL::Draw(bool is_indexed, u32 instance_count) {
|
||||
void RasterizerOpenGL::DrawIndirect() {
|
||||
const auto& params = maxwell3d->draw_manager.indirect_state;
|
||||
buffer_cache.SetDrawIndirect(¶ms);
|
||||
buffer_cache.SetDrawInstanceCount(0);
|
||||
PrepareDraw(params.is_indexed, [this, ¶ms](GLenum primitive_mode) {
|
||||
if (params.is_byte_count) {
|
||||
const GPUVAddr tfb_object_base_addr = params.indirect_start_address - 4U;
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
#include "video_core/renderer_vulkan/present/sgsr.h"
|
||||
#include "video_core/renderer_vulkan/present/fxaa.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/smaa.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);
|
||||
SetAntiAliasPass(device);
|
||||
#ifdef HAS_RESHADE
|
||||
SetPostProcessPass(device);
|
||||
#endif
|
||||
|
||||
// Finish any pending renderpass
|
||||
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);
|
||||
}
|
||||
|
||||
#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);
|
||||
const VkExtent2D render_extent{
|
||||
.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() {
|
||||
for (const u64 tick : resource_ticks) {
|
||||
scheduler.Wait(tick);
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
#include "video_core/renderer_vulkan/present/fsr.h"
|
||||
#include "video_core/renderer_vulkan/present/sgsr.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"
|
||||
|
||||
namespace Layout {
|
||||
@@ -66,6 +69,9 @@ private:
|
||||
|
||||
void RefreshResources(const Device& device, const Tegra::FramebufferConfig& framebuffer);
|
||||
void SetAntiAliasPass(const Device& device);
|
||||
#ifdef HAS_RESHADE
|
||||
void SetPostProcessPass(const Device& device);
|
||||
#endif
|
||||
void ReleaseRawImages();
|
||||
|
||||
u64 CalculateBufferSize(const Tegra::FramebufferConfig& framebuffer) const;
|
||||
@@ -95,6 +101,11 @@ private:
|
||||
Settings::AntiAliasing anti_alias_setting{};
|
||||
std::variant<std::monostate, FXAA, SMAA> anti_alias{};
|
||||
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{};
|
||||
};
|
||||
|
||||
|
||||
@@ -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/present.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/vk_blit_screen.h"
|
||||
#include "video_core/renderer_vulkan/vk_rasterizer.h"
|
||||
@@ -183,6 +187,12 @@ try
|
||||
scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); });
|
||||
}
|
||||
|
||||
#ifdef HAS_RESHADE
|
||||
VideoCore::ReloadFxCatalog();
|
||||
VideoCore::FxChain::Instance().LoadFromSettings();
|
||||
VideoCore::FxChain::Instance().DropUnknownEntries();
|
||||
#endif
|
||||
|
||||
Report();
|
||||
} catch (const vk::Exception& exception) {
|
||||
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
|
||||
|
||||
@@ -585,6 +585,29 @@ void BufferCacheRuntime::BindQuadIndexBuffer(PrimitiveTopology topology, u32 fir
|
||||
}
|
||||
}
|
||||
|
||||
void BufferCacheRuntime::BindVertexBuffer(u32 index, VkBuffer buffer, u32 offset, u32 size, u32 stride) {
|
||||
if (index >= device.GetMaxVertexInputBindings()) {
|
||||
return;
|
||||
}
|
||||
if (device.IsExtExtendedDynamicStateSupported()) {
|
||||
scheduler.Record([index, buffer, offset, size, stride](vk::CommandBuffer cmdbuf) {
|
||||
const VkDeviceSize vk_offset = buffer != VK_NULL_HANDLE ? offset : 0;
|
||||
const VkDeviceSize vk_size = buffer != VK_NULL_HANDLE ? size : VK_WHOLE_SIZE;
|
||||
const VkDeviceSize vk_stride = stride;
|
||||
cmdbuf.BindVertexBuffers2EXT(index, 1, &buffer, &vk_offset, &vk_size, &vk_stride);
|
||||
});
|
||||
} else {
|
||||
if (!device.HasNullDescriptor() && buffer == VK_NULL_HANDLE) {
|
||||
ReserveNullBuffer();
|
||||
buffer = *null_buffer;
|
||||
offset = 0;
|
||||
}
|
||||
scheduler.Record([index, buffer, offset](vk::CommandBuffer cmdbuf) {
|
||||
cmdbuf.BindVertexBuffer(index, buffer, offset);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
|
||||
boost::container::static_vector<VkBuffer, VideoCommon::NUM_VERTEX_BUFFERS> buffer_handles(bindings.buffers.size());
|
||||
for (u32 i = 0; i < bindings.buffers.size(); ++i) {
|
||||
|
||||
@@ -138,6 +138,8 @@ public:
|
||||
|
||||
void BindQuadIndexBuffer(PrimitiveTopology topology, u32 first, u32 count);
|
||||
|
||||
void BindVertexBuffer(u32 index, VkBuffer buffer, u32 offset, u32 size, u32 stride);
|
||||
|
||||
void BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings);
|
||||
|
||||
void BindTransformFeedbackBuffer(u32 index, VkBuffer buffer, u32 offset, u32 size);
|
||||
|
||||
@@ -22,13 +22,7 @@
|
||||
#include "video_core/host_shaders/resolve_conditional_render_comp_spv.h"
|
||||
#include "video_core/host_shaders/vulkan_quad_indexed_comp_spv.h"
|
||||
#include "video_core/host_shaders/vulkan_uint8_comp_spv.h"
|
||||
#include "video_core/host_shaders/block_linear_unswizzle_2d_comp_spv.h"
|
||||
#include "video_core/host_shaders/block_linear_unswizzle_2d_nonarrow_comp_spv.h"
|
||||
#include "video_core/host_shaders/block_linear_unswizzle_3d_bcn_comp_spv.h"
|
||||
#include "video_core/host_shaders/block_linear_unswizzle_3d_comp_spv.h"
|
||||
#include "video_core/host_shaders/block_linear_unswizzle_3d_nonarrow_comp_spv.h"
|
||||
#include "video_core/host_shaders/pitch_unswizzle_comp_spv.h"
|
||||
#include "video_core/host_shaders/pitch_unswizzle_nonarrow_comp_spv.h"
|
||||
#include "video_core/renderer_vulkan/vk_compute_pass.h"
|
||||
#include "video_core/surface.h"
|
||||
#include "video_core/renderer_vulkan/vk_descriptor_pool.h"
|
||||
@@ -878,291 +872,4 @@ void BlockLinearUnswizzle3DPass::UnswizzleChunk(
|
||||
});
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u32 UNSWIZZLE_BINDING_INPUT_BUFFER = 0;
|
||||
constexpr u32 UNSWIZZLE_BINDING_OUTPUT_IMAGE = 1;
|
||||
constexpr size_t UNSWIZZLE_NUM_BINDINGS = 2;
|
||||
|
||||
constexpr std::array<VkDescriptorSetLayoutBinding, UNSWIZZLE_NUM_BINDINGS>
|
||||
UNSWIZZLE_DESCRIPTOR_SET_BINDINGS{{
|
||||
{
|
||||
.binding = UNSWIZZLE_BINDING_INPUT_BUFFER,
|
||||
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.descriptorCount = 1,
|
||||
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
|
||||
.pImmutableSamplers = nullptr,
|
||||
},
|
||||
{
|
||||
.binding = UNSWIZZLE_BINDING_OUTPUT_IMAGE,
|
||||
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
||||
.descriptorCount = 1,
|
||||
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
|
||||
.pImmutableSamplers = nullptr,
|
||||
},
|
||||
}};
|
||||
|
||||
constexpr std::array<VkDescriptorUpdateTemplateEntry, UNSWIZZLE_NUM_BINDINGS>
|
||||
UNSWIZZLE_DESCRIPTOR_UPDATE_TEMPLATE{{
|
||||
{
|
||||
.dstBinding = UNSWIZZLE_BINDING_INPUT_BUFFER,
|
||||
.dstArrayElement = 0,
|
||||
.descriptorCount = 1,
|
||||
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.offset = UNSWIZZLE_BINDING_INPUT_BUFFER * sizeof(DescriptorUpdateEntry),
|
||||
.stride = sizeof(DescriptorUpdateEntry),
|
||||
},
|
||||
{
|
||||
.dstBinding = UNSWIZZLE_BINDING_OUTPUT_IMAGE,
|
||||
.dstArrayElement = 0,
|
||||
.descriptorCount = 1,
|
||||
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
||||
.offset = UNSWIZZLE_BINDING_OUTPUT_IMAGE * sizeof(DescriptorUpdateEntry),
|
||||
.stride = sizeof(DescriptorUpdateEntry),
|
||||
},
|
||||
}};
|
||||
|
||||
constexpr DescriptorBankInfo UNSWIZZLE_BANK_INFO{
|
||||
.uniform_buffers = 0,
|
||||
.storage_buffers = 1,
|
||||
.texture_buffers = 0,
|
||||
.image_buffers = 0,
|
||||
.textures = 0,
|
||||
.images = 1,
|
||||
.score = 2,
|
||||
};
|
||||
|
||||
[[nodiscard]] std::span<const u32> UnswizzleSpv(const Device& device,
|
||||
std::span<const u32> extended,
|
||||
std::span<const u32> narrow) {
|
||||
if (device.IsStorageBuffer8BitAccessSupported() &&
|
||||
device.IsStorageBuffer16BitAccessSupported()) {
|
||||
return extended;
|
||||
}
|
||||
return narrow;
|
||||
}
|
||||
|
||||
struct PitchUnswizzlePushConstants {
|
||||
alignas(8) std::array<u32, 2> origin;
|
||||
alignas(8) std::array<s32, 2> destination;
|
||||
u32 bytes_per_block;
|
||||
u32 pitch;
|
||||
};
|
||||
|
||||
void RecordUnswizzleEntryBarrier(Scheduler& scheduler, VkPipeline vk_pipeline, VkImage vk_image,
|
||||
VkImageAspectFlags aspect_mask, bool is_initialized) {
|
||||
scheduler.Record([vk_pipeline, vk_image, aspect_mask,
|
||||
is_initialized](vk::CommandBuffer cmdbuf) {
|
||||
VkAccessFlags src_access = VK_ACCESS_NONE;
|
||||
VkImageLayout old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
if (is_initialized) {
|
||||
src_access = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT |
|
||||
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
old_layout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
}
|
||||
const VkImageMemoryBarrier image_barrier{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||
.pNext = nullptr,
|
||||
.srcAccessMask = src_access,
|
||||
.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
|
||||
.oldLayout = old_layout,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = vk_image,
|
||||
.subresourceRange{
|
||||
.aspectMask = aspect_mask,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = VK_REMAINING_MIP_LEVELS,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = VK_REMAINING_ARRAY_LAYERS,
|
||||
},
|
||||
};
|
||||
VkPipelineStageFlags src_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
if (is_initialized) {
|
||||
src_stage = vk::PIPELINE_STAGE_GRAPHICS_COMPUTE_TRANSFER;
|
||||
}
|
||||
cmdbuf.PipelineBarrier(src_stage, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, image_barrier);
|
||||
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, vk_pipeline);
|
||||
});
|
||||
}
|
||||
|
||||
void RecordUnswizzleExitBarrier(Scheduler& scheduler, VkImage vk_image,
|
||||
VkImageAspectFlags aspect_mask) {
|
||||
scheduler.Record([vk_image, aspect_mask](vk::CommandBuffer cmdbuf) {
|
||||
const VkImageMemoryBarrier image_barrier{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||
.pNext = nullptr,
|
||||
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
|
||||
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT |
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT,
|
||||
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = vk_image,
|
||||
.subresourceRange{
|
||||
.aspectMask = aspect_mask,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = VK_REMAINING_MIP_LEVELS,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = VK_REMAINING_ARRAY_LAYERS,
|
||||
},
|
||||
};
|
||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
vk::PIPELINE_STAGE_GRAPHICS_COMPUTE_TRANSFER, 0, image_barrier);
|
||||
});
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
BlockLinearUnswizzle2DPass::BlockLinearUnswizzle2DPass(
|
||||
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue_)
|
||||
: ComputePass(device_, scheduler_, descriptor_pool_, UNSWIZZLE_DESCRIPTOR_SET_BINDINGS,
|
||||
UNSWIZZLE_DESCRIPTOR_UPDATE_TEMPLATE, UNSWIZZLE_BANK_INFO,
|
||||
COMPUTE_PUSH_CONSTANT_RANGE<sizeof(
|
||||
VideoCommon::Accelerated::BlockLinearSwizzle2DParams)>,
|
||||
UnswizzleSpv(device_, BLOCK_LINEAR_UNSWIZZLE_2D_COMP_SPV,
|
||||
BLOCK_LINEAR_UNSWIZZLE_2D_NONARROW_COMP_SPV)),
|
||||
scheduler{scheduler_}, compute_pass_descriptor_queue{compute_pass_descriptor_queue_} {}
|
||||
|
||||
BlockLinearUnswizzle2DPass::~BlockLinearUnswizzle2DPass() = default;
|
||||
|
||||
void BlockLinearUnswizzle2DPass::Unswizzle(
|
||||
Image& image, const StagingBufferRef& map,
|
||||
std::span<const VideoCommon::SwizzleParameters> swizzles) {
|
||||
using namespace VideoCommon::Accelerated;
|
||||
scheduler.RequestOutsideRenderPassOperationContext();
|
||||
const VkPipeline vk_pipeline = *pipeline;
|
||||
const VkImageAspectFlags aspect_mask = image.AspectMask();
|
||||
const VkImage vk_image = image.Handle();
|
||||
const bool is_initialized = image.ExchangeInitialization();
|
||||
RecordUnswizzleEntryBarrier(scheduler, vk_pipeline, vk_image, aspect_mask, is_initialized);
|
||||
|
||||
const u32 num_layers = static_cast<u32>(image.info.resources.layers);
|
||||
for (const VideoCommon::SwizzleParameters& swizzle : swizzles) {
|
||||
const size_t input_offset = swizzle.buffer_offset + map.offset;
|
||||
const u32 num_dispatches_x = Common::DivCeil(swizzle.num_tiles.width, 32U);
|
||||
const u32 num_dispatches_y = Common::DivCeil(swizzle.num_tiles.height, 32U);
|
||||
|
||||
compute_pass_descriptor_queue.Acquire(scheduler, 2);
|
||||
compute_pass_descriptor_queue.AddBuffer(map.buffer, input_offset,
|
||||
image.guest_size_bytes - swizzle.buffer_offset);
|
||||
compute_pass_descriptor_queue.AddImage(image.StorageImageView(swizzle.level));
|
||||
const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()};
|
||||
|
||||
const auto params = MakeBlockLinearSwizzle2DParams(swizzle, image.info);
|
||||
scheduler.Record([this, num_dispatches_x, num_dispatches_y, num_layers, params,
|
||||
descriptor_data](vk::CommandBuffer cmdbuf) {
|
||||
const VkDescriptorSet set = descriptor_allocator.Commit();
|
||||
device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data);
|
||||
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {});
|
||||
cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, params);
|
||||
cmdbuf.Dispatch(num_dispatches_x, num_dispatches_y, num_layers);
|
||||
});
|
||||
}
|
||||
RecordUnswizzleExitBarrier(scheduler, vk_image, aspect_mask);
|
||||
}
|
||||
|
||||
BlockLinearUnswizzleImage3DPass::BlockLinearUnswizzleImage3DPass(
|
||||
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue_)
|
||||
: ComputePass(device_, scheduler_, descriptor_pool_, UNSWIZZLE_DESCRIPTOR_SET_BINDINGS,
|
||||
UNSWIZZLE_DESCRIPTOR_UPDATE_TEMPLATE, UNSWIZZLE_BANK_INFO,
|
||||
COMPUTE_PUSH_CONSTANT_RANGE<sizeof(BlockLinearSwizzle3DParams)>,
|
||||
UnswizzleSpv(device_, BLOCK_LINEAR_UNSWIZZLE_3D_COMP_SPV,
|
||||
BLOCK_LINEAR_UNSWIZZLE_3D_NONARROW_COMP_SPV)),
|
||||
scheduler{scheduler_}, compute_pass_descriptor_queue{compute_pass_descriptor_queue_} {}
|
||||
|
||||
BlockLinearUnswizzleImage3DPass::~BlockLinearUnswizzleImage3DPass() = default;
|
||||
|
||||
void BlockLinearUnswizzleImage3DPass::Unswizzle(
|
||||
Image& image, const StagingBufferRef& map,
|
||||
std::span<const VideoCommon::SwizzleParameters> swizzles) {
|
||||
using namespace VideoCommon::Accelerated;
|
||||
scheduler.RequestOutsideRenderPassOperationContext();
|
||||
const VkPipeline vk_pipeline = *pipeline;
|
||||
const VkImageAspectFlags aspect_mask = image.AspectMask();
|
||||
const VkImage vk_image = image.Handle();
|
||||
const bool is_initialized = image.ExchangeInitialization();
|
||||
RecordUnswizzleEntryBarrier(scheduler, vk_pipeline, vk_image, aspect_mask, is_initialized);
|
||||
|
||||
for (const VideoCommon::SwizzleParameters& swizzle : swizzles) {
|
||||
const size_t input_offset = swizzle.buffer_offset + map.offset;
|
||||
const u32 num_dispatches_x = Common::DivCeil(swizzle.num_tiles.width, 16U);
|
||||
const u32 num_dispatches_y = Common::DivCeil(swizzle.num_tiles.height, 8U);
|
||||
const u32 num_dispatches_z = Common::DivCeil(swizzle.num_tiles.depth, 8U);
|
||||
|
||||
compute_pass_descriptor_queue.Acquire(scheduler, 2);
|
||||
compute_pass_descriptor_queue.AddBuffer(map.buffer, input_offset,
|
||||
image.guest_size_bytes - swizzle.buffer_offset);
|
||||
compute_pass_descriptor_queue.AddImage(image.StorageImageView(swizzle.level));
|
||||
const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()};
|
||||
|
||||
const auto params = MakeBlockLinearSwizzle3DParams(swizzle, image.info);
|
||||
scheduler.Record([this, num_dispatches_x, num_dispatches_y, num_dispatches_z, params,
|
||||
descriptor_data](vk::CommandBuffer cmdbuf) {
|
||||
const VkDescriptorSet set = descriptor_allocator.Commit();
|
||||
device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data);
|
||||
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {});
|
||||
cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, params);
|
||||
cmdbuf.Dispatch(num_dispatches_x, num_dispatches_y, num_dispatches_z);
|
||||
});
|
||||
}
|
||||
RecordUnswizzleExitBarrier(scheduler, vk_image, aspect_mask);
|
||||
}
|
||||
|
||||
PitchUnswizzlePass::PitchUnswizzlePass(
|
||||
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue_)
|
||||
: ComputePass(device_, scheduler_, descriptor_pool_, UNSWIZZLE_DESCRIPTOR_SET_BINDINGS,
|
||||
UNSWIZZLE_DESCRIPTOR_UPDATE_TEMPLATE, UNSWIZZLE_BANK_INFO,
|
||||
COMPUTE_PUSH_CONSTANT_RANGE<sizeof(PitchUnswizzlePushConstants)>,
|
||||
UnswizzleSpv(device_, PITCH_UNSWIZZLE_COMP_SPV,
|
||||
PITCH_UNSWIZZLE_NONARROW_COMP_SPV)),
|
||||
scheduler{scheduler_}, compute_pass_descriptor_queue{compute_pass_descriptor_queue_} {}
|
||||
|
||||
PitchUnswizzlePass::~PitchUnswizzlePass() = default;
|
||||
|
||||
void PitchUnswizzlePass::Unswizzle(Image& image, const StagingBufferRef& map,
|
||||
std::span<const VideoCommon::SwizzleParameters> swizzles) {
|
||||
scheduler.RequestOutsideRenderPassOperationContext();
|
||||
const VkPipeline vk_pipeline = *pipeline;
|
||||
const VkImageAspectFlags aspect_mask = image.AspectMask();
|
||||
const VkImage vk_image = image.Handle();
|
||||
const bool is_initialized = image.ExchangeInitialization();
|
||||
RecordUnswizzleEntryBarrier(scheduler, vk_pipeline, vk_image, aspect_mask, is_initialized);
|
||||
|
||||
const u32 bytes_per_block = VideoCore::Surface::BytesPerBlock(image.info.format);
|
||||
const u32 pitch = image.info.pitch;
|
||||
for (const VideoCommon::SwizzleParameters& swizzle : swizzles) {
|
||||
const size_t input_offset = swizzle.buffer_offset + map.offset;
|
||||
const u32 num_dispatches_x = Common::DivCeil(swizzle.num_tiles.width, 32U);
|
||||
const u32 num_dispatches_y = Common::DivCeil(swizzle.num_tiles.height, 32U);
|
||||
|
||||
compute_pass_descriptor_queue.Acquire(scheduler, 2);
|
||||
compute_pass_descriptor_queue.AddBuffer(map.buffer, input_offset,
|
||||
image.guest_size_bytes - swizzle.buffer_offset);
|
||||
compute_pass_descriptor_queue.AddImage(image.StorageImageView(swizzle.level));
|
||||
const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()};
|
||||
|
||||
const PitchUnswizzlePushConstants params{
|
||||
.origin{0, 0},
|
||||
.destination{0, 0},
|
||||
.bytes_per_block = bytes_per_block,
|
||||
.pitch = pitch,
|
||||
};
|
||||
scheduler.Record([this, num_dispatches_x, num_dispatches_y, params,
|
||||
descriptor_data](vk::CommandBuffer cmdbuf) {
|
||||
const VkDescriptorSet set = descriptor_allocator.Commit();
|
||||
device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data);
|
||||
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {});
|
||||
cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, params);
|
||||
cmdbuf.Dispatch(num_dispatches_x, num_dispatches_y, 1);
|
||||
});
|
||||
}
|
||||
RecordUnswizzleExitBarrier(scheduler, vk_image, aspect_mask);
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
||||
|
||||
@@ -164,49 +164,4 @@ private:
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue;
|
||||
};
|
||||
|
||||
class BlockLinearUnswizzle2DPass final : public ComputePass {
|
||||
public:
|
||||
explicit BlockLinearUnswizzle2DPass(
|
||||
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue_);
|
||||
~BlockLinearUnswizzle2DPass();
|
||||
|
||||
void Unswizzle(Image& image, const StagingBufferRef& map,
|
||||
std::span<const VideoCommon::SwizzleParameters> swizzles);
|
||||
|
||||
private:
|
||||
Scheduler& scheduler;
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue;
|
||||
};
|
||||
|
||||
class BlockLinearUnswizzleImage3DPass final : public ComputePass {
|
||||
public:
|
||||
explicit BlockLinearUnswizzleImage3DPass(
|
||||
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue_);
|
||||
~BlockLinearUnswizzleImage3DPass();
|
||||
|
||||
void Unswizzle(Image& image, const StagingBufferRef& map,
|
||||
std::span<const VideoCommon::SwizzleParameters> swizzles);
|
||||
|
||||
private:
|
||||
Scheduler& scheduler;
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue;
|
||||
};
|
||||
|
||||
class PitchUnswizzlePass final : public ComputePass {
|
||||
public:
|
||||
explicit PitchUnswizzlePass(const Device& device_, Scheduler& scheduler_,
|
||||
DescriptorPool& descriptor_pool_,
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue_);
|
||||
~PitchUnswizzlePass();
|
||||
|
||||
void Unswizzle(Image& image, const StagingBufferRef& map,
|
||||
std::span<const VideoCommon::SwizzleParameters> swizzles);
|
||||
|
||||
private:
|
||||
Scheduler& scheduler;
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue;
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
||||
|
||||
@@ -694,11 +694,9 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
|
||||
const size_t num_vertex_arrays = (std::min)(
|
||||
Maxwell::NumVertexArrays, static_cast<size_t>(device.GetMaxVertexInputBindings()));
|
||||
for (size_t index = 0; index < num_vertex_arrays; ++index) {
|
||||
const bool instanced = ((key.state.enabled_divisors >> index) & 1) != 0;
|
||||
auto rate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||
if (instanced) {
|
||||
rate = VK_VERTEX_INPUT_RATE_INSTANCE;
|
||||
}
|
||||
const bool instanced = key.state.binding_divisors[index] != 0;
|
||||
const auto rate =
|
||||
instanced ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX;
|
||||
vertex_bindings.push_back({
|
||||
.binding = static_cast<u32>(index),
|
||||
.stride = key.state.vertex_strides[index],
|
||||
@@ -707,7 +705,7 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
|
||||
if (instanced) {
|
||||
vertex_binding_divisors.push_back({
|
||||
.binding = static_cast<u32>(index),
|
||||
.divisor = device.GetVertexAttribDivisor(key.state.binding_divisors[index]),
|
||||
.divisor = key.state.binding_divisors[index],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,12 +137,10 @@ VkRect2D GetScissorState(const Maxwell& regs, size_t index, u32 up_scale = 1, u3
|
||||
max_y = (std::max)(max_y, 0);
|
||||
|
||||
if (src.enable) {
|
||||
const s32 min_x = static_cast<s32>(src.min_x.Value());
|
||||
const s32 max_x = static_cast<s32>(src.max_x.Value());
|
||||
scissor.offset.x = scale_up(min_x);
|
||||
scissor.offset.x = scale_up(src.min_x);
|
||||
scissor.offset.y = scale_up(min_y);
|
||||
scissor.extent.width = scale_up((std::max)(max_x - min_x, 0));
|
||||
scissor.extent.height = scale_up((std::max)(max_y - min_y, 0));
|
||||
scissor.extent.width = scale_up(src.max_x - src.min_x);
|
||||
scissor.extent.height = scale_up(max_y - min_y);
|
||||
} else {
|
||||
scissor.offset.x = 0;
|
||||
scissor.offset.y = 0;
|
||||
@@ -262,7 +260,6 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
|
||||
}
|
||||
|
||||
void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) {
|
||||
buffer_cache.SetDrawInstanceCount(instance_count);
|
||||
PrepareDraw(is_indexed, [this, is_indexed, instance_count] {
|
||||
const auto& draw_state = maxwell3d->draw_manager.draw_state;
|
||||
const u32 num_instances{instance_count};
|
||||
@@ -298,7 +295,6 @@ void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) {
|
||||
void RasterizerVulkan::DrawIndirect() {
|
||||
const auto& params = maxwell3d->draw_manager.indirect_state;
|
||||
buffer_cache.SetDrawIndirect(¶ms);
|
||||
buffer_cache.SetDrawInstanceCount(0);
|
||||
PrepareDraw(params.is_indexed, [this, ¶ms] {
|
||||
const auto indirect_buffer = buffer_cache.GetDrawIndirectBuffer();
|
||||
const auto& buffer = indirect_buffer.first;
|
||||
@@ -1921,19 +1917,13 @@ void RasterizerVulkan::UpdateVertexInput(Tegra::Engines::Maxwell3D::Regs& regs)
|
||||
for (u32 binding = 0; binding < max_bindings; ++binding) {
|
||||
const auto& input_binding{regs.vertex_streams[binding]};
|
||||
const bool is_instanced{regs.vertex_stream_instances.IsInstancingEnabled(binding)};
|
||||
auto input_rate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||
u32 divisor = 1;
|
||||
if (is_instanced) {
|
||||
input_rate = VK_VERTEX_INPUT_RATE_INSTANCE;
|
||||
divisor = device.GetVertexAttribDivisor(input_binding.frequency);
|
||||
}
|
||||
bindings.push_back({
|
||||
.sType = VK_STRUCTURE_TYPE_VERTEX_INPUT_BINDING_DESCRIPTION_2_EXT,
|
||||
.pNext = nullptr,
|
||||
.binding = binding,
|
||||
.stride = input_binding.stride,
|
||||
.inputRate = input_rate,
|
||||
.divisor = divisor,
|
||||
.inputRate = is_instanced ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX,
|
||||
.divisor = is_instanced ? input_binding.frequency : 1,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -449,9 +449,7 @@ void Scheduler::EndRenderPass()
|
||||
| VK_ACCESS_COLOR_ATTACHMENT_READ_BIT
|
||||
| VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT
|
||||
| VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT
|
||||
| VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT
|
||||
| VK_ACCESS_TRANSFER_READ_BIT
|
||||
| VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
| VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
@@ -462,7 +460,7 @@ void Scheduler::EndRenderPass()
|
||||
}
|
||||
cmdbuf.EndRenderPass();
|
||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, vk::PIPELINE_STAGE_GRAPHICS_COMPUTE_TRANSFER,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, vk::PIPELINE_STAGE_GRAPHICS_COMPUTE,
|
||||
0, nullptr, nullptr, vk::Span(barriers.data(), num_images));
|
||||
if (has_transform_feedback) {
|
||||
static constexpr VkMemoryBarrier XFB_OUTPUT_BARRIER{
|
||||
|
||||
@@ -160,55 +160,6 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
|
||||
info.size.depth == 1;
|
||||
}
|
||||
|
||||
[[nodiscard]] PixelFormat UnswizzleViewFormat(u32 bytes_per_block) {
|
||||
switch (bytes_per_block) {
|
||||
case 1:
|
||||
return PixelFormat::R8_UINT;
|
||||
case 2:
|
||||
return PixelFormat::R16_UINT;
|
||||
case 4:
|
||||
return PixelFormat::R32_UINT;
|
||||
case 8:
|
||||
return PixelFormat::R32G32_UINT;
|
||||
case 16:
|
||||
return PixelFormat::R32G32B32A32_UINT;
|
||||
default:
|
||||
return PixelFormat::Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr u32 UNSWIZZLE_WORKGROUP_INVOCATIONS = 32 * 32;
|
||||
|
||||
[[nodiscard]] bool SupportsAcceleratedUnswizzleDevice(const Device& device) {
|
||||
return device.IsKhrImageFormatListSupported() &&
|
||||
device.GetMaxComputeWorkGroupInvocations() >= UNSWIZZLE_WORKGROUP_INVOCATIONS;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool SupportsAcceleratedUnswizzle(const Device& device, const ImageInfo& info) {
|
||||
if (!SupportsAcceleratedUnswizzleDevice(device)) {
|
||||
return false;
|
||||
}
|
||||
if (info.num_samples > 1) {
|
||||
return false;
|
||||
}
|
||||
if (info.type != ImageType::e2D && info.type != ImageType::e3D &&
|
||||
info.type != ImageType::Linear) {
|
||||
return false;
|
||||
}
|
||||
const PixelFormat view_format =
|
||||
UnswizzleViewFormat(VideoCore::Surface::BytesPerBlock(info.format));
|
||||
if (view_format == PixelFormat::Invalid) {
|
||||
return false;
|
||||
}
|
||||
if (!VideoCore::Surface::IsViewCompatible(info.format, view_format, false, true)) {
|
||||
return false;
|
||||
}
|
||||
const auto host_format =
|
||||
MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, view_format);
|
||||
return device.IsFormatSupported(host_format.format, VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT,
|
||||
FormatType::Optimal);
|
||||
}
|
||||
|
||||
[[nodiscard]] VkImageCreateInfo MakeImageCreateInfo(const Device& device, const ImageInfo& info,
|
||||
std::optional<VkFormat> format_override = {}) {
|
||||
auto format_info =
|
||||
@@ -297,18 +248,8 @@ constexpr u32 UNSWIZZLE_WORKGROUP_INVOCATIONS = 32 * 32;
|
||||
return allocator.CreateImage(image_ci);
|
||||
}
|
||||
|
||||
[[nodiscard]] VkImageViewType StorageViewType(ImageType type) {
|
||||
if (type == ImageType::e3D) {
|
||||
return VK_IMAGE_VIEW_TYPE_3D;
|
||||
}
|
||||
if (type == ImageType::Linear) {
|
||||
return VK_IMAGE_VIEW_TYPE_2D;
|
||||
}
|
||||
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
}
|
||||
|
||||
[[nodiscard]] vk::ImageView MakeStorageView(const vk::Device& device, u32 level, VkImage image,
|
||||
VkFormat format, VkImageViewType view_type) {
|
||||
VkFormat format) {
|
||||
static constexpr VkImageViewUsageCreateInfo storage_image_view_usage_create_info{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
@@ -319,7 +260,7 @@ constexpr u32 UNSWIZZLE_WORKGROUP_INVOCATIONS = 32 * 32;
|
||||
.pNext = &storage_image_view_usage_create_info,
|
||||
.flags = 0,
|
||||
.image = image,
|
||||
.viewType = view_type,
|
||||
.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY,
|
||||
.format = format,
|
||||
.components{
|
||||
.r = VK_COMPONENT_SWIZZLE_IDENTITY,
|
||||
@@ -717,11 +658,18 @@ void CopyBufferToImage(vk::CommandBuffer cmdbuf, VkBuffer src_buffer, VkImage im
|
||||
.subresourceRange = subresource_range,
|
||||
};
|
||||
|
||||
cmdbuf.PipelineBarrier(vk::PIPELINE_STAGE_GRAPHICS_COMPUTE, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
|
||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
|
||||
read_barrier);
|
||||
cmdbuf.CopyBufferToImage(src_buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, copies);
|
||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, vk::PIPELINE_STAGE_GRAPHICS_COMPUTE, 0,
|
||||
nullptr, nullptr, write_barrier);
|
||||
// TODO: Move this to another API
|
||||
cmdbuf.PipelineBarrier(
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
0, nullptr, nullptr, write_barrier);
|
||||
}
|
||||
|
||||
[[nodiscard]] VkImageBlit MakeImageBlit(const Region2D& dst_region, const Region2D& src_region,
|
||||
@@ -1020,14 +968,6 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched
|
||||
bl3d_unswizzle_pass.emplace(device, scheduler, descriptor_pool,
|
||||
staging_buffer_pool, compute_pass_descriptor_queue);
|
||||
}
|
||||
if (SupportsAcceleratedUnswizzleDevice(device)) {
|
||||
bl_unswizzle_2d_pass.emplace(device, scheduler, descriptor_pool,
|
||||
compute_pass_descriptor_queue);
|
||||
bl_unswizzle_image_3d_pass.emplace(device, scheduler, descriptor_pool,
|
||||
compute_pass_descriptor_queue);
|
||||
pitch_unswizzle_pass.emplace(device, scheduler, descriptor_pool,
|
||||
compute_pass_descriptor_queue);
|
||||
}
|
||||
}
|
||||
|
||||
void TextureCacheRuntime::Finish() {
|
||||
@@ -1962,12 +1902,16 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
|
||||
if (runtime->device.HasDebuggingToolAttached()) {
|
||||
original_image.SetObjectNameEXT(VideoCommon::Name(*this).c_str());
|
||||
}
|
||||
if (False(flags & VideoCommon::ImageFlagBits::Converted) &&
|
||||
SupportsAcceleratedUnswizzle(runtime->device, info)) {
|
||||
flags |= VideoCommon::ImageFlagBits::AcceleratedUpload;
|
||||
}
|
||||
current_image = &Image::original_image;
|
||||
storage_image_views.resize(info.resources.levels);
|
||||
if (WillUseAcceleratedAstcDecode(runtime->device, info)) {
|
||||
const auto& device = runtime->device.GetLogical();
|
||||
const VkFormat storage_format = VK_FORMAT_A8B8G8R8_UNORM_PACK32;
|
||||
for (s32 level = 0; level < info.resources.levels; ++level) {
|
||||
storage_image_views[level] =
|
||||
MakeStorageView(device, level, *original_image, storage_format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image::Image(const VideoCommon::NullImageParams& params) : VideoCommon::ImageBase{params} {}
|
||||
@@ -2091,7 +2035,7 @@ void Image::UploadMemory(VkBuffer buffer, VkDeviceSize offset,
|
||||
temp_vk_image, info.format, info.num_samples,
|
||||
{image_copies.data(), image_copies.size()}, false);
|
||||
}
|
||||
InitializationFor(current_image) = true;
|
||||
initialized = true;
|
||||
runtime->ReleaseMsaaScratchImage(temp_vk_image);
|
||||
|
||||
if (is_rescaled) {
|
||||
@@ -2112,7 +2056,7 @@ void Image::UploadMemory(VkBuffer buffer, VkDeviceSize offset,
|
||||
const VkBuffer src_buffer = buffer;
|
||||
const VkImage vk_image = *original_image;
|
||||
const VkImageAspectFlags vk_aspect_mask = aspect_mask;
|
||||
const bool was_initialized = std::exchange(InitializationFor(&Image::original_image), true);
|
||||
const bool was_initialized = std::exchange(initialized, true);
|
||||
|
||||
scheduler->Record([src_buffer, vk_image, vk_aspect_mask, was_initialized,
|
||||
vk_copies](vk::CommandBuffer cmdbuf) {
|
||||
@@ -2377,46 +2321,16 @@ void Image::DownloadMemory(const StagingBufferRef& map, std::span<const BufferIm
|
||||
DownloadMemory(buffers, offsets, copies);
|
||||
}
|
||||
|
||||
std::vector<vk::ImageView>& Image::StorageViewsFor(vk::Image Image::*image) {
|
||||
if (image == &Image::scaled_image) {
|
||||
if (scaled_storage_image_views.empty()) {
|
||||
scaled_storage_image_views.resize(info.resources.levels);
|
||||
}
|
||||
return scaled_storage_image_views;
|
||||
}
|
||||
return storage_image_views;
|
||||
}
|
||||
|
||||
bool& Image::InitializationFor(vk::Image Image::*image) noexcept {
|
||||
if (image == &Image::scaled_image) {
|
||||
return scaled_initialized;
|
||||
}
|
||||
return original_initialized;
|
||||
}
|
||||
|
||||
VkImageView Image::StorageImageView(s32 level) noexcept {
|
||||
const bool astc_decode = WillUseAcceleratedAstcDecode(runtime->device, info);
|
||||
const bool unswizzle_upload =
|
||||
!astc_decode && True(flags & ImageFlagBits::AcceleratedUpload);
|
||||
vk::Image Image::*target = current_image;
|
||||
if (astc_decode || unswizzle_upload) {
|
||||
target = &Image::original_image;
|
||||
}
|
||||
auto& view = StorageViewsFor(target)[level];
|
||||
auto& view = storage_image_views[level];
|
||||
if (!view) {
|
||||
auto format_info =
|
||||
MaxwellToVK::SurfaceFormat(runtime->device, FormatType::Optimal, true, info.format);
|
||||
if (astc_decode) {
|
||||
if (WillUseAcceleratedAstcDecode(runtime->device, info)) {
|
||||
format_info.format = VK_FORMAT_A8B8G8R8_UNORM_PACK32;
|
||||
}
|
||||
if (unswizzle_upload) {
|
||||
const PixelFormat view_format =
|
||||
UnswizzleViewFormat(VideoCore::Surface::BytesPerBlock(info.format));
|
||||
format_info = MaxwellToVK::SurfaceFormat(runtime->device, FormatType::Optimal, false,
|
||||
view_format);
|
||||
}
|
||||
view = MakeStorageView(runtime->device.GetLogical(), level, *(this->*target),
|
||||
format_info.format, StorageViewType(info.type));
|
||||
view = MakeStorageView(runtime->device.GetLogical(), level, *(this->*current_image),
|
||||
format_info.format);
|
||||
}
|
||||
return *view;
|
||||
}
|
||||
@@ -2456,7 +2370,6 @@ bool Image::ScaleUp(bool ignore) {
|
||||
}
|
||||
if (NeedsScaleHelper()) {
|
||||
if (!BlitScaleHelper(true)) {
|
||||
flags &= ~ImageFlagBits::Rescaled;
|
||||
current_image = &Image::original_image;
|
||||
return false;
|
||||
}
|
||||
@@ -2646,10 +2559,6 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI
|
||||
if (device->IsExtAstcDecodeModeSupported() && IsLdrAstcFormat(format_info.format)) {
|
||||
view_next = &astc_decode_mode;
|
||||
}
|
||||
auto subresource_range = MakeSubresourceRange(aspect_mask, info.range);
|
||||
if (True(flags & VideoCommon::ImageViewFlagBits::Slice)) {
|
||||
subresource_range.levelCount = 1;
|
||||
}
|
||||
const VkImageViewCreateInfo create_info{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||
.pNext = view_next,
|
||||
@@ -2658,7 +2567,7 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI
|
||||
.viewType = VkImageViewType{},
|
||||
.format = format_info.format,
|
||||
.components = swizzle_mapping,
|
||||
.subresourceRange = subresource_range,
|
||||
.subresourceRange = MakeSubresourceRange(aspect_mask, info.range),
|
||||
};
|
||||
const auto create = [&](TextureType tex_type, std::optional<u32> num_layers) {
|
||||
VkImageViewCreateInfo ci{create_info};
|
||||
@@ -3232,19 +3141,6 @@ void TextureCacheRuntime::AccelerateImageUpload(
|
||||
return astc_decoder_pass->Assemble(image, map, swizzles);
|
||||
}
|
||||
|
||||
if (bl_unswizzle_2d_pass && image.info.type == ImageType::e2D) {
|
||||
return bl_unswizzle_2d_pass->Unswizzle(image, map, swizzles);
|
||||
}
|
||||
|
||||
if (bl_unswizzle_image_3d_pass && image.info.type == ImageType::e3D &&
|
||||
!IsPixelFormatBCn(image.info.format)) {
|
||||
return bl_unswizzle_image_3d_pass->Unswizzle(image, map, swizzles);
|
||||
}
|
||||
|
||||
if (pitch_unswizzle_pass && image.info.type == ImageType::Linear) {
|
||||
return pitch_unswizzle_pass->Unswizzle(image, map, swizzles);
|
||||
}
|
||||
|
||||
if (!Settings::values.gpu_unswizzle_enabled.GetValue() || !bl3d_unswizzle_pass) {
|
||||
if (IsPixelFormatBCn(image.info.format) && image.info.type == ImageType::e3D) {
|
||||
ASSERT(false && "GPU unswizzle is disabled for BCn 3D texture");
|
||||
|
||||
@@ -159,9 +159,6 @@ public:
|
||||
std::optional<ASTCDecoderPass> astc_decoder_pass;
|
||||
|
||||
std::optional<BlockLinearUnswizzle3DPass> bl3d_unswizzle_pass;
|
||||
std::optional<BlockLinearUnswizzle2DPass> bl_unswizzle_2d_pass;
|
||||
std::optional<BlockLinearUnswizzleImage3DPass> bl_unswizzle_image_3d_pass;
|
||||
std::optional<PitchUnswizzlePass> pitch_unswizzle_pass;
|
||||
const Settings::ResolutionScalingInfo& resolution;
|
||||
std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
|
||||
|
||||
@@ -353,7 +350,7 @@ public:
|
||||
|
||||
/// Returns true when the image is already initialized and mark it as initialized
|
||||
[[nodiscard]] bool ExchangeInitialization() noexcept {
|
||||
return std::exchange(InitializationFor(current_image), true);
|
||||
return std::exchange(initialized, true);
|
||||
}
|
||||
|
||||
VkImageView StorageImageView(s32 level) noexcept;
|
||||
@@ -373,10 +370,6 @@ private:
|
||||
|
||||
bool NeedsScaleHelper() const;
|
||||
|
||||
std::vector<vk::ImageView>& StorageViewsFor(vk::Image Image::*image);
|
||||
|
||||
bool& InitializationFor(vk::Image Image::*image) noexcept;
|
||||
|
||||
Scheduler* scheduler{};
|
||||
TextureCacheRuntime* runtime{};
|
||||
|
||||
@@ -394,10 +387,8 @@ private:
|
||||
vk::Image Image::*current_image{};
|
||||
|
||||
std::vector<vk::ImageView> storage_image_views;
|
||||
std::vector<vk::ImageView> scaled_storage_image_views;
|
||||
VkImageAspectFlags aspect_mask = 0;
|
||||
bool original_initialized = false;
|
||||
bool scaled_initialized = false;
|
||||
bool initialized = false;
|
||||
|
||||
std::optional<Framebuffer> scale_framebuffer;
|
||||
std::optional<Framebuffer> normal_framebuffer;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -26,8 +23,8 @@ struct BlockLinearSwizzle2DParams {
|
||||
};
|
||||
|
||||
struct BlockLinearSwizzle3DParams {
|
||||
alignas(16) std::array<u32, 3> origin;
|
||||
alignas(16) std::array<s32, 3> destination;
|
||||
std::array<u32, 3> origin;
|
||||
std::array<s32, 3> destination;
|
||||
u32 bytes_per_block_log2;
|
||||
u32 slice_size;
|
||||
u32 block_size;
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "video_core/engines/kepler_compute.h"
|
||||
#include "video_core/guest_memory.h"
|
||||
#include "video_core/host1x/gpu_device_memory_manager.h"
|
||||
#include "video_core/texture_cache/accelerated_swizzle.h"
|
||||
#include "video_core/texture_cache/image_view_base.h"
|
||||
#include "video_core/texture_cache/samples_helper.h"
|
||||
#include "video_core/texture_cache/texture_cache_base.h"
|
||||
@@ -280,11 +279,11 @@ void TextureCache<P>::CheckFeedbackLoop(std::span<const ImageViewInOut> views) {
|
||||
|
||||
const ImageId view_image_id = slot_image_views[view.id].image_id;
|
||||
{
|
||||
bool is_feedback = false;
|
||||
bool is_continue = false;
|
||||
for (size_t i = 0; i < 8; ++i)
|
||||
is_feedback |= (rt_active_mask & (1u << i)) && view_image_id == rt_image_id[i];
|
||||
if (is_feedback)
|
||||
return true;
|
||||
is_continue |= (rt_active_mask & (1u << i)) && view_image_id == rt_image_id[i];
|
||||
if (is_continue)
|
||||
continue;
|
||||
}
|
||||
if (depth_active && view_image_id == rt_depth_image_id) {
|
||||
return true;
|
||||
@@ -627,25 +626,14 @@ void TextureCache<P>::DownloadMemory(DAddr cpu_addr, size_t size) {
|
||||
std::ranges::sort(images, [this](ImageId lhs, ImageId rhs) {
|
||||
return slot_images[lhs].modification_tick < slot_images[rhs].modification_tick;
|
||||
});
|
||||
size_t total_size_bytes = 0;
|
||||
for (const ImageId image_id : images) {
|
||||
total_size_bytes += slot_images[image_id].unswizzled_size_bytes;
|
||||
}
|
||||
auto download_map = runtime.DownloadStagingBuffer(total_size_bytes);
|
||||
for (const ImageId image_id : images) {
|
||||
Image& image = slot_images[image_id];
|
||||
auto map = runtime.DownloadStagingBuffer(image.unswizzled_size_bytes);
|
||||
const auto copies = FixSmallVectorADL(FullDownloadCopies(image.info));
|
||||
image.DownloadMemory(download_map, copies);
|
||||
download_map.offset += image.unswizzled_size_bytes;
|
||||
}
|
||||
runtime.Finish();
|
||||
std::span<u8> download_span = download_map.mapped_span;
|
||||
for (const ImageId image_id : images) {
|
||||
const ImageBase& image = slot_images[image_id];
|
||||
const auto copies = FixSmallVectorADL(FullDownloadCopies(image.info));
|
||||
SwizzleImage(*gpu_memory, image.gpu_addr, image.info, copies, download_span,
|
||||
image.DownloadMemory(map, copies);
|
||||
runtime.Finish();
|
||||
SwizzleImage(*gpu_memory, image.gpu_addr, image.info, copies, map.mapped_span,
|
||||
swizzle_data_buffer);
|
||||
download_span = download_span.subspan(image.unswizzled_size_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1134,12 +1122,6 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) {
|
||||
|
||||
TrackImage(image, image_id);
|
||||
|
||||
if (image.info.rescaleable &&
|
||||
IsRegionGpuModified(image.cpu_addr, image.guest_size_bytes)) {
|
||||
runtime.TransitionImageLayout(image);
|
||||
return;
|
||||
}
|
||||
|
||||
if (image.info.num_samples > 1 && !runtime.CanUploadMSAA()) {
|
||||
LOG_WARNING(HW_GPU, "MSAA image uploads are not implemented");
|
||||
runtime.TransitionImageLayout(image);
|
||||
@@ -1175,7 +1157,8 @@ void TextureCache<P>::UploadImageContents(Image& image, StagingBuffer& staging)
|
||||
const GPUVAddr gpu_addr = image.gpu_addr;
|
||||
|
||||
if (True(image.flags & ImageFlagBits::AcceleratedUpload)) {
|
||||
gpu_memory->ReadBlockUnsafe(gpu_addr, mapped_span.data(), image.guest_size_bytes);
|
||||
gpu_memory->ReadBlock(gpu_addr, mapped_span.data(), mapped_span.size_bytes(),
|
||||
VideoCommon::CacheType::NoTextureCache);
|
||||
const auto uploads = FullUploadSwizzles(image.info);
|
||||
runtime.AccelerateImageUpload(image, staging, FixSmallVectorADL(uploads), 0, 0);
|
||||
return;
|
||||
@@ -1282,9 +1265,6 @@ ImageId TextureCache<P>::FindImage(const ImageInfo& info, GPUVAddr gpu_addr,
|
||||
|
||||
template <class P>
|
||||
bool TextureCache<P>::ImageCanRescale(ImageBase& image) {
|
||||
if (!Settings::values.resolution_info.active) {
|
||||
return false;
|
||||
}
|
||||
if (!image.info.rescaleable) {
|
||||
return false;
|
||||
}
|
||||
@@ -1372,12 +1352,12 @@ void TextureCache<P>::QueueAsyncDecode(Image& image, ImageId image_id) {
|
||||
decode->image_id = image_id;
|
||||
async_decodes.push_back(std::move(decode));
|
||||
|
||||
Common::ScratchBuffer<u8> local_unswizzle_data_buffer(image.unswizzled_size_bytes);
|
||||
std::vector<u8> local_unswizzle_data_buffer(image.unswizzled_size_bytes, 0);
|
||||
Tegra::Memory::GpuGuestMemory<u8, Tegra::Memory::GuestMemoryFlags::UnsafeRead> swizzle_data(*gpu_memory, image.gpu_addr, image.guest_size_bytes, &swizzle_data_buffer);
|
||||
auto copies = UnswizzleImage(*gpu_memory, image.gpu_addr, image.info, swizzle_data, local_unswizzle_data_buffer);
|
||||
const size_t out_size = MapSizeBytes(image);
|
||||
|
||||
auto func = [out_size, copies = std::move(copies), info = image.info,
|
||||
auto func = [out_size, copies, info = image.info,
|
||||
input = std::move(local_unswizzle_data_buffer),
|
||||
async_decode = decode_ptr]() mutable {
|
||||
async_decode->decoded_data.resize_destructive(out_size);
|
||||
@@ -1446,16 +1426,18 @@ void TextureCache<P>::TickAsyncUnswizzle() {
|
||||
Image& image = slot_images[task.image_id];
|
||||
|
||||
if (!task.initialized) {
|
||||
task.total_size = image.guest_size_bytes;
|
||||
task.total_size = MapSizeBytes(image);
|
||||
task.staging_buffer = runtime.UploadStagingBuffer(task.total_size, true);
|
||||
|
||||
const auto layout = FullUploadSwizzles(task.info);
|
||||
const auto params =
|
||||
VideoCommon::Accelerated::MakeBlockLinearSwizzle3DParams(layout.front(), task.info);
|
||||
task.bytes_per_slice = params.slice_size;
|
||||
task.chunked = task.info.block.depth == 0;
|
||||
const auto& info = image.info;
|
||||
const u32 bytes_per_block = BytesPerBlock(info.format);
|
||||
const u32 width_blocks = Common::DivCeil(info.size.width, 4u);
|
||||
const u32 height_blocks = Common::DivCeil(info.size.height, 4u);
|
||||
|
||||
const u32 stride = width_blocks * bytes_per_block;
|
||||
const u32 aligned_height = height_blocks;
|
||||
task.bytes_per_slice = static_cast<size_t>(stride) * aligned_height;
|
||||
task.last_submitted_offset = 0;
|
||||
task.slices_submitted = 0;
|
||||
task.initialized = true;
|
||||
}
|
||||
|
||||
@@ -1470,39 +1452,31 @@ void TextureCache<P>::TickAsyncUnswizzle() {
|
||||
if (copy_amount == 0) copy_amount = task.bytes_per_slice;
|
||||
}
|
||||
|
||||
gpu_memory->ReadBlockUnsafe(image.gpu_addr + task.current_offset,
|
||||
task.staging_buffer.mapped_span.data() + task.current_offset,
|
||||
copy_amount);
|
||||
gpu_memory->ReadBlock(image.gpu_addr + task.current_offset,
|
||||
task.staging_buffer.mapped_span.data() + task.current_offset,
|
||||
copy_amount);
|
||||
task.current_offset += copy_amount;
|
||||
}
|
||||
|
||||
const bool is_final_batch = task.current_offset >= task.total_size;
|
||||
const size_t bytes_ready = task.current_offset - task.last_submitted_offset;
|
||||
const u32 complete_slices = static_cast<u32>(bytes_ready / task.bytes_per_slice);
|
||||
|
||||
if (task.chunked) {
|
||||
const size_t bytes_ready = task.current_offset - task.last_submitted_offset;
|
||||
const u32 complete_slices = static_cast<u32>(bytes_ready / task.bytes_per_slice);
|
||||
if (complete_slices >= swizzle_slices_per_batch || (is_final_batch && complete_slices > 0)) {
|
||||
const u32 z_start = static_cast<u32>(task.last_submitted_offset / task.bytes_per_slice);
|
||||
const u32 slices_to_process = (std::min)(complete_slices, swizzle_slices_per_batch);
|
||||
const u32 z_count = (std::min)(slices_to_process, image.info.size.depth - z_start);
|
||||
|
||||
if (complete_slices >= swizzle_slices_per_batch || (is_final_batch && complete_slices > 0)) {
|
||||
const u32 z_start = task.slices_submitted;
|
||||
const u32 slices_to_process = (std::min)(complete_slices, swizzle_slices_per_batch);
|
||||
const u32 z_count = (std::min)(slices_to_process, image.info.size.depth - z_start);
|
||||
|
||||
if (z_count > 0) {
|
||||
const auto uploads = FullUploadSwizzles(task.info);
|
||||
runtime.AccelerateImageUpload(image, task.staging_buffer,
|
||||
FixSmallVectorADL(uploads), z_start, z_count);
|
||||
task.last_submitted_offset += static_cast<size_t>(z_count) * task.bytes_per_slice;
|
||||
task.slices_submitted += z_count;
|
||||
}
|
||||
if (z_count > 0) {
|
||||
const auto uploads = FullUploadSwizzles(task.info);
|
||||
runtime.AccelerateImageUpload(image, task.staging_buffer, FixSmallVectorADL(uploads), z_start, z_count);
|
||||
task.last_submitted_offset += (static_cast<size_t>(z_count) * task.bytes_per_slice);
|
||||
}
|
||||
} else if (is_final_batch && task.slices_submitted == 0) {
|
||||
const auto uploads = FullUploadSwizzles(task.info);
|
||||
runtime.AccelerateImageUpload(image, task.staging_buffer, FixSmallVectorADL(uploads), 0,
|
||||
image.info.size.depth);
|
||||
task.slices_submitted = image.info.size.depth;
|
||||
}
|
||||
|
||||
const bool all_slices_submitted = task.slices_submitted >= image.info.size.depth;
|
||||
// Check if complete
|
||||
const u32 slices_submitted = static_cast<u32>(task.last_submitted_offset / task.bytes_per_slice);
|
||||
const bool all_slices_submitted = slices_submitted >= image.info.size.depth;
|
||||
|
||||
if (is_final_batch && all_slices_submitted) {
|
||||
runtime.FreeDeferredStagingBuffer(task.staging_buffer);
|
||||
|
||||
@@ -139,8 +139,6 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
|
||||
AsyncBuffer staging_buffer;
|
||||
size_t last_submitted_offset = 0;
|
||||
size_t bytes_per_slice;
|
||||
u32 slices_submitted = 0;
|
||||
bool chunked = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -1219,11 +1219,6 @@ bool Device::GetSuitability(bool requires_swapchain) {
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_PROPERTIES_EXT;
|
||||
SetNext(next, properties.transform_feedback);
|
||||
}
|
||||
if (extensions.vertex_attribute_divisor) {
|
||||
properties.vertex_attribute_divisor.sType =
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_ATTRIBUTE_DIVISOR_PROPERTIES_EXT;
|
||||
SetNext(next, properties.vertex_attribute_divisor);
|
||||
}
|
||||
if (extensions.maintenance5) {
|
||||
properties.maintenance5.sType =
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_PROPERTIES_KHR;
|
||||
|
||||
@@ -70,7 +70,6 @@ VK_DEFINE_HANDLE(VmaAllocator)
|
||||
FEATURE(EXT, ProvokingVertex, PROVOKING_VERTEX, provoking_vertex) \
|
||||
FEATURE(EXT, Robustness2, ROBUSTNESS_2, robustness2) \
|
||||
FEATURE(EXT, TransformFeedback, TRANSFORM_FEEDBACK, transform_feedback) \
|
||||
FEATURE(EXT, VertexAttributeDivisor, VERTEX_ATTRIBUTE_DIVISOR, vertex_attribute_divisor) \
|
||||
FEATURE(EXT, VertexInputDynamicState, VERTEX_INPUT_DYNAMIC_STATE, vertex_input_dynamic_state) \
|
||||
FEATURE(KHR, Maintenance5, MAINTENANCE_5, maintenance5) \
|
||||
FEATURE(KHR, Maintenance6, MAINTENANCE_6, maintenance6) \
|
||||
@@ -93,6 +92,7 @@ VK_DEFINE_HANDLE(VmaAllocator)
|
||||
EXTENSION(EXT, SHADER_STENCIL_EXPORT, shader_stencil_export) \
|
||||
EXTENSION(EXT, SHADER_VIEWPORT_INDEX_LAYER, shader_viewport_index_layer) \
|
||||
EXTENSION(EXT, TOOLING_INFO, tooling_info) \
|
||||
EXTENSION(EXT, VERTEX_ATTRIBUTE_DIVISOR, vertex_attribute_divisor) \
|
||||
EXTENSION(KHR, CREATE_RENDERPASS_2, create_renderpass2) \
|
||||
EXTENSION(KHR, DEPTH_STENCIL_RESOLVE, depth_stencil_resolve) \
|
||||
EXTENSION(KHR, DRAW_INDIRECT_COUNT, draw_indirect_count) \
|
||||
@@ -359,7 +359,6 @@ public:
|
||||
|
||||
#define FN_MAX_LIMIT_LIST \
|
||||
FN_MAX_LIMIT_ELEM(ComputeSharedMemorySize) \
|
||||
FN_MAX_LIMIT_ELEM(ComputeWorkGroupInvocations) \
|
||||
FN_MAX_LIMIT_ELEM(PerStageDescriptorSampledImages) \
|
||||
FN_MAX_LIMIT_ELEM(PerStageResources) \
|
||||
FN_MAX_LIMIT_ELEM(DescriptorSetSamplers) \
|
||||
@@ -690,32 +689,6 @@ FN_MAX_LIMIT_LIST
|
||||
return features.host_query_reset.hostQueryReset != VK_FALSE;
|
||||
}
|
||||
|
||||
u32 GetMaxVertexAttribDivisor() const {
|
||||
const u32 reported = properties.vertex_attribute_divisor.maxVertexAttribDivisor;
|
||||
if (reported == 0) {
|
||||
return 1;
|
||||
}
|
||||
return reported;
|
||||
}
|
||||
|
||||
bool IsVertexAttributeInstanceRateZeroDivisorSupported() const {
|
||||
return features.vertex_attribute_divisor.vertexAttributeInstanceRateZeroDivisor == VK_TRUE;
|
||||
}
|
||||
|
||||
u32 GetVertexAttribDivisor(u32 frequency) const {
|
||||
const u32 max_divisor = GetMaxVertexAttribDivisor();
|
||||
if (frequency == 0) {
|
||||
if (IsVertexAttributeInstanceRateZeroDivisorSupported()) {
|
||||
return 0;
|
||||
}
|
||||
return max_divisor;
|
||||
}
|
||||
if (frequency > max_divisor) {
|
||||
return max_divisor;
|
||||
}
|
||||
return frequency;
|
||||
}
|
||||
|
||||
/// Returns true if the device supports VK_EXT_transform_feedback.
|
||||
bool IsExtTransformFeedbackSupported() const {
|
||||
return extensions.transform_feedback;
|
||||
@@ -1216,7 +1189,6 @@ private:
|
||||
VkPhysicalDeviceDescriptorBufferPropertiesEXT descriptor_buffer{};
|
||||
VkPhysicalDeviceSubgroupSizeControlProperties subgroup_size_control{};
|
||||
VkPhysicalDeviceTransformFeedbackPropertiesEXT transform_feedback{};
|
||||
VkPhysicalDeviceVertexAttributeDivisorPropertiesEXT vertex_attribute_divisor{};
|
||||
VkPhysicalDeviceMaintenance5PropertiesKHR maintenance5{};
|
||||
VkPhysicalDeviceDepthStencilResolveProperties depth_stencil_resolve{};
|
||||
VkPhysicalDeviceCustomBorderColorPropertiesEXT custom_border_color{};
|
||||
|
||||
@@ -250,6 +250,12 @@ if (YUZU_CRASH_DUMPS)
|
||||
target_compile_definitions(yuzu PRIVATE YUZU_CRASH_DUMPS)
|
||||
endif()
|
||||
|
||||
if (ENABLE_RESHADE)
|
||||
target_sources(yuzu PRIVATE
|
||||
configuration/configure_post_processing.cpp
|
||||
configuration/configure_post_processing.h)
|
||||
endif()
|
||||
|
||||
if (CXX_CLANG)
|
||||
target_compile_definitions(yuzu PRIVATE
|
||||
$<$<VERSION_LESS:$<CXX_COMPILER_VERSION>,15>:CANNOT_EXPLICITLY_INSTANTIATE>)
|
||||
|
||||
@@ -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_Status_Bar"/>
|
||||
<addaction name="action_Show_Performance_Overlay"/>
|
||||
<addaction name="action_Post_Processing_Shaders"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="menu_Reset_Window_Size"/>
|
||||
<addaction name="menu_View_Debugging"/>
|
||||
@@ -610,6 +611,11 @@
|
||||
<string>Show &Performance Overlay</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Post_Processing_Shaders">
|
||||
<property name="text">
|
||||
<string>Post-Processing &Shaders...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Carousel_View">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
#include "common/settings_enums.h"
|
||||
#include "frontend_common/settings_generator.h"
|
||||
#include "render/performance_overlay.h"
|
||||
#ifdef HAS_RESHADE
|
||||
#include "configuration/configure_post_processing.h"
|
||||
#endif
|
||||
#include "updater/update_dialog.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_Status_Bar, &MainWindow::OnToggleStatusBar);
|
||||
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_900, &MainWindow::ResetWindowSize900);
|
||||
@@ -3900,6 +3908,22 @@ void MainWindow::OnTogglePerfOverlay() {
|
||||
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() {
|
||||
// Resets metadata cache and reloads
|
||||
QtCommon::Game::ResetMetadata(false);
|
||||
|
||||
@@ -56,6 +56,9 @@ class QSlider;
|
||||
class QHBoxLayout;
|
||||
class WaitTreeWidget;
|
||||
class PerformanceOverlay;
|
||||
#ifdef HAS_RESHADE
|
||||
class ConfigurePostProcessing;
|
||||
#endif
|
||||
enum class GameListOpenTarget;
|
||||
enum class DumpRomFSTarget;
|
||||
class GameListPlaceholder;
|
||||
@@ -392,6 +395,9 @@ private slots:
|
||||
void OnToggleFilterBar();
|
||||
void OnToggleStatusBar();
|
||||
void OnTogglePerfOverlay();
|
||||
#ifdef HAS_RESHADE
|
||||
void OnPostProcessingShaders();
|
||||
#endif
|
||||
void OnGameListRefresh();
|
||||
void InitializeHotkeys();
|
||||
void ToggleFullscreen();
|
||||
@@ -496,6 +502,9 @@ private:
|
||||
QTimer shutdown_timer;
|
||||
OverlayDialog* shutdown_dialog{};
|
||||
PerformanceOverlay* perf_overlay = nullptr;
|
||||
#ifdef HAS_RESHADE
|
||||
ConfigurePostProcessing* post_processing_dialog = nullptr;
|
||||
#endif
|
||||
|
||||
GameListPlaceholder* game_list_placeholder = nullptr;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user