Compare commits

..

3 Commits

Author SHA1 Message Date
CamilleLaVey aa8d2000f4 Fix build 2026-09-04 04:00:56 -04:00
CamilleLaVey 821765189a add reshade toggle 2026-09-04 03:41:24 -04:00
CamilleLaVey f24b14e7cf [vulkan] Initial implementation of post-processing shaders 2026-09-04 03:19:15 -04:00
58 changed files with 4154 additions and 413 deletions
@@ -0,0 +1,28 @@
From cc15da16e533b2a801934eab2dfeaf3c3949a1dc Mon Sep 17 00:00:00 2001
From: crueter <crueter@eden-emu.dev>
Date: Mon, 8 Sep 2025 12:28:55 -0400
Subject: [PATCH] [cmake] disable NEON runtime check on clang-cl
When enabling runtime NEON checking for clang-cl, the linker would error out with `undefined symbol: __emit`, since clang doesn't actually implement this instruction. Therefore it makes sense to disable the runtime check by default on this platform, until either this is fixed or a clang-cl compatible intrinsic check is added (I don't have enough knowledge of MSVC to do this)
---
cmake/OpusConfig.cmake | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/cmake/OpusConfig.cmake b/cmake/OpusConfig.cmake
index e9319fbad..d0f459e88 100644
--- a/cmake/OpusConfig.cmake
+++ b/cmake/OpusConfig.cmake
@@ -71,7 +71,12 @@ elseif(OPUS_CPU_ARM AND NOT OPUS_DISABLE_INTRINSICS)
opus_detect_neon(COMPILER_SUPPORT_NEON)
if(COMPILER_SUPPORT_NEON)
option(OPUS_USE_NEON "Option to enable NEON" ON)
- option(OPUS_MAY_HAVE_NEON "Does runtime check for neon support" ON)
+ if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+ set(NEON_RUNTIME_CHECK_DEFAULT OFF)
+ else()
+ set(NEON_RUNTIME_CHECK_DEFAULT ON)
+ endif()
+ option(OPUS_MAY_HAVE_NEON "Does runtime check for neon support" ${NEON_RUNTIME_CHECK_DEFAULT})
option(OPUS_PRESUME_NEON "Assume target CPU has NEON support" OFF)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64")
set(OPUS_PRESUME_NEON ON)
+153
View File
@@ -0,0 +1,153 @@
From bf455b67b4eaa446ffae5d25410b141b7b1b1082 Mon Sep 17 00:00:00 2001
From: crueter <crueter@eden-emu.dev>
Date: Mon, 8 Sep 2025 12:08:20 -0400
Subject: [PATCH] [cmake] `OPUS_INSTALL` option; only default install if root
project
Signed-off-by: crueter <crueter@eden-emu.dev>
---
CMakeLists.txt | 112 ++++++++++++++++++++++++++++---------------------
1 file changed, 64 insertions(+), 48 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fcf034b19..08b5e16f8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,6 +4,13 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(OpusPackageVersion)
get_package_version(PACKAGE_VERSION PROJECT_VERSION)
+# root project detection
+if(DEFINED PROJECT_NAME)
+ set(root_project OFF)
+else()
+ set(root_project ON)
+endif()
+
project(Opus LANGUAGES C VERSION ${PROJECT_VERSION})
include(OpusFunctions)
@@ -83,12 +90,16 @@ set(OPUS_DNN_FLOAT_DEBUG_HELP_STR "Run DNN computations as float for debugging p
option(OPUS_DNN_FLOAT_DEBUG ${OPUS_DNN_FLOAT_DEBUG_HELP_STR} OFF)
add_feature_info(OPUS_DNN_FLOAT_DEBUG OPUS_DNN_FLOAT_DEBUG ${OPUS_DNN_FLOAT_DEBUG_HELP_STR})
+set(OPUS_INSTALL_HELP_STR "Install Opus targets")
+option(OPUS_INSTALL ${OPUS_INSTALL_HELP_STR} ${root_project})
+add_feature_info(OPUS_INSTALL OPUS_INSTALL ${OPUS_INSTALL_HELP_STR})
+
set(OPUS_INSTALL_PKG_CONFIG_MODULE_HELP_STR "install pkg-config module.")
-option(OPUS_INSTALL_PKG_CONFIG_MODULE ${OPUS_INSTALL_PKG_CONFIG_MODULE_HELP_STR} ON)
+option(OPUS_INSTALL_PKG_CONFIG_MODULE ${OPUS_INSTALL_PKG_CONFIG_MODULE_HELP_STR} ${OPUS_INSTALL})
add_feature_info(OPUS_INSTALL_PKG_CONFIG_MODULE OPUS_INSTALL_PKG_CONFIG_MODULE ${OPUS_INSTALL_PKG_CONFIG_MODULE_HELP_STR})
set(OPUS_INSTALL_CMAKE_CONFIG_MODULE_HELP_STR "install CMake package config module.")
-option(OPUS_INSTALL_CMAKE_CONFIG_MODULE ${OPUS_INSTALL_CMAKE_CONFIG_MODULE_HELP_STR} ON)
+option(OPUS_INSTALL_CMAKE_CONFIG_MODULE ${OPUS_INSTALL_CMAKE_CONFIG_MODULE_HELP_STR} ${OPUS_INSTALL})
add_feature_info(OPUS_INSTALL_CMAKE_CONFIG_MODULE OPUS_INSTALL_CMAKE_CONFIG_MODULE ${OPUS_INSTALL_CMAKE_CONFIG_MODULE_HELP_STR})
set(OPUS_DRED_HELP_STR "enable DRED.")
@@ -613,53 +624,58 @@ if(OPUS_BUILD_FRAMEWORK)
OUTPUT_NAME Opus)
endif()
-install(TARGETS opus
- EXPORT OpusTargets
- ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
- LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
- FRAMEWORK DESTINATION ${CMAKE_INSTALL_PREFIX}
- PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/opus)
-
-if(OPUS_INSTALL_PKG_CONFIG_MODULE)
- set(prefix ${CMAKE_INSTALL_PREFIX})
- set(exec_prefix ${CMAKE_INSTALL_PREFIX})
- set(libdir ${CMAKE_INSTALL_FULL_LIBDIR})
- set(includedir ${CMAKE_INSTALL_FULL_INCLUDEDIR})
- set(VERSION ${PACKAGE_VERSION})
- if(HAVE_LIBM)
- set(LIBM "-lm")
+if (OPUS_INSTALL)
+ install(TARGETS opus
+ EXPORT OpusTargets
+ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+ FRAMEWORK DESTINATION ${CMAKE_INSTALL_PREFIX}
+ PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/opus)
+
+ if(OPUS_INSTALL_PKG_CONFIG_MODULE)
+ set(prefix ${CMAKE_INSTALL_PREFIX})
+ set(exec_prefix ${CMAKE_INSTALL_PREFIX})
+ set(libdir ${CMAKE_INSTALL_FULL_LIBDIR})
+ set(includedir ${CMAKE_INSTALL_FULL_INCLUDEDIR})
+ set(VERSION ${PACKAGE_VERSION})
+ if(HAVE_LIBM)
+ set(LIBM "-lm")
+ endif()
+ configure_file(opus.pc.in opus.pc)
+ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/opus.pc
+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
+ endif()
+
+ if(OPUS_INSTALL_CMAKE_CONFIG_MODULE)
+ set(CPACK_GENERATOR TGZ)
+ include(CPack)
+ set(CMAKE_INSTALL_PACKAGEDIR ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
+ install(EXPORT OpusTargets
+ NAMESPACE Opus::
+ DESTINATION ${CMAKE_INSTALL_PACKAGEDIR})
+
+ include(CMakePackageConfigHelpers)
+
+ set(INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR})
+ configure_package_config_file(
+ ${PROJECT_SOURCE_DIR}/cmake/OpusConfig.cmake.in
+ OpusConfig.cmake
+ INSTALL_DESTINATION
+ ${CMAKE_INSTALL_PACKAGEDIR}
+ PATH_VARS
+ INCLUDE_INSTALL_DIR
+ INSTALL_PREFIX
+ ${CMAKE_INSTALL_PREFIX})
+
+ write_basic_package_version_file(OpusConfigVersion.cmake
+ VERSION ${PROJECT_VERSION}
+ COMPATIBILITY SameMajorVersion)
+
+ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/OpusConfig.cmake
+ ${CMAKE_CURRENT_BINARY_DIR}/OpusConfigVersion.cmake
+ DESTINATION ${CMAKE_INSTALL_PACKAGEDIR})
endif()
- configure_file(opus.pc.in opus.pc)
- install(FILES ${CMAKE_CURRENT_BINARY_DIR}/opus.pc
- DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
-endif()
-
-if(OPUS_INSTALL_CMAKE_CONFIG_MODULE)
- set(CPACK_GENERATOR TGZ)
- include(CPack)
- set(CMAKE_INSTALL_PACKAGEDIR ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
- install(EXPORT OpusTargets
- NAMESPACE Opus::
- DESTINATION ${CMAKE_INSTALL_PACKAGEDIR})
-
- include(CMakePackageConfigHelpers)
-
- set(INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR})
- configure_package_config_file(${PROJECT_SOURCE_DIR}/cmake/OpusConfig.cmake.in
- OpusConfig.cmake
- INSTALL_DESTINATION
- ${CMAKE_INSTALL_PACKAGEDIR}
- PATH_VARS
- INCLUDE_INSTALL_DIR
- INSTALL_PREFIX
- ${CMAKE_INSTALL_PREFIX})
- write_basic_package_version_file(OpusConfigVersion.cmake
- VERSION ${PROJECT_VERSION}
- COMPATIBILITY SameMajorVersion)
- install(FILES ${CMAKE_CURRENT_BINARY_DIR}/OpusConfig.cmake
- ${CMAKE_CURRENT_BINARY_DIR}/OpusConfigVersion.cmake
- DESTINATION ${CMAKE_INSTALL_PACKAGEDIR})
endif()
if(OPUS_BUILD_PROGRAMS)
+17
View File
@@ -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)
@@ -461,6 +463,21 @@ if (NOT YUZU_STATIC_ROOM)
if (ZLIB_ADDED)
add_library(ZLIB::ZLIB ALIAS zlibstatic)
endif()
# Opus
AddJsonPackage(opus)
if (Opus_ADDED)
if (MSVC AND CXX_CLANG)
target_compile_options(opus PRIVATE
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-implicit-function-declaration>
)
endif()
endif()
if (NOT TARGET Opus::opus)
add_library(Opus::opus ALIAS opus)
endif()
endif()
if(NOT TARGET Boost::headers)
+22
View File
@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2022 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
find_package(PkgConfig QUIET)
pkg_search_module(OPUS QUIET IMPORTED_TARGET opus)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Opus
REQUIRED_VARS OPUS_LINK_LIBRARIES
VERSION_VAR OPUS_VERSION
)
if (MSYS2)
FixMsysPath(PkgConfig::OPUS)
endif()
if (Opus_FOUND AND NOT TARGET Opus::opus)
add_library(Opus::opus ALIAS PkgConfig::OPUS)
endif()
+21 -1
View File
@@ -90,7 +90,7 @@
"name": "ffmpeg",
"package": "FFmpeg",
"repo": "crueter-ci/FFmpeg",
"version": "9.0.1-1788303113-bf1b838f2a"
"version": "9.0.1-1788120736-bf1b838f2a"
},
"fmt": {
"hash": "f0da82c545b01692e9fd30fdfb613dbb8dd9716983dcd0ff19ac2a8d36f74beb5540ef38072fdecc1e34191b3682a8542ecbf3a61ef287dbba0a2679d4e023f2",
@@ -218,6 +218,21 @@
"repo": "jimmy-park/openssl-cmake",
"version": "3.6.2"
},
"opus": {
"find_args": "MODULE",
"hash": "9506147b0de35befda8633ff272981cc2575c860874791bd455b752f797fd7dbd1079f0ba42ccdd7bb1fe6773fa5e84b3d75667c2883dd1fb2d0e4a5fa4f8387",
"min_version": "1.3",
"options": [
"OPUS_PRESUME_NEON ON"
],
"package": "Opus",
"patches": [
"0001-disable-clang-runtime-neon.patch",
"0002-no-install.patch"
],
"repo": "xiph/opus",
"version": "a3f0ec02b3"
},
"quazip": {
"hash": "609c240c7f029ac26a37d8fbab51bc16284e05e128b78b9b9c0e95d083538c36047a67d682759ac990e4adb0eeb90f04f1ea7fe2253bbda7e7e3bcce32e53dd8",
"min_version": "1.3",
@@ -231,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",
+62
View File
@@ -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;
}
}
+45
View File
@@ -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;
}
}
+50
View File
@@ -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;
}
}
+10 -9
View File
@@ -60,6 +60,7 @@ All other dependencies will be downloaded and built by [CPM](https://github.com/
* [ZLIB](https://www.zlib.net/) 1.2+
* [zstd](https://facebook.github.io/zstd/) 1.5+
* [enet](http://enet.bespin.org/) 1.3+
* [Opus](https://opus-codec.org/) 1.3+
Vulkan 1.3.274+ is also needed:
@@ -120,7 +121,7 @@ sudo emerge -a \
dev-libs/boost dev-libs/openssl dev-libs/discord-rpc \
dev-util/spirv-tools dev-util/spirv-headers dev-util/vulkan-headers \
dev-util/vulkan-utility-libraries dev-util/glslang \
media-gfx/renderdoc media-libs/libva media-video/ffmpeg \
media-gfx/renderdoc media-libs/libva media-libs/opus media-video/ffmpeg \
media-libs/VulkanMemoryAllocator media-libs/libsdl3 media-libs/cubeb \
net-libs/enet \
sys-libs/zlib \
@@ -152,7 +153,7 @@ Required USE flags:
<summary>Arch Linux</summary>
```sh
sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glslang libzip lz4 ninja nlohmann-json openssl qt6-base qt6-multimedia qt6-charts sdl3 zlib zstd zip unzip vulkan-headers vulkan-utility-libraries libusb spirv-tools spirv-headers
sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glslang libzip lz4 ninja nlohmann-json openssl opus qt6-base qt6-multimedia qt6-charts sdl3 zlib zstd zip unzip vulkan-headers vulkan-utility-libraries libusb spirv-tools spirv-headers
```
* Building with QT Web Engine requires `qt6-webengine` as well.
@@ -165,7 +166,7 @@ sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glsl
<summary>Ubuntu, Debian, Mint Linux</summary>
```sh
sudo apt-get install autoconf cmake g++ gcc git glslang-tools libglu1-mesa-dev libhidapi-dev libpulse-dev libtool libudev-dev libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-xkb1 libxext-dev libxkbcommon-x11-0 mesa-common-dev nasm ninja-build qt6-base-private-dev catch2 libfmt-dev liblz4-dev nlohmann-json3-dev libzstd-dev libssl-dev libavfilter-dev libavcodec-dev libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev qt6-tools-dev qt6-charts-dev libvulkan-dev spirv-tools spirv-headers libusb-1.0-0-dev libxbyak-dev libboost-dev libboost-fiber-dev libboost-context-dev libsdl3-dev libasound2t64 vulkan-utility-libraries-dev
sudo apt-get install autoconf cmake g++ gcc git glslang-tools libglu1-mesa-dev libhidapi-dev libpulse-dev libtool libudev-dev libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-xkb1 libxext-dev libxkbcommon-x11-0 mesa-common-dev nasm ninja-build qt6-base-private-dev catch2 libfmt-dev liblz4-dev nlohmann-json3-dev libzstd-dev libssl-dev libavfilter-dev libavcodec-dev libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev qt6-tools-dev qt6-charts-dev libvulkan-dev spirv-tools spirv-headers libusb-1.0-0-dev libxbyak-dev libboost-dev libboost-fiber-dev libboost-context-dev libsdl3-dev libopus-dev libasound2t64 vulkan-utility-libraries-dev
```
* Ubuntu 26.04, Linux Mint 22.3, or Debian 13 or later is required.
@@ -212,7 +213,7 @@ First, enable the community repository; [see here](https://wiki.alpinelinux.org/
# Enable the community repository
setup-apkrepos -c
# Install
apk add g++ git cmake make mesa-dev qt6-qtbase-dev qt6-qtbase-private-dev libquazip1-qt6 ffmpeg-dev qt6-charts-dev libusb-dev libtool boost-dev sdl3-dev zstd-dev vulkan-utility-libraries spirv-tools-dev openssl-dev nlohmann-json lz4-dev jq patch
apk add g++ git cmake make mesa-dev qt6-qtbase-dev qt6-qtbase-private-dev libquazip1-qt6 ffmpeg-dev qt6-charts-dev libusb-dev libtool boost-dev sdl3-dev zstd-dev vulkan-utility-libraries spirv-tools-dev openssl-dev nlohmann-json lz4-dev opus-dev jq patch
```
</details>
@@ -260,7 +261,7 @@ brew install molten-vk
As root run:
```sh
pkg install devel/cmake devel/sdl3 devel/boost-libs devel/catch2 devel/libfmt devel/nlohmann-json devel/ninja devel/nasm devel/autoconf devel/pkgconf devel/qt6-base x11-toolkits/qt6-charts devel/simpleini net/enet multimedia/ffnvcodec-headers multimedia/ffmpeg archivers/liblz4 lang/gcc12 graphics/glslang graphics/vulkan-utility-libraries graphics/spirv-tools www/cpp-httplib graphics/vulkan-utility-libraries graphics/vulkan-headers graphics/spirv-headers quazip-qt6
pkg install devel/cmake devel/sdl3 devel/boost-libs devel/catch2 devel/libfmt devel/nlohmann-json devel/ninja devel/nasm devel/autoconf devel/pkgconf devel/qt6-base x11-toolkits/qt6-charts devel/simpleini net/enet multimedia/ffnvcodec-headers multimedia/ffmpeg audio/opus archivers/liblz4 lang/gcc12 graphics/glslang graphics/vulkan-utility-libraries graphics/spirv-tools www/cpp-httplib graphics/vulkan-utility-libraries graphics/vulkan-headers graphics/spirv-headers quazip-qt6
```
If using FreeBSD 12 or prior, use `devel/pkg-config` instead.
@@ -274,7 +275,7 @@ If using FreeBSD 12 or prior, use `devel/pkg-config` instead.
For NetBSD +10.1:
```sh
pkgin install git cmake boost fmtlib SDL3 catch2 libjwt spirv-headers spirv-tools ffmpeg7 libva nlohmann-json jq qt6-qtbase qt6-qtcharts qt6-qtmultimedia qt6-qttools cpp-httplib lz4 vulkan-headers nasm autoconf enet pkg-config libusb1 libcxx frozen
pkgin install git cmake boost fmtlib SDL3 catch2 libjwt spirv-headers spirv-tools ffmpeg7 libva nlohmann-json jq libopus qt6-qtbase qt6-qtcharts qt6-qtmultimedia qt6-qttools cpp-httplib lz4 vulkan-headers nasm autoconf enet pkg-config libusb1 libcxx frozen
```
[Caveats](./Caveats.md#netbsd).
@@ -305,7 +306,7 @@ pkg install gcc14 git cmake unzip nasm autoconf bash pkgconf ffmpeg glslang gmak
<summary>OpenIndiana</summary>
```sh
sudo pkg install git cmake qt6 boost glslang libzip library/lz4 libusb-1 nlohmann-json openssl sdl3 zlib compress/zstd unzip pkg-config nasm autoconf mesa library/libdrm header-drm developer/fmt
sudo pkg install git cmake qt6 boost glslang libzip library/lz4 libusb-1 nlohmann-json openssl opus sdl3 zlib compress/zstd unzip pkg-config nasm autoconf mesa library/libdrm header-drm developer/fmt
```
[Caveats](./Caveats.md#openindiana).
@@ -329,7 +330,7 @@ sudo pkgin install git cmake autoconf build-essential libusb-1 nasm gcc13
```sh
BASE="git make autoconf libtool automake-wrapper jq patch"
MINGW="qt6-base qt6-charts qt6-tools qt6-translations qt6-svg cmake toolchain clang python-pip openssl vulkan-memory-allocator vulkan-devel glslang boost fmt lz4 nlohmann-json zlib zstd enet libusb openssl SDL3"
MINGW="qt6-base qt6-charts qt6-tools qt6-translations qt6-svg cmake toolchain clang python-pip openssl vulkan-memory-allocator vulkan-devel glslang boost fmt lz4 nlohmann-json zlib zstd enet opus libusb openssl SDL3"
# Either x86_64 or clang-aarch64 (Windows on ARM)
packages="$BASE"
for pkg in $MINGW; do
@@ -355,7 +356,7 @@ pacman -Syuu --needed --noconfirm $packages
<summary>HaikuOS</summary>
```sh
pkgman install git cmake patch libfmt_devel nlohmann_json lz4_devel boost1.90_devel vulkan_devel qt6_base_devel qt6_declarative_devel libsdl3_devel ffmpeg7_devel libx11_devel enet_devel catch2_devel quazip1_qt5_devel qt6_5compat_devel glslang qt6_devel qt6_charts_devel cubeb_devel simpleini quazip_qt6_devel
pkgman install git cmake patch libfmt_devel nlohmann_json lz4_devel opus_devel boost1.90_devel vulkan_devel qt6_base_devel qt6_declarative_devel libsdl3_devel ffmpeg7_devel libx11_devel enet_devel catch2_devel quazip1_qt5_devel qt6_5compat_devel glslang qt6_devel qt6_charts_devel cubeb_devel simpleini quazip_qt6_devel
```
[Caveats](./Caveats.md#haikuos).
+33
View File
@@ -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)
+1 -1
View File
@@ -12,7 +12,7 @@ pkgs.mkShellNoCC {
git cmake clang gnumake patch jq pkg-config
# libraries
openssl boost fmt nlohmann_json lz4 zlib zstd
enet vulkan-headers vulkan-utility-libraries
enet libopus vulkan-headers vulkan-utility-libraries
spirv-tools spirv-headers vulkan-loader unzip
glslang python3 httplib cpp-jwt ffmpeg-headless
libusb1 cubeb
@@ -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),
@@ -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")
@@ -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,
@@ -79,6 +79,13 @@ class LicensesFragment : Fragment() {
R.string.license_ffmpeg_copyright,
R.string.license_ffmpeg_text
),
License(
R.string.license_opus,
R.string.license_opus_description,
R.string.license_opus_link,
R.string.license_opus_copyright,
R.string.license_opus_text
),
License(
R.string.license_sirit,
R.string.license_sirit_description,
@@ -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>
@@ -1762,6 +1774,51 @@ RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
</string>
<string name="license_opus" translatable="false">Opus</string>
<string name="license_opus_description" translatable="false">Modern audio compression for the internet</string>
<string name="license_opus_link" translatable="false">https://github.com/xiph/opus</string>
<string name="license_opus_copyright" translatable="false">Copyright 20012011 Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo</string>
<string name="license_opus_text" translatable="false">
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:\n\n
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.\n\n
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.\n\n
- Neither the name of Internet Society, IETF or IETF Trust, nor the
names of specific contributors, may be used to endorse or promote
products derived from this software without specific prior written
permission.\n\n
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS\'\' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n
Opus is subject to the royalty-free patent licenses which are
specified at:\n\n
Xiph.Org Foundation:
https://datatracker.ietf.org/ipr/1524/ \n\n
Microsoft Corporation:
https://datatracker.ietf.org/ipr/1914/ \n\n
Broadcom Corporation:
https://datatracker.ietf.org/ipr/1526/
</string>
<string name="license_sirit" translatable="false">Sirit</string>
<string name="license_sirit_description" translatable="false">A runtime SPIR-V assembler</string>
+2 -8
View File
@@ -226,14 +226,8 @@ else()
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-sign-conversion>)
endif()
if (YUZU_USE_EXTERNAL_FFMPEG)
add_dependencies(audio_core ffmpeg-build)
endif()
target_include_directories(audio_core PUBLIC ${FFmpeg_INCLUDE_DIR})
target_link_libraries(audio_core PRIVATE ${FFmpeg_LIBRARIES})
target_link_options(audio_core PRIVATE ${FFmpeg_LDFLAGS})
target_link_libraries(audio_core PUBLIC common core)
target_include_directories(audio_core PRIVATE ${OPUS_INCLUDE_DIRS})
target_link_libraries(audio_core PUBLIC common core Opus::opus)
if (ENABLE_CUBEB)
target_sources(audio_core PRIVATE
@@ -1,72 +1,107 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavcodec/codec.h>
#include <libavcodec/packet.h>
#include <libavutil/channel_layout.h>
#include <libavutil/frame.h>
}
#include "audio_core/adsp/apps/opus/opus_decode_object.h"
#include "core/hle/service/audio/errors.h"
#include "common/assert.h"
namespace AudioCore::ADSP::OpusDecoder {
namespace {
bool IsValidChannelCount(u32 channel_count) {
return channel_count == 1 || channel_count == 2;
}
} // namespace
u32 OpusDecodeObject::GetWorkBufferSize(u32 channel_count) {
if (channel_count == 1 || channel_count == 2)
return u32(sizeof(OpusDecodeObject)) + 16 * channel_count;
return 0;
}
Result OpusDecodeObject::InitializeDecoder(u32 sample_rate, u32 channel_count) {
if ((codec = avcodec_find_decoder(AV_CODEC_ID_OPUS)) == nullptr)
return Service::Audio::ResultLibOpusInvalidState;
if ((avctx = avcodec_alloc_context3(codec)) == nullptr)
return Service::Audio::ResultLibOpusInvalidState;
avctx->sample_rate = sample_rate;
av_channel_layout_default(&avctx->ch_layout, channel_count);
return ResultSuccess;
}
Result OpusDecodeObject::Shutdown() {
if (avctx) {
avcodec_free_context(&avctx);
avctx = nullptr;
if (!IsValidChannelCount(channel_count)) {
return 0;
}
return ResultSuccess;
return static_cast<u32>(sizeof(OpusDecodeObject)) + opus_decoder_get_size(channel_count);
}
Result OpusDecodeObject::ResetDecoder() {
if (avctx) {
avcodec_flush_buffers(avctx);
return ResultSuccess;
OpusDecodeObject& OpusDecodeObject::Initialize(u64 buffer, u64 buffer2) {
auto* new_decoder = reinterpret_cast<OpusDecodeObject*>(buffer);
auto* comparison = reinterpret_cast<OpusDecodeObject*>(buffer2);
if (new_decoder->magic == DecodeObjectMagic) {
if (!new_decoder->initialized ||
(new_decoder->initialized && new_decoder->self == comparison)) {
new_decoder->state_valid = true;
}
} else {
new_decoder->initialized = false;
new_decoder->state_valid = true;
}
return Service::Audio::ResultLibOpusInvalidState;
return *new_decoder;
}
Result OpusDecodeObject::Decode(u32& out_sample_count, u64 output_data, u64 output_data_size, u64 input_data, u64 input_data_size) {
s32 OpusDecodeObject::InitializeDecoder(u32 sample_rate, u32 channel_count) {
if (!state_valid) {
return OPUS_INVALID_STATE;
}
if (initialized) {
return OPUS_OK;
}
// Unfortunately libopus does not expose the OpusDecoder struct publicly, so we can't include
// it in this class. Nintendo does not allocate memory, which is why we have a workbuffer
// provided.
// We could use _create and have libopus allocate it for us, but then we have to separately
// track which decoder is being used between this and multistream in order to call the correct
// destroy from the host side.
// This is a bit cringe, but is safe as these objects are only ever initialized inside the given
// workbuffer, and GetWorkBufferSize will guarantee there's enough space to follow.
decoder = (LibOpusDecoder*)(this + 1);
s32 ret = opus_decoder_init(decoder, sample_rate, channel_count);
if (ret == OPUS_OK) {
magic = DecodeObjectMagic;
initialized = true;
state_valid = true;
self = this;
final_range = 0;
}
return ret;
}
s32 OpusDecodeObject::Shutdown() {
if (!state_valid) {
return OPUS_INVALID_STATE;
}
if (initialized) {
magic = 0x0;
initialized = false;
state_valid = false;
self = nullptr;
final_range = 0;
decoder = nullptr;
}
return OPUS_OK;
}
s32 OpusDecodeObject::ResetDecoder() {
return opus_decoder_ctl(decoder, OPUS_RESET_STATE);
}
s32 OpusDecodeObject::Decode(u32& out_sample_count, u64 output_data, u64 output_data_size,
u64 input_data, u64 input_data_size) {
ASSERT(initialized);
out_sample_count = 0;
if (avctx) {
AVPacket* avpkt = av_packet_alloc();
av_new_packet(avpkt, int(input_data_size));
std::memcpy(avpkt->data, reinterpret_cast<const u8*>(input_data), input_data_size);
avcodec_send_packet(avctx, avpkt);
AVFrame* frame = av_frame_alloc();
avcodec_receive_frame(avctx, frame);
std::memcpy(reinterpret_cast<u16*>(output_data), frame->data, output_data_size);
av_frame_free(&frame);
av_packet_free(&avpkt);
out_sample_count = frame->nb_samples;
return ResultSuccess;
if (!state_valid) {
return OPUS_INVALID_STATE;
}
return Service::Audio::ResultLibOpusInvalidState;
auto ret_code_or_samples = opus_decode(
decoder, reinterpret_cast<const u8*>(input_data), static_cast<opus_int32>(input_data_size),
reinterpret_cast<opus_int16*>(output_data), static_cast<opus_int32>(output_data_size), 0);
if (ret_code_or_samples < OPUS_OK) {
return ret_code_or_samples;
}
out_sample_count = ret_code_or_samples;
return opus_decoder_ctl(decoder, OPUS_GET_FINAL_RANGE_REQUEST, &final_range);
}
} // namespace AudioCore::ADSP::OpusDecoder
@@ -1,30 +1,38 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
#include "core/hle/result.h"
#include <opus.h>
struct AVCodec;
struct AVCodecContext;
#include "common/common_types.h"
namespace AudioCore::ADSP::OpusDecoder {
using LibOpusDecoder = ::OpusDecoder;
static constexpr u32 DecodeObjectMagic = 0xDEADBEEF;
class OpusDecodeObject {
public:
static u32 GetWorkBufferSize(u32 channel_count);
static OpusDecodeObject& Initialize(u64 buffer, u64 buffer2);
Result InitializeDecoder(u32 sample_rate, u32 channel_count);
Result Shutdown();
Result ResetDecoder();
Result Decode(u32& out_sample_count, u64 output_data, u64 output_data_size, u64 input_data, u64 input_data_size);
s32 InitializeDecoder(u32 sample_rate, u32 channel_count);
s32 Shutdown();
s32 ResetDecoder();
s32 Decode(u32& out_sample_count, u64 output_data, u64 output_data_size, u64 input_data,
u64 input_data_size);
u32 GetFinalRange() const noexcept {
return final_range;
}
AVCodec const* codec = nullptr;
AVCodecContext* avctx = nullptr;
private:
u32 magic;
bool initialized;
bool state_valid;
OpusDecodeObject* self;
u32 final_range;
LibOpusDecoder* decoder;
};
static_assert(std::is_trivially_constructible_v<OpusDecodeObject>);
} // namespace AudioCore::ADSP::OpusDecoder
+196 -193
View File
@@ -5,16 +5,17 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <chrono>
#include "audio_core/adsp/apps/opus/opus_decode_object.h"
#include "audio_core/adsp/apps/opus/opus_multistream_decode_object.h"
#include "audio_core/adsp/apps/opus/shared_memory.h"
#include "audio_core/audio_core.h"
#include "audio_core/common/common.h"
#include "common/logging.h"
#include "common/thread.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/service/audio/errors.h"
namespace AudioCore::ADSP::OpusDecoder {
@@ -48,16 +49,11 @@ OpusDecoder::~OpusDecoder() {
// Shutdown the thread
Send(Direction::DSP, Message::Shutdown);
auto msg = Receive(Direction::Host);
ASSERT_MSG(msg == Message::ShutdownOK, "Expected Opus shutdown code {}, got {}", Message::ShutdownOK, msg);
ASSERT_MSG(msg == Message::ShutdownOK, "Expected Opus shutdown code {}, got {}",
Message::ShutdownOK, msg);
main_thread.request_stop();
main_thread.join();
running = false;
// Must shutdown as there are AV allocations which are manual
for (auto& e : decode_objects)
e.second.Shutdown();
for (auto& e : ms_decode_objects)
e.second.Shutdown();
}
void OpusDecoder::Send(Direction dir, u32 message) {
@@ -72,195 +68,202 @@ void OpusDecoder::Init(std::stop_token stop_token) {
Common::SetCurrentThreadName("DSP_OpusDecoder_Init");
if (Receive(Direction::DSP, stop_token) != Message::Start) {
LOG_ERROR(Service_Audio, "DSP OpusDecoder failed to receive Start message. Opus initialization failed.");
LOG_ERROR(Service_Audio,
"DSP OpusDecoder failed to receive Start message. Opus initialization failed.");
return;
}
// Main OpusDecoder thread, responsible for processing the incoming Opus packets.
main_thread = std::jthread([this](std::stop_token thread_stop_token) {
Common::SetCurrentThreadName("DSP_OpusDecoder_Main");
while (!thread_stop_token.stop_requested()) {
auto msg = Receive(Direction::DSP, thread_stop_token);
switch (msg) {
case Shutdown:
Send(Direction::Host, Message::ShutdownOK);
return;
case GetWorkBufferSize: {
auto channel_count = s32(shared_memory->host_send_data[0]);
ASSERT(IsValidChannelCount(channel_count));
shared_memory->dsp_return_data[0] = OpusDecodeObject::GetWorkBufferSize(channel_count);
Send(Direction::Host, Message::GetWorkBufferSizeOK);
break;
}
case InitializeDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
auto buffer_size = shared_memory->host_send_data[1];
auto sample_rate = s32(shared_memory->host_send_data[2]);
auto channel_count = s32(shared_memory->host_send_data[3]);
ASSERT(sample_rate >= 0);
ASSERT(IsValidChannelCount(channel_count));
ASSERT(buffer_size >= OpusDecodeObject::GetWorkBufferSize(channel_count));
if (auto const it = decode_objects.find(buffer); it != decode_objects.end()) {
it->second.Shutdown();
shared_memory->dsp_return_data[0] = it->second.InitializeDecoder(sample_rate, channel_count).raw;
} else {
OpusDecodeObject obj{};
shared_memory->dsp_return_data[0] = obj.InitializeDecoder(sample_rate, channel_count).raw;
decode_objects.insert_or_assign(buffer, obj);
}
Send(Direction::Host, Message::InitializeDecodeObjectOK);
break;
}
case ShutdownDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
//[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
if (auto const it = decode_objects.find(buffer); it != decode_objects.end()) {
shared_memory->dsp_return_data[0] = it->second.Shutdown().raw;
} else {
LOG_ERROR(Audio_DSP, "operating unregistered buffer {}", buffer);
shared_memory->dsp_return_data[0] = Service::Audio::ResultLibOpusInvalidState.raw;
}
Send(Direction::Host, Message::ShutdownDecodeObjectOK);
break;
}
case DecodeInterleaved: {
auto start_time = system.CoreTiming().GetGlobalTimeUs();
auto buffer = shared_memory->host_send_data[0];
auto input_data = shared_memory->host_send_data[1];
auto input_data_size = shared_memory->host_send_data[2];
auto output_data = shared_memory->host_send_data[3];
auto output_data_size = shared_memory->host_send_data[4];
//auto final_range = static_cast<u32>(shared_memory->host_send_data[5]);
auto reset_requested = shared_memory->host_send_data[6];
u32 decoded_samples{0};
if (auto const it = decode_objects.find(buffer); it != decode_objects.end()) {
auto res = ResultSuccess;
if (reset_requested)
res = it->second.ResetDecoder();
if (res == ResultSuccess)
res = it->second.Decode(decoded_samples, output_data, output_data_size, input_data, input_data_size);
auto end_time = system.CoreTiming().GetGlobalTimeUs();
shared_memory->dsp_return_data[0] = res.raw;
shared_memory->dsp_return_data[1] = decoded_samples;
shared_memory->dsp_return_data[2] = (end_time - start_time).count();
} else {
LOG_ERROR(Audio_DSP, "operating unregistered buffer {}", buffer);
shared_memory->dsp_return_data[0] = Service::Audio::ResultLibOpusInvalidState.raw;
}
Send(Direction::Host, Message::DecodeInterleavedOK);
break;
}
case MapMemory: {
[[maybe_unused]] auto buffer = shared_memory->host_send_data[0];
[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
Send(Direction::Host, Message::MapMemoryOK);
break;
}
case UnmapMemory: {
[[maybe_unused]] auto buffer = shared_memory->host_send_data[0];
[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
Send(Direction::Host, Message::UnmapMemoryOK);
break;
}
case GetWorkBufferSizeForMultiStream: {
auto total_stream_count = s32(shared_memory->host_send_data[0]);
auto stereo_stream_count = s32(shared_memory->host_send_data[1]);
ASSERT(IsValidMultiStreamStreamCounts(total_stream_count, stereo_stream_count));
shared_memory->dsp_return_data[0] = OpusMultiStreamDecodeObject::GetWorkBufferSize(
total_stream_count, stereo_stream_count);
Send(Direction::Host, Message::GetWorkBufferSizeForMultiStreamOK);
break;
}
case InitializeMultiStreamDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
auto buffer_size = shared_memory->host_send_data[1];
auto sample_rate = s32(shared_memory->host_send_data[2]);
auto channel_count = s32(shared_memory->host_send_data[3]);
auto total_stream_count = s32(shared_memory->host_send_data[4]);
auto stereo_stream_count = s32(shared_memory->host_send_data[5]);
// Nintendo seem to have a bug here, they try to use &host_send_data[6] for the channel
// mappings, but [6] is never set, and there is not enough room in the argument data for
// more than 40 channels, when 255 are possible.
// It also means the mapping values are undefined, though likely always 0,
// and the mappings given by the game are ignored. The mappings are copied to this
// dedicated buffer host side, so let's do as intended.
auto mappings = shared_memory->channel_mapping.data();
ASSERT(IsValidMultiStreamStreamCounts(total_stream_count, stereo_stream_count));
ASSERT(sample_rate >= 0);
ASSERT(buffer_size >= OpusMultiStreamDecodeObject::GetWorkBufferSize(total_stream_count, stereo_stream_count));
if (auto const it = ms_decode_objects.find(buffer); it != ms_decode_objects.end()) {
it->second.Shutdown();
shared_memory->dsp_return_data[0] = it->second.InitializeDecoder(sample_rate, total_stream_count, channel_count, stereo_stream_count, mappings).raw;
} else {
OpusMultiStreamDecodeObject obj{};
shared_memory->dsp_return_data[0] = obj.InitializeDecoder(sample_rate, total_stream_count, channel_count, stereo_stream_count, mappings).raw;
ms_decode_objects.insert_or_assign(buffer, obj);
}
Send(Direction::Host, Message::InitializeMultiStreamDecodeObjectOK);
break;
}
case ShutdownMultiStreamDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
//[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
if (auto const it = ms_decode_objects.find(buffer); it != ms_decode_objects.end()) {
shared_memory->dsp_return_data[0] = it->second.Shutdown().raw;
} else {
LOG_ERROR(Audio_DSP, "operating unregistered buffer {}", buffer);
shared_memory->dsp_return_data[0] = Service::Audio::ResultLibOpusInvalidState.raw;
}
Send(Direction::Host, Message::ShutdownMultiStreamDecodeObjectOK);
break;
}
case DecodeInterleavedForMultiStream: {
auto start_time = system.CoreTiming().GetGlobalTimeUs();
auto buffer = shared_memory->host_send_data[0];
auto input_data = shared_memory->host_send_data[1];
auto input_data_size = shared_memory->host_send_data[2];
auto output_data = shared_memory->host_send_data[3];
auto output_data_size = shared_memory->host_send_data[4];
//auto final_range = static_cast<u32>(shared_memory->host_send_data[5]);
auto reset_requested = shared_memory->host_send_data[6];
u32 decoded_samples{0};
if (auto const it = ms_decode_objects.find(buffer); it != ms_decode_objects.end()) {
auto res = ResultSuccess;
if (reset_requested)
res = it->second.ResetDecoder();
if (res == ResultSuccess)
res = it->second.Decode(decoded_samples, output_data, output_data_size, input_data, input_data_size);
auto end_time = system.CoreTiming().GetGlobalTimeUs();
shared_memory->dsp_return_data[0] = res.raw;
shared_memory->dsp_return_data[1] = decoded_samples;
shared_memory->dsp_return_data[2] = (end_time - start_time).count();
} else {
LOG_ERROR(Audio_DSP, "operating unregistered buffer {}", buffer);
shared_memory->dsp_return_data[0] = Service::Audio::ResultLibOpusInvalidState.raw;
}
Send(Direction::Host, Message::DecodeInterleavedForMultiStreamOK);
break;
}
default:
LOG_ERROR(Audio_DSP, "Invalid OpusDecoder command {}", msg);
continue;
}
}
});
main_thread = std::jthread([this](std::stop_token st) { Main(st); });
running = true;
Send(Direction::Host, Message::StartOK);
}
void OpusDecoder::Main(std::stop_token stop_token) {
Common::SetCurrentThreadName("DSP_OpusDecoder_Main");
while (!stop_token.stop_requested()) {
auto msg = Receive(Direction::DSP, stop_token);
switch (msg) {
case Shutdown:
Send(Direction::Host, Message::ShutdownOK);
return;
case GetWorkBufferSize: {
auto channel_count = static_cast<s32>(shared_memory->host_send_data[0]);
ASSERT(IsValidChannelCount(channel_count));
shared_memory->dsp_return_data[0] = OpusDecodeObject::GetWorkBufferSize(channel_count);
Send(Direction::Host, Message::GetWorkBufferSizeOK);
} break;
case InitializeDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
auto buffer_size = shared_memory->host_send_data[1];
auto sample_rate = static_cast<s32>(shared_memory->host_send_data[2]);
auto channel_count = static_cast<s32>(shared_memory->host_send_data[3]);
ASSERT(sample_rate >= 0);
ASSERT(IsValidChannelCount(channel_count));
ASSERT(buffer_size >= OpusDecodeObject::GetWorkBufferSize(channel_count));
auto& decoder_object = OpusDecodeObject::Initialize(buffer, buffer);
shared_memory->dsp_return_data[0] =
decoder_object.InitializeDecoder(sample_rate, channel_count);
Send(Direction::Host, Message::InitializeDecodeObjectOK);
} break;
case ShutdownDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
auto& decoder_object = OpusDecodeObject::Initialize(buffer, buffer);
shared_memory->dsp_return_data[0] = decoder_object.Shutdown();
Send(Direction::Host, Message::ShutdownDecodeObjectOK);
} break;
case DecodeInterleaved: {
auto start_time = system.CoreTiming().GetGlobalTimeUs();
auto buffer = shared_memory->host_send_data[0];
auto input_data = shared_memory->host_send_data[1];
auto input_data_size = shared_memory->host_send_data[2];
auto output_data = shared_memory->host_send_data[3];
auto output_data_size = shared_memory->host_send_data[4];
auto final_range = static_cast<u32>(shared_memory->host_send_data[5]);
auto reset_requested = shared_memory->host_send_data[6];
u32 decoded_samples{0};
auto& decoder_object = OpusDecodeObject::Initialize(buffer, buffer);
s32 error_code{OPUS_OK};
if (reset_requested) {
error_code = decoder_object.ResetDecoder();
}
if (error_code == OPUS_OK) {
error_code = decoder_object.Decode(decoded_samples, output_data, output_data_size,
input_data, input_data_size);
}
if (error_code == OPUS_OK) {
if (final_range && decoder_object.GetFinalRange() != final_range) {
error_code = OPUS_INVALID_PACKET;
}
}
auto end_time = system.CoreTiming().GetGlobalTimeUs();
shared_memory->dsp_return_data[0] = error_code;
shared_memory->dsp_return_data[1] = decoded_samples;
shared_memory->dsp_return_data[2] = (end_time - start_time).count();
Send(Direction::Host, Message::DecodeInterleavedOK);
} break;
case MapMemory: {
[[maybe_unused]] auto buffer = shared_memory->host_send_data[0];
[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
Send(Direction::Host, Message::MapMemoryOK);
} break;
case UnmapMemory: {
[[maybe_unused]] auto buffer = shared_memory->host_send_data[0];
[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
Send(Direction::Host, Message::UnmapMemoryOK);
} break;
case GetWorkBufferSizeForMultiStream: {
auto total_stream_count = static_cast<s32>(shared_memory->host_send_data[0]);
auto stereo_stream_count = static_cast<s32>(shared_memory->host_send_data[1]);
ASSERT(IsValidMultiStreamStreamCounts(total_stream_count, stereo_stream_count));
shared_memory->dsp_return_data[0] = OpusMultiStreamDecodeObject::GetWorkBufferSize(
total_stream_count, stereo_stream_count);
Send(Direction::Host, Message::GetWorkBufferSizeForMultiStreamOK);
} break;
case InitializeMultiStreamDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
auto buffer_size = shared_memory->host_send_data[1];
auto sample_rate = static_cast<s32>(shared_memory->host_send_data[2]);
auto channel_count = static_cast<s32>(shared_memory->host_send_data[3]);
auto total_stream_count = static_cast<s32>(shared_memory->host_send_data[4]);
auto stereo_stream_count = static_cast<s32>(shared_memory->host_send_data[5]);
// Nintendo seem to have a bug here, they try to use &host_send_data[6] for the channel
// mappings, but [6] is never set, and there is not enough room in the argument data for
// more than 40 channels, when 255 are possible.
// It also means the mapping values are undefined, though likely always 0,
// and the mappings given by the game are ignored. The mappings are copied to this
// dedicated buffer host side, so let's do as intended.
auto mappings = shared_memory->channel_mapping.data();
ASSERT(IsValidMultiStreamStreamCounts(total_stream_count, stereo_stream_count));
ASSERT(sample_rate >= 0);
ASSERT(buffer_size >= OpusMultiStreamDecodeObject::GetWorkBufferSize(
total_stream_count, stereo_stream_count));
auto& decoder_object = OpusMultiStreamDecodeObject::Initialize(buffer, buffer);
shared_memory->dsp_return_data[0] = decoder_object.InitializeDecoder(
sample_rate, total_stream_count, channel_count, stereo_stream_count, mappings);
Send(Direction::Host, Message::InitializeMultiStreamDecodeObjectOK);
} break;
case ShutdownMultiStreamDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
auto& decoder_object = OpusMultiStreamDecodeObject::Initialize(buffer, buffer);
shared_memory->dsp_return_data[0] = decoder_object.Shutdown();
Send(Direction::Host, Message::ShutdownMultiStreamDecodeObjectOK);
} break;
case DecodeInterleavedForMultiStream: {
auto start_time = system.CoreTiming().GetGlobalTimeUs();
auto buffer = shared_memory->host_send_data[0];
auto input_data = shared_memory->host_send_data[1];
auto input_data_size = shared_memory->host_send_data[2];
auto output_data = shared_memory->host_send_data[3];
auto output_data_size = shared_memory->host_send_data[4];
auto final_range = static_cast<u32>(shared_memory->host_send_data[5]);
auto reset_requested = shared_memory->host_send_data[6];
u32 decoded_samples{0};
auto& decoder_object = OpusMultiStreamDecodeObject::Initialize(buffer, buffer);
s32 error_code{OPUS_OK};
if (reset_requested) {
error_code = decoder_object.ResetDecoder();
}
if (error_code == OPUS_OK) {
error_code = decoder_object.Decode(decoded_samples, output_data, output_data_size,
input_data, input_data_size);
}
if (error_code == OPUS_OK) {
if (final_range && decoder_object.GetFinalRange() != final_range) {
error_code = OPUS_INVALID_PACKET;
}
}
auto end_time = system.CoreTiming().GetGlobalTimeUs();
shared_memory->dsp_return_data[0] = error_code;
shared_memory->dsp_return_data[1] = decoded_samples;
shared_memory->dsp_return_data[2] = (end_time - start_time).count();
Send(Direction::Host, Message::DecodeInterleavedForMultiStreamOK);
} break;
default:
LOG_ERROR(Service_Audio, "Invalid OpusDecoder command {}", msg);
continue;
}
}
}
} // namespace AudioCore::ADSP::OpusDecoder
+9 -13
View File
@@ -6,11 +6,9 @@
#pragma once
#include <memory>
#include <thread>
#include "common/container/unordered_map.h"
#include "audio_core/adsp/apps/opus/opus_decode_object.h"
#include "audio_core/adsp/apps/opus/opus_multistream_decode_object.h"
#include "audio_core/adsp/apps/opus/shared_memory.h"
#include "audio_core/adsp/mailbox.h"
#include "common/common_types.h"
@@ -70,30 +68,28 @@ public:
}
private:
/// @brief Initializing thread, launched at audio_core boot to avoid blocking the main emu boot thread.
/**
* Initializing thread, launched at audio_core boot to avoid blocking the main emu boot thread.
*/
void Init(std::stop_token stop_token);
/**
* Main OpusDecoder thread, responsible for processing the incoming Opus packets.
*/
void Main(std::stop_token stop_token);
/// Mailbox to communicate messages with the host, drives the main thread
Mailbox mailbox;
/// Core system
Core::System& system;
/// Structure shared with the host, input data set by the host before sending a mailbox message,
/// and the responses are written back by the OpusDecoder.
SharedMemory* shared_memory{};
/// Mailbox to communicate messages with the host, drives the main thread
Mailbox mailbox;
/// Init thread
std::jthread init_thread{};
/// Main thread
std::jthread main_thread{};
::Common::unordered_map<u64, OpusDecodeObject> decode_objects;
::Common::unordered_map<u64, OpusMultiStreamDecodeObject> ms_decode_objects;
/// The current state
bool running{};
/// Structure shared with the host, input data set by the host before sending a mailbox message,
/// and the responses are written back by the OpusDecoder.
SharedMemory* shared_memory{};
};
} // namespace AudioCore::ADSP::OpusDecoder
@@ -4,17 +4,8 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavcodec/codec.h>
#include <libavcodec/packet.h>
#include <libavutil/channel_layout.h>
#include <libavutil/frame.h>
}
#include "audio_core/adsp/apps/opus/opus_multistream_decode_object.h"
#include "core/hle/result.h"
#include "core/hle/service/audio/errors.h"
#include "common/assert.h"
namespace AudioCore::ADSP::OpusDecoder {
@@ -22,61 +13,101 @@ namespace {
constexpr u32 OpusStreamCountMax = 255;
bool IsValidStreamCounts(u32 total_stream_count, u32 stereo_stream_count) {
return total_stream_count > 0 && total_stream_count <= OpusStreamCountMax
&& s32(stereo_stream_count) >= 0 && stereo_stream_count <= total_stream_count;
return total_stream_count > 0 && total_stream_count <= OpusStreamCountMax &&
static_cast<s32>(stereo_stream_count) >= 0 &&
stereo_stream_count <= total_stream_count;
}
} // namespace
u32 OpusMultiStreamDecodeObject::GetWorkBufferSize(u32 total_stream_count, u32 stereo_stream_count) {
if (IsValidStreamCounts(total_stream_count, stereo_stream_count))
return u32(sizeof(OpusMultiStreamDecodeObject)) + 2556 * (total_stream_count * stereo_stream_count);
u32 OpusMultiStreamDecodeObject::GetWorkBufferSize(u32 total_stream_count,
u32 stereo_stream_count) {
if (IsValidStreamCounts(total_stream_count, stereo_stream_count)) {
return static_cast<u32>(sizeof(OpusMultiStreamDecodeObject)) +
opus_multistream_decoder_get_size(total_stream_count, stereo_stream_count);
}
return 0;
}
Result OpusMultiStreamDecodeObject::InitializeDecoder(u32 sample_rate, u32 total_stream_count, u32 channel_count, u32 stereo_stream_count, u8* mappings) {
if ((codec = avcodec_find_decoder(AV_CODEC_ID_OPUS)) == nullptr)
return Service::Audio::ResultLibOpusInvalidState;
if ((avctx = avcodec_alloc_context3(codec)) == nullptr)
return Service::Audio::ResultLibOpusInvalidState;
avctx->sample_rate = sample_rate;
av_channel_layout_default(&avctx->ch_layout, channel_count);
return ResultSuccess;
}
OpusMultiStreamDecodeObject& OpusMultiStreamDecodeObject::Initialize(u64 buffer, u64 buffer2) {
auto* new_decoder = reinterpret_cast<OpusMultiStreamDecodeObject*>(buffer);
auto* comparison = reinterpret_cast<OpusMultiStreamDecodeObject*>(buffer2);
Result OpusMultiStreamDecodeObject::Shutdown() {
if (avctx) {
avcodec_free_context(&avctx);
avctx = nullptr;
if (new_decoder->magic == DecodeMultiStreamObjectMagic) {
if (!new_decoder->initialized ||
(new_decoder->initialized && new_decoder->self == comparison)) {
new_decoder->state_valid = true;
}
} else {
new_decoder->initialized = false;
new_decoder->state_valid = true;
}
return ResultSuccess;
return *new_decoder;
}
Result OpusMultiStreamDecodeObject::ResetDecoder() {
if (avctx) {
avcodec_flush_buffers(avctx);
return ResultSuccess;
s32 OpusMultiStreamDecodeObject::InitializeDecoder(u32 sample_rate, u32 total_stream_count,
u32 channel_count, u32 stereo_stream_count,
u8* mappings) {
if (!state_valid) {
return OPUS_INVALID_STATE;
}
return Service::Audio::ResultLibOpusInvalidState;
if (initialized) {
return OPUS_OK;
}
// See OpusDecodeObject::InitializeDecoder for an explanation of this
decoder = (LibOpusMSDecoder*)(this + 1);
s32 ret = opus_multistream_decoder_init(decoder, sample_rate, channel_count, total_stream_count,
stereo_stream_count, mappings);
if (ret == OPUS_OK) {
magic = DecodeMultiStreamObjectMagic;
initialized = true;
state_valid = true;
self = this;
final_range = 0;
}
return ret;
}
Result OpusMultiStreamDecodeObject::Decode(u32& out_sample_count, u64 output_data, u64 output_data_size, u64 input_data, u64 input_data_size) {
s32 OpusMultiStreamDecodeObject::Shutdown() {
if (!state_valid) {
return OPUS_INVALID_STATE;
}
if (initialized) {
magic = 0x0;
initialized = false;
state_valid = false;
self = nullptr;
final_range = 0;
decoder = nullptr;
}
return OPUS_OK;
}
s32 OpusMultiStreamDecodeObject::ResetDecoder() {
return opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE);
}
s32 OpusMultiStreamDecodeObject::Decode(u32& out_sample_count, u64 output_data,
u64 output_data_size, u64 input_data, u64 input_data_size) {
ASSERT(initialized);
out_sample_count = 0;
if (avctx) {
AVPacket* avpkt = av_packet_alloc();
av_new_packet(avpkt, int(input_data_size));
std::memcpy(avpkt->data, reinterpret_cast<const u8*>(input_data), input_data_size);
avcodec_send_packet(avctx, avpkt);
AVFrame* frame = av_frame_alloc();
avcodec_receive_frame(avctx, frame);
std::memcpy(reinterpret_cast<u16*>(output_data), frame->data, output_data_size);
av_frame_free(&frame);
av_packet_free(&avpkt);
out_sample_count = frame->nb_samples;
return ResultSuccess;
if (!state_valid) {
return OPUS_INVALID_STATE;
}
return Service::Audio::ResultLibOpusInvalidState;
auto ret_code_or_samples = opus_multistream_decode(
decoder, reinterpret_cast<const u8*>(input_data), static_cast<opus_int32>(input_data_size),
reinterpret_cast<opus_int16*>(output_data), static_cast<opus_int32>(output_data_size), 0);
if (ret_code_or_samples < OPUS_OK) {
return ret_code_or_samples;
}
out_sample_count = ret_code_or_samples;
return opus_multistream_decoder_ctl(decoder, OPUS_GET_FINAL_RANGE_REQUEST, &final_range);
}
} // namespace AudioCore::ADSP::OpusDecoder
@@ -1,30 +1,39 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
#include "core/hle/result.h"
#include <opus_multistream.h>
struct AVCodec;
struct AVCodecContext;
#include "common/common_types.h"
namespace AudioCore::ADSP::OpusDecoder {
using LibOpusMSDecoder = ::OpusMSDecoder;
static constexpr u32 DecodeMultiStreamObjectMagic = 0xDEADBEEF;
class OpusMultiStreamDecodeObject {
public:
static u32 GetWorkBufferSize(u32 total_stream_count, u32 stereo_stream_count);
static OpusMultiStreamDecodeObject& Initialize(u64 buffer, u64 buffer2);
Result InitializeDecoder(u32 sample_rate, u32 total_stream_count, u32 channel_count, u32 stereo_stream_count, u8* mappings);
Result Shutdown();
Result ResetDecoder();
Result Decode(u32& out_sample_count, u64 output_data, u64 output_data_size, u64 input_data, u64 input_data_size);
s32 InitializeDecoder(u32 sample_rate, u32 total_stream_count, u32 channel_count,
u32 stereo_stream_count, u8* mappings);
s32 Shutdown();
s32 ResetDecoder();
s32 Decode(u32& out_sample_count, u64 output_data, u64 output_data_size, u64 input_data,
u64 input_data_size);
u32 GetFinalRange() const noexcept {
return final_range;
}
AVCodec const* codec = nullptr;
AVCodecContext* avctx = nullptr;
private:
u32 magic;
bool initialized;
bool state_valid;
OpusMultiStreamDecodeObject* self;
u32 final_range;
LibOpusMSDecoder* decoder;
};
static_assert(std::is_trivially_constructible_v<OpusMultiStreamDecodeObject>);
} // namespace AudioCore::ADSP::OpusDecoder
@@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "common/common_types.h"
namespace AudioCore::ADSP {
static constexpr u32 DECODE_OBJECT_MAGIC = 0xDEADBEEF;
struct LibOpusDecoder {
u32 magic;
bool initialized;
bool state_valid;
LibOpusDecoder* self;
u32 final_range;
void* decoder;
};
static_assert(sizeof(LibOpusDecoder) == 32);
static constexpr u32 DECODE_MULTISTREAM_OBJECT_MAGIC = 0xDEADBEEF;
struct LibOpusMultistreamDecoder {
u32 magic;
bool initialized;
bool state_valid;
LibOpusMultistreamDecoder* self;
u32 final_range;
void* decoder;
};
static_assert(sizeof(LibOpusMultistreamDecoder) == 32);
}
@@ -1,11 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_funcs.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::OpusDecoder {
+48 -23
View File
@@ -9,18 +9,39 @@
#include "audio_core/audio_core.h"
#include "audio_core/opus/hardware_opus.h"
#include "core/core.h"
#include "core/hle/result.h"
namespace AudioCore::OpusDecoder {
namespace {
using namespace Service::Audio;
static constexpr Result ResultCodeFromLibOpusErrorCode(u64 error_code) {
s32 error{static_cast<s32>(error_code)};
ASSERT(error <= OPUS_OK);
switch (error) {
case OPUS_ALLOC_FAIL:
R_THROW(ResultLibOpusAllocFail);
case OPUS_INVALID_STATE:
R_THROW(ResultLibOpusInvalidState);
case OPUS_UNIMPLEMENTED:
R_THROW(ResultLibOpusUnimplemented);
case OPUS_INVALID_PACKET:
R_THROW(ResultLibOpusInvalidPacket);
case OPUS_INTERNAL_ERROR:
R_THROW(ResultLibOpusInternalError);
case OPUS_BUFFER_TOO_SMALL:
R_THROW(ResultBufferTooSmall);
case OPUS_BAD_ARG:
R_THROW(ResultLibOpusBadArg);
case OPUS_OK:
R_RETURN(ResultSuccess);
}
UNREACHABLE();
}
} // namespace
HardwareOpus::HardwareOpus(Core::System& system_)
: system{system_}
, opus_decoder{system.AudioCore().ADSP().OpusDecoder()}
{
: system{system_}, opus_decoder{system.AudioCore().ADSP().OpusDecoder()} {
opus_decoder.SetSharedMemory(shared_memory);
}
@@ -71,7 +92,7 @@ Result HardwareOpus::InitializeDecodeObject(u32 sample_rate, u32 channel_count,
R_THROW(ResultInvalidOpusDSPReturnCode);
}
R_RETURN(Result(u32(shared_memory.dsp_return_data[0])));
R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
}
Result HardwareOpus::InitializeMultiStreamDecodeObject(u32 sample_rate, u32 channel_count,
@@ -99,7 +120,7 @@ Result HardwareOpus::InitializeMultiStreamDecodeObject(u32 sample_rate, u32 chan
R_THROW(ResultInvalidOpusDSPReturnCode);
}
R_RETURN(Result(u32(shared_memory.dsp_return_data[0])));
R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
}
Result HardwareOpus::ShutdownDecodeObject(void* buffer, u64 buffer_size) {
@@ -113,7 +134,7 @@ Result HardwareOpus::ShutdownDecodeObject(void* buffer, u64 buffer_size) {
"Expected Opus shutdown code {}, got {}",
ADSP::OpusDecoder::Message::ShutdownDecodeObjectOK, msg);
R_RETURN(Result(u32(shared_memory.dsp_return_data[0])));
R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
}
Result HardwareOpus::ShutdownMultiStreamDecodeObject(void* buffer, u64 buffer_size) {
@@ -128,7 +149,7 @@ Result HardwareOpus::ShutdownMultiStreamDecodeObject(void* buffer, u64 buffer_si
"Expected Opus shutdown code {}, got {}",
ADSP::OpusDecoder::Message::ShutdownMultiStreamDecodeObjectOK, msg);
R_RETURN(Result(u32(shared_memory.dsp_return_data[0])));
R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
}
Result HardwareOpus::DecodeInterleaved(u32& out_sample_count, void* output_data,
@@ -152,12 +173,12 @@ Result HardwareOpus::DecodeInterleaved(u32& out_sample_count, void* output_data,
R_THROW(ResultInvalidOpusDSPReturnCode);
}
auto error_code = s32(shared_memory.dsp_return_data[0]);
if (error_code == ResultSuccess.raw) {
out_sample_count = u32(shared_memory.dsp_return_data[1]);
auto error_code{static_cast<s32>(shared_memory.dsp_return_data[0])};
if (error_code == OPUS_OK) {
out_sample_count = static_cast<u32>(shared_memory.dsp_return_data[1]);
out_time_taken = 1000 * shared_memory.dsp_return_data[2];
}
R_RETURN(Result(u32(error_code)));
R_RETURN(ResultCodeFromLibOpusErrorCode(error_code));
}
Result HardwareOpus::DecodeInterleavedForMultiStream(u32& out_sample_count, void* output_data,
@@ -166,27 +187,29 @@ Result HardwareOpus::DecodeInterleavedForMultiStream(u32& out_sample_count, void
void* buffer, u64& out_time_taken,
bool reset) {
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = u64(buffer);
shared_memory.host_send_data[1] = u64(input_data);
shared_memory.host_send_data[0] = (u64)buffer;
shared_memory.host_send_data[1] = (u64)input_data;
shared_memory.host_send_data[2] = input_data_size;
shared_memory.host_send_data[3] = u64(output_data);
shared_memory.host_send_data[3] = (u64)output_data;
shared_memory.host_send_data[4] = output_data_size;
shared_memory.host_send_data[5] = 0;
shared_memory.host_send_data[6] = reset;
opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStream);
opus_decoder.Send(ADSP::Direction::DSP,
ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStream);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStreamOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}", ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStreamOK, msg);
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStreamOK, msg);
R_THROW(ResultInvalidOpusDSPReturnCode);
}
auto const error_code = shared_memory.dsp_return_data[0];
if (error_code == ResultSuccess.raw) {
auto error_code{static_cast<s32>(shared_memory.dsp_return_data[0])};
if (error_code == OPUS_OK) {
out_sample_count = static_cast<u32>(shared_memory.dsp_return_data[1]);
out_time_taken = 1000 * shared_memory.dsp_return_data[2];
}
R_RETURN(Result(u32(shared_memory.dsp_return_data[0])));
R_RETURN(ResultCodeFromLibOpusErrorCode(error_code));
}
Result HardwareOpus::MapMemory(void* buffer, u64 buffer_size) {
@@ -197,7 +220,8 @@ Result HardwareOpus::MapMemory(void* buffer, u64 buffer_size) {
opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::MapMemory);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::MapMemoryOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}", ADSP::OpusDecoder::Message::MapMemoryOK, msg);
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::MapMemoryOK, msg);
R_THROW(ResultInvalidOpusDSPReturnCode);
}
R_SUCCEED();
@@ -211,7 +235,8 @@ Result HardwareOpus::UnmapMemory(void* buffer, u64 buffer_size) {
opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::UnmapMemory);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::UnmapMemoryOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}", ADSP::OpusDecoder::Message::UnmapMemoryOK, msg);
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::UnmapMemoryOK, msg);
R_THROW(ResultInvalidOpusDSPReturnCode);
}
R_SUCCEED();
+2 -3
View File
@@ -1,12 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include <opus.h>
#include "audio_core/adsp/apps/opus/opus_decoder.h"
#include "audio_core/adsp/apps/opus/shared_memory.h"
#include "audio_core/adsp/mailbox.h"
+1
View File
@@ -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"
+1
View File
@@ -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);
+1
View File
@@ -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.
+8
View File
@@ -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};
+1
View File
@@ -1205,6 +1205,7 @@ else()
endif()
endif()
target_include_directories(core PRIVATE ${OPUS_INCLUDE_DIRS})
target_link_libraries(core PUBLIC common PRIVATE audio_core hid_core network video_core nx_tzdb tz)
if (BOOST_NO_HEADERS)
@@ -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 "
+17
View File
@@ -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
@@ -367,7 +382,9 @@ endif()
if (YUZU_USE_EXTERNAL_FFMPEG)
add_dependencies(video_core ffmpeg-build)
endif()
target_include_directories(video_core PUBLIC ${FFmpeg_INCLUDE_DIR})
target_link_libraries(video_core PRIVATE ${FFmpeg_LIBRARIES})
target_link_options(video_core PRIVATE ${FFmpeg_LDFLAGS})
+285
View File
@@ -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
+76
View File
@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include <atomic>
#include <map>
#include <mutex>
#include <span>
#include <string>
#include <string_view>
#include <vector>
#include "common/common_types.h"
namespace VideoCore {
struct FxChainEntry {
std::string file;
std::string technique;
std::map<std::string, std::array<f32, 4>> values;
};
struct FxChainSnapshot {
std::vector<FxChainEntry> entries;
u64 generation{};
};
std::vector<FxChainEntry> ParseFxChain(std::string_view value);
std::string SerializeFxChain(std::span<const FxChainEntry> entries);
class FxChain {
public:
static FxChain& Instance();
FxChainSnapshot Snapshot() const;
std::vector<FxChainEntry> Entries() const;
size_t Size() const;
void Append(std::string_view file, std::string_view technique);
void Replace(size_t index, std::string_view file, std::string_view technique);
void Remove(size_t index);
void Move(size_t index, int delta);
void Clear();
void SetValue(size_t index, std::string_view uniform, const std::array<f32, 4>& value);
std::array<f32, 4> GetValue(size_t index, std::string_view uniform) const;
bool HasValue(size_t index, std::string_view uniform) const;
void ResetValues(size_t index);
void LoadFromSettings();
void StoreToSettings() const;
void DropUnknownEntries();
private:
FxChain() = default;
mutable std::mutex mutex;
std::vector<FxChainEntry> entries;
std::atomic<u64> generation{1};
};
} // namespace VideoCore
@@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <cstring>
#include <memory>
#include <set>
#include "effect_codegen.hpp"
#include "effect_parser.hpp"
#include "effect_preprocessor.hpp"
#include "common/fs/fs.h"
#include "common/fs/fs_util.h"
#include "video_core/post_processing/fx_compile.h"
#include "video_core/post_processing/fx_effect.h"
namespace VideoCore {
FxCompileResult CompileFxEffect(const std::filesystem::path& path, u32 width, u32 height,
u32 color_bit_depth) {
FxCompileResult result;
if (!Common::FS::Exists(path)) {
result.error = "Effect file not found: " + Common::FS::PathToUTF8String(path);
return result;
}
reshadefx::preprocessor preprocessor;
preprocessor.add_macro_definition("__RESHADE__", "50000");
preprocessor.add_macro_definition("__RESHADE_PERFORMANCE_MODE__", "1");
preprocessor.add_macro_definition("__RENDERER__", "0x20000");
preprocessor.add_macro_definition("__VENDOR__", "0");
preprocessor.add_macro_definition("__DEVICE__", "0");
preprocessor.add_macro_definition("__APPLICATION__", "0");
preprocessor.add_macro_definition("BUFFER_WIDTH", std::to_string(width));
preprocessor.add_macro_definition("BUFFER_HEIGHT", std::to_string(height));
preprocessor.add_macro_definition("BUFFER_RCP_WIDTH", "(1.0 / BUFFER_WIDTH)");
preprocessor.add_macro_definition("BUFFER_RCP_HEIGHT", "(1.0 / BUFFER_HEIGHT)");
preprocessor.add_macro_definition("BUFFER_COLOR_DEPTH", std::to_string(color_bit_depth));
preprocessor.add_macro_definition("BUFFER_COLOR_BIT_DEPTH", std::to_string(color_bit_depth));
for (const auto& include : GetFxIncludePaths(path)) {
preprocessor.add_include_path(include);
}
if (!preprocessor.append_file(path)) {
result.error = preprocessor.errors();
if (result.error.empty()) {
result.error = "Failed to preprocess " + Common::FS::PathToUTF8String(path);
}
return result;
}
std::unique_ptr<reshadefx::codegen> backend(
reshadefx::create_codegen_spirv(true, false, false, false, false));
reshadefx::parser parser;
if (!parser.parse(preprocessor.output(), backend.get())) {
result.error = parser.errors();
if (result.error.empty()) {
result.error = "Failed to parse " + Common::FS::PathToUTF8String(path);
}
return result;
}
result.module = backend->module();
std::set<std::string> wanted;
for (const auto& technique : result.module.techniques) {
for (const auto& pass : technique.passes) {
if (!pass.vs_entry_point.empty()) {
wanted.insert(pass.vs_entry_point);
}
if (!pass.ps_entry_point.empty()) {
wanted.insert(pass.ps_entry_point);
}
}
}
for (const auto& name : wanted) {
std::string binary;
std::string assembly;
std::string errors;
if (!backend->assemble_code_for_entry_point(name, binary, assembly, errors)) {
result.error = "Failed to assemble entry point '" + name + "': " + errors;
return result;
}
if (binary.size() % sizeof(u32) != 0) {
result.error = "Entry point '" + name + "' produced a malformed SPIR-V module";
return result;
}
std::vector<u32> words(binary.size() / sizeof(u32));
std::memcpy(words.data(), binary.data(), binary.size());
result.entry_points.emplace(name, std::move(words));
}
if (result.entry_points.empty()) {
result.error = "Effect declares no usable entry points";
}
return result;
}
} // namespace VideoCore
@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <filesystem>
#include <map>
#include <string>
#include <vector>
#include "common/common_types.h"
#include "effect_module.hpp"
namespace VideoCore {
struct FxCompileResult {
reshadefx::effect_module module;
std::map<std::string, std::vector<u32>> entry_points;
std::string error;
bool Succeeded() const {
return error.empty() && !entry_points.empty();
}
};
FxCompileResult CompileFxEffect(const std::filesystem::path& path, u32 width, u32 height,
u32 color_bit_depth);
} // namespace VideoCore
@@ -0,0 +1,283 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include "common/fs/fs.h"
#include "common/fs/fs_util.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "video_core/post_processing/fx_compile.h"
#include "video_core/post_processing/fx_effect.h"
namespace VideoCore {
namespace {
constexpr u32 CATALOG_PROBE_WIDTH = 1280;
constexpr u32 CATALOG_PROBE_HEIGHT = 720;
constexpr u32 CATALOG_PROBE_DEPTH = 8;
std::vector<FxEffectDesc> catalog;
const reshadefx::annotation* FindAnnotation(const std::vector<reshadefx::annotation>& annotations,
std::string_view name) {
const auto it = std::find_if(annotations.begin(), annotations.end(),
[&](const reshadefx::annotation& a) { return a.name == name; });
if (it == annotations.end()) {
return nullptr;
}
return &*it;
}
std::string AnnotationString(const std::vector<reshadefx::annotation>& annotations,
std::string_view name) {
const reshadefx::annotation* a = FindAnnotation(annotations, name);
if (a == nullptr) {
return std::string();
}
return a->value.string_data;
}
bool AnnotationFloat(const std::vector<reshadefx::annotation>& annotations, std::string_view name,
f32& out) {
const reshadefx::annotation* a = FindAnnotation(annotations, name);
if (a == nullptr) {
return false;
}
if (a->type.is_floating_point()) {
out = a->value.as_float[0];
return true;
}
if (a->type.is_integral()) {
out = static_cast<f32>(a->value.as_int[0]);
return true;
}
return false;
}
FxUiType ParseUiType(std::string_view value) {
if (value == "slider") {
return FxUiType::Slider;
}
if (value == "drag") {
return FxUiType::Drag;
}
if (value == "combo") {
return FxUiType::Combo;
}
if (value == "radio") {
return FxUiType::Radio;
}
if (value == "check" || value == "checkbox") {
return FxUiType::CheckBox;
}
if (value == "color") {
return FxUiType::Color;
}
if (value == "input") {
return FxUiType::InputBox;
}
return FxUiType::Hidden;
}
std::vector<std::string> SplitItems(const std::string& items) {
std::vector<std::string> out;
std::string current;
for (const char c : items) {
if (c == '\0') {
out.push_back(current);
current.clear();
continue;
}
current += c;
}
if (!current.empty()) {
out.push_back(current);
}
return out;
}
FxUniformDesc DescribeUniform(const reshadefx::uniform& info) {
FxUniformDesc desc;
desc.name = info.name;
desc.components = std::min<u32>(info.type.components(), 4);
if (info.type.is_boolean()) {
desc.kind = FxUniformKind::Boolean;
} else if (info.type.is_integral()) {
desc.kind = FxUniformKind::Integer;
} else {
desc.kind = FxUniformKind::Floating;
}
desc.label = AnnotationString(info.annotations, "ui_label");
if (desc.label.empty()) {
desc.label = info.name;
}
desc.tooltip = AnnotationString(info.annotations, "ui_tooltip");
desc.category = AnnotationString(info.annotations, "ui_category");
desc.ui_type = ParseUiType(AnnotationString(info.annotations, "ui_type"));
desc.items = SplitItems(AnnotationString(info.annotations, "ui_items"));
if (desc.kind == FxUniformKind::Boolean) {
desc.ui_min = 0.0f;
desc.ui_max = 1.0f;
desc.ui_step = 1.0f;
} else if (desc.kind == FxUniformKind::Integer) {
desc.ui_min = 0.0f;
desc.ui_max = 100.0f;
desc.ui_step = 1.0f;
}
void(AnnotationFloat(info.annotations, "ui_min", desc.ui_min));
void(AnnotationFloat(info.annotations, "ui_max", desc.ui_max));
void(AnnotationFloat(info.annotations, "ui_step", desc.ui_step));
if (desc.ui_step <= 0.0f) {
desc.ui_step = 0.01f;
if (desc.kind != FxUniformKind::Floating) {
desc.ui_step = 1.0f;
}
}
if (desc.ui_max < desc.ui_min) {
std::swap(desc.ui_min, desc.ui_max);
}
if (info.has_initializer_value) {
for (u32 i = 0; i < desc.components; ++i) {
if (desc.kind == FxUniformKind::Floating) {
desc.default_value[i] = info.initializer_value.as_float[i];
} else {
desc.default_value[i] = static_cast<f32>(info.initializer_value.as_int[i]);
}
}
}
return desc;
}
FxEffectDesc DescribeEffect(const std::filesystem::path& path, const std::filesystem::path& root) {
FxEffectDesc desc;
desc.file = Common::FS::PathToUTF8String(std::filesystem::relative(path, root));
desc.name = Common::FS::PathToUTF8String(path.stem());
const auto compiled =
CompileFxEffect(path, CATALOG_PROBE_WIDTH, CATALOG_PROBE_HEIGHT, CATALOG_PROBE_DEPTH);
if (!compiled.Succeeded()) {
desc.error = compiled.error;
return desc;
}
for (const auto& technique : compiled.module.techniques) {
desc.techniques.push_back(technique.name);
}
for (const auto& uniform : compiled.module.uniforms) {
FxUniformDesc uniform_desc = DescribeUniform(uniform);
if (uniform_desc.ui_type == FxUiType::Hidden) {
continue;
}
desc.uniforms.push_back(std::move(uniform_desc));
}
return desc;
}
} // Anonymous namespace
std::filesystem::path GetFxRootDirectory() {
return Common::FS::GetEdenPath(Common::FS::EdenPath::PostShaderDir);
}
std::vector<std::filesystem::path> GetFxIncludePaths(const std::filesystem::path& effect_path) {
const auto root = GetFxRootDirectory();
std::vector<std::filesystem::path> paths;
paths.push_back(effect_path.parent_path());
paths.push_back(root);
paths.push_back(root / "Shaders");
const auto last = std::unique(paths.begin(), paths.end());
paths.erase(last, paths.end());
return paths;
}
std::filesystem::path ResolveFxTexturePath(const std::filesystem::path& effect_path,
std::string_view source) {
const auto root = GetFxRootDirectory();
const std::filesystem::path name{source};
const std::array candidates{
effect_path.parent_path() / name,
root / "Textures" / name,
root / name,
};
for (const auto& candidate : candidates) {
if (Common::FS::Exists(candidate)) {
return candidate;
}
}
return std::filesystem::path();
}
void ReloadFxCatalog() {
catalog.clear();
const auto root = GetFxRootDirectory();
if (!Common::FS::Exists(root)) {
void(Common::FS::CreateDirs(root));
return;
}
std::vector<std::filesystem::path> effect_files;
Common::FS::IterateDirEntriesRecursively(
root,
[&](const std::filesystem::directory_entry& entry) {
if (entry.path().extension() == ".fx") {
effect_files.push_back(entry.path());
}
return true;
},
Common::FS::DirEntryFilter::File);
std::sort(effect_files.begin(), effect_files.end());
for (const auto& file : effect_files) {
FxEffectDesc desc = DescribeEffect(file, root);
if (!desc.error.empty()) {
LOG_WARNING(Render, "Post-processing effect '{}' failed to compile:\n{}", desc.file,
desc.error);
}
catalog.push_back(std::move(desc));
}
const size_t usable = std::count_if(catalog.begin(), catalog.end(),
[](const FxEffectDesc& d) { return d.Valid(); });
LOG_INFO(Render, "Loaded {} post-processing effects ({} usable)", catalog.size(), usable);
}
const std::vector<FxEffectDesc>& GetFxCatalog() {
return catalog;
}
const FxEffectDesc* FindFxEffect(std::string_view file) {
const auto it = std::find_if(catalog.begin(), catalog.end(),
[&](const FxEffectDesc& d) { return d.file == file; });
if (it == catalog.end()) {
return nullptr;
}
return &*it;
}
const FxUniformDesc* FindFxUniform(const FxEffectDesc& effect, std::string_view name) {
const auto it = std::find_if(effect.uniforms.begin(), effect.uniforms.end(),
[&](const FxUniformDesc& u) { return u.name == name; });
if (it == effect.uniforms.end()) {
return nullptr;
}
return &*it;
}
} // namespace VideoCore
@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#include "common/common_types.h"
namespace VideoCore {
enum class FxUniformKind {
Boolean,
Integer,
Floating,
};
enum class FxUiType {
Hidden,
Slider,
Drag,
Combo,
Radio,
CheckBox,
Color,
InputBox,
};
struct FxUniformDesc {
std::string name;
std::string label;
std::string tooltip;
std::string category;
FxUniformKind kind{FxUniformKind::Floating};
u32 components{1};
FxUiType ui_type{FxUiType::Hidden};
f32 ui_min{0.0f};
f32 ui_max{1.0f};
f32 ui_step{0.01f};
std::vector<std::string> items;
std::array<f32, 4> default_value{};
};
struct FxEffectDesc {
std::string file;
std::string name;
std::vector<std::string> techniques;
std::vector<FxUniformDesc> uniforms;
std::string error;
bool Valid() const {
return error.empty() && !techniques.empty();
}
};
std::filesystem::path GetFxRootDirectory();
std::vector<std::filesystem::path> GetFxIncludePaths(const std::filesystem::path& effect_path);
std::filesystem::path ResolveFxTexturePath(const std::filesystem::path& effect_path,
std::string_view source);
void ReloadFxCatalog();
const std::vector<FxEffectDesc>& GetFxCatalog();
const FxEffectDesc* FindFxEffect(std::string_view file);
const FxUniformDesc* FindFxUniform(const FxEffectDesc& effect, std::string_view name);
} // namespace VideoCore
@@ -18,6 +18,10 @@
#include "video_core/renderer_vulkan/present/sgsr.h"
#include "video_core/renderer_vulkan/present/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());
+6
View File
@@ -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{};
};
+6
View File
@@ -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 &amp;Performance Overlay</string>
</property>
</action>
<action name="action_Post_Processing_Shaders">
<property name="text">
<string>Post-Processing &amp;Shaders...</string>
</property>
</action>
<action name="action_Carousel_View">
<property name="checkable">
<bool>true</bool>
+24
View File
@@ -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);
+9
View File
@@ -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;