diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d6f2036ee9..223bd4b9f1 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -21,22 +21,38 @@ powershell.exe -Command "./BuildRelease.bat [PRESET_NAME]" **Available Presets** (from CMakePresets.json): -- `ALL` (default) - Builds for SE/AE/VR in single binary -- `SE` - Skyrim Special Edition only -- `AE` - Anniversary Edition only -- `VR` - Skyrim VR only -- `ALL-TRACY` - Includes Tracy profiler support -- `ALL-WITH-AUTO-DEPLOYMENT` - Auto-deploys to configured Skyrim directories when template used. +- `ALL` (default) - Builds universal binary supporting SE/AE/VR runtime detection +- `SE` - Skyrim Special Edition only (compile-time targeting) +- `AE` - Anniversary Edition only (compile-time targeting) +- `VR` - Skyrim VR only (compile-time targeting) +- `PRE-AE` - SE + VR (excludes AE) +- `FLATRIM` - SE + AE (excludes VR) +- `ALL-TRACY` - Universal binary with Tracy profiler support enabled + +**User Preset Template**: + +- `ALL-WITH-AUTO-DEPLOYMENT` - Extends `ALL` with `AUTO_PLUGIN_DEPLOYMENT=ON` (copy template to use) ### Development Setup 1. Copy `CMakeUserPresets.json.template` → `CMakeUserPresets.json` 2. Configure `CommunityShadersOutputDir` for auto-deployment to Skyrim installations -3. Set build options in user preset: - - `AUTO_PLUGIN_DEPLOYMENT`: Auto-copy to Skyrim dirs - - `AIO_ZIP_TO_DIST`: Creates all-in-one distribution package - - `ZIP_TO_DIST`: Creates individual feature packages - - `TRACY_SUPPORT`: Enables performance profiling +3. Set build options in user preset or CMake cache: + +**Build Options** (CMake cache variables): + +- `AUTO_PLUGIN_DEPLOYMENT` (default: OFF) - Auto-copy build output to `CommunityShadersOutputDir` +- `ZIP_TO_DIST` (default: ON) - Creates individual feature packages as 7z files in `/dist` +- `AIO_ZIP_TO_DIST` (default: ON) - Creates all-in-one distribution package as 7z in `/dist` +- `TRACY_SUPPORT` (default: OFF) - Enables Tracy profiler integration for performance analysis + +**Auto-Deployment Configuration**: + +Set `CommunityShadersOutputDir` environment variable to semicolon-separated Skyrim Data directories: + +``` +CommunityShadersOutputDir=F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/SkyrimVR/Data;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data +``` ### Shader Development and Testing @@ -73,8 +89,72 @@ hlslkit-generate-defines --log CommunityShaders.log hlslkit-buffer-scan --features-dir features/ ``` +### Custom CMake Targets + +**Package and Deployment Targets**: + +```bash +# Prepare AIO package structure (automatic with AIO_ZIP_TO_DIST or AUTO_PLUGIN_DEPLOYMENT) +cmake --build ./build/ALL --target PREPARE_AIO + +# Prepare shaders only (useful for CI shader validation) +cmake --build ./build/ALL --target prepare_shaders + +# Copy shaders to deployment directories (when AUTO_PLUGIN_DEPLOYMENT=ON) +cmake --build ./build/ALL --target COPY_SHADERS + +# Create AIO zip package (when AIO_ZIP_TO_DIST=ON) +cmake --build ./build/ALL --target AIO_ZIP_PACKAGE +``` + +**Development Targets**: + +```bash +# Format all C++ and HLSL code (requires clang-format) +cmake --build ./build/ALL --target FORMAT_CODE + +# Generate shader validation configs from game logs (requires PowerShell) +cmake --build ./build/ALL --target generate_shader_configs +``` + ## Architecture Overview +### Manual packaging targets (detailed) + +The project also provides a set of manual packaging targets that create distributable 7z packages or install the project into the AIO folder. These targets are useful when you want precise control over packaging (CI artifacts, local QA, or manual deployment). + +Quick commands: + +```bash +# Create the Core package (includes CORE features + plugin DLL) +cmake --build ./build/ALL --target Package-Core + +# Create a manual AIO package (.7z) via install + tar +cmake --build ./build/ALL --target Package-AIO-Manual + +# Create an individual feature package (name is sanitized from the feature folder) +cmake --build ./build/ALL --target Package- + +# Install into the AIO folder (installs to build//aio) +cmake --build ./build/ALL --target AIO + +# Alternatively use cmake --install to install to a custom prefix +cmake --install ./build/ALL --prefix # installs files according to CMake install() rules +``` + +Notes and behaviour: + +- `Package-Core` collects everything marked as CORE and the built plugin into a temporary folder, then tars it to `dist/${PROJECT_NAME}-${UTC_NOW}.7z`. +- `Package-` targets are generated per feature directory (non-CORE features). They create `${FEATURE}-${UTC_NOW}.7z` in `dist/`. +- `Package-AIO-Manual` performs an install to the AIO folder and then creates a single AIO archive. This is similar to the automated `AIO_ZIP_PACKAGE`, but wired as an explicit file-producing custom target (useful for CI reproducibility). +- `AIO` target runs `cmake --install` with the `aio` prefix so you can locally inspect the AIO folder layout without creating an archive. +- The install-based packaging uses the CMake `install()` rules defined near the top of `CMakeLists.txt` (the project installs `SKSE/Plugins`, copies `package/` and feature folders, and removes the Core placeholder). This makes manual installs and CI artifacts consistent with the runtime AIO layout. + +Where to look in `CMakeLists.txt`: + +- Manual packaging targets are defined in the "Manual packaging targets (Package-XXX)" section and create files under `${CMAKE_SOURCE_DIR}/dist`. +- The `install()` rules near the top of the file show what gets placed into the AIO layout when running `cmake --install`. + ### Plugin Architecture **Core Pattern**: Feature-driven modular system where each graphics enhancement is an independent `Feature` class that can be enabled/disabled at runtime. diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 2def78906d..0765e27928 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,26 +1,27 @@ # CodeRabbit AI Configuration -instructions: | - When reviewing PRs, please provide suggestions for: - - 1. **Conventional Commit Titles** (if not following https://www.conventionalcommits.org/ or - if the existing title does not describe the code changes): - Format: type(scope): description - Length: 50 characters limit for title, 72 for body - Style: lowercase description, no ending period - Examples: - - feat(vr): add cross-eye sampling - - fix(water): resolve flowmap bug - - docs: update shader documentation +reviews: + path_instructions: + - path: "**/*" + instructions: | + When reviewing PRs, please provide suggestions for: - 2. **Issue References** (if PR fixes bugs or implements features): - Suggest adding appropriate GitHub keywords: - - "Fixes #123" or "Closes #123" for bug fixes - - "Implements #123" or "Addresses #123" for features - - "Related to #123" for partial implementations + 1. **Conventional Commit Titles** (if not following https://www.conventionalcommits.org/ or + if the existing title does not describe the code changes): + Format: type(scope): description + Length: 50 characters limit for title, 72 for body + Style: lowercase description, no ending period + Examples: + - feat(vr): add cross-eye sampling + - fix(water): resolve flowmap bug + - docs: update shader documentation - Otherwise, use your standard review approach focusing on code quality. + 2. **Issue References** (if PR fixes bugs or implements features): + Suggest adding appropriate GitHub keywords: + - "Fixes #123" or "Closes #123" for bug fixes + - "Implements #123" or "Addresses #123" for features + - "Related to #123" for partial implementations -reviews: + Otherwise, use your standard review approach focusing on code quality. path_filters: - "**/*.hlsl" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c845498d4c..6116d995ba 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -36,6 +36,9 @@ on: - "vcpkg-configuration.json" - ".gitmodules" - "extern/**" + - "cmake/**" + - ".github/workflows/**" + - ".github/configs/**" permissions: contents: read @@ -52,7 +55,7 @@ jobs: if: ${{ github.event_name == 'pull_request_target' }} outputs: should-build: ${{ steps.changed-files.outputs.build_any_changed == 'true' || steps.changed-files.outputs.cpp_any_changed == 'true' || steps.changed-files.conclusion == 'failure' }} - hlsl-should-build: ${{ steps.changed-files.outputs.hlsl_any_changed == 'true' || steps.changed-files.conclusion == 'failure' }} + hlsl-should-build: ${{ steps.changed-files.outputs.hlsl_any_changed == 'true' || steps.changed-files.outputs.cmake_any_changed == 'true' || steps.changed-files.outputs.ci_any_changed == 'true' || steps.changed-files.conclusion == 'failure' }} steps: - uses: actions/checkout@v4 with: @@ -86,6 +89,13 @@ jobs: - 'vcpkg-configuration.json' - '.gitmodules' - 'extern/**' + cmake: + - 'CMakeLists.txt' + - 'CMakePresets.json' + - 'cmake/**' + ci: + - '.github/workflows/**' + - '.github/configs/**' hlsl: - '**.hlsl' - '**.hlsli' @@ -387,6 +397,37 @@ jobs: with: vcpkgJsonGlob: vcpkg.json + - name: Locate fxc.exe + if: steps.check-hlsl.outputs.skip != 'true' + id: find_fxc + shell: pwsh + run: | + # Try to find fxc.exe on PATH first + $fxcCmd = Get-Command -Name fxc.exe -ErrorAction SilentlyContinue + if ($fxcCmd) { + $fxcPath = $fxcCmd.Source + Write-Host "Found fxc.exe at $fxcPath" + Add-Content -Path $env:GITHUB_OUTPUT -Value "fxc_path=$fxcPath" + } else { + # Try known Windows SDK locations (x64) + $fxcPath = '' + $sdkRoot = 'C:\Program Files (x86)\Windows Kits\10\bin' + if (Test-Path $sdkRoot) { + $versions = Get-ChildItem -Path $sdkRoot -Directory | Sort-Object -Descending + foreach ($v in $versions) { + $candidate = Join-Path $v.FullName 'x64\fxc.exe' + if (Test-Path $candidate) { $fxcPath = $candidate; break } + } + } + if ($fxcPath -ne '') { + Write-Host "Found fxc.exe at $fxcPath" + Add-Content -Path $env:GITHUB_OUTPUT -Value "fxc_path=$fxcPath" + } else { + Write-Warning "fxc.exe not found in PATH or common SDK locations" + Add-Content -Path $env:GITHUB_OUTPUT -Value "fxc_path=" + } + } + - name: Cache CMake build output if: steps.check-hlsl.outputs.skip != 'true' uses: actions/cache@v4 @@ -437,7 +478,12 @@ jobs: - name: Validate shader compilation (${{ matrix.config.name }}) if: steps.check-hlsl.outputs.skip != 'true' - run: hlslkit-compile --shader-dir build/ALL/aio/Shaders --output-dir build/ShaderCache --config ${{ matrix.config.file }} --max-warnings 0 --suppress-warnings X1519 + run: | + if [ -z "${{ steps.find_fxc.outputs.fxc_path }}" ]; then + echo "fxc.exe not found - shader validation requires fxc.exe. Set --fxc to a valid path or ensure fxc.exe is in PATH." >&2 + exit 1 + fi + hlslkit-compile --fxc "${{ steps.find_fxc.outputs.fxc_path }}" --shader-dir build/ALL/aio/Shaders --output-dir build/ShaderCache --config ${{ matrix.config.file }} --max-warnings 0 --suppress-warnings X1519 shell: bash - name: Upload shader validation logs diff --git a/.gitmodules b/.gitmodules index cd68154717..d4fb422d95 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "extern/Streamline-DX12"] path = extern/Streamline-DX12 url = https://github.com/NVIDIAGameWorks/Streamline.git +[submodule "extern/FidelityFX-SDK"] + path = extern/FidelityFX-SDK + url = https://github.com/MapleHinata/FidelityFX-SDK diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 057b5960a8..a4f5a4fcf5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,6 +22,11 @@ repos: additional_dependencies: ["prettier@3.1.0"] files: \.(json|md|yml|yaml)$ exclude: ^(\.\/)?(extern|include|build|dist)\/ + - repo: https://github.com/BlankSpruce/gersemi + rev: 0.22.3 + hooks: + - id: gersemi + files: '(^CMakeLists.txt$|.*\\.cmake$)' ci: autofix_commit_msg: | style: 🎨 apply pre-commit.ci formatting diff --git a/AI-INSTRUCTIONS.md b/AI-INSTRUCTIONS.md index 9c531bd233..56b9ddfe66 100644 --- a/AI-INSTRUCTIONS.md +++ b/AI-INSTRUCTIONS.md @@ -26,6 +26,26 @@ SKSE plugin providing advanced DirectX 11 graphics modifications for Skyrim SE/A - **Shader Test**: `hlslkit-compile --shader-dir [target]` (install via pip first) - **Feature Access**: `globals::features::*` namespace +### Build Options + +**Runtime Presets**: `ALL` (universal), `SE`, `AE`, `VR`, `PRE-AE`, `FLATRIM`, `ALL-TRACY` + +**CMake Options** (set in user preset): + +- `AUTO_PLUGIN_DEPLOYMENT=ON` - Auto-copy to `CommunityShadersOutputDir` +- `ZIP_TO_DIST=ON` (default) - Create individual feature 7z packages +- `AIO_ZIP_TO_DIST=ON` (default) - Create all-in-one 7z package +- `TRACY_SUPPORT=ON` - Enable Tracy profiler integration + +### Custom CMake Targets + +**Quick targets** (common): + +- `PREPARE_AIO`, `prepare_shaders`, `COPY_SHADERS`, `AIO_ZIP_PACKAGE` +- `FORMAT_CODE`, `generate_shader_configs` + +For full details about manual packaging targets (Package-Core, Package-AIO-Manual, Package-, AIO) and example workflows, see the "Manual packaging targets (detailed)" section in `.claude/CLAUDE.md` to avoid duplication. + ### AI Assistant Role **Act as an experienced graphics programming and Skyrim modding expert.** diff --git a/CMakeLists.txt b/CMakeLists.txt index 407913e691..f5c253123c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,20 +1,50 @@ cmake_minimum_required(VERSION 3.21) +cmake_policy(SET CMP0116 NEW) +set(CMAKE_POLICY_WARNING_CMP0116 OFF) + +if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0.0") + message( + ERROR + "EASTL will fail to install with vcpkg using cmake 4.0+, remove this line if the port get fixed." + ) +endif() project( - CommunityShaders - VERSION 1.4.0 - LANGUAGES CXX + # gersemi: ignore + CommunityShaders + VERSION 1.4.6 + LANGUAGES CXX ) +# default install path +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set_property( + CACHE CMAKE_INSTALL_PREFIX + PROPERTY VALUE "${CMAKE_CURRENT_BINARY_DIR}/aio" + ) +endif() + list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") # ######################################################################################################################## # ## Build options # ######################################################################################################################## message("Options:") -option(AUTO_PLUGIN_DEPLOYMENT "Copy the build output and addons to env:CommunityShadersOutputDir." OFF) -option(ZIP_TO_DIST "Zip the base mod and addons to their own 7z file in dist." ON) -option(AIO_ZIP_TO_DIST "Zip the base mod and addons to a AIO 7z file in dist." ON) +option( + AUTO_PLUGIN_DEPLOYMENT + "Copy the build output and addons to env:CommunityShadersOutputDir." + OFF +) +option( + ZIP_TO_DIST + "Zip the base mod and addons to their own 7z file in dist." + ON +) +option( + AIO_ZIP_TO_DIST + "Zip the base mod and addons to a AIO 7z file in dist." + ON +) option(TRACY_SUPPORT "Enable support for tracy profiler" OFF) message("\tAuto plugin deployment: ${AUTO_PLUGIN_DEPLOYMENT}") message("\tZip to dist: ${ZIP_TO_DIST}") @@ -45,18 +75,18 @@ find_package(efsw CONFIG REQUIRED) find_package(Tracy CONFIG REQUIRED) find_package(directx-headers CONFIG REQUIRED) add_subdirectory(${CMAKE_SOURCE_DIR}/cmake/Streamline) -include(XeSS-SDK) find_path(DETOURS_INCLUDE_DIRS "detours/detours.h") find_library(DETOURS_LIBRARY detours REQUIRED) +include(FidelityFX-SDK) target_compile_definitions( - ${PROJECT_NAME} - PRIVATE - "$<$:TRACY_SUPPORT>" + ${PROJECT_NAME} + PRIVATE "$<$:TRACY_SUPPORT>" ) -file(GLOB FEATURE_SHADER_DIRS +file( + GLOB FEATURE_SHADER_DIRS RELATIVE "${CMAKE_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/features/*/Shaders" ) @@ -64,51 +94,54 @@ file(GLOB FEATURE_SHADER_DIRS foreach(_dir IN LISTS FEATURE_SHADER_DIRS) target_include_directories( ${PROJECT_NAME} - PRIVATE - "${CMAKE_SOURCE_DIR}/${_dir}" + PRIVATE "${CMAKE_SOURCE_DIR}/${_dir}" ) endforeach() target_include_directories( - ${PROJECT_NAME} - PRIVATE - ${BSHOSHANY_THREAD_POOL_INCLUDE_DIRS} - ${CLIB_UTIL_INCLUDE_DIRS} - "${CMAKE_SOURCE_DIR}/package/Shaders" - ${DETOURS_INCLUDE_DIRS} + ${PROJECT_NAME} + PRIVATE + ${BSHOSHANY_THREAD_POOL_INCLUDE_DIRS} + ${CLIB_UTIL_INCLUDE_DIRS} + "${CMAKE_SOURCE_DIR}/package/Shaders" + ${DETOURS_INCLUDE_DIRS} ) target_link_libraries( - ${PROJECT_NAME} - PRIVATE - Microsoft::CppWinRT - magic_enum::magic_enum - xbyak::xbyak - nlohmann_json::nlohmann_json - imgui::imgui - EASTL - Microsoft::DirectXTK - Microsoft::DirectXTex - pystring::pystring - unordered_dense::unordered_dense - efsw::efsw - Tracy::TracyClient - Streamline - d3d12.lib - Microsoft::DirectX-Headers - ${DETOURS_LIBRARY} + ${PROJECT_NAME} + PRIVATE + Microsoft::CppWinRT + magic_enum::magic_enum + xbyak::xbyak + nlohmann_json::nlohmann_json + imgui::imgui + EASTL + Microsoft::DirectXTK + Microsoft::DirectXTex + pystring::pystring + unordered_dense::unordered_dense + efsw::efsw + Tracy::TracyClient + Streamline + d3d12.lib + Microsoft::DirectX-Headers + ${DETOURS_LIBRARY} ) # https://gitlab.kitware.com/cmake/cmake/-/issues/24922#note_1371990 if(MSVC_VERSION GREATER_EQUAL 1936 AND MSVC_IDE) # 17.6+ - # When using /std:c++latest, "Build ISO C++23 Standard Library Modules" defaults to "Yes". - # Default to "No" instead. - # - # As of CMake 3.26.4, there isn't a way to control this property - # (https://gitlab.kitware.com/cmake/cmake/-/issues/24922), - # We'll use the MSBuild project system instead - # (https://learn.microsoft.com/en-us/cpp/build/reference/vcxproj-file-structure) - file(CONFIGURE OUTPUT "${CMAKE_BINARY_DIR}/Directory.Build.props" CONTENT [==[ + # When using /std:c++latest, "Build ISO C++23 Standard Library Modules" defaults to "Yes". + # Default to "No" instead. + # + # As of CMake 3.26.4, there isn't a way to control this property + # (https://gitlab.kitware.com/cmake/cmake/-/issues/24922), + # We'll use the MSBuild project system instead + # (https://learn.microsoft.com/en-us/cpp/build/reference/vcxproj-file-structure) + file( + CONFIGURE + OUTPUT "${CMAKE_BINARY_DIR}/Directory.Build.props" + CONTENT + [==[ @@ -116,51 +149,76 @@ if(MSVC_VERSION GREATER_EQUAL 1936 AND MSVC_IDE) # 17.6+ -]==] @ONLY) +]==] + @ONLY + ) endif() # ####################################################################################################################### # # Feature version detection # ####################################################################################################################### -file(GLOB_RECURSE FEATURE_CONFIG_FILES - LIST_DIRECTORIES false - CONFIGURE_DEPENDS - "features/*/Shaders/Features/*.ini" +file( + GLOB_RECURSE FEATURE_CONFIG_FILES + LIST_DIRECTORIES false + CONFIGURE_DEPENDS + "features/*/Shaders/Features/*.ini" ) foreach(FEATURE_PATH ${FEATURE_CONFIG_FILES}) - get_filename_component(FEATURE ${FEATURE_PATH} NAME_WE) - file(READ "${FEATURE_PATH}" CONFIG_VALUE) - string(STRIP "${CONFIG_VALUE}" CONFIG_VALUE) - if(CONFIG_VALUE) - string(REGEX MATCH "Version = ([0-9]+)-([0-9]+)-([0-9]+)" _ "${CONFIG_VALUE}") - if(DEFINED CMAKE_MATCH_1 AND DEFINED CMAKE_MATCH_2 AND DEFINED CMAKE_MATCH_3) - set(ver_major ${CMAKE_MATCH_1}) - set(ver_minor ${CMAKE_MATCH_2}) - set(ver_patch ${CMAKE_MATCH_3}) - list(APPEND FEATURE_VERSIONS "\t\t{\"${FEATURE}\"sv, {${ver_major},${ver_minor},${ver_patch}}}") - else() - message(WARNING "Feature config file '${FEATURE_PATH}' does not contain a valid version string. Skipping.") - endif() - else() - message(WARNING "Feature config file '${FEATURE_PATH}' is empty or contains only whitespace. Skipping version detection for this feature.") - endif() + get_filename_component(FEATURE ${FEATURE_PATH} NAME_WE) + file(READ "${FEATURE_PATH}" CONFIG_VALUE) + string(STRIP "${CONFIG_VALUE}" CONFIG_VALUE) + if(CONFIG_VALUE) + string( + REGEX MATCH + "Version = ([0-9]+)-([0-9]+)-([0-9]+)" + _ + "${CONFIG_VALUE}" + ) + if( + DEFINED CMAKE_MATCH_1 + AND DEFINED CMAKE_MATCH_2 + AND DEFINED CMAKE_MATCH_3 + ) + set(ver_major ${CMAKE_MATCH_1}) + set(ver_minor ${CMAKE_MATCH_2}) + set(ver_patch ${CMAKE_MATCH_3}) + list( + APPEND + FEATURE_VERSIONS + "\t\t{\"${FEATURE}\"sv, {${ver_major},${ver_minor},${ver_patch}}}" + ) + else() + message( + WARNING + "Feature config file '${FEATURE_PATH}' does not contain a valid version string. Skipping." + ) + endif() + else() + message( + WARNING + "Feature config file '${FEATURE_PATH}' is empty or contains only whitespace. Skipping version detection for this feature." + ) + endif() endforeach() -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${FEATURE_CONFIG_FILES}") +set_property( + DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS "${FEATURE_CONFIG_FILES}" +) string(REPLACE ";" ",\n" FEATURE_VERSIONS "${FEATURE_VERSIONS}") configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/cmake/FeatureVersions.h.in - ${CMAKE_CURRENT_BINARY_DIR}/cmake/FeatureVersions.h - @ONLY + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/FeatureVersions.h.in + ${CMAKE_CURRENT_BINARY_DIR}/cmake/FeatureVersions.h + @ONLY ) target_sources( - "${PROJECT_NAME}" - PRIVATE - ${CMAKE_CURRENT_BINARY_DIR}/cmake/FeatureVersions.h + "${PROJECT_NAME}" + PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/cmake/FeatureVersions.h ) # ####################################################################################################################### @@ -169,12 +227,34 @@ target_sources( find_program(CLANG_FORMAT_PATH clang-format) if(CLANG_FORMAT_PATH) - add_custom_target(FORMAT_CODE - COMMAND ${CLANG_FORMAT_PATH} -i -style=file ${CPP_SOURCES};${HLSL_FILES} - COMMENT "Running clang format for cpp and hlsl files" - ) + add_custom_target( + FORMAT_CODE + COMMAND ${CLANG_FORMAT_PATH} -i -style=file ${CPP_SOURCES};${HLSL_FILES} + COMMENT "Running clang format for cpp and hlsl files" + ) endif() +# ####################################################################################################################### +# # HLSL additional include directories for VS intellisense +# ####################################################################################################################### + +set(HLSL_INCLUDE_DIRS ${FEATURE_SHADER_DIRS} "package/Shaders") + +set(HLSL_INCLUDE_JSON "") +foreach(dir IN LISTS HLSL_INCLUDE_DIRS) + if(HLSL_INCLUDE_JSON STREQUAL "") + set(HLSL_INCLUDE_JSON " \"${dir}\"") + else() + set(HLSL_INCLUDE_JSON "${HLSL_INCLUDE_JSON},\n \"${dir}\"") + endif() +endforeach() + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/shadertoolsconfig.json.in" + "${CMAKE_CURRENT_SOURCE_DIR}/shadertoolsconfig.json" + @ONLY +) + # ####################################################################################################################### # # Shader validation config generation # ####################################################################################################################### @@ -183,11 +263,16 @@ endif() # This requires hlslkit and valid Skyrim installations with recent log files find_program(POWERSHELL_PATH pwsh powershell) if(POWERSHELL_PATH) - add_custom_target(generate_shader_configs - COMMAND ${POWERSHELL_PATH} -ExecutionPolicy Bypass -File "${CMAKE_SOURCE_DIR}/.github/configs/generate-shader-configs.ps1" -OutputDir "${CMAKE_SOURCE_DIR}/.github/configs" - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - COMMENT "Generating shader validation configuration files from Skyrim log files" - ) + add_custom_target( + generate_shader_configs + COMMAND + ${POWERSHELL_PATH} -ExecutionPolicy Bypass -File + "${CMAKE_SOURCE_DIR}/.github/configs/generate-shader-configs.ps1" + -OutputDir "${CMAKE_SOURCE_DIR}/.github/configs" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT + "Generating shader validation configuration files from Skyrim log files" + ) endif() # ####################################################################################################################### @@ -196,122 +281,588 @@ endif() file(GLOB FEATURE_PATHS LIST_DIRECTORIES true ${CMAKE_SOURCE_DIR}/features/*) string(TIMESTAMP UTC_NOW "%Y-%m-%dT%H-%MZ" UTC) +# Set AIO directory path used by multiple targets below +set(AIO_DIR "${CMAKE_CURRENT_BINARY_DIR}/aio") + +# ####################################################################################################################### +# # CMake install() infrastructure for manual packaging +# ####################################################################################################################### + +# Append a '/' to the end of each feature path for installation all its contents but not itself +set(FEATURE_PATHS_SLASH ${FEATURE_PATHS}) +list(TRANSFORM FEATURE_PATHS_SLASH APPEND /) + +# Install logic for AIO package +# To copy AIO package at a folder do `${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${AIO_DIR}` +install(CODE "file(REMOVE_RECURSE \${CMAKE_INSTALL_PREFIX})") +install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION SKSE/Plugins COMPONENT SKSE) +install( + FILES $ + DESTINATION SKSE/Plugins + COMPONENT SKSE +) +install( + DIRECTORY ${CMAKE_SOURCE_DIR}/package/ ${FEATURE_PATHS_SLASH} + DESTINATION . + COMPONENT Shaders +) +install(CODE "file(REMOVE \${CMAKE_INSTALL_PREFIX}/Core)" COMPONENT Shaders) + +# ####################################################################################################################### +# # Automatic AIO preparation (incremental copy system) +# ####################################################################################################################### + if(AUTO_PLUGIN_DEPLOYMENT OR AIO_ZIP_TO_DIST) - set(AIO_DIR "${CMAKE_CURRENT_BINARY_DIR}/aio") - message("Copying package folder with dll/pdb with all features to ${AIO_DIR}") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E remove_directory "${AIO_DIR}" - COMMAND ${CMAKE_COMMAND} -E make_directory "${AIO_DIR}/SKSE/Plugins" - COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/package "${AIO_DIR}" - COMMAND ${CMAKE_COMMAND} -E copy_directory ${FEATURE_PATHS} "${AIO_DIR}" - COMMAND ${CMAKE_COMMAND} -E copy $ "${AIO_DIR}/SKSE/Plugins/" - COMMAND ${CMAKE_COMMAND} -E copy $ "${AIO_DIR}/SKSE/Plugins/" - COMMAND ${CMAKE_COMMAND} -E remove "${AIO_DIR}/CORE" - ) - add_custom_command( - OUTPUT copy_shaders.stamp - COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/package "${AIO_DIR}" - COMMAND ${CMAKE_COMMAND} -E copy_directory ${FEATURE_PATHS} "${AIO_DIR}" - COMMAND ${CMAKE_COMMAND} -E touch copy_shaders.stamp - DEPENDS ${HLSL_FILES} - ) - - # Standalone target for preparing shaders for CI validation - # This allows shader validation to run without waiting for the full build - add_custom_target(prepare_shaders - DEPENDS copy_shaders.stamp - COMMENT "Preparing shaders for validation" - ) + message("Preparing AIO package in ${AIO_DIR}") + + # Prepare AIO only when sources change. Gather package + feature files as + # inputs so the prepare step runs only when something actually changed. + file( + GLOB_RECURSE _AIO_PACKAGE_FILES + LIST_DIRECTORIES FALSE + "${CMAKE_SOURCE_DIR}/package/*" + ) + foreach(_fpath IN LISTS FEATURE_PATHS) + file(GLOB_RECURSE _tmp LIST_DIRECTORIES FALSE "${_fpath}/*") + list(APPEND _AIO_PACKAGE_FILES ${_tmp}) + endforeach() + + # Prepare AIO by copying files only when different. This avoids updating + # timestamps for unchanged files and prevents downstream incremental + # deploys from copying everything every build. + set(_prepare_aio_cmds) + + # Ensure SKSE/Plugins dir exists and copy built plugin files + list( + APPEND + _prepare_aio_cmds + COMMAND + ${CMAKE_COMMAND} + -E + make_directory + "${AIO_DIR}/SKSE/Plugins" + ) + list( + APPEND + _prepare_aio_cmds + COMMAND + ${CMAKE_COMMAND} + -E + copy_if_different + $ + "${AIO_DIR}/SKSE/Plugins/$" + ) + list( + APPEND + _prepare_aio_cmds + COMMAND + ${CMAKE_COMMAND} + -E + copy_if_different + $ + "${AIO_DIR}/SKSE/Plugins/$" + ) + + # Copy package files + file( + GLOB_RECURSE _AIO_PACKAGE_SOURCE_FILES + LIST_DIRECTORIES FALSE + "${CMAKE_SOURCE_DIR}/package/*" + ) + foreach(_src IN LISTS _AIO_PACKAGE_SOURCE_FILES) + file(RELATIVE_PATH _rel "${CMAKE_SOURCE_DIR}/package" "${_src}") + set(_dst "${AIO_DIR}/${_rel}") + get_filename_component(_dst_dir "${_dst}" DIRECTORY) + list( + APPEND + _prepare_aio_cmds + COMMAND + ${CMAKE_COMMAND} + -E + make_directory + "${_dst_dir}" + COMMAND + ${CMAKE_COMMAND} + -E + copy_if_different + "${_src}" + "${_dst}" + ) + endforeach() + + # Copy feature folders (only files, preserve existing files in AIO) + foreach(_fpath IN LISTS FEATURE_PATHS) + if(EXISTS "${_fpath}") + file( + GLOB_RECURSE _feature_files + LIST_DIRECTORIES FALSE + "${_fpath}/*" + ) + foreach(_src IN LISTS _feature_files) + file(RELATIVE_PATH _rel "${_fpath}" "${_src}") + set(_dst "${AIO_DIR}/${_rel}") + get_filename_component(_dst_dir "${_dst}" DIRECTORY) + list( + APPEND + _prepare_aio_cmds + COMMAND + ${CMAKE_COMMAND} + -E + make_directory + "${_dst_dir}" + COMMAND + ${CMAKE_COMMAND} + -E + copy_if_different + "${_src}" + "${_dst}" + ) + endforeach() + endif() + endforeach() + + # Remove CORE from AIO if it exists (keep rest intact) + list( + APPEND + _prepare_aio_cmds + COMMAND + ${CMAKE_COMMAND} + -E + remove + "${AIO_DIR}/CORE" + ) + list( + APPEND + _prepare_aio_cmds + COMMAND + ${CMAKE_COMMAND} + -E + touch + ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp + ) + + add_custom_command( + OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp + ${_prepare_aio_cmds} + DEPENDS ${_AIO_PACKAGE_FILES} ${PROJECT_NAME} + ) + + add_custom_target( + PREPARE_AIO + ALL + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp + ) + # Only copy shaders when HLSL files change; copy individually so unchanged + # files do not get their timestamps updated. + file( + GLOB_RECURSE _package_shaders + LIST_DIRECTORIES FALSE + "${CMAKE_SOURCE_DIR}/package/Shaders/*" + ) + set(_shader_copy_cmds) + foreach(_src IN LISTS _package_shaders) + file(RELATIVE_PATH _rel "${CMAKE_SOURCE_DIR}/package/Shaders" "${_src}") + set(_dst "${AIO_DIR}/Shaders/${_rel}") + get_filename_component(_dst_dir "${_dst}" DIRECTORY) + list( + APPEND + _shader_copy_cmds + COMMAND + ${CMAKE_COMMAND} + -E + make_directory + "${_dst_dir}" + COMMAND + ${CMAKE_COMMAND} + -E + copy_if_different + "${_src}" + "${_dst}" + ) + endforeach() + # feature shader folders + foreach(_fpath IN LISTS FEATURE_PATHS) + if(EXISTS "${_fpath}/Shaders") + file( + GLOB_RECURSE _feat_shaders + LIST_DIRECTORIES FALSE + "${_fpath}/Shaders/*" + ) + get_filename_component(_feat_name "${_fpath}" NAME) + foreach(_src IN LISTS _feat_shaders) + file(RELATIVE_PATH _rel "${_fpath}/Shaders" "${_src}") + # Place feature shader files directly under AIO_DIR/Shaders to preserve expected include paths + # This matches the package shader layout and ensures includes like "TerrainShadows/..." resolve correctly + set(_dst "${AIO_DIR}/Shaders/${_rel}") + get_filename_component(_dst_dir "${_dst}" DIRECTORY) + list( + APPEND + _shader_copy_cmds + COMMAND + ${CMAKE_COMMAND} + -E + make_directory + "${_dst_dir}" + COMMAND + ${CMAKE_COMMAND} + -E + copy_if_different + "${_src}" + "${_dst}" + ) + endforeach() + endif() + endforeach() + + add_custom_command( + OUTPUT copy_shaders.stamp + COMMAND + ${CMAKE_COMMAND} -E make_directory "${AIO_DIR}/Shaders" + ${_shader_copy_cmds} + COMMAND ${CMAKE_COMMAND} -E touch copy_shaders.stamp + DEPENDS ${HLSL_FILES} + COMMENT "Copying changed shaders into AIO/Shaders" + ) + + # Standalone target for preparing shaders for CI validation + # This allows shader validation to run without waiting for the full build + add_custom_target( + prepare_shaders + DEPENDS copy_shaders.stamp + COMMENT "Preparing shaders for validation" + ) endif() # Automatic deployment to CommunityShaders output directory. if(AUTO_PLUGIN_DEPLOYMENT) - foreach(DEPLOY_TARGET $ENV{CommunityShadersOutputDir}) - message("Copying AIO to ${DEPLOY_TARGET}") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory ${AIO_DIR} "${DEPLOY_TARGET}" - ) - - string(MD5 DEPLOY_TARGET_HASH ${DEPLOY_TARGET}) - - add_custom_command( - OUTPUT ${DEPLOY_TARGET_HASH}.stamp - COMMAND ${CMAKE_COMMAND} -E copy_directory "${AIO_DIR}/Shaders" "${DEPLOY_TARGET}/Shaders" - COMMAND ${CMAKE_COMMAND} -E touch ${DEPLOY_TARGET_HASH}.stamp - DEPENDS copy_shaders.stamp - ) - - list(APPEND DEPLOY_TARGET_HASHES ${DEPLOY_TARGET_HASH}.stamp) - - endforeach() - - add_custom_target(COPY_SHADERS ALL - DEPENDS - copy_shaders.stamp - ${DEPLOY_TARGET_HASHES} - ) - - if(NOT DEFINED ENV{CommunityShadersOutputDir}) - message("When using AUTO_PLUGIN_DEPLOYMENT option, you need to set environment variable 'CommunityShadersOutputDir'") - endif() + set(DEPLOY_TARGET_HASHES) + if(WIN32) + # Write a small wrapper once at configure time to normalize robocopy exit codes. + set(ROBOCOPY_WRAPPER "${CMAKE_BINARY_DIR}/robocopy_wrapper.cmd") + file( + WRITE + ${ROBOCOPY_WRAPPER} + "@echo off\r\nrem Robocopy wrapper: forwards all args to robocopy and normalizes exit codes\r\nrobocopy %*\r\nset rc=%ERRORLEVEL%\r\nif %rc% GEQ 8 exit /b %rc%\r\nexit /b 0\r\n" + ) + + foreach(DEPLOY_TARGET $ENV{CommunityShadersOutputDir}) + message("Deploying AIO to ${DEPLOY_TARGET} (incremental)") + + # Ensure destination root exists + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_TARGET}" + ) + + string(MD5 DEPLOY_TARGET_HASH ${DEPLOY_TARGET}) + + # Incremental copy (non-shaders) - produce a stamp so the COPY_SHADERS + # target can depend on both shader and non-shader deploy steps. + add_custom_command( + OUTPUT ${DEPLOY_TARGET_HASH}_deploy.stamp + COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_TARGET}" + COMMAND + ${ROBOCOPY_WRAPPER} "${AIO_DIR}" "${DEPLOY_TARGET}" "/E" + "/XD" "${AIO_DIR}/Shaders" "/COPY:DAT" "/XO" "/R:1" "/W:1" + "/NFL" "/NDL" "/NJH" "/NJS" + COMMAND + ${CMAKE_COMMAND} -E touch ${DEPLOY_TARGET_HASH}_deploy.stamp + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp + COMMENT + "Incremental deploy (excluding Shaders) to ${DEPLOY_TARGET} (robocopy-wrapper)" + ) + + # Ensure plugin DLL/PDB are copied directly to the target SKSE/Plugins + # folder in case robocopy rules do not copy them as expected. + add_custom_command( + OUTPUT ${DEPLOY_TARGET_HASH}_plugin.stamp + COMMAND + ${CMAKE_COMMAND} -E make_directory + "${DEPLOY_TARGET}/SKSE/Plugins" + COMMAND + ${CMAKE_COMMAND} -E copy_if_different + $ + "${DEPLOY_TARGET}/SKSE/Plugins/$" + COMMAND + ${CMAKE_COMMAND} -E copy_if_different + $ + "${DEPLOY_TARGET}/SKSE/Plugins/$" + COMMAND + ${CMAKE_COMMAND} -E touch ${DEPLOY_TARGET_HASH}_plugin.stamp + DEPENDS + ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp + ${PROJECT_NAME} + COMMENT "Copy plugin DLL/PDB to ${DEPLOY_TARGET}/SKSE/Plugins" + ) + + list(APPEND DEPLOY_TARGET_HASHES ${DEPLOY_TARGET_HASH}_plugin.stamp) + + # Incremental shader copy (only changed/new shader files are copied) + add_custom_command( + OUTPUT ${DEPLOY_TARGET_HASH}.stamp + COMMAND + ${CMAKE_COMMAND} -E make_directory + "${DEPLOY_TARGET}/Shaders" + COMMAND + ${ROBOCOPY_WRAPPER} "${AIO_DIR}/Shaders" + "${DEPLOY_TARGET}/Shaders" "/E" "/COPY:DAT" "/XO" "/R:1" + "/W:1" "/NFL" "/NDL" "/NJH" "/NJS" + COMMAND ${CMAKE_COMMAND} -E touch ${DEPLOY_TARGET_HASH}.stamp + DEPENDS + copy_shaders.stamp + ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp + COMMENT + "Incremental shader copy to ${DEPLOY_TARGET}/Shaders (robocopy-wrapper)" + ) + + list(APPEND DEPLOY_TARGET_HASHES ${DEPLOY_TARGET_HASH}_deploy.stamp) + list(APPEND DEPLOY_TARGET_HASHES ${DEPLOY_TARGET_HASH}.stamp) + endforeach() + else() + # AUTO_PLUGIN_DEPLOYMENT is enabled but the host is not Windows. Do + # not attempt to deploy to local Skyrim directories on non-Windows + # systems; instead provide a minimal COPY_SHADERS target so CI jobs + # that only prepare shaders still work. + message( + WARNING + "AUTO_PLUGIN_DEPLOYMENT is enabled but not supported on this platform; skipping deployment to CommunityShadersOutputDir" + ) + endif() + + add_custom_target( + COPY_SHADERS + ALL + DEPENDS copy_shaders.stamp ${DEPLOY_TARGET_HASHES} + COMMENT "Prepare and copy AIO/shaders to CommunityShadersOutputDir" + ) +endif() + +if(NOT DEFINED ENV{CommunityShadersOutputDir}) + message( + "When using AUTO_PLUGIN_DEPLOYMENT option, you need to set environment variable 'CommunityShadersOutputDir'" + ) endif() # Zip base CommunityShaders and all addons as their own 7z in dist folder if(ZIP_TO_DIST) - set(ZIP_DIR "${CMAKE_CURRENT_BINARY_DIR}/zip") - message("Copying base CommunityShader into ${ZIP_DIR}.") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E remove_directory "${ZIP_DIR}" ${CMAKE_SOURCE_DIR}/dist - COMMAND ${CMAKE_COMMAND} -E make_directory "${ZIP_DIR}/SKSE/Plugins" ${CMAKE_SOURCE_DIR}/dist - COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/package "${ZIP_DIR}" - COMMAND ${CMAKE_COMMAND} -E copy $ "${ZIP_DIR}/SKSE/Plugins/" - COMMAND ${CMAKE_COMMAND} -E copy $ "${ZIP_DIR}/SKSE/Plugins/" - ) - foreach(FEATURE_PATH ${FEATURE_PATHS}) - if (EXISTS "${FEATURE_PATH}/CORE") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory ${FEATURE_PATH} "${ZIP_DIR}" - ) - endif() - endforeach() - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E remove "${ZIP_DIR}/CORE" - ) - - set(TARGET_ZIP "${PROJECT_NAME}-${UTC_NOW}.7z") - message("Zipping ${ZIP_DIR} to ${CMAKE_SOURCE_DIR}/dist/${TARGET_ZIP}") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E tar cf ${CMAKE_SOURCE_DIR}/dist/${TARGET_ZIP} --format=7zip -- . - WORKING_DIRECTORY ${ZIP_DIR} - ) - - foreach(FEATURE_PATH ${FEATURE_PATHS}) - if (EXISTS "${FEATURE_PATH}/CORE") - continue() - endif() - get_filename_component(FEATURE ${FEATURE_PATH} NAME) - message("Zipping ${FEATURE_PATH} to ${CMAKE_SOURCE_DIR}/dist/${FEATURE}-${UTC_NOW}.7z") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E tar cf ${CMAKE_SOURCE_DIR}/dist/${FEATURE}-${UTC_NOW}.7z --format=7zip -- . - WORKING_DIRECTORY ${FEATURE_PATH} - ) - endforeach() + set(ZIP_DIR "${CMAKE_CURRENT_BINARY_DIR}/zip") + message("Copying base CommunityShader into ${ZIP_DIR}.") + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E remove_directory "${ZIP_DIR}" + ${CMAKE_SOURCE_DIR}/dist + COMMAND + ${CMAKE_COMMAND} -E make_directory "${ZIP_DIR}/SKSE/Plugins" + ${CMAKE_SOURCE_DIR}/dist + COMMAND + ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/package + "${ZIP_DIR}" + COMMAND + ${CMAKE_COMMAND} -E copy $ + "${ZIP_DIR}/SKSE/Plugins/" + COMMAND + ${CMAKE_COMMAND} -E copy $ + "${ZIP_DIR}/SKSE/Plugins/" + ) + foreach(FEATURE_PATH ${FEATURE_PATHS}) + if(EXISTS "${FEATURE_PATH}/CORE") + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E copy_directory ${FEATURE_PATH} + "${ZIP_DIR}" + ) + endif() + endforeach() + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E remove "${ZIP_DIR}/CORE" + ) + + set(TARGET_ZIP "${PROJECT_NAME}-${UTC_NOW}.7z") + message("Zipping ${ZIP_DIR} to ${CMAKE_SOURCE_DIR}/dist/${TARGET_ZIP}") + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E tar cf ${CMAKE_SOURCE_DIR}/dist/${TARGET_ZIP} + --format=7zip -- . + WORKING_DIRECTORY ${ZIP_DIR} + ) + + foreach(FEATURE_PATH ${FEATURE_PATHS}) + if(EXISTS "${FEATURE_PATH}/CORE") + continue() + endif() + get_filename_component(FEATURE ${FEATURE_PATH} NAME) + message( + "Zipping ${FEATURE_PATH} to ${CMAKE_SOURCE_DIR}/dist/${FEATURE}-${UTC_NOW}.7z" + ) + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E tar cf + ${CMAKE_SOURCE_DIR}/dist/${FEATURE}-${UTC_NOW}.7z --format=7zip + -- . + WORKING_DIRECTORY ${FEATURE_PATH} + ) + endforeach() endif() # Create a AIO zip for easier testing if(AIO_ZIP_TO_DIST) - if(NOT ZIP_TO_DIST) - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_SOURCE_DIR}/dist - COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_SOURCE_DIR}/dist - ) - endif() - - set(TARGET_AIO_ZIP "${PROJECT_NAME}_AIO-${UTC_NOW}.7z") - message("Zipping ${AIO_DIR} to ${CMAKE_SOURCE_DIR}/dist/${TARGET_AIO_ZIP}") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E tar cf ${CMAKE_SOURCE_DIR}/dist/${TARGET_AIO_ZIP} --format=7zip -- . - WORKING_DIRECTORY ${AIO_DIR} - ) + if(NOT ZIP_TO_DIST) + add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E remove_directory ${CMAKE_SOURCE_DIR}/dist + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_SOURCE_DIR}/dist + ) + endif() + + # Create a stamp-producing custom command for the AIO archive so CMake + # only rebuilds the archive when its inputs change. The archive filename + # keeps the UTC timestamp as before, but the command writes a stable + # stamp file that CMake can track as OUTPUT. + set(TARGET_AIO_ZIP "${PROJECT_NAME}_AIO-${UTC_NOW}.7z") + set(AIO_ARCHIVE "${CMAKE_SOURCE_DIR}/dist/${TARGET_AIO_ZIP}") + set(AIO_ZIP_STAMP "${CMAKE_CURRENT_BINARY_DIR}/aio_package.stamp") + + message("Zipping ${AIO_DIR} to ${AIO_ARCHIVE}") + + add_custom_command( + OUTPUT ${AIO_ZIP_STAMP} + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_SOURCE_DIR}/dist" + COMMAND ${CMAKE_COMMAND} -E tar cf ${AIO_ARCHIVE} --format=7zip -- . + COMMAND ${CMAKE_COMMAND} -E touch ${AIO_ZIP_STAMP} + WORKING_DIRECTORY ${AIO_DIR} + DEPENDS PREPARE_AIO + COMMENT "Creating AIO archive ${AIO_ARCHIVE}" + ) + + add_custom_target(AIO_ZIP_PACKAGE ALL DEPENDS ${AIO_ZIP_STAMP}) endif() + +if(NOT DEFINED ENV{CommunityShadersOutputDir}) + message( + "When using AUTO_PLUGIN_DEPLOYMENT option, you need to set environment variable 'CommunityShadersOutputDir'" + ) +endif() + +# ####################################################################################################################### +# # Manual packaging targets (Package-XXX) +# ####################################################################################################################### + +set(DIST_PATH "${CMAKE_SOURCE_DIR}/dist") +file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/dist") + +set(CORE_PACKAGE "${DIST_PATH}/${PROJECT_NAME}-${UTC_NOW}.7z") + +# CORE_SOURCES = all content copied to the AIO directory + the SKSE plugin dll +file( + GLOB_RECURSE CORE_SOURCES + CONFIGURE_DEPENDS + "${CMAKE_SOURCE_DIR}/package/*" +) +# Add SKSE plugin dll as dependency (use target-file generator expression so CMake +# knows the actual output path of the target at build time) +list(APPEND CORE_SOURCES "$") + +set(CORE_FEATURE_PATHS "${CMAKE_SOURCE_DIR}/package/") + +foreach(FEATURE_PATH ${FEATURE_PATHS}) + if(EXISTS "${FEATURE_PATH}/CORE") + list(APPEND CORE_FEATURE_PATHS "${FEATURE_PATH}/") + file(GLOB_RECURSE FEATURE_SOURCES CONFIGURE_DEPENDS "${FEATURE_PATH}/*") + list(APPEND CORE_SOURCES ${FEATURE_SOURCES}) + endif() +endforeach() + +# Core package +set(FEATURE_PATH "${CMAKE_BINARY_DIR}/Core") +file(MAKE_DIRECTORY ${FEATURE_PATH}) +add_custom_command( + OUTPUT ${CORE_PACKAGE} + DEPENDS ${CORE_SOURCES} + COMMAND + ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${FEATURE_PATH} + --component SKSE + COMMAND + ${CMAKE_COMMAND} -E copy_directory ${CORE_FEATURE_PATHS} ${FEATURE_PATH} + COMMAND ${CMAKE_COMMAND} -E rm -f -- ${FEATURE_PATH}/Core + COMMAND ${CMAKE_COMMAND} -E tar cfv ${CORE_PACKAGE} --format=7zip -- . + WORKING_DIRECTORY ${FEATURE_PATH} + COMMENT "Creating Core zip package" +) +add_custom_target("Package-Core" DEPENDS ${CORE_PACKAGE}) + +# Feature packages +foreach(FEATURE_PATH ${FEATURE_PATHS}) + if(EXISTS "${FEATURE_PATH}/CORE") + continue() + endif() + + list(APPEND CORE_FEATURE_PATHS "${FEATURE_PATH}/") + file(GLOB_RECURSE FEATURE_SOURCES CONFIGURE_DEPENDS "${FEATURE_PATH}/*") + list(APPEND CORE_SOURCES ${FEATURE_SOURCES}) + + get_filename_component(FEATURE ${FEATURE_PATH} NAME) + set(FEATURE_PACKAGE "${DIST_PATH}/${FEATURE}-${UTC_NOW}.7z") + + add_custom_command( + OUTPUT ${FEATURE_PACKAGE} + COMMAND + ${CMAKE_COMMAND} -E tar cfv ${FEATURE_PACKAGE} --format=7zip -- . + WORKING_DIRECTORY "${FEATURE_PATH}" + DEPENDS ${FEATURE_SOURCES} + COMMENT "Creating ${FEATURE} zip package" + ) + + string(REPLACE " " "" FEATURE ${FEATURE}) + string(REPLACE "-" "" FEATURE ${FEATURE}) + add_custom_target("Package-${FEATURE}" DEPENDS ${FEATURE_PACKAGE}) +endforeach() + +# AIO Folder target +add_custom_command( + OUTPUT ${AIO_DIR}/SKSE/Plugins/${PROJECT_NAME}.dll + DEPENDS ${CORE_SOURCES} + COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${AIO_DIR} + COMMENT "Installing to AIO folder" +) +add_custom_target("AIO" DEPENDS ${AIO_DIR}/SKSE/Plugins/${PROJECT_NAME}.dll) + +# Manual AIO package target +set(AIO_PACKAGE "${DIST_PATH}/${PROJECT_NAME}_AIO-${UTC_NOW}.7z") +add_custom_command( + OUTPUT ${AIO_PACKAGE} + DEPENDS ${CORE_SOURCES} + COMMAND ${CMAKE_COMMAND} -E make_directory ${AIO_DIR} + COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${AIO_DIR} + COMMAND + ${CMAKE_COMMAND} -E chdir ${AIO_DIR} ${CMAKE_COMMAND} -E tar cfv + ${AIO_PACKAGE} --format=7zip -- . + COMMENT "Creating AIO zip package (manual)" +) +add_custom_target("Package-AIO-Manual" DEPENDS ${AIO_PACKAGE}) + +message("*************************************************************") +message("Community Shaders configuration complete") +message("To prepare a ZIP package of AIO, Core, or Features") +message(" Build cmake targets:") +message(" - Package-Core: Core package") +message(" - Package-AIO-Manual: AIO package (manual)") +message(" - Package-: Individual feature packages") +message(" Or use cmake --install for custom deployment:") +message(" cmake --install ./build/ALL --prefix ") +message("Try switching to build preset 'Dev' for faster iteration time") +message("*************************************************************") diff --git a/CMakePresets.json b/CMakePresets.json index d1f88649e9..1966d5f729 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -52,72 +52,51 @@ "inherits": ["common", "vcpkg", "win64", "msvc"] }, { - "name": "AE", + "name": "ALL", "cacheVariables": { "ENABLE_SKYRIM_AE": "ON", - "ENABLE_SKYRIM_SE": "OFF", - "ENABLE_SKYRIM_VR": "OFF" - }, - "inherits": "skyrim" - }, - { - "name": "SE", - "cacheVariables": { - "ENABLE_SKYRIM_AE": "OFF", "ENABLE_SKYRIM_SE": "ON", - "ENABLE_SKYRIM_VR": "OFF" + "ENABLE_SKYRIM_VR": "ON", + "AUTO_PLUGIN_DEPLOYMENT": "OFF" }, "inherits": "skyrim" - }, + } + ], + "buildPresets": [ { - "name": "VR", - "cacheVariables": { - "ENABLE_SKYRIM_AE": "OFF", - "ENABLE_SKYRIM_SE": "OFF", - "ENABLE_SKYRIM_VR": "ON" - }, - "inherits": "skyrim" + "name": "Dev", + "description": "Default build preset for developers, generate an AIO folder thats ready to copy", + "configurePreset": "ALL", + "configuration": "Release", + "targets": ["CommunityShaders", "AIO"] }, { "name": "ALL", - "cacheVariables": { - "ENABLE_SKYRIM_AE": "ON", - "ENABLE_SKYRIM_SE": "ON", - "ENABLE_SKYRIM_VR": "ON" - }, - "inherits": "skyrim" + "description": "(Deprecated), kept for for CI, use Package instead", + "configurePreset": "ALL", + "configuration": "Release", + "targets": ["CommunityShaders", "Package-AIO-Manual"] }, { - "name": "PRE-AE", - "cacheVariables": { - "ENABLE_SKYRIM_AE": "OFF", - "ENABLE_SKYRIM_SE": "ON", - "ENABLE_SKYRIM_VR": "ON" - }, - "inherits": "skyrim" + "name": "Package", + "description": "Build preset to generate a AIO zip package in /dist folder, mostly for CI", + "configurePreset": "ALL", + "configuration": "Release", + "targets": ["CommunityShaders", "Package-AIO-Manual"] }, { - "name": "FLATRIM", - "cacheVariables": { - "ENABLE_SKYRIM_AE": "ON", - "ENABLE_SKYRIM_SE": "ON", - "ENABLE_SKYRIM_VR": "OFF" - }, - "inherits": "skyrim" + "name": "Shaders", + "description": "Build preset to copy shaders into /AIO folder for shader validation", + "configurePreset": "ALL", + "configuration": "Release", + "targets": ["prepare_shaders"] }, { - "name": "ALL-TRACY", - "cacheVariables": { - "TRACY_SUPPORT": "ON" - }, - "inherits": "ALL" - } - ], - "buildPresets": [ - { - "name": "ALL", + "name": "Debug", + "description": "Debug build for CS SKSE plugin, generate an AIO folder thats ready to copy", "configurePreset": "ALL", - "configuration": "Release" + "configuration": "Debug", + "targets": ["CommunityShaders", "AIO"] } ] } diff --git a/README.md b/README.md index 3fb25e4975..5c9cd88434 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,23 @@ SKSE core plugin for community-driven advanced graphics modifications. - Any terminal of your choice (e.g., PowerShell) - [Visual Studio Community 2022](https://visualstudio.microsoft.com/) - Desktop development with C++ + - CMake Tools for Windows + - HLSL Tools +- [Git](https://git-scm.com/downloads) + - Edit the `PATH` environment variable and add the Git.exe install path as a new value + +## Optional Requirements + +``` +CMake & Vcpkg comes with Visual Studio in Developer Command Prompts already. +Install them manually only if you want them in everywhere. +``` + - [CMake](https://cmake.org/) + - No need to install manually if you have Visual Studio CMake Tools installed + - CMake 4.0+ is **not** supported right now - Edit the `PATH` environment variable and add the cmake.exe install path as a new value - Instructions for finding and editing the `PATH` environment variable can be found [here](https://www.java.com/en/download/help/path.html) -- [Git](https://git-scm.com/downloads) - - Edit the `PATH` environment variable and add the Git.exe install path as a new value - [Vcpkg](https://github.com/microsoft/vcpkg) - Install vcpkg using the directions in vcpkg's [Quick Start Guide](https://github.com/microsoft/vcpkg#quick-start-windows) - After install, add a new environment variable named `VCPKG_ROOT` with the value as the path to the folder containing vcpkg @@ -40,29 +52,74 @@ SKSE core plugin for community-driven advanced graphics modifications. - [VR Address Library for SKSEVR](https://www.nexusmods.com/skyrimspecialedition/mods/58101) - Needed for VR -## Register Visual Studio as a Generator +## Build Instructions -- Open `x64 Native Tools Command Prompt` -- Run `cmake` -- Close the cmd window +### Clone the Repository with submodules -Or, in powershell run: +To clone the repository with all submodules, run the following command in your terminal: -```pwsh -& "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" amd64 +```bash +git clone https://github.com/doodlum/skyrim-community-shaders.git --recursive +cd skyrim-community-shaders ``` -## Clone and Build +### Visual Studio build -Open terminal (e.g., PowerShell) and run the following commands: +To build the project, just open `./skyrim-community-shaders` with Visual Studio's "Open Folder" feature. (Ensure you have `CMake Tools for Windows` selected when installing VS) +Follow the prompts to `Configure` and `Build` the project. +It should generate the AIO package in the `./build/ALL/aio` folder by default. + +#### Zip package & Optional targets + +If you change the `Solution Explorer` into `CMake Targets View`, you can find optional targets to create zip packages for each feature. +Right click on the target and select `Build` to create the zip package in `./dist/`. + +### Advanced build with CMake in command line + +Open the "Developer PowerShell for VS 2022" or the "x64 Native Tools Command Prompt" (these set up the Visual Studio toolchain for you). + +Then from the repository root run: + +```pwsh +# Generate the build files (uses the ALL preset) +cmake --preset ALL + +# Build using the preset +cmake --build --preset ALL + +# Install an AIO package somewhere, e.g. $MOD_FOLDER +cmake --install --preset ALL -- --prefix $MOD_FOLDER ``` -git clone https://github.com/doodlum/skyrim-community-shaders.git --recursive -cd skyrim-community-shaders -.\BuildRelease.bat + +# Notes + +- If you prefer to run the VC environment manually, launch Developer PowerShell or the x64 Native Tools prompt instead of calling vcvarsall.bat directly from PowerShell. +- The convenience wrapper `BuildRelease.bat` also captures these steps. + +#### Build a zip package + +You can build zip packages for optional cmake targets. +Currently support `AIO_ZIP_PACKAGE`, `Package-AIO-Manual`, `Package-Core`, and `Package-`: + +```pwsh +# Create a AIO package in ./dist/ +# Automated AIO zip (requires AIO_ZIP_TO_DIST=ON) +cmake --build ./build/ALL --config Release --target AIO_ZIP_PACKAGE + +# Manual AIO package (install + tar) +cmake --build ./build/ALL --config Release --target Package-AIO-Manual + +# Create a CommunityShaders core package in ./dist/ +cmake --build ./build/ALL --config Release --target Package-Core + +# Create a feature package in ./dist/ (example: GrassLighting) +cmake --build ./build/ALL --config Release --target Package-GrassLighting ``` -### CMAKE Options (optional) +For more details about packaging targets, options, and the difference between automated and manual packaging, see the "Manual packaging targets (detailed)" section in `.claude/CLAUDE.md`. + +#### CMAKE Options (optional) If you want an example CMakeUserPreset to start off with you can copy the `CMakeUserPresets.json.template` -> `CMakeUserPresets.json` @@ -72,19 +129,6 @@ If you want an example CMakeUserPreset to start off with you can copy the `CMake - Make sure `"AUTO_PLUGIN_DEPLOYMENT"` is set to `"ON"` in `CMakeUserPresets.json` - Change the `"CommunityShadersOutputDir"` value to match your desired outputs, if you want multiple folders you can separate them by `;` is shown in the template example -#### AIO_ZIP_TO_DIST - -- This option is default `"ON"` -- Make sure `"AIO_ZIP_TO_DIST"` is set to `"ON"` in `CMakeUserPresets.json` -- This will create a `CommunityShaders_AIO.7z` archive in /dist containing all features and base mod - -#### ZIP_TO_DIST - -- This option is default `"ON"` -- Make sure `"ZIP_TO_DIST"` is set to `"ON"` in `CMakeUserPresets.json` -- This will create a zip for each feature and one for the base Community shaders in /dist -- If having a file with name `CORE` in the root of the features folder it will instead be merged into the core zip - #### TRACY_SUPPORT - This option is default `"OFF"` @@ -124,6 +168,31 @@ If you run into `Access violation` build errors during step 3, you can try addin docker run -it --rm --isolation=process -v .:C:/skyrim-community-shaders skyrim-community-shaders:latest ``` +## Debugging + +### Launching MO2-SKSE-Skyrim from commandline + +1. Open Steam +2. Close ModOrganizer GUI +3. Add `ModOrganizer.exe` (MO2 Folder) to your PATH, or use the path of it +4. Run the commands: + +```pwsh +# Change Working Directory +cd "C:/Program Files (x86)/Steam/steamapps/common/Skyrim Special Edition" +# Launch SKSE with MO2 +ModOrganizer.exe --log run "C:\Program Files (x86)\Steam\steamapps\common\Skyrim Special Edition\skse64_loader.exe" +``` + +### Capture with RenderDoc + +In Launch Application Menu, use the following settings: + +- Executable Path: `PATH/TO/ModOrganizer.exe` +- Working Directory: `C:/Program Files (x86)/Steam/steamapps/common/Skyrim Special Edition` +- Command-line Arguments: `--log run "C:\Program Files (x86)\Steam\steamapps\common\Skyrim Special Edition\skse64_loader.exe"` +- [x] **Capture Child Process** + ## License ### Default @@ -132,7 +201,7 @@ docker run -it --rm --isolation=process -v .:C:/skyrim-community-shaders skyrim- Specifically, the Modded Code includes: - Skyrim (and its variants) -- Hardware drivers to enable additional functionality provided via proprietary SDKs, such as [Nvidia DLSS](https://developer.nvidia.com/rtx/dlss/get-started), [AMD FidelityFX FSR3](https://gpuopen.com/fidelityfx-super-resolution-3/), and [Intel XeSS](https://github.com/intel/xess) +- Hardware drivers to enable additional functionality provided via proprietary SDKs, such as [Nvidia DLSS](https://developer.nvidia.com/rtx/dlss/get-started) and [AMD FidelityFX FSR3](https://gpuopen.com/fidelityfx-super-resolution-3/) The Modding Libraries include: diff --git a/cmake/FidelityFX-SDK.cmake b/cmake/FidelityFX-SDK.cmake new file mode 100644 index 0000000000..1c61ab4c61 --- /dev/null +++ b/cmake/FidelityFX-SDK.cmake @@ -0,0 +1,15 @@ +set(FFX_API_VK OFF) +set(FFX_API_DX12 OFF) +set(FFX_ALL OFF) +set(FFX_FSR3 ON) +set(FFX_FSR ON) +set(FFX_AUTO_COMPILE_SHADERS 1) + +add_subdirectory(${CMAKE_SOURCE_DIR}/extern/FidelityFX-SDK/sdk) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE + ffx_backend_dx11_x64 + ffx_fsr3_x64 +) diff --git a/cmake/XeSS-SDK.cmake b/cmake/XeSS-SDK.cmake deleted file mode 100644 index 9a67982d24..0000000000 --- a/cmake/XeSS-SDK.cmake +++ /dev/null @@ -1,35 +0,0 @@ -# XeSS SDK Configuration -# This file configures the Intel XeSS SDK integration for the project - -# XeSS is dynamically loaded at runtime, so we don't need to link against static libraries -# The XeSS DLL (libxess.dll) should be placed in the Data/SKSE/Plugins/XeSS directory - -# Find XeSS headers installed by vcpkg port -find_path(INTEL_XESS_INCLUDE_DIRS "xess/xess.h") - -if(INTEL_XESS_INCLUDE_DIRS) - message(STATUS "XeSS SDK headers found via vcpkg at ${INTEL_XESS_INCLUDE_DIRS}") - target_include_directories( - ${PROJECT_NAME} - PRIVATE - ${INTEL_XESS_INCLUDE_DIRS} - ) -else() - message(WARNING "XeSS SDK headers not found - XeSS compilation may fail") - message(STATUS "Make sure intel-xess is installed via vcpkg") -endif() - -# Link required D3D12 libraries for interop -target_link_libraries( - ${PROJECT_NAME} - PRIVATE - d3d12.lib - dxgi.lib -) - -# Add preprocessor definition to enable XeSS support -target_compile_definitions( - ${PROJECT_NAME} - PRIVATE - XESS_SUPPORT=1 -) \ No newline at end of file diff --git a/cmake/ports/intel-xess/portfile.cmake b/cmake/ports/intel-xess/portfile.cmake deleted file mode 100644 index 3b878f5b7b..0000000000 --- a/cmake/ports/intel-xess/portfile.cmake +++ /dev/null @@ -1,15 +0,0 @@ -# Intel XeSS SDK - headers only -vcpkg_from_github( - OUT_SOURCE_PATH SOURCE_PATH - REPO intel/xess - REF v2.1.0 - SHA512 6129abf9a271c366e8d04f2676ec8f39858cd8e1530b0178911a0c5e1c616db56bc6c577aa3cec2d63f23310cedb658f5e7b463469bb467482bb40af59ed155a - HEAD_REF main -) - -# Install only the necessary header files (not the entire repo) -set(XESS_HEADERS_SOURCE ${SOURCE_PATH}/inc/xess) -file(INSTALL ${XESS_HEADERS_SOURCE} DESTINATION ${CURRENT_PACKAGES_DIR}/include) - -# Install copyright -vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt") \ No newline at end of file diff --git a/cmake/ports/intel-xess/vcpkg.json b/cmake/ports/intel-xess/vcpkg.json deleted file mode 100644 index 0d047fbd76..0000000000 --- a/cmake/ports/intel-xess/vcpkg.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "intel-xess", - "version": "2.1.0", - "port-version": 1, - "description": "Intel Xe Super Sampling (XeSS) SDK - AI-based upscaling technology (headers only)", - "homepage": "https://github.com/intel/xess", - "supports": "windows" -} diff --git a/cmake/ports/renderdoc/README.md b/cmake/ports/renderdoc/README.md new file mode 100644 index 0000000000..5899a32b17 --- /dev/null +++ b/cmake/ports/renderdoc/README.md @@ -0,0 +1,58 @@ +# RenderDoc vcpkg Port + +This is a custom vcpkg port for RenderDoc's in-application API. + +## What This Port Provides + +This port installs: + +- **API Header**: `renderdoc_app.h` from the RenderDoc GitHub repository +- **Documentation**: Usage instructions and license information + +## What This Port Does NOT Provide + +This port does **not** include the `renderdoc.dll` runtime library because: + +1. **Size**: The DLL is ~24MB, which is large for vcpkg package management +2. **Runtime Deployment**: The DLL must be deployed with the application at runtime, not at build time +3. **Version Flexibility**: Users may want specific versions for testing or compatibility +4. **Distribution**: The DLL should be packaged with the final application distribution + +## Getting the Runtime DLL + +The `renderdoc.dll` should be obtained from official RenderDoc releases: + +- Download page: https://renderdoc.org/builds + +For Community Shaders, the DLL is stored in `package/SKSE/Plugins/Renderdoc/renderdoc.dll` and is deployed as part of the mod package. + +## Usage + +After vcpkg installs this port: + +```cpp +#include + +// Load the DLL at runtime +HMODULE mod = LoadLibraryW(L"renderdoc.dll"); +if(mod) { + pRENDERDOC_GetAPI RENDERDOC_GetAPI = + (pRENDERDOC_GetAPI)GetProcAddress(mod, "RENDERDOC_GetAPI"); + // ... use the API +} +``` + +See the [official documentation](https://renderdoc.org/docs/in_application_api.html) for complete usage details. + +## Port Maintenance + +To update the RenderDoc version: + +1. Update the `REF` in `portfile.cmake` to the new tag/version +2. Update the `SHA512` hash (vcpkg will provide the correct hash on first build attempt) +3. Update the `version` in `vcpkg.json` +4. Test the build with `vcpkg install renderdoc --overlay-ports=cmake/ports` + +## License + +RenderDoc is licensed under the MIT License. See the LICENSE.md file installed by this port. diff --git a/cmake/ports/renderdoc/portfile.cmake b/cmake/ports/renderdoc/portfile.cmake new file mode 100644 index 0000000000..1c50aaca03 --- /dev/null +++ b/cmake/ports/renderdoc/portfile.cmake @@ -0,0 +1,35 @@ +# RenderDoc in-application API (header-only port) +# Note: The runtime DLL must be manually obtained and deployed with the application +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO baldurk/renderdoc + REF v1.40 + SHA512 6581d1fe7ba069e74d09b64a1de0a413bc0d1e775a45cce87bb8ea125fc2d67e9846439acc802882c0d717028e251f9f44fd896e2022e310456adafa675bf85a + HEAD_REF v1.x +) + +# Install the API header file +file(INSTALL "${SOURCE_PATH}/renderdoc/api/app/renderdoc_app.h" + DESTINATION "${CURRENT_PACKAGES_DIR}/include/Renderdoc") + +# Install copyright/license +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.md") + +# Create usage file with instructions for getting the runtime DLL +file(WRITE "${CURRENT_PACKAGES_DIR}/share/${PORT}/usage" +"RenderDoc provides an in-application API for frame capture and debugging. + +This port provides the renderdoc_app.h header file for compile-time integration. + +To use RenderDoc in your application: +1. Include the header: #include +2. Load renderdoc.dll at runtime using LoadLibrary +3. Initialize the API as described in the documentation + +To obtain renderdoc.dll: +- Download the portable zip from: https://renderdoc.org/builds +- Or build from source: https://github.com/baldurk/renderdoc +- Deploy renderdoc.dll alongside your application + +API Documentation: https://renderdoc.org/docs/in_application_api.html +") diff --git a/cmake/ports/renderdoc/vcpkg.json b/cmake/ports/renderdoc/vcpkg.json new file mode 100644 index 0000000000..2d69b5977e --- /dev/null +++ b/cmake/ports/renderdoc/vcpkg.json @@ -0,0 +1,8 @@ +{ + "name": "renderdoc", + "version": "1.40", + "description": "RenderDoc in-application API for frame capture and debugging", + "homepage": "https://github.com/baldurk/renderdoc", + "license": "MIT", + "supports": "windows" +} diff --git a/cmake/shadertoolsconfig.json.in b/cmake/shadertoolsconfig.json.in new file mode 100644 index 0000000000..b9a589d719 --- /dev/null +++ b/cmake/shadertoolsconfig.json.in @@ -0,0 +1,8 @@ +{ + "root": true, + "hlsl.preprocessorDefinitions": { + }, + "hlsl.additionalIncludeDirectories": [ +@HLSL_INCLUDE_JSON@ + ] +} \ No newline at end of file diff --git a/extern/CommonLibSSE-NG b/extern/CommonLibSSE-NG index 89f81499ad..f343b8cf75 160000 --- a/extern/CommonLibSSE-NG +++ b/extern/CommonLibSSE-NG @@ -1 +1 @@ -Subproject commit 89f81499ad8e7991bece06855523663300c563c7 +Subproject commit f343b8cf75cace4ff942ef744ea34c406aa7f8e2 diff --git a/extern/FidelityFX-SDK b/extern/FidelityFX-SDK new file mode 160000 index 0000000000..8138c9dc08 --- /dev/null +++ b/extern/FidelityFX-SDK @@ -0,0 +1 @@ +Subproject commit 8138c9dc086154706643a03def91f3d01d391cd0 diff --git a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli index f870e1ca50..424aa0ddb7 100644 --- a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli +++ b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli @@ -10,8 +10,8 @@ namespace DynamicCubemaps { - TextureCube EnvReflectionsTexture : register(t30); - TextureCube EnvTexture : register(t31); + TextureCube EnvReflectionsTexture : register(t30); + TextureCube EnvTexture : register(t31); #if !defined(WATER) @@ -22,9 +22,6 @@ namespace DynamicCubemaps # endif { float3 R = reflect(-V, N); - float NoV = saturate(dot(N, V)); - - float level = roughness * 7.0; // Horizon specular occlusion // https://marmosetco.tumblr.com/post/81245981087 @@ -35,46 +32,110 @@ namespace DynamicCubemaps return horizon; # else + float NoV = saturate(dot(N, V)); + + float level = roughness * 7.0; + float3 finalIrradiance = 0; + float directionalAmbientColorSpecular = Color::RGBToLuminance( + max(0, mul(SharedData::DirectionalAmbient, float4(R, 1.0))) + ) * Color::ReflectionNormalisationScale; + # if defined(IBL) && defined(LIGHTING) const bool inWorld = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld); const bool inReflection = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InReflection); if (SharedData::iblSettings.EnableDiffuseIBL && SharedData::iblSettings.UseStaticIBL && !inWorld && !inReflection) { float3 specularIrradiance = ImageBasedLighting::StaticSpecularIBLTexture.SampleLevel(SampColorSampler, R.xzy, level).xyz; - finalIrradiance += specularIrradiance; + finalIrradiance = specularIrradiance; return finalIrradiance; } # endif # if defined(SKYLIGHTING) if (SharedData::InInterior) { - float3 specularIrradiance = Color::GammaToLinear(EnvTexture.SampleLevel(SampColorSampler, R, level).xyz); - - finalIrradiance += specularIrradiance; +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL && SharedData::iblSettings.EnableInterior) { + directionalAmbientColorSpecular *= SharedData::iblSettings.DALCAmount; + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-R, 0), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; + float iblColorLuminance = Color::RGBToLuminance(Color::LinearToGamma(iblColor)); + directionalAmbientColorSpecular += iblColorLuminance; + } +# endif + float3 specularIrradiance = EnvTexture.SampleLevel(SampColorSampler, R, level).xyz; + + float specularIrradianceLuminance = Color::RGBToLuminance(EnvTexture.SampleLevel(SampColorSampler, R, 15)); + + specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; + specularIrradiance = Color::GammaToLinear(specularIrradiance); + + finalIrradiance = specularIrradiance; return finalIrradiance; } sh2 specularLobe = SphericalHarmonics::FauxSpecularLobe(N, -V, roughness); float skylightingSpecular = SphericalHarmonics::FuncProductIntegral(skylighting, specularLobe); + skylightingSpecular = saturate(skylightingSpecular); skylightingSpecular = Skylighting::mixSpecular(SharedData::skylightingSettings, skylightingSpecular); - float3 specularIrradiance = 1; +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL) { + directionalAmbientColorSpecular *= SharedData::iblSettings.DALCAmount; + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-R, skylightingSpecular), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; + float iblColorLuminance = Color::RGBToLuminance(Color::LinearToGamma(iblColor)); + directionalAmbientColorSpecular += iblColorLuminance; + } +# endif - if (skylightingSpecular < 1.0) - specularIrradiance = Color::GammaToLinear(EnvTexture.SampleLevel(SampColorSampler, R, level).xyz); + float3 specularIrradianceReflections = 0.0; - float3 specularIrradianceReflections = 1.0; + if (skylightingSpecular > 0.0){ + specularIrradianceReflections = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); + + float specularIrradianceReflectionsLuminance = Color::RGBToLuminance(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, 15)); + + specularIrradianceReflections = (specularIrradianceReflections / max(specularIrradianceReflectionsLuminance, 0.001)) * directionalAmbientColorSpecular; + specularIrradianceReflections = Color::GammaToLinear(specularIrradianceReflections); + } - if (skylightingSpecular > 0.0) - specularIrradianceReflections = Color::GammaToLinear(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level).xyz); + float3 specularIrradiance = 0.0; + + if (skylightingSpecular < 1.0){ + specularIrradiance = EnvTexture.SampleLevel(SampColorSampler, R, level); + + float specularIrradianceLuminance = Color::RGBToLuminance(EnvTexture.SampleLevel(SampColorSampler, R, 15)); + + directionalAmbientColorSpecular = Color::GammaToLinear(directionalAmbientColorSpecular); + directionalAmbientColorSpecular *= skylightingSpecular; + directionalAmbientColorSpecular = Color::LinearToGamma(directionalAmbientColorSpecular); + + specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; + specularIrradiance = Color::GammaToLinear(specularIrradiance); + } finalIrradiance = lerp(specularIrradiance, specularIrradianceReflections, skylightingSpecular); # else - float3 specularIrradiance = Color::GammaToLinear(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level).xyz); +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL) { + directionalAmbientColorSpecular *= SharedData::iblSettings.DALCAmount; + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-R), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; + float iblColorLuminance = Color::RGBToLuminance(Color::LinearToGamma(iblColor)); + directionalAmbientColorSpecular += iblColorLuminance; + } +# endif + + float3 specularIrradiance = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); + + float specularIrradianceLuminance = Color::RGBToLuminance(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, 15)); - finalIrradiance += specularIrradiance; + specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; + specularIrradiance = Color::GammaToLinear(specularIrradiance); + + finalIrradiance = specularIrradiance; # endif return finalIrradiance; # endif @@ -103,6 +164,7 @@ namespace DynamicCubemaps # else float3 finalIrradiance = 0; + float directionalAmbientColorSpecular = Color::RGBToLuminance(max(0, mul(SharedData::DirectionalAmbient, float4(R, 1.0)))) * Color::ReflectionNormalisationScale; # if defined(IBL) && defined(LIGHTING) const bool inWorld = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld); @@ -116,33 +178,86 @@ namespace DynamicCubemaps # if defined(SKYLIGHTING) if (SharedData::InInterior) { - float3 specularIrradiance = Color::GammaToLinear(EnvTexture.SampleLevel(SampColorSampler, R, level).xyz); - - finalIrradiance += specularIrradiance; - +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL && SharedData::iblSettings.EnableInterior) { + directionalAmbientColorSpecular *= SharedData::iblSettings.DALCAmount; + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-R, 0), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; + float iblColorLuminance = Color::RGBToLuminance(Color::LinearToGamma(iblColor)); + directionalAmbientColorSpecular += iblColorLuminance; + } +# endif + float3 specularIrradiance = EnvTexture.SampleLevel(SampColorSampler, R, level).xyz; + + float specularIrradianceLuminance = Color::RGBToLuminance(EnvTexture.SampleLevel(SampColorSampler, R, 15)); + + specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; + specularIrradiance = Color::GammaToLinear(specularIrradiance); + + finalIrradiance = specularIrradiance; return horizon * (F0 * specularBRDF.x + specularBRDF.y) * finalIrradiance; } sh2 specularLobe = SphericalHarmonics::FauxSpecularLobe(N, -V, roughness); float skylightingSpecular = SphericalHarmonics::FuncProductIntegral(skylighting, specularLobe); + skylightingSpecular = saturate(skylightingSpecular); skylightingSpecular = Skylighting::mixSpecular(SharedData::skylightingSettings, skylightingSpecular); - float3 specularIrradiance = 1; +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL) { + directionalAmbientColorSpecular *= SharedData::iblSettings.DALCAmount; + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-R, skylightingSpecular), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; + float iblColorLuminance = Color::RGBToLuminance(Color::LinearToGamma(iblColor)); + directionalAmbientColorSpecular += iblColorLuminance; + } +# endif + + directionalAmbientColorSpecular *= skylightingSpecular; + + float3 specularIrradiance = 1.0; - if (skylightingSpecular < 1.0) - specularIrradiance = Color::GammaToLinear(EnvTexture.SampleLevel(SampColorSampler, R, level).xyz); + if (skylightingSpecular < 1.0){ + specularIrradiance = EnvTexture.SampleLevel(SampColorSampler, R, level); + + float specularIrradianceLuminance = Color::RGBToLuminance(EnvTexture.SampleLevel(SampColorSampler, R, 15)); + + specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; + specularIrradiance = Color::GammaToLinear(specularIrradiance); + } float3 specularIrradianceReflections = 1.0; - if (skylightingSpecular > 0.0) - specularIrradianceReflections = Color::GammaToLinear(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level).xyz); + if (skylightingSpecular > 0.0){ + specularIrradianceReflections = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); + + float specularIrradianceReflectionsLuminance = Color::RGBToLuminance(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, 15)); + + specularIrradianceReflections = (specularIrradianceReflections / max(specularIrradianceReflectionsLuminance, 0.001)) * directionalAmbientColorSpecular; + specularIrradianceReflections = Color::GammaToLinear(specularIrradianceReflections); + } finalIrradiance = lerp(specularIrradiance, specularIrradianceReflections, skylightingSpecular); # else - float3 specularIrradiance = Color::GammaToLinear(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level)); +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL) { + directionalAmbientColorSpecular *= SharedData::iblSettings.DALCAmount; + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-R), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; + float iblColorLuminance = Color::RGBToLuminance(Color::LinearToGamma(iblColor)); + directionalAmbientColorSpecular += iblColorLuminance; + } +# endif + + float3 specularIrradiance = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); + + float specularIrradianceLuminance = Color::RGBToLuminance(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, 15)); + + specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; + specularIrradiance = Color::GammaToLinear(specularIrradiance); - finalIrradiance += specularIrradiance; + finalIrradiance = specularIrradiance; # endif return horizon * (F0 * specularBRDF.x + specularBRDF.y) * finalIrradiance; # endif diff --git a/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini b/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini index 13720e22c7..71d4a35bea 100644 --- a/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini +++ b/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini @@ -1,2 +1,2 @@ [Info] -Version = 2-2-0 \ No newline at end of file +Version = 2-2-2 \ No newline at end of file diff --git a/features/Extended Materials/Shaders/Features/ExtendedMaterials.ini b/features/Extended Materials/Shaders/Features/ExtendedMaterials.ini index 01aaedc093..312d7ff985 100644 --- a/features/Extended Materials/Shaders/Features/ExtendedMaterials.ini +++ b/features/Extended Materials/Shaders/Features/ExtendedMaterials.ini @@ -1,2 +1,2 @@ [Info] -Version = 1-0-2 \ No newline at end of file +Version = 1-1-0 \ No newline at end of file diff --git a/features/Grass Collision/Shaders/Features/GrassCollision.ini b/features/Grass Collision/Shaders/Features/GrassCollision.ini index e8de3c616b..5a20fcd76d 100644 --- a/features/Grass Collision/Shaders/Features/GrassCollision.ini +++ b/features/Grass Collision/Shaders/Features/GrassCollision.ini @@ -1,2 +1,2 @@ [Info] -Version = 2-1-0 \ No newline at end of file +Version = 3-0-2 \ No newline at end of file diff --git a/features/Grass Collision/Shaders/GrassCollision/CollisionUpdateCS.hlsl b/features/Grass Collision/Shaders/GrassCollision/CollisionUpdateCS.hlsl new file mode 100644 index 0000000000..3b9e06c12b --- /dev/null +++ b/features/Grass Collision/Shaders/GrassCollision/CollisionUpdateCS.hlsl @@ -0,0 +1,109 @@ + +#include "Common/FrameBuffer.hlsli" +#include "Common/SharedData.hlsli" + +cbuffer PerFrameCB : register(b0) +{ + float2 PosOffset; // cell origin in camera space + uint2 ArrayOrigin; // xy: array origin (clipmap wrapping) + + int2 ValidMargin; + float TimeDelta; + uint BoundingBoxCount; + + float CameraHeightDelta; +} + +struct BoundingBoxPacked +{ + float2 MinExtent; + float2 MaxExtent; + uint IndexStart; + uint IndexEnd; + float2 pad0; +}; + +StructuredBuffer CollisionBoundingBoxes : register(t0); + +StructuredBuffer CollisionInstances : register(t1); + +RWTexture2D Collision : register(u0); + +groupshared BoundingBoxPacked SharedBoundingBoxes[64]; + +[numthreads(8, 8, 1)] void main( + uint3 groupId + : SV_GroupID, uint3 dispatchThreadId + : SV_DispatchThreadID, uint3 groupThreadId + : SV_GroupThreadID, uint groupIndex + : SV_GroupIndex) { + + if (groupIndex < BoundingBoxCount) + SharedBoundingBoxes[groupIndex] = CollisionBoundingBoxes[groupIndex]; + + GroupMemoryBarrierWithGroupSync(); + + const uint TEXTURE_SIZE = 512; + const float WORLD_SIZE = 4096; + float2 ZRANGE = float2(2048.0, -2048.0); + + uint2 cellID = uint2(max(int2(dispatchThreadId.xy) - ArrayOrigin, 0) % TEXTURE_SIZE); + + float2 cellCentreMS = cellID + 0.5 - TEXTURE_SIZE / 2; + cellCentreMS = cellCentreMS / TEXTURE_SIZE * WORLD_SIZE + PosOffset.xy; + + // Check if the cell is newly added + uint2 validMin = (uint2)max(0, ValidMargin.xy); + uint2 validMax = TEXTURE_SIZE - 1 + (uint2)min(0, ValidMargin.xy); + bool isValid = all(cellID >= validMin) && all(cellID <= validMax); + + float2 collision = max(ZRANGE.x, ZRANGE.y); + float2 previousCollision = collision; + + float2 fadeRate = TimeDelta * 100 * float2(0.01, 1.0); + + if (isValid) { + previousCollision = Collision[dispatchThreadId.xy]; + previousCollision = lerp(ZRANGE.x, ZRANGE.y, previousCollision); + + // Apply camera height change + previousCollision += CameraHeightDelta; + + // Temporal decay + collision = previousCollision + fadeRate; + } + + for (uint i = 0; i < BoundingBoxCount; i++) { + BoundingBoxPacked boundingBox = SharedBoundingBoxes[i]; + // Test high level collision + if (all(cellCentreMS >= boundingBox.MinExtent && cellCentreMS <= boundingBox.MaxExtent)){ + // Process collision data + for (uint j = boundingBox.IndexStart; j < boundingBox.IndexEnd; j++) { + float4 collisionInstance = CollisionInstances[j]; + float radius = collisionInstance.w; + // Check if collision can lower the height + if (collisionInstance.z - radius < collision.y){ + // Get the lowest point of the sphere at this cell position + float dist = distance(collisionInstance.xy, cellCentreMS); + // Check if we're within the sphere's radius + if (dist < radius) { + // Get sphere geometry + float heightFromCenter = sqrt(radius * radius - dist * dist); + float height = collisionInstance.z - heightFromCenter; + + collision.x = min(collision.x, height); + + if (height < collision.y) { + collision.y = height; + } + } + } + } + } + } + + collision = (collision - ZRANGE.x) / (ZRANGE.y - ZRANGE.x); + previousCollision = (previousCollision - ZRANGE.x) / (ZRANGE.y - ZRANGE.x); + + Collision[dispatchThreadId.xy] = float4(collision, previousCollision); +} \ No newline at end of file diff --git a/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli b/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli index c03e83e75b..75572ff19c 100644 --- a/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli +++ b/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli @@ -1,40 +1,169 @@ namespace GrassCollision { - struct CollisionData - { - float4 centre[2]; - }; + Texture2D Collision : register(t100); cbuffer GrassCollisionPerFrame : register(b5) { - CollisionData collisionData[256]; - uint numCollisions; + float2 PosOffset; // cell origin in camera space + uint2 ArrayOrigin; // xy: array origin (clipmap wrapping) + + int2 ValidMargin; + float TimeDelta; + uint BoundingBoxCount; + + float CameraHeightDelta; + } + + const static uint TEXTURE_SIZE = 512; + const static float WORLD_SIZE = 4096; + const static float CELL_SIZE = WORLD_SIZE / TEXTURE_SIZE; + const static float2 ZRANGE = float2(2048.0, -2048.0); + + float ProceduralAnimation(float x, float distanceFromCenter) { + float fadeRate = 250; + x /= fadeRate; + x /= distanceFromCenter; + x *= 100; + float frequency = 4 * Math::PI; + return cos(x * frequency) * exp(-x * 4); } - float3 GetDisplacedPosition(VS_INPUT input, float3 position, uint eyeIndex = 0) + void GetCollision(float3 worldPosition, float maximumDepth, float distanceFromCenter, out float collisionHeights, out float collisionAmount, out float previousCollisionHeights, out float previousCollisionAmount) { - float3 worldPosition = mul(World[eyeIndex], float4(position, 1.0)).xyz; - float alpha = pow(saturate(input.Color.w), 0.25); - - if (length(worldPosition) < 2048.0 && alpha > 0.0) { - float3 displacement = 0.0; - - for (uint i = 0; i < numCollisions; i++) { - float dist = distance(collisionData[i].centre[eyeIndex].xyz, worldPosition); - float power = 1.0 - saturate(dist / collisionData[i].centre[0].w); - float3 direction = worldPosition - collisionData[i].centre[eyeIndex].xyz; - float3 shift = power * power * direction; - shift.xy *= 1.0 + saturate(shift.z); - shift.z = min(shift.z, 0); - displacement += shift; - displacement -= length(shift); - } - - displacement *= alpha * 0.5; - - return displacement; + float2 positionMSAdjusted = worldPosition.xy - PosOffset.xy; + float2 uv = positionMSAdjusted / WORLD_SIZE + .5; + + float2 cellVxCoord = uv * TEXTURE_SIZE; + int2 cell000 = floor(cellVxCoord - 0.5); + float2 bilinearPos = cellVxCoord - 0.5 - cell000; + + int2 cellID = cell000; + + collisionHeights = 0.0; + collisionAmount = 0.0; + + previousCollisionHeights = 0.0; + previousCollisionAmount = 0.0; + + float wsum = 0; + + for (int i = 0; i < 2; i++) + for (int j = 0; j < 2; j++) + { + int2 offset = int2(i, j); + int2 cellID = cell000 + offset; + + if (any(cellID < 0) || any((uint2)cellID >= TEXTURE_SIZE)) + continue; + + float2 cellCentreMS = cellID + 0.5 - TEXTURE_SIZE / 2; + cellCentreMS = cellCentreMS * CELL_SIZE; + + float2 bilinearWeights = 1 - abs(offset - bilinearPos); + float w = bilinearWeights.x * bilinearWeights.y; + + uint2 cellTexID = (cellID + ArrayOrigin.xy) % TEXTURE_SIZE; + + float4 collisionSample = Collision[cellTexID]; + collisionSample = lerp(ZRANGE.x, ZRANGE.y, collisionSample); + + collisionHeights += collisionSample.x * w; + collisionAmount += max(0, min(maximumDepth, worldPosition.z - collisionSample.x)) * ProceduralAnimation(collisionSample.y - collisionSample.x, distanceFromCenter) * w; + + previousCollisionHeights += collisionSample.z * w; + previousCollisionAmount += max(0, min(maximumDepth, worldPosition.z - collisionSample.z)) * ProceduralAnimation(collisionSample.w - collisionSample.z, distanceFromCenter) * w; + + wsum += w; } - return 0.0; + if (wsum > 0.0){ + collisionHeights /= wsum; + collisionAmount /= wsum; + previousCollisionHeights /= wsum; + previousCollisionAmount /= wsum; + } else { + collisionHeights = TEXTURE_SIZE; + collisionAmount = 0.0; + previousCollisionHeights = TEXTURE_SIZE; + previousCollisionAmount = 0.0; + } + + } + + float3 ComputeNormalFromHeights(float h0, float hX, float hY, float delta) + { + float3 tangentX = float3(delta, 0, hX - h0); + float3 tangentY = float3(0, delta, hY - h0); + float3 crossProd = cross(tangentX, tangentY) * float3(1.0, 1.0, 0.1); + + float lenSq = dot(crossProd, crossProd); + return lenSq > 1e-12 ? -crossProd * rsqrt(lenSq) : float3(0, 0, -1); + } + + void ComputeCollision(float3 worldPosition, float maximumDepth, float distanceFromCenter, float delta, out float3 collision, out float3 previousCollision) + { + // Sample collision at three points forming a small triangle + float collisionCenter; + float collisionX; + float collisionY; + + float collisionCenterAmount; + float collisionXAmount; + float collisionYAmount; + + float previousCollisionCenter; + float previousCollisionX; + float previousCollisionY; + + float previousCollisionCenterAmount; + float previousCollisionXAmount; + float previousCollisionYAmount; + + GetCollision(worldPosition + float3(-delta, -delta, 0), maximumDepth, distanceFromCenter, collisionCenter, collisionCenterAmount, previousCollisionCenter, previousCollisionCenterAmount); + GetCollision(worldPosition + float3(delta, 0, 0), maximumDepth, distanceFromCenter, collisionX, collisionXAmount, previousCollisionX, previousCollisionXAmount); + GetCollision(worldPosition + float3(0, delta, 0), maximumDepth, distanceFromCenter, collisionY, collisionYAmount, previousCollisionY, previousCollisionYAmount); + + // Process current collision + float3 currentAmounts = float3(collisionCenterAmount, collisionXAmount, collisionYAmount); + float avgCurrentAmount = dot(currentAmounts, float3(1.0, 1.0, 1.0)) / 3.0; + collision = ComputeNormalFromHeights(collisionCenter, collisionX, collisionY, delta) * avgCurrentAmount; + + // Process previous collision + float3 previousAmounts = float3(previousCollisionCenterAmount, previousCollisionXAmount, previousCollisionYAmount); + float avgPreviousAmount = dot(previousAmounts, float3(1.0, 1.0, 1.0)) / 3.0; + previousCollision = ComputeNormalFromHeights(previousCollisionCenter, previousCollisionX, previousCollisionY, delta) * avgPreviousAmount; + } + + void GetDisplacedPosition(VS_INPUT input, float3 position, out float3 displacement, out float3 previousDisplacement) + { + float3 worldPosition = mul(World[0], float4(position.xyz, 1.0)).xyz; + float nearFactor = smoothstep(2048.0, 0.0, length(worldPosition)); + + if (input.Color.w > 0.0 && nearFactor > 0.0) { + float3 worldPositionCentre = mul(World[0], float4(input.InstanceData1.xyz, 1.0)).xyz; + + // Limit stretching + float3 remappedWorldPosition = lerp(worldPosition, worldPositionCentre, float3(0.95, 0.95, 0.0)); + + float distanceFromCenter = length(worldPosition - worldPositionCentre) + 0.01; + float maximumDepth = worldPosition.z - worldPositionCentre.z; + + // Return base collision + float3 collision, previousCollision; + ComputeCollision(remappedWorldPosition, maximumDepth, distanceFromCenter, CELL_SIZE, collision, previousCollision); + + // Do not let collision move upwards + collision.z = -abs(collision.z); + previousCollision.z = -abs(previousCollision.z); + + // Scale grass by wind amount (detect rocks and bottom of some grass) + float alpha = saturate(input.Color.w * 10.0); + + displacement = collision * alpha * nearFactor * 0.75; + previousDisplacement = previousCollision * alpha * nearFactor * 0.75; + } else { + displacement = 0.0; + previousDisplacement = 0.0; + } } -} +} \ No newline at end of file diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index e3c6a9ae10..d98b284bfd 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -67,6 +67,7 @@ namespace Hair const float NdotV = saturate(dot(N, V)); const float VNdotV = dot(VN, V); const float VNdotL = dot(VN, L); + const float satVNdotL = saturate(VNdotL); const float HdotV = saturate(dot(H, V)); const float HdotL = saturate(dot(H, L)); const float wrapped = 0.5; @@ -74,7 +75,7 @@ namespace Hair // [Yibing Jiang 2016, "The Process of Creating Volumetric-based Materials in Uncharted 4"] // https://advances.realtimerendering.com/s2016 dirDiffuse = saturate(oNdotL + wrapped) / (1 + wrapped); - float3 scatterColor = lerp(float3(0.992, 0.808, 0.518), baseColor, 0.5); + float3 scatterColor = pow(baseColor, 0.5); dirDiffuse = saturate(scatterColor + NdotL) * dirDiffuse * lightColor * SharedData::hairSpecularSettings.DiffuseMult; float3 TshiftPrimary; @@ -119,7 +120,7 @@ namespace Hair float cosThetaD = sqrt((1 + cosThetaL * cosThetaV + NdotV * NdotL) / 2.0); const float3 Lp = L - NdotL * N; - const float3 Vp = V - NdotL * N; + const float3 Vp = V - NdotV * N; const float cosPhi = dot(Lp, Vp) * rsqrt(dot(Lp, Lp) * dot(Vp, Vp) + EPSILON_DIVISION); const float cosHalfPhi = sqrt(saturate(0.5 + 0.5 * cosPhi)); @@ -185,7 +186,7 @@ namespace Hair const float wrap = 1; float wrappedNdotL = saturate((dot(fakeN, L) + wrap) / ((1 + wrap) * (1 + wrap))); float diffuseScatter = (1 / Math::PI) * lerp(wrappedNdotL, diffuseKajiya, 0.33); - float luma = Color::RGBToLuminance2(baseColor); + float luma = max(Color::RGBToLuminance(baseColor), 1e-4); float3 scatterTint = shadow < 1 ? pow(abs(baseColor / luma), 1 - shadow) : 1; S += sqrt(baseColor) * diffuseScatter * scatterTint; @@ -205,74 +206,51 @@ namespace Hair T = ShiftTangent(T, N, shift); } - const float cosThetaV = dot(VN, V); - float backlit = SharedData::hairSpecularSettings.Transmission; - dirSpecular += D_Marschner(L, V, T, roughness, baseColor, 0, backlit) * lightColor * SharedData::hairSpecularSettings.SpecularMult; + dirTransmission += D_Marschner(L, V, T, roughness, baseColor, 0, backlit) * lightColor * SharedData::hairSpecularSettings.SpecularMult; dirTransmission += GetHairDiffuseAttenuationKajiyaKay(T, V, L, selfShadow, baseColor) * lightColor * SharedData::hairSpecularSettings.DiffuseMult; } - void GetHairDirectLight(out float3 dirDiffuse, out float3 dirSpecular, out float3 dirTransmission, float3 T, float3 L, float3 V, float3 N, float3 VN, float3 lightColor, float shininess, float selfShadow, float2 uv, float3 baseColor) + void GetHairDirectLight(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) { + const float3 T = normalize(context.worldNormal); + const float3 V = normalize(context.viewDir); + const float3 N = normalize(context.vertexNormal); + const float3 VN = normalize(tbnTr[2]); + const float3 L = normalize(context.lightDir); + if (SharedData::hairSpecularSettings.HairMode == 0) { - GetHairDirectLightScheuermann(dirDiffuse, dirSpecular, dirTransmission, T, L, V, N, VN, lightColor, shininess, selfShadow, uv, baseColor); + GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context.lightColor, material.Shininess, context.hairShadow, uv, material.BaseColor); } else { - GetHairDirectLightMarschner(dirDiffuse, dirSpecular, dirTransmission, T, L, V, N, VN, lightColor, shininess, selfShadow, uv, baseColor); + GetHairDirectLightMarschner(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context.lightColor, material.Shininess, context.hairShadow, uv, material.BaseColor); } } - void GetHairIndirectSpecularLobeWeights(out float3 diffuseLobeWeight, out float3 specularLobeWeightPrimary, out float3 specularLobeWeightSecondary, float3 T, float3 N, float3 V, float3 VN, float shininess, float2 uv, float3 baseColor) + void GetHairIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext context, MaterialProperties material, float2 uv) { - const float roughnessPrimary = pow(abs(2.0 / (shininess + 2.0)), 0.25); - const float roughnessSecondary = pow(abs(2.0 / (shininess * 0.5 + 2.0)), 0.25); - const float NdotV = saturate(dot(N, V)); + lobeWeights = (IndirectLobeWeights)0; - if (SharedData::hairSpecularSettings.HairMode == 1) { - specularLobeWeightPrimary = 0; - specularLobeWeightSecondary = 0; - float3 L = normalize(V - N * dot(V, N)); + float3 T = normalize(context.worldNormal); + const float3 V = normalize(context.viewDir); + const float3 N = normalize(context.vertexNormal); + if (SharedData::hairSpecularSettings.HairMode == 1) { if (SharedData::hairSpecularSettings.EnableTangentShift) { const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5; T = ShiftTangent(T, N, shift); } + float3 L = normalize(V - T * dot(V, T)); - specularLobeWeightPrimary = D_Marschner(L, V, T, roughnessPrimary, baseColor, 0.2, 0) * Math::PI; - diffuseLobeWeight = GetHairDiffuseAttenuationKajiyaKay(T, V, L, 1, baseColor) * Math::PI; + lobeWeights.diffuse = D_Marschner(L, V, T, 1 - saturate(material.Shininess * 0.01), material.BaseColor, 0.2, 0) * Math::PI * SharedData::hairSpecularSettings.SpecularIndirectMult; + lobeWeights.diffuse += GetHairDiffuseAttenuationKajiyaKay(T, V, L, 1, material.BaseColor) * Math::PI * SharedData::hairSpecularSettings.DiffuseIndirectMult; return; } else { - float NdotVshifted = NdotV; - float NdotVshifted2 = NdotV; - - if (SharedData::hairSpecularSettings.EnableTangentShift) { - const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5; - NdotVshifted = saturate(dot(ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.PrimaryTangentShift), V)); - NdotVshifted2 = saturate(dot(ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.SecondaryTangentShift), V)); - } - - diffuseLobeWeight = baseColor; - specularLobeWeightPrimary = 0; - specularLobeWeightSecondary = 0; - - const float2 specularBRDFPrimary = BRDF::EnvBRDF(roughnessPrimary, NdotVshifted); - const float2 specularBRDFSecondary = BRDF::EnvBRDF(roughnessSecondary, NdotVshifted2); - - const float3 F0 = HairF0(); - specularLobeWeightPrimary = F0 * specularBRDFPrimary.x + specularBRDFPrimary.y; - diffuseLobeWeight *= (1 - specularLobeWeightPrimary); - diffuseLobeWeight = saturate(diffuseLobeWeight); - specularLobeWeightPrimary *= 1 + F0 * (1 / (specularBRDFPrimary.x + specularBRDFPrimary.y) - 1); - - specularLobeWeightSecondary = F0 * specularBRDFSecondary.x + specularBRDFSecondary.y; - specularLobeWeightSecondary *= 1 + F0 * (1 / (specularBRDFSecondary.x + specularBRDFSecondary.y) - 1); - specularLobeWeightSecondary *= baseColor; - - float3 R = reflect(-V, N); - float horizon = min(1.0 + dot(R, VN), 1.0); - horizon = horizon * horizon; - specularLobeWeightPrimary *= horizon; - specularLobeWeightSecondary *= horizon; + lobeWeights.diffuse = saturate(material.BaseColor * SharedData::hairSpecularSettings.DiffuseIndirectMult); + float2 hairBRDF = BRDF::EnvBRDF(material.Roughness, saturate(dot(N, V))); + float3 hairSpecularLobe = material.F0 * hairBRDF.x + hairBRDF.y; + lobeWeights.diffuse *= (1 - hairSpecularLobe); + lobeWeights.specular = saturate(hairSpecularLobe * SharedData::hairSpecularSettings.SpecularIndirectMult); } } @@ -318,36 +296,5 @@ namespace Hair } return lerp(1.0, shadow, SharedData::hairSpecularSettings.SelfShadowStrength); } - -#if defined(DYNAMIC_CUBEMAPS) -# if defined(SKYLIGHTING) - float3 GetHairDynamicCubemapSpecularIrradiance(float2 uv, float2 ScreenUV, float3 T, float3 N, float3 VN, float3 V, float glossiness, float3 specLobePrim, float3 specLobeSec, sh2 skylighting) -# else - float3 GetHairDynamicCubemapSpecularIrradiance(float2 uv, float2 ScreenUV, float3 T, float3 N, float3 VN, float3 V, float glossiness, float3 specLobePrim, float3 specLobeSec) -# endif - { - float3 SpecularIrradiance = 0; - float3 N1 = N; - float3 N2 = N; - - const float roughnessPrimary = SharedData::hairSpecularSettings.HairMode == 1 ? 1.0 : pow(abs(2.0 / (glossiness + 2.0)), 0.25); - const float roughnessSecondary = pow(abs(2.0 / (glossiness * 0.5 + 2.0)), 0.25); - - if (SharedData::hairSpecularSettings.EnableTangentShift) { - const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5; - N1 = ShiftNormal(T, N, shift + (SharedData::hairSpecularSettings.HairMode == 1 ? 0.0 : SharedData::hairSpecularSettings.PrimaryTangentShift)); - N2 = ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.SecondaryTangentShift); - } - -# if defined(SKYLIGHTING) - SpecularIrradiance += DynamicCubemaps::GetDynamicCubemapSpecularIrradiance(ScreenUV, N1, VN, V, roughnessPrimary, skylighting) * specLobePrim; - SpecularIrradiance += DynamicCubemaps::GetDynamicCubemapSpecularIrradiance(ScreenUV, N2, VN, V, roughnessSecondary, skylighting) * specLobeSec; -# else - SpecularIrradiance += DynamicCubemaps::GetDynamicCubemapSpecularIrradiance(ScreenUV, N1, VN, V, roughnessPrimary) * specLobePrim; - SpecularIrradiance += DynamicCubemaps::GetDynamicCubemapSpecularIrradiance(ScreenUV, N2, VN, V, roughnessSecondary) * specLobeSec; -# endif - return SpecularIrradiance; - } -#endif } #endif //__HAIR_DEPENDENCY_HLSL__ \ No newline at end of file diff --git a/features/IBL/Shaders/IBL/DiffuseIBLCS.hlsl b/features/IBL/Shaders/IBL/DiffuseIBLCS.hlsl index 4909a7ac0a..1adcd02b34 100644 --- a/features/IBL/Shaders/IBL/DiffuseIBLCS.hlsl +++ b/features/IBL/Shaders/IBL/DiffuseIBLCS.hlsl @@ -3,14 +3,9 @@ #include "Common/SharedData.hlsli" #include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" -TextureCube ReflectionTexture : register(t0); +TextureCube EnvTexture : register(t0); RWTexture2D IBLTexture : register(u0); -#if defined(DYNAMIC_CUBEMAPS) -TextureCube EnvTexture : register(t1); -TextureCube EnvReflectionsTexture : register(t2); -#endif - SamplerState LinearSampler : register(s0); // Performance optimization: Use 16x16 samples (256 total) instead of 32x32 (1024) @@ -40,21 +35,7 @@ void main(uint3 dispatchID : SV_DispatchThreadID, uint groupIndex : SV_GroupInde float3 rayDir = SphericalHarmonics::GetUniformSphereSample(sampleCoord.x, sampleCoord.y); // Sample cubemap with optimized direction -#if defined(DYNAMIC_CUBEMAPS) - float3 color = 0; - const float dcAmount = saturate(SharedData::iblSettings.DynamicCubemapsAmount); - if (dcAmount <= 0.001f) { - color = ReflectionTexture.SampleLevel(LinearSampler, -rayDir, 0).xyz; - } else if (dcAmount >= 0.999f) { - color = EnvReflectionsTexture.SampleLevel(LinearSampler, -rayDir, 0).xyz; - } else { - const float3 base = ReflectionTexture.SampleLevel(LinearSampler, -rayDir, 0).xyz; - const float3 dynamicCubemap = EnvReflectionsTexture.SampleLevel(LinearSampler, -rayDir, 0).xyz; - color = lerp(base, dynamicCubemap, dcAmount); - } -#else - float3 color = ReflectionTexture.SampleLevel(LinearSampler, -rayDir, 0).xyz; -#endif + float3 color = EnvTexture.SampleLevel(LinearSampler, -rayDir, 0).xyz; // Compute spherical harmonics basis for this direction sh2 sh = SphericalHarmonics::Evaluate(rayDir); diff --git a/features/IBL/Shaders/IBL/IBL.hlsli b/features/IBL/Shaders/IBL/IBL.hlsli index 75bfde122f..4845114efd 100644 --- a/features/IBL/Shaders/IBL/IBL.hlsli +++ b/features/IBL/Shaders/IBL/IBL.hlsli @@ -9,14 +9,14 @@ namespace ImageBasedLighting { -#if defined(IBL_AMBIENTCOMPOSITE) - Texture2D DiffuseIBLTexture : register(t8); -#elif defined(IBL_DEFERRED) +#if defined(IBL_DEFERRED) Texture2D DiffuseIBLTexture : register(t14); + Texture2D DiffuseSkyIBLTexture : register(t15); #else Texture2D DiffuseIBLTexture : register(t76); - TextureCube StaticDiffuseIBLTexture : register(t77); - TextureCube StaticSpecularIBLTexture : register(t78); + Texture2D DiffuseSkyIBLTexture : register(t77); + TextureCube StaticDiffuseIBLTexture : register(t78); + TextureCube StaticSpecularIBLTexture : register(t79); #endif float3 GetDiffuseIBL(float3 rayDir) { @@ -29,6 +29,41 @@ namespace ImageBasedLighting return float3(colorR, colorG, colorB) / Math::PI; } + float3 GetSkyDiffuseIBL(float3 rayDir) + { + sh2 shR = DiffuseSkyIBLTexture.Load(int3(0, 0, 0)); + sh2 shG = DiffuseSkyIBLTexture.Load(int3(1, 0, 0)); + sh2 shB = DiffuseSkyIBLTexture.Load(int3(2, 0, 0)); + float colorR = SphericalHarmonics::SHHallucinateZH3Irradiance(shR, rayDir); + float colorG = SphericalHarmonics::SHHallucinateZH3Irradiance(shG, rayDir); + float colorB = SphericalHarmonics::SHHallucinateZH3Irradiance(shB, rayDir); + return float3(colorR, colorG, colorB) / Math::PI; + } + +#if defined(SKYLIGHTING) && !defined(INTERIOR) + float3 GetIBLColor(float3 rayDir, float skylighting) +#else + float3 GetIBLColor(float3 rayDir) +#endif + { + float3 color = 0; + if (SharedData::InInterior) + { + color = GetDiffuseIBL(rayDir); + } + else +#if defined(SKYLIGHTING) + { + color = lerp(GetDiffuseIBL(rayDir), GetSkyDiffuseIBL(rayDir), skylighting); + } +#else + { + color = GetSkyDiffuseIBL(rayDir); + } +#endif + return color; + } + #if defined(LIGHTING) float3 GetStaticDiffuseIBL(float3 N, SamplerState samp) { @@ -39,7 +74,7 @@ namespace ImageBasedLighting float3 GetFogIBLColor(float3 fogColor) { float3 directionalAmbientColor = max(0, mul(SharedData::DirectionalAmbient, float4(float3(0, 0, 0), 1.0))).xyz; - float3 iblColor = directionalAmbientColor * SharedData::iblSettings.DALCAmount + Color::Saturation(GetDiffuseIBL(float3(0, 0, 0)), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; + float3 iblColor = directionalAmbientColor * SharedData::iblSettings.DALCAmount + Color::Saturation(GetSkyDiffuseIBL(float3(0, 0, 0)), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; if (SharedData::iblSettings.PreserveFogLuminance) { const float fogLuminance = Color::RGBToLuminance(fogColor); const float iblLuminance = Color::RGBToLuminance(iblColor); diff --git a/features/Light Limit Fix/CORE b/features/Light Limit Fix/CORE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/features/Light Limit Fix/ParticleLights/enblightglow.ini b/features/Light Limit Fix/ParticleLights/enblightglow.ini deleted file mode 100644 index ec6858e383..0000000000 --- a/features/Light Limit Fix/ParticleLights/enblightglow.ini +++ /dev/null @@ -1,2 +0,0 @@ -[Light] -Cull = true \ No newline at end of file diff --git a/features/Light Limit Fix/ParticleLights/fxglowenb.ini b/features/Light Limit Fix/ParticleLights/fxglowenb.ini deleted file mode 100644 index ec6858e383..0000000000 --- a/features/Light Limit Fix/ParticleLights/fxglowenb.ini +++ /dev/null @@ -1,2 +0,0 @@ -[Light] -Cull = true \ No newline at end of file diff --git a/features/Light Limit Fix/ParticleLights/glowsoft01_enbl.ini b/features/Light Limit Fix/ParticleLights/glowsoft01_enbl.ini deleted file mode 100644 index ec6858e383..0000000000 --- a/features/Light Limit Fix/ParticleLights/glowsoft01_enbl.ini +++ /dev/null @@ -1,2 +0,0 @@ -[Light] -Cull = true \ No newline at end of file diff --git a/features/Light Limit Fix/ParticleLights/po3_fullalpha.ini b/features/Light Limit Fix/ParticleLights/po3_fullalpha.ini deleted file mode 100644 index ec6858e383..0000000000 --- a/features/Light Limit Fix/ParticleLights/po3_fullalpha.ini +++ /dev/null @@ -1,2 +0,0 @@ -[Light] -Cull = true \ No newline at end of file diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl index e9b9f1466d..aa0f8d2145 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl +++ b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl @@ -4,10 +4,10 @@ cbuffer PerFrame : register(b0) { - float LightsNear; - float LightsFar; - uint2 pad0; - uint4 ClusterSize; + float LightsNear; + float LightsFar; + uint2 pad0; // Padding for 16-byte alignment: 8 -> 16 bytes + uint4 ClusterSize; } float3 GetPositionVS(float2 texcoord, float depth, int eyeIndex = 0) diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl index 29e98d297e..c4f0005162 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl +++ b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl @@ -3,9 +3,9 @@ cbuffer PerFrame : register(b0) { - uint LightCount; - uint3 pad; - uint4 ClusterSize; + uint LightCount; + uint3 pad0; // Padding for 16-byte alignment: 4 -> 16 bytes + uint4 ClusterSize; } //references diff --git a/features/Physical Sky/Shaders/PhysicalSky/Common.hlsli b/features/Physical Sky/Shaders/PhysicalSky/Common.hlsli index 78f13bf0fb..3b5a7c1c94 100644 --- a/features/Physical Sky/Shaders/PhysicalSky/Common.hlsli +++ b/features/Physical Sky/Shaders/PhysicalSky/Common.hlsli @@ -28,8 +28,8 @@ Texture2D TexMsLut : register(t1); Texture2D TexSvLut : register(t2); Texture3D TexApLut : register(t3); #elif defined(PS_DEFERRED_RSRCS) -Texture3D TexApLut : register(t15); -Texture2D TexApShadow : register(t16); +Texture3D TexApLut : register(t16); +Texture2D TexApShadow : register(t17); #else Texture2D TexTrLut : register(t61); Texture2D TexSvLut : register(t62); @@ -117,6 +117,28 @@ float2 SkyViewLutUv(float3 rayDir) return frac(float2(u, v)); } +// url: http://www.physics.hmc.edu/faculty/esin/a101/limbdarkening.pdf +float3 LimbDarkenHestroffer(float norm_dist) +{ + float mu = sqrt(1.0 - norm_dist * norm_dist); + + // coefficient for RGB wavelength (680, 550, 440) + float3 a0 = float3(0.34685, 0.26073, 0.15248); + float3 a1 = float3(1.37539, 1.27428, 1.38517); + float3 a2 = float3(-2.04425, -1.30352, -1.49615); + float3 a3 = float3(2.70493, 1.47085, 1.99886); + float3 a4 = float3(-1.94290, -0.96618, -1.48155); + float3 a5 = float3(0.55999, 0.26384, 0.44119); + + float mu2 = mu * mu; + float mu3 = mu2 * mu; + float mu4 = mu2 * mu2; + float mu5 = mu4 * mu; + + float3 factor = a0 + a1 * mu + a2 * mu2 + a3 * mu3 + a4 * mu4 + a5 * mu5; + return factor; +} + float3 InvSkyViewLutUv(float2 uv) { float azimuth = uv.x * 2 * Math::PI; diff --git a/features/RenderDoc/CORE b/features/RenderDoc/CORE new file mode 100644 index 0000000000..981ce0eb21 --- /dev/null +++ b/features/RenderDoc/CORE @@ -0,0 +1 @@ +# This file marks RenderDoc as a core feature for AIO packaging diff --git a/features/RenderDoc/README.md b/features/RenderDoc/README.md new file mode 100644 index 0000000000..b21da0fd5f --- /dev/null +++ b/features/RenderDoc/README.md @@ -0,0 +1,5 @@ +RenderDoc integration feature + +Place renderdoc.dll and any helper runtime DLLs into this folder before packaging. This folder is treated as a core feature and will be included into the AIO package. + +When installing to the game Data directory, the RenderDoc DLLs should end up in Renderdoc/ for runtime loading by the plugin. diff --git a/features/RenderDoc/Renderdoc/LICENSE.md b/features/RenderDoc/Renderdoc/LICENSE.md new file mode 100644 index 0000000000..bf230fa713 --- /dev/null +++ b/features/RenderDoc/Renderdoc/LICENSE.md @@ -0,0 +1,25 @@ +# The MIT License (MIT) + +Copyright (c) 2015-2025 Baldur Karlsson + +Copyright (c) 2014 Crytek + +Copyright (c) 1998-2018 [Third party code and tools](docs/credits_acknowledgements.rst) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/features/RenderDoc/Renderdoc/README.md b/features/RenderDoc/Renderdoc/README.md new file mode 100644 index 0000000000..03791b3af1 --- /dev/null +++ b/features/RenderDoc/Renderdoc/README.md @@ -0,0 +1,42 @@ +# RenderDoc Runtime DLL + +This directory contains the RenderDoc runtime library for frame capture functionality. + +## Version + +Current version: **1.40** + +## Source + +The `renderdoc.dll` file must be manually obtained from official RenderDoc releases: + +- Website: https://renderdoc.org/builds +- Direct download (MSI): https://renderdoc.org/stable/ +- GitHub releases: https://github.com/baldurk/renderdoc/releases/tag/ + +## Installation Steps + +1. Download the Windows x64 installer (MSI) from the link above +2. Install RenderDoc or extract the MSI using a tool like 7-Zip +3. Copy `renderdoc.dll` from the installation directory (typically `C:\Program Files\RenderDoc\`) +4. Place it in this directory (`features/RenderDoc/Renderdoc/`) +5. The DLL will be deployed with Community Shaders mod package + +## License + +RenderDoc is licensed under the MIT License. See LICENSE.md for details. + +## Updating + +To update to a newer version of RenderDoc: + +1. Update the vcpkg port version in `cmake/ports/renderdoc/vcpkg.json` +2. Update the REF in `cmake/ports/renderdoc/portfile.cmake` +3. Download the new Windows x64 installer from https://renderdoc.org/builds +4. Extract `renderdoc.dll` and replace it in this directory +5. Update the version number in this README +6. Verify LICENSE.md is current (check RenderDoc repository) + +## API Header + +The compile-time API header (`renderdoc_app.h`) is automatically managed by the vcpkg port at `cmake/ports/renderdoc/` and is fetched from the RenderDoc GitHub repository during build. diff --git a/features/RenderDoc/Renderdoc/renderdoc.dll b/features/RenderDoc/Renderdoc/renderdoc.dll new file mode 100644 index 0000000000..a8e33e9b48 Binary files /dev/null and b/features/RenderDoc/Renderdoc/renderdoc.dll differ diff --git a/features/RenderDoc/Shaders/Features/RenderDoc.ini b/features/RenderDoc/Shaders/Features/RenderDoc.ini new file mode 100644 index 0000000000..9bb09287c3 --- /dev/null +++ b/features/RenderDoc/Shaders/Features/RenderDoc.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 \ No newline at end of file diff --git a/features/Skylighting/Shaders/Features/Skylighting.ini b/features/Skylighting/Shaders/Features/Skylighting.ini index de2206be54..aa64ffab24 100644 --- a/features/Skylighting/Shaders/Features/Skylighting.ini +++ b/features/Skylighting/Shaders/Features/Skylighting.ini @@ -1,2 +1,2 @@ [Info] -Version = 1-1-1 \ No newline at end of file +Version = 1-2-2 \ No newline at end of file diff --git a/features/Skylighting/Shaders/Skylighting/Skylighting.hlsli b/features/Skylighting/Shaders/Skylighting/Skylighting.hlsli index ecffa681d1..0ae3be0eb5 100644 --- a/features/Skylighting/Shaders/Skylighting/Skylighting.hlsli +++ b/features/Skylighting/Shaders/Skylighting/Skylighting.hlsli @@ -8,7 +8,7 @@ namespace Skylighting { -#ifdef PSHADER +#if defined(PSHADER) Texture3D SkylightingProbeArray : register(t50); Texture2DArray stbn_vec3_2Dx1D_128x128x64 : register(t51); #endif @@ -35,6 +35,26 @@ namespace Skylighting return lerp(params.MinSpecularVisibility, 1.0, saturate(visibility)); } +#if defined(PSHADER) + void applySkylighting(inout float3 diffuseColor, inout float3 directionalAmbientColor, float3 albedo, float skylightingDiffuse) + { + float maxScale = 1.0; + if (directionalAmbientColor.x > 0.0) + maxScale = min(maxScale, diffuseColor.x / directionalAmbientColor.x); + if (directionalAmbientColor.y > 0.0) + maxScale = min(maxScale, diffuseColor.y / directionalAmbientColor.y); + if (directionalAmbientColor.z > 0.0) + maxScale = min(maxScale, diffuseColor.z / directionalAmbientColor.z); + directionalAmbientColor *= maxScale; + + diffuseColor = max(0.0, diffuseColor - directionalAmbientColor); + + directionalAmbientColor = Color::LinearToGamma(Color::GammaToLinear(directionalAmbientColor) * Color::MultiBounceAO(Color::GammaToLinear(albedo / Color::PBRLightingScale), skylightingDiffuse)); + + diffuseColor += directionalAmbientColor; + } +#endif + sh2 sample(SharedData::SkylightingSettings params, Texture3D probeArray, Texture2DArray blueNoise, float2 screenPosition, float3 positionMS, float3 normalWS) { const static sh2 unitSH = float4(sqrt(4 * Math::PI), 0, 0, 0); @@ -138,4 +158,4 @@ namespace Skylighting } } -#endif \ No newline at end of file +#endif diff --git a/features/Skylighting/Shaders/Skylighting/UpdateProbesCS.hlsl b/features/Skylighting/Shaders/Skylighting/UpdateProbesCS.hlsl index 916fcbcbba..32ec115d1a 100644 --- a/features/Skylighting/Shaders/Skylighting/UpdateProbesCS.hlsl +++ b/features/Skylighting/Shaders/Skylighting/UpdateProbesCS.hlsl @@ -12,8 +12,7 @@ SamplerComparisonState comparisonSampler : register(s0); const float fadeInThreshold = 15; const static sh2 unitSH = float4(sqrt(4.0 * Math::PI), 0, 0, 0); const SharedData::SkylightingSettings settings = SharedData::skylightingSettings; - - uint3 cellID = (uint3)max(int3(dtid) - settings.ArrayOrigin.xyz, 0) % Skylighting::ARRAY_DIM; + uint3 cellID = uint3(max(int3(dtid) - settings.ArrayOrigin.xyz, 0) % Skylighting::ARRAY_DIM); uint3 validMin = (uint3)max(0, settings.ValidMargin.xyz); uint3 validMax = Skylighting::ARRAY_DIM - 1 + (uint3)min(0, settings.ValidMargin.xyz); bool isValid = all(cellID >= validMin) && all(cellID <= validMax); // check if the cell is newly added diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli index 14dce4d7c4..34a44b08cc 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli @@ -108,7 +108,13 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA float2 sampleUV = texCoord + uvOffset; float2 clampedUV = clamp(sampleUV, float2(0.0f, 0.0f), float2(1.0f, 1.0f)); uint2 samplePixcoord = uint2(clampedUV * SharedData::BufferDim.xy); - float3 sampleColor = Color::GammaToLinear(ColorTexture[samplePixcoord].xyz / max(AlbedoTexture[samplePixcoord].xyz, EPSILON_SSS_ALBEDO)); + float maskSample = MaskTexture[samplePixcoord].x; + bool mask = maskSample > 1e-5f; + + if (!mask) + continue; + + float3 sampleColor = Color::GammaToLinear(ColorTexture[samplePixcoord].xyz * maskSample / max(AlbedoTexture[samplePixcoord].xyz, EPSILON_SSS_ALBEDO)); float sampleDepth = SharedData::GetScreenDepth(DepthTexture[samplePixcoord].x); float3 sampleNormalVS = GBuffer::DecodeNormal(NormalTexture[samplePixcoord].xy); float3 sampleNormalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(sampleNormalVS, 0)).xyz); @@ -116,12 +122,9 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA float deltaDepth = (sampleDepth - centerDepth) * 10.f / GAME_UNIT_TO_CM; // convert to mm float radiusSampledInMM = sqrt(radius * radius + deltaDepth * deltaDepth); - float maskSample = MaskTexture[samplePixcoord].x; - bool mask = maskSample > 1e-5f; - float3 diffusionProfile = GetBurleyProfile(diffuseMeanFreePath.xyz, s3d, radiusSampledInMM); float normalWeight = sqrt(saturate(dot(sampleNormalWS, normalWS) * 0.5f + 0.5f)); - float3 sampleWeight = mask ? (diffusionProfile / pdf) * normalWeight : 0.0f; + float3 sampleWeight = (diffusionProfile / pdf) * normalWeight * maskSample; colorSum += sampleWeight * sampleColor; weightSum += sampleWeight; @@ -130,6 +133,7 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA colorSum *= any(weightSum == 0.0f) ? 0.0f : (1.0f / weightSum); colorSum = lerp(colorSum, originalColor, saturate(centerWeight)); float3 color = Color::LinearToGamma(colorSum) * AlbedoTexture[DTid.xy].xyz; + color = lerp(centerColor.xyz, color, saturate(sssAmount)); float4 outColor = float4(color, ColorTexture[DTid.xy].w); return outColor; diff --git a/features/Upscaling/Shaders/Features/Upscaling.ini b/features/Upscaling/Shaders/Features/Upscaling.ini index 19f01444dc..de2206be54 100644 --- a/features/Upscaling/Shaders/Features/Upscaling.ini +++ b/features/Upscaling/Shaders/Features/Upscaling.ini @@ -1,2 +1,2 @@ [Info] -Version = 1-0-0 \ No newline at end of file +Version = 1-1-1 \ No newline at end of file diff --git a/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl b/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl index 58c2318c51..8ddddeae08 100644 --- a/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl +++ b/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl @@ -12,11 +12,7 @@ Texture2D MotionVectorMask : register(t2); Texture2D DepthMask : register(t3); RWTexture2D ReactiveMask : register(u0); - -#if defined(DLSS) || defined(FSR) RWTexture2D TransparencyCompositionMask : register(u1); -#endif - RWTexture2D MotionVectorOutput : register(u2); [numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { @@ -27,14 +23,14 @@ RWTexture2D MotionVectorOutput : register(u2); float2 taaMask = TAAMask[dispatchID.xy]; float transparencyCompositionMask = NormalsWaterMask[dispatchID.xy].z; +#if defined(DLSS) float depth = DepthMask[dispatchID.xy]; + float nearFactor = smoothstep(4096.0 * 2.5, 0.0, SharedData::GetScreenDepth(depth)); -#if defined(DLSS) || defined(XESS) // Find longest motion vector in 5x5 neighborhood float2 motionVector = MotionVectorMask[dispatchID.xy]; float2 longestMotionVector = motionVector; float maxMotionLengthSq = dot(motionVector, motionVector); -#endif [unroll] for (int y = -2; y <= 2; y++) { @@ -50,10 +46,6 @@ RWTexture2D MotionVectorOutput : register(u2); // Take neighbor if it's longer AND closer if (neighborDepth < depth){ - taaMask.x = min(taaMask.x, TAAMask[samplePos].x); - -#if defined(DLSS) || defined(XESS) - float2 neighborMotionVector = MotionVectorMask[samplePos]; // Square motion vector for length @@ -63,21 +55,15 @@ RWTexture2D MotionVectorOutput : register(u2); maxMotionLengthSq = motionLengthSq; longestMotionVector = neighborMotionVector; } -#endif } } } -#if defined(DLSS) || defined(XESS) - MotionVectorOutput[dispatchID.xy] = longestMotionVector; + MotionVectorOutput[dispatchID.xy] = lerp(longestMotionVector, motionVector, nearFactor); #endif -#if defined(DLSS) || defined(FSR) float reactiveMask = taaMask.x * 0.1 + taaMask.y; ReactiveMask[dispatchID.xy] = reactiveMask; + TransparencyCompositionMask[dispatchID.xy] = transparencyCompositionMask; -#else - float reactiveMask = taaMask.x * 0.01 + taaMask.y; - ReactiveMask[dispatchID.xy] = reactiveMask + transparencyCompositionMask * 0.1; -#endif } \ No newline at end of file diff --git a/features/Upscaling/Shaders/Upscaling/FidelityFX/amd_fidelityfx_upscaler_dx12.dll b/features/Upscaling/Shaders/Upscaling/FidelityFX/amd_fidelityfx_upscaler_dx12.dll deleted file mode 100644 index 4bd446fd7b..0000000000 Binary files a/features/Upscaling/Shaders/Upscaling/FidelityFX/amd_fidelityfx_upscaler_dx12.dll and /dev/null differ diff --git a/features/Upscaling/Shaders/Upscaling/RCAS/RCAS.hlsl b/features/Upscaling/Shaders/Upscaling/RCAS/RCAS.hlsl new file mode 100644 index 0000000000..50587525b2 --- /dev/null +++ b/features/Upscaling/Shaders/Upscaling/RCAS/RCAS.hlsl @@ -0,0 +1,116 @@ +// FidelityFX Super Resolution - Robust Contrast Adaptive Sharpening (RCAS) +// Based on https://github.com/GPUOpen-Effects/FidelityFX-FSR/blob/master/ffx-fsr/ffx_fsr1.h +// +// Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#define FSR_RCAS_LIMIT (0.25 - (1.0 / 16.0)) + +cbuffer RCASConfig : register(b0) +{ + float sharpness; + float3 pad; +}; + +Texture2D Source : register(t0); +RWTexture2D Dest : register(u0); + +[numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID) +{ + uint2 texDim; + Dest.GetDimensions(texDim.x, texDim.y); + + if (DTid.x >= texDim.x || DTid.y >= texDim.y) + return; + + // Algorithm uses minimal 3x3 pixel neighborhood. + // b + // d e f + // h + int2 sp = int2(DTid.xy); + float3 b = Source.Load(int3(sp + int2(0, -1), 0)).rgb; + float3 d = Source.Load(int3(sp + int2(-1, 0), 0)).rgb; + float3 e = Source.Load(int3(sp, 0)).rgb; + float3 f = Source.Load(int3(sp + int2(1, 0), 0)).rgb; + float3 h = Source.Load(int3(sp + int2(0, 1), 0)).rgb; + + // Rename (32-bit) or regroup (16-bit). + float bR = b.r; + float bG = b.g; + float bB = b.b; + float dR = d.r; + float dG = d.g; + float dB = d.b; + float eR = e.r; + float eG = e.g; + float eB = e.b; + float fR = f.r; + float fG = f.g; + float fB = f.b; + float hR = h.r; + float hG = h.g; + float hB = h.b; + + // Luma times 2. + float bL = bB * 0.5 + (bR * 0.5 + bG); + float dL = dB * 0.5 + (dR * 0.5 + dG); + float eL = eB * 0.5 + (eR * 0.5 + eG); + float fL = fB * 0.5 + (fR * 0.5 + fG); + float hL = hB * 0.5 + (hR * 0.5 + hG); + + // Noise detection. + float nz = 0.25 * bL + 0.25 * dL + 0.25 * fL + 0.25 * hL - eL; + nz = saturate(abs(nz) * rcp(max(max(max(bL, dL), max(eL, fL)), hL) - min(min(min(bL, dL), min(eL, fL)), hL))); + nz = -0.5 * nz + 1.0; + + // Min and max of ring. + float mn4R = min(min(min(bR, dR), fR), hR); + float mn4G = min(min(min(bG, dG), fG), hG); + float mn4B = min(min(min(bB, dB), fB), hB); + float mx4R = max(max(max(bR, dR), fR), hR); + float mx4G = max(max(max(bG, dG), fG), hG); + float mx4B = max(max(max(bB, dB), fB), hB); + + // Immediate constants for peak range. + float2 peakC = float2(1.0, -1.0 * 4.0); + + // Limiters, these need to be high precision RCPs. + float hitMinR = min(mn4R, eR) * rcp(4.0 * mx4R); + float hitMinG = min(mn4G, eG) * rcp(4.0 * mx4G); + float hitMinB = min(mn4B, eB) * rcp(4.0 * mx4B); + float hitMaxR = (peakC.x - max(mx4R, eR)) * rcp(4.0 * mn4R + peakC.y); + float hitMaxG = (peakC.x - max(mx4G, eG)) * rcp(4.0 * mn4G + peakC.y); + float hitMaxB = (peakC.x - max(mx4B, eB)) * rcp(4.0 * mn4B + peakC.y); + float lobeR = max(-hitMinR, hitMaxR); + float lobeG = max(-hitMinG, hitMaxG); + float lobeB = max(-hitMinB, hitMaxB); + float lobe = max(-FSR_RCAS_LIMIT, min(max(lobeR, max(lobeG, lobeB)), 0.0)) * sharpness; + + // Apply noise removal. + lobe *= nz; + + // Resolve, which needs the medium precision rcp approximation to avoid visible tonality changes. + float rcpL = rcp(4.0 * lobe + 1.0); + float pixR = (lobe * bR + lobe * dR + lobe * hR + lobe * fR + eR) * rcpL; + float pixG = (lobe * bG + lobe * dG + lobe * hG + lobe * fG + eG) * rcpL; + float pixB = (lobe * bB + lobe * dB + lobe * hB + lobe * fB + eB) * rcpL; + + Dest[DTid.xy] = float4(pixR, pixG, pixB, 1.0); +} diff --git a/features/Upscaling/Shaders/Upscaling/Streamline/sl.nis.dll b/features/Upscaling/Shaders/Upscaling/Streamline/sl.nis.dll new file mode 100644 index 0000000000..25553dc87d Binary files /dev/null and b/features/Upscaling/Shaders/Upscaling/Streamline/sl.nis.dll differ diff --git a/features/Upscaling/Shaders/Upscaling/XeSS/LICENSE.txt b/features/Upscaling/Shaders/Upscaling/XeSS/LICENSE.txt deleted file mode 100644 index e49c64eb9c..0000000000 --- a/features/Upscaling/Shaders/Upscaling/XeSS/LICENSE.txt +++ /dev/null @@ -1,27 +0,0 @@ -Intel Simplified Software License (Version October 2022) - -Intel(R) Xe Super Sampling (XeSS) SDK: Copyright (C) 2025 Intel Corporation - -Use and Redistribution. You may use and redistribute the software, which is provided in binary form only, (the "Software"), without modification, provided the following conditions are met: - -* Redistributions must reproduce the above copyright notice and these terms of use in the Software and in the documentation and/or other materials provided with the distribution. -* Neither the name of Intel nor the names of its suppliers may be used to endorse or promote products derived from this Software without specific prior written permission. -* No reverse engineering, decompilation, or disassembly of the Software is permitted, nor any modification or alteration of the Software or its operation at any time, including during execution. - -No other licenses. Except as provided in the preceding section, Intel grants no licenses or other rights by implication, estoppel or otherwise to, patent, copyright, trademark, trade name, service mark or other intellectual property licenses or rights of Intel. - -Third party software. "Third Party Software" means the files (if any) listed in the "third-party-software.txt" or other similarly-named text file that may be included with the Software. Third Party Software, even if included with the distribution of the Software, may be governed by separate license terms, including without limitation, third party license terms, open source software notices and terms, and/or other Intel software license terms. These separate license terms solely govern Your use of the Third Party Software. - -DISCLAIMER. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED. THIS SOFTWARE IS NOT INTENDED FOR USE IN SYSTEMS OR APPLICATIONS WHERE FAILURE OF THE SOFTWARE MAY CAUSE PERSONAL INJURY OR DEATH AND YOU AGREE THAT YOU ARE FULLY RESPONSIBLE FOR ANY CLAIMS, COSTS, DAMAGES, EXPENSES, AND ATTORNEYS' FEES ARISING OUT OF ANY SUCH USE, EVEN IF ANY CLAIM ALLEGES THAT INTEL WAS NEGLIGENT REGARDING THE DESIGN OR MANUFACTURE OF THE SOFTWARE. - -LIMITATION OF LIABILITY. IN NO EVENT WILL INTEL 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. - -No support. Intel may make changes to the Software, at any time without notice, and is not obligated to support, update or provide training for the Software. - -Termination. Your right to use the Software is terminated in the event of your breach of this license. - -Feedback. Should you provide Intel with comments, modifications, corrections, enhancements or other input (“Feedback”) related to the Software, Intel will be free to use, disclose, reproduce, license or otherwise distribute or exploit the Feedback in its sole discretion without any obligations or restrictions of any kind, including without limitation, intellectual property rights or licensing obligations. - -Compliance with laws. You agree to comply with all relevant laws and regulations governing your use, transfer, import or export (or prohibition thereof) of the Software. - -Governing law. All disputes will be governed by the laws of the United States of America and the State of Delaware without reference to conflict of law principles and subject to the exclusive jurisdiction of the state or federal courts sitting in the State of Delaware, and each party agrees that it submits to the personal jurisdiction and venue of those courts and waives any objections. THE UNITED NATIONS CONVENTION ON CONTRACTS FOR THE INTERNATIONAL SALE OF GOODS (1980) IS SPECIFICALLY EXCLUDED AND WILL NOT APPLY TO THE SOFTWARE. diff --git a/features/Upscaling/Shaders/Upscaling/XeSS/libxess.dll b/features/Upscaling/Shaders/Upscaling/XeSS/libxess.dll deleted file mode 100644 index 8583438c67..0000000000 Binary files a/features/Upscaling/Shaders/Upscaling/XeSS/libxess.dll and /dev/null differ diff --git a/features/Water Effects/Shaders/WaterEffects/WaterParallax.hlsli b/features/Water Effects/Shaders/WaterEffects/WaterParallax.hlsli index dd8d34b2ba..36023b743f 100644 --- a/features/Water Effects/Shaders/WaterEffects/WaterParallax.hlsli +++ b/features/Water Effects/Shaders/WaterEffects/WaterParallax.hlsli @@ -86,4 +86,126 @@ namespace WaterEffects return parallaxOffsetTS.xy * parallaxAmount; } + +#if defined(FLOWMAP) + float GetFlowmapHeight(PS_INPUT input, float2 uvShift, float multiplier, float offset, float mipLevel) + { + FlowmapData flowData = GetFlowmapDataUV(input, uvShift); + float2 baseUV = offset + (flowData.flowVector - float2(multiplier * ((0.001 * ReflectionColor.w) * flowData.color.w), 0)); + return FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, baseUV, mipLevel).w; + } + + float GetFlowmapBlendedHeight(PS_INPUT input, float2 normalMul, float2 uvShift, float mipLevel) + { + float height0 = GetFlowmapHeight(input, uvShift, 9.92, 0, mipLevel); + float height1 = GetFlowmapHeight(input, float2(0, uvShift.y), 10.64, 0.27, mipLevel); + float height2 = GetFlowmapHeight(input, 0.0.xx, 8, 0, mipLevel); + float height3 = GetFlowmapHeight(input, float2(uvShift.x, 0), 8.48, 0.62, mipLevel); + + float blendedHeight = + normalMul.y * (normalMul.x * height2 + (1 - normalMul.x) * height3) + + (1 - normalMul.y) * (normalMul.x * height1 + (1 - normalMul.x) * height0); + + return blendedHeight; + } + + float GetFlowmapParallaxAmount(PS_INPUT input, float2 flowmapDims, float3 viewDirection) + { + float viewDotUp = -viewDirection.z; + + if (viewDotUp < 0.05) + return 0.0; + + float2 parallaxDir = viewDirection.xy / -viewDirection.z; + parallaxDir.y = -parallaxDir.y; + + float parallaxScale = 0.008 * saturate(viewDotUp * 2.0); + parallaxDir *= parallaxScale; + + float2 uvShiftPx = 1 / (128 * flowmapDims); + + int numSteps = (int)lerp(32.0, 8.0, viewDotUp); + float stepSize = rcp((float)numSteps); + + float currBound = 0.0; + float currHeight = 1.0; + float prevHeight = 1.0; + + [loop] for (int i = 0; i < numSteps && currHeight > currBound; i++) + { + prevHeight = currHeight; + currBound += stepSize; + + PS_INPUT offsetInput = input; + offsetInput.TexCoord3.xy = input.TexCoord3.xy + currBound * parallaxDir; + + float2 cellBlend = 0.5 + -(-0.5 + abs(frac(offsetInput.TexCoord2.zw * (64 * flowmapDims)) * 2 - 1)); + currHeight = 1.0 - GetFlowmapBlendedHeight(offsetInput, cellBlend, uvShiftPx, 0); + } + + float prevBound = currBound - stepSize; + float delta2 = prevBound - prevHeight; + float delta1 = currBound - currHeight; + float denominator = delta2 - delta1; + + return denominator != 0.0 ? (currBound * delta2 - prevBound * delta1) / denominator : currBound; + } + + float GetFlowmapParallaxHeight(PS_INPUT input, float2 currentOffset, float3 normalScalesRcp, float mipLevel) + { + float height = Normals01Tex.SampleLevel(Normals01Sampler, input.TexCoord1.xy + currentOffset * normalScalesRcp.x, mipLevel).w; + height *= NormalsAmplitude.x; + return 1.0 - height; + } + + float2 GetFlowmapParallaxUVOffset(PS_INPUT input, float3 viewDirection, float3 normalScalesRcp) + { + float2 parallaxOffsetTS = viewDirection.xy / -viewDirection.z; + parallaxOffsetTS *= 80.0; + + float2 textureDims; + Normals01Tex.GetDimensions(textureDims.x, textureDims.y); +#if defined(VR) + textureDims /= 16.0; +#else + textureDims /= 8.0; +#endif + float2 texCoordsPerSize = input.TexCoord1.xy * textureDims; + float2 dxSize = ddx(texCoordsPerSize); + float2 dySize = ddy(texCoordsPerSize); + float2 dTexCoords = dxSize * dxSize + dySize * dySize; + float minTexCoordDelta = max(dTexCoords.x, dTexCoords.y); + float mipLevel = max(0.5 * log2(minTexCoordDelta), 0); +#if defined(VR) + mipLevel += 4; +#else + mipLevel += 3; +#endif + + float stepSize = rcp(16.0); + float currBound = 0.0; + float currHeight = 1.0; + float prevHeight = 1.0; + + [loop] while (currHeight > currBound) + { + prevHeight = currHeight; + currBound += stepSize; + currHeight = GetFlowmapParallaxHeight(input, currBound * parallaxOffsetTS.xy, normalScalesRcp, mipLevel); + } + + float prevBound = currBound - stepSize; + float delta2 = prevBound - prevHeight; + float delta1 = currBound - currHeight; + float denominator = delta2 - delta1; + float parallaxAmount = (currBound * delta2 - prevBound * delta1) / denominator; + + return parallaxOffsetTS.xy * parallaxAmount; + } + + float2 GetFlowmapParallaxOffset(PS_INPUT input, float2 flowmapDimensions, float3 viewDirection, float3 normalScalesRcp) + { + return GetFlowmapParallaxUVOffset(input, viewDirection, normalScalesRcp); + } +#endif } diff --git a/include/FidelityFX/upscalers/include/ffx_upscale.h b/include/FidelityFX/upscalers/include/ffx_upscale.h deleted file mode 100644 index 2dd434e0cd..0000000000 --- a/include/FidelityFX/upscalers/include/ffx_upscale.h +++ /dev/null @@ -1,218 +0,0 @@ -// This file is part of the FidelityFX SDK. -// -// Copyright (C) 2025 Advanced Micro Devices, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and /or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#pragma once -#include "../../api/include/ffx_api.h" -#include "../../api/include/ffx_api_types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -enum FfxApiUpscaleQualityMode -{ - FFX_UPSCALE_QUALITY_MODE_NATIVEAA = 0, ///< Perform upscaling with a per-dimension upscaling ratio of 1.0x. - FFX_UPSCALE_QUALITY_MODE_QUALITY = 1, ///< Perform upscaling with a per-dimension upscaling ratio of 1.5x. - FFX_UPSCALE_QUALITY_MODE_BALANCED = 2, ///< Perform upscaling with a per-dimension upscaling ratio of 1.7x. - FFX_UPSCALE_QUALITY_MODE_PERFORMANCE = 3, ///< Perform upscaling with a per-dimension upscaling ratio of 2.0x. - FFX_UPSCALE_QUALITY_MODE_ULTRA_PERFORMANCE = 4 ///< Perform upscaling with a per-dimension upscaling ratio of 3.0x. -}; - -enum FfxApiCreateContextUpscaleFlags -{ - FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE = (1<<0), ///< A bit indicating if the input color data provided is using a high-dynamic range. - FFX_UPSCALE_ENABLE_DISPLAY_RESOLUTION_MOTION_VECTORS = (1<<1), ///< A bit indicating if the motion vectors are rendered at display resolution. - FFX_UPSCALE_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION = (1<<2), ///< A bit indicating that the motion vectors have the jittering pattern applied to them. - FFX_UPSCALE_ENABLE_DEPTH_INVERTED = (1<<3), ///< A bit indicating that the input depth buffer data provided is inverted [1..0]. - FFX_UPSCALE_ENABLE_DEPTH_INFINITE = (1<<4), ///< A bit indicating that the input depth buffer data provided is using an infinite far plane. - FFX_UPSCALE_ENABLE_AUTO_EXPOSURE = (1<<5), ///< A bit indicating if automatic exposure should be applied to input color data. - FFX_UPSCALE_ENABLE_DYNAMIC_RESOLUTION = (1<<6), ///< A bit indicating that the application uses dynamic resolution scaling. - FFX_UPSCALE_ENABLE_DEBUG_CHECKING = (1<<7), ///< A bit indicating that the runtime should check some API values and report issues. - FFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE = (1<<8), ///< A bit indicating that the color resource contains perceptual (gamma corrected) colors - FFX_UPSCALE_ENABLE_DEBUG_VISUALIZATION = (1<<9), ///< A bit indicating if debug visualization is allowed. (memory consumption could increase) -}; - -enum FfxApiDispatchFsrUpscaleFlags -{ - FFX_UPSCALE_FLAG_DRAW_DEBUG_VIEW = (1 << 0), ///< A bit indicating that the output resource will contain debug views with relevant information. - FFX_UPSCALE_FLAG_NON_LINEAR_COLOR_SRGB = (1 << 1), ///< A bit indicating that the input color resource contains perceptual sRGB colors - FFX_UPSCALE_FLAG_NON_LINEAR_COLOR_PQ = (1 << 2), ///< A bit indicating that the input color resource contains perceptual PQ colors -}; - -enum FfxApiDispatchUpscaleAutoreactiveFlags -{ - FFX_UPSCALE_AUTOREACTIVEFLAGS_APPLY_TONEMAP = (1<<0), - FFX_UPSCALE_AUTOREACTIVEFLAGS_APPLY_INVERSETONEMAP = (1<<1), - FFX_UPSCALE_AUTOREACTIVEFLAGS_APPLY_THRESHOLD = (1<<2), - FFX_UPSCALE_AUTOREACTIVEFLAGS_USE_COMPONENTS_MAX = (1<<3), -}; - -#define FFX_API_EFFECT_ID_UPSCALE 0x00010000u - -#define FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE 0x00010000u -struct ffxCreateContextDescUpscale -{ - ffxCreateContextDescHeader header; - uint32_t flags; ///< Zero or a combination of values from FfxApiCreateContextUpscaleFlags. - struct FfxApiDimensions2D maxRenderSize; ///< The maximum size that rendering will be performed at. - struct FfxApiDimensions2D maxUpscaleSize; ///< The size of the presentation resolution targeted by the upscaling process. - ffxApiMessage fpMessage; ///< A pointer to a function that can receive messages from the runtime. May be null. -}; - -#define FFX_API_DISPATCH_DESC_TYPE_UPSCALE 0x00010001u -struct ffxDispatchDescUpscale -{ - ffxDispatchDescHeader header; - void* commandList; ///< Command list to record upscaling rendering commands into. - struct FfxApiResource color; ///< Color buffer for the current frame (at render resolution). - struct FfxApiResource depth; ///< 32bit depth values for the current frame (at render resolution). - struct FfxApiResource motionVectors; ///< 2-dimensional motion vectors (at render resolution if FFX_FSR_ENABLE_DISPLAY_RESOLUTION_MOTION_VECTORS is not set). - struct FfxApiResource exposure; ///< Optional resource containing a 1x1 exposure value. - struct FfxApiResource reactive; ///< Optional resource containing alpha value of reactive objects in the scene. - struct FfxApiResource transparencyAndComposition; ///< Optional resource containing alpha value of special objects in the scene. - struct FfxApiResource output; ///< Output color buffer for the current frame (at presentation resolution). - struct FfxApiFloatCoords2D jitterOffset; ///< The subpixel jitter offset applied to the camera. - struct FfxApiFloatCoords2D motionVectorScale; ///< The scale factor to apply to motion vectors. - struct FfxApiDimensions2D renderSize; ///< The resolution that was used for rendering the input resources. - struct FfxApiDimensions2D upscaleSize; ///< The resolution that the upscaler will upscale to (optional, assumed maxUpscaleSize otherwise). - bool enableSharpening; ///< Enable an additional sharpening pass. - float sharpness; ///< The sharpness value between 0 and 1, where 0 is no additional sharpness and 1 is maximum additional sharpness. - float frameTimeDelta; ///< The time elapsed since the last frame (expressed in milliseconds). - float preExposure; ///< The pre exposure value (must be > 0.0f) - bool reset; ///< A boolean value which when set to true, indicates the camera has moved discontinuously. - float cameraNear; ///< The distance to the near plane of the camera. - float cameraFar; ///< The distance to the far plane of the camera. - float cameraFovAngleVertical; ///< The camera angle field of view in the vertical direction (expressed in radians). - float viewSpaceToMetersFactor; ///< The scale factor to convert view space units to meters - uint32_t flags; ///< Zero or a combination of values from FfxApiDispatchFsrUpscaleFlags. -}; - -#define FFX_API_QUERY_DESC_TYPE_UPSCALE_GETUPSCALERATIOFROMQUALITYMODE 0x00010002u -struct ffxQueryDescUpscaleGetUpscaleRatioFromQualityMode -{ - ffxQueryDescHeader header; - uint32_t qualityMode; ///< The desired quality mode for FSR upscaling. - float* pOutUpscaleRatio; ///< A pointer to a float which will hold the upscaling the per-dimension upscaling ratio. -}; - -#define FFX_API_QUERY_DESC_TYPE_UPSCALE_GETRENDERRESOLUTIONFROMQUALITYMODE 0x00010003u -struct ffxQueryDescUpscaleGetRenderResolutionFromQualityMode -{ - ffxQueryDescHeader header; - uint32_t displayWidth; ///< The target display resolution width. - uint32_t displayHeight; ///< The target display resolution height. - uint32_t qualityMode; ///< The desired quality mode for FSR upscaling. - uint32_t* pOutRenderWidth; ///< A pointer to a uint32_t which will hold the calculated render resolution width. - uint32_t* pOutRenderHeight; ///< A pointer to a uint32_t which will hold the calculated render resolution height. -}; - -#define FFX_API_QUERY_DESC_TYPE_UPSCALE_GETJITTERPHASECOUNT 0x00010004u -struct ffxQueryDescUpscaleGetJitterPhaseCount -{ - ffxQueryDescHeader header; - uint32_t renderWidth; ///< The render resolution width. - uint32_t displayWidth; ///< The output resolution width. - int32_t* pOutPhaseCount; ///< A pointer to a int32_t which will hold the jitter phase count for the scaling factor between renderWidth and displayWidth. -}; - -#define FFX_API_QUERY_DESC_TYPE_UPSCALE_GETJITTEROFFSET 0x00010005u -struct ffxQueryDescUpscaleGetJitterOffset -{ - ffxQueryDescHeader header; - int32_t index; ///< The index within the jitter sequence. - int32_t phaseCount; ///< The length of jitter phase. See ffxQueryDescFsrGetJitterPhaseCount. - float* pOutX; ///< A pointer to a float which will contain the subpixel jitter offset for the x dimension. - float* pOutY; ///< A pointer to a float which will contain the subpixel jitter offset for the y dimension. -}; - -#define FFX_API_DISPATCH_DESC_TYPE_UPSCALE_GENERATEREACTIVEMASK 0x00010006u -struct ffxDispatchDescUpscaleGenerateReactiveMask -{ - ffxDispatchDescHeader header; - void* commandList; ///< The FfxCommandList to record FSRUPSCALE rendering commands into. - struct FfxApiResource colorOpaqueOnly; ///< A FfxApiResource containing the opaque only color buffer for the current frame (at render resolution). - struct FfxApiResource colorPreUpscale; ///< A FfxApiResource containing the opaque+translucent color buffer for the current frame (at render resolution). - struct FfxApiResource outReactive; ///< A FfxApiResource containing the surface to generate the reactive mask into. - struct FfxApiDimensions2D renderSize; ///< The resolution that was used for rendering the input resources. - float scale; ///< A value to scale the output - float cutoffThreshold; ///< A threshold value to generate a binary reactive mask - float binaryValue; ///< A value to set for the binary reactive mask - uint32_t flags; ///< Flags to determine how to generate the reactive mask -}; - -#define FFX_API_CONFIGURE_DESC_TYPE_UPSCALE_KEYVALUE 0x00010007u -struct ffxConfigureDescUpscaleKeyValue -{ - ffxConfigureDescHeader header; - uint64_t key; ///< Configuration key, member of the FfxApiConfigureUpscaleKey enumeration. - uint64_t u64; ///< Integer value or enum value to set. - void* ptr; ///< Pointer to set or pointer to value to set. -}; - -enum FfxApiConfigureUpscaleKey -{ - FFX_API_CONFIGURE_UPSCALE_KEY_FVELOCITYFACTOR = 0, //Override constant buffer fVelocityFactor. The float value is casted from void * ptr. Value of 0.0f can improve temporal stability of bright pixels. Default value is 1.0f. Value is clamped to [0.0f, 1.0f]. - FFX_API_CONFIGURE_UPSCALE_KEY_FREACTIVENESSSCALE = 1, //Override constant buffer fReactivenessScale. The float value is casted from void * ptr. Meant for development purpose to test if writing a larger value to reactive mask, reduces ghosting. Default value is 1.0f. Value is clamped to [0.0f, +infinity]. - FFX_API_CONFIGURE_UPSCALE_KEY_FSHADINGCHANGESCALE = 2, //Override fShadingChangeScale. Increasing this scales fsr3.1 computed shading change value at read to have higher reactiveness. Default value is 1.0f. Value is clamped to [0.0f, +infinity]. - FFX_API_CONFIGURE_UPSCALE_KEY_FACCUMULATIONADDEDPERFRAME = 3, // Override constant buffer fAccumulationAddedPerFrame. Corresponds to amount of accumulation added per frame at pixel coordinate where disocclusion occured or when reactive mask value is > 0.0f. Decreasing this and drawing the ghosting object (IE no mv) to reactive mask with value close to 1.0f can decrease temporal ghosting. Decreasing this value could result in more thin feature pixels flickering. Default value is 0.333. Value is clamped to [0.0f, 1.0f]. - FFX_API_CONFIGURE_UPSCALE_KEY_FMINDISOCCLUSIONACCUMULATION = 4, //Override constant buffer fMinDisocclusionAccumulation. Increasing this value may reduce white pixel temporal flickering around swaying thin objects that are disoccluding one another often. Too high value may increase ghosting. A sufficiently negative value means for pixel coordinate at frame N that is disoccluded, add fAccumulationAddedPerFrame starting at frame N+2. Default value is -0.333. Value is clamped to [-1.0f, 1.0f]. -}; - -#define FFX_API_QUERY_DESC_TYPE_UPSCALE_GPU_MEMORY_USAGE 0x00010008u -struct ffxQueryDescUpscaleGetGPUMemoryUsage -{ - ffxQueryDescHeader header; - struct FfxApiEffectMemoryUsage* gpuMemoryUsageUpscaler; -}; - -#define FFX_API_QUERY_DESC_TYPE_UPSCALE_GPU_MEMORY_USAGE_V2 0x00010009u -struct ffxQueryDescUpscaleGetGPUMemoryUsageV2 -{ - ffxQueryDescHeader header; - void* device; ///< For DX12: pointer to ID3D12Device. For VK, pointer to VkDevice. App needs to fill out before Query() call. - struct FfxApiDimensions2D maxRenderSize; ///< App needs to fill out before Query() call. - struct FfxApiDimensions2D maxUpscaleSize; ///< App needs to fill out before Query() call. - uint32_t flags; ///< Zero or a combination of values from FfxApiCreateContextUpscaleFlags. App needs to fill out before Query() call. - struct FfxApiEffectMemoryUsage* gpuMemoryUsageUpscaler; ///< Output values by Query() call. -}; - -enum FfxApiQueryResourceIdentifiers -{ - FFX_API_QUERY_RESOURCE_INPUT_COLOR = (1<<0), // Color buffer for the current frame (at render resolution). - FFX_API_QUERY_RESOURCE_INPUT_DEPTH = (1<<1), // 32bit depth values for the current frame (at render resolution). - FFX_API_QUERY_RESOURCE_INPUT_MV = (1<<2), // 2-dimensional motion vectors (at render resolution if FFX_FSR_ENABLE_DISPLAY_RESOLUTION_MOTION_VECTORS is not set). - FFX_API_QUERY_RESOURCE_INPUT_EXPOSURE = (1<<3), // A 1x1 texture containing exposure value or the FFX_UPSCALE_ENABLE_AUTO_EXPOSURE set at context creation. - FFX_API_QUERY_RESOURCE_INPUT_REACTIVEMASK = (1<<4), // - FFX_API_QUERY_RESOURCE_INPUT_TRANSPARENCYCOMPOSITION = (1<<5), // -}; - -#define FFX_API_QUERY_DESC_TYPE_UPSCALE_GET_RESOURCE_REQUIREMENTS 0x0001000au -struct ffxQueryDescUpscaleGetResourceRequirements -{ - ffxQueryDescHeader header; - uint64_t required_resources; // resources 64b bitfield, that given current context state, are required for effect correctness. - uint64_t optional_resources; // resources 64b bitfield, that given current context state, will be consumed if provided, but are optional. -}; - -#ifdef __cplusplus -} -#endif diff --git a/include/FidelityFX/upscalers/include/ffx_upscale.hpp b/include/FidelityFX/upscalers/include/ffx_upscale.hpp deleted file mode 100644 index cb76fa9c4c..0000000000 --- a/include/FidelityFX/upscalers/include/ffx_upscale.hpp +++ /dev/null @@ -1,88 +0,0 @@ -// This file is part of the FidelityFX SDK. -// -// Copyright (C) 2025 Advanced Micro Devices, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and /or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#pragma once - -#include "../../api/include/ffx_api.hpp" -#include "ffx_upscale.h" - -// Helper types for header initialization. Api definition is in .h file. - -namespace ffx -{ - -template<> -struct struct_type : std::integral_constant {}; - -struct CreateContextDescUpscale : public InitHelper {}; - -template<> -struct struct_type : std::integral_constant {}; - -struct DispatchDescUpscale : public InitHelper {}; - -template<> -struct struct_type : std::integral_constant {}; - -struct QueryDescUpscaleGetUpscaleRatioFromQualityMode : public InitHelper {}; - -template<> -struct struct_type : std::integral_constant {}; - -struct QueryDescUpscaleGetRenderResolutionFromQualityMode : public InitHelper {}; - -template<> -struct struct_type : std::integral_constant {}; - -struct QueryDescUpscaleGetJitterPhaseCount : public InitHelper {}; - -template<> -struct struct_type : std::integral_constant {}; - -struct QueryDescUpscaleGetJitterOffset : public InitHelper {}; - -template<> -struct struct_type : std::integral_constant {}; - -struct DispatchDescUpscaleGenerateReactiveMask : public InitHelper {}; - -template<> -struct struct_type : std::integral_constant {}; - -struct ConfigureDescUpscaleKeyValue : public InitHelper {}; - -template<> -struct struct_type : std::integral_constant {}; - -struct QueryDescUpscaleGetGPUMemoryUsage : public InitHelper {}; - -template<> -struct struct_type : std::integral_constant {}; - -struct QueryDescUpscaleGetGPUMemoryUsageV2 : public InitHelper {}; - -template<> -struct struct_type : std::integral_constant {}; - -struct QueryDescUpscaleGetResourceRequirements : public InitHelper {}; - -} diff --git a/include/PCH.h b/include/PCH.h index 5c8b210c2c..654e026d28 100644 --- a/include/PCH.h +++ b/include/PCH.h @@ -57,6 +57,13 @@ namespace stl T::func = vtbl.write_vfunc(idx, T::thunk); } + template + void write_vfunc(REL::VariantOffset offset) + { + REL::Relocation vtbl{ offset }; + T::func = vtbl.write_vfunc(idx, T::thunk); + } + template void write_thunk_jmp(std::uintptr_t a_src) { @@ -157,7 +164,7 @@ namespace DX #include using json = nlohmann::json; -#include +#include #include #include diff --git a/template/New Feature/Shaders/Features/NewFeature.ini b/new-feature-template/New Feature/Shaders/Features/NewFeature.ini similarity index 100% rename from template/New Feature/Shaders/Features/NewFeature.ini rename to new-feature-template/New Feature/Shaders/Features/NewFeature.ini diff --git a/template/New Feature/Shaders/NewFeature/nonexistent.cs.hlsl b/new-feature-template/New Feature/Shaders/NewFeature/nonexistent.cs.hlsl similarity index 100% rename from template/New Feature/Shaders/NewFeature/nonexistent.cs.hlsl rename to new-feature-template/New Feature/Shaders/NewFeature/nonexistent.cs.hlsl diff --git a/template/NewFeature.cpp b/new-feature-template/NewFeature.cpp similarity index 100% rename from template/NewFeature.cpp rename to new-feature-template/NewFeature.cpp diff --git a/template/NewFeature.h b/new-feature-template/NewFeature.h similarity index 100% rename from template/NewFeature.h rename to new-feature-template/NewFeature.h diff --git a/new-feature-template/NewFeatureReadme.md b/new-feature-template/NewFeatureReadme.md new file mode 100644 index 0000000000..85c06059fa --- /dev/null +++ b/new-feature-template/NewFeatureReadme.md @@ -0,0 +1,100 @@ +# New Feature Development Reference + +Quick reference for creating new graphics features in Community Shaders. + +## File Structure + +Template files to copy and customize: + +- `template/NewFeature.h` → Copy to `src/Features/YourFeature.h` +- `template/NewFeature.cpp` → Copy to `src/Features/YourFeature.cpp` +- `template/New Feature/` → Copy to `features/YourFeature/` + +## Core System Registration + +These are the **required** files that must be modified for your feature to appear in settings: + +### 1. `src/Globals.h` + +**Forward Declaration** - Add at top with other feature declarations (~lines 1-30): + +struct YourFeature; + +**Feature Instance Declaration** - Add in `globals::features` namespace (~lines 51-80): + +extern YourFeature yourFeature; + +### 2. `src/Globals.cpp` + +**Feature Instance Definition** - Add in `globals::features` namespace (~lines 48-75): + +YourFeature yourFeature{}; + +### 3. `src/Feature.cpp` + +**Feature Registration** - Add to features vector in `GetFeatureList()` (~lines 200-225): + +&globals::features::yourFeature, + +## Template Customization + +### Class Names + +- Replace all `NewFeature` → `YourFeature` +- Replace all `"New Feature"` → `"Your Feature Name"` +- Replace all `"NewFeature"` → `"YourFeature"` + +### Metadata + +- `GetCategory()` → Choose: "Lighting", "Effects", "Rendering", "Performance", "Terrain", "Water", "Atmosphere" +- `GetFeatureModLink()` → Update Nexus mod ID +- `GetFeatureSummary()` → Update description and features list +- `GetShaderDefineName()` → Your preprocessor define name +- `HasShaderDefine()` → Which shader types use your define + +### Settings + +- Update `Settings` struct with your parameters +- Update `CbData` struct for shader constant buffer (must be 16-byte aligned) +- Update NLOHMANN serialization macro +- Customize `DrawSettings()` UI controls +- Update shader compilation paths in `CompileShaders()` + +### VR Support + +Set `SupportsVR()` return value: + +- `return true;` - Feature works in VR +- `return false;` - Feature disabled in VR builds + +## Naming Conventions + +| Component | Convention | Example | +| ------------------ | ---------- | ----------------------- | +| C++ Class | PascalCase | `YourFeature` | +| Instance Variable | camelCase | `yourFeature` | +| Display Name | Spaces | `"Your Feature Name"` | +| Short Name | PascalCase | `"YourFeature"` | +| Features Directory | PascalCase | `features/YourFeature/` | +| Shader Directory | PascalCase | `YourFeature/` | + +## Automatic Build Integration + +The build system automatically handles: + +- Shader compilation and validation +- Settings persistence (JSON serialization) +- UI menu integration +- Feature lifecycle management +- Cross-platform builds (SE/AE/VR) + +Build with: `./BuildRelease.bat ALL` + +## Testing Checklist + +- [ ] Feature appears in settings menu +- [ ] Settings save/load correctly +- [ ] Shaders compile without errors +- [ ] Feature works in-game +- [ ] VR compatibility (if enabled) +- [ ] No build errors diff --git a/package/Interface/CommunityShaders/Fonts/CrimsonPro/CrimsonPro-Light.ttf b/package/Interface/CommunityShaders/Fonts/CrimsonPro/CrimsonPro-Light.ttf new file mode 100644 index 0000000000..e0a6383241 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/CrimsonPro/CrimsonPro-Light.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/CrimsonPro/CrimsonPro-Regular.ttf b/package/Interface/CommunityShaders/Fonts/CrimsonPro/CrimsonPro-Regular.ttf new file mode 100644 index 0000000000..f5666b9beb Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/CrimsonPro/CrimsonPro-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/CrimsonPro/CrimsonPro-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/CrimsonPro/CrimsonPro-SemiBold.ttf new file mode 100644 index 0000000000..93e11c5049 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/CrimsonPro/CrimsonPro-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-Light.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-Light.ttf new file mode 100644 index 0000000000..0dcb2fba5b Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-Light.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-Regular.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-Regular.ttf new file mode 100644 index 0000000000..601ae945eb Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-SemiBold.ttf new file mode 100644 index 0000000000..5e0b41df1a Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexMono/OFL.txt b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/OFL.txt new file mode 100644 index 0000000000..924704d1ee --- /dev/null +++ b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/OFL.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans-Light.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans-Light.ttf new file mode 100644 index 0000000000..56e7db7dcc Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans-Light.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans-Regular.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans-Regular.ttf new file mode 100644 index 0000000000..5387ad48cc Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans-SemiBold.ttf new file mode 100644 index 0000000000..a63f1c5629 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans_Condensed-Light.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans_Condensed-Light.ttf new file mode 100644 index 0000000000..f8f53b9d26 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans_Condensed-Light.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans_Condensed-Regular.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans_Condensed-Regular.ttf new file mode 100644 index 0000000000..fd7f8a04da Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans_Condensed-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans_Condensed-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans_Condensed-SemiBold.ttf new file mode 100644 index 0000000000..a715d679a7 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/IBMPlexSans_Condensed-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSans/OFL.txt b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/OFL.txt new file mode 100644 index 0000000000..924704d1ee --- /dev/null +++ b/package/Interface/CommunityShaders/Fonts/IBMPlexSans/OFL.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/IBMPlexSerif-Light.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/IBMPlexSerif-Light.ttf new file mode 100644 index 0000000000..1bd25000e6 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/IBMPlexSerif-Light.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/IBMPlexSerif-Regular.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/IBMPlexSerif-Regular.ttf new file mode 100644 index 0000000000..35f454ceac Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/IBMPlexSerif-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/IBMPlexSerif-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/IBMPlexSerif-SemiBold.ttf new file mode 100644 index 0000000000..74b9b580a8 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/IBMPlexSerif-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/OFL.txt b/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/OFL.txt new file mode 100644 index 0000000000..924704d1ee --- /dev/null +++ b/package/Interface/CommunityShaders/Fonts/IBMPlexSerif/OFL.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/package/Interface/CommunityShaders/Fonts/Inter/Inter_24pt-Light.ttf b/package/Interface/CommunityShaders/Fonts/Inter/Inter_24pt-Light.ttf new file mode 100644 index 0000000000..1a2a6f252d Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Inter/Inter_24pt-Light.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Inter/Inter_24pt-Regular.ttf b/package/Interface/CommunityShaders/Fonts/Inter/Inter_24pt-Regular.ttf new file mode 100644 index 0000000000..6b088a7119 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Inter/Inter_24pt-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Inter/Inter_24pt-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/Inter/Inter_24pt-SemiBold.ttf new file mode 100644 index 0000000000..ceb8576abc Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Inter/Inter_24pt-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Inter/OFL.txt b/package/Interface/CommunityShaders/Fonts/Inter/OFL.txt new file mode 100644 index 0000000000..0a9f42111d --- /dev/null +++ b/package/Interface/CommunityShaders/Fonts/Inter/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/package/Interface/CommunityShaders/Fonts/Jost/Jost-Light.ttf b/package/Interface/CommunityShaders/Fonts/Jost/Jost-Light.ttf new file mode 100644 index 0000000000..8cf8779c93 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Jost/Jost-Light.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Jost-Regular.ttf b/package/Interface/CommunityShaders/Fonts/Jost/Jost-Regular.ttf similarity index 100% rename from package/Interface/CommunityShaders/Fonts/Jost-Regular.ttf rename to package/Interface/CommunityShaders/Fonts/Jost/Jost-Regular.ttf diff --git a/package/Interface/CommunityShaders/Fonts/Jost/Jost-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/Jost/Jost-SemiBold.ttf new file mode 100644 index 0000000000..b3a3c4fd1b Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Jost/Jost-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/OFL.txt b/package/Interface/CommunityShaders/Fonts/Jost/OFL.txt similarity index 100% rename from package/Interface/CommunityShaders/Fonts/OFL.txt rename to package/Interface/CommunityShaders/Fonts/Jost/OFL.txt diff --git a/package/Interface/CommunityShaders/Fonts/OpenDyslexic/OFL.txt b/package/Interface/CommunityShaders/Fonts/OpenDyslexic/OFL.txt new file mode 100644 index 0000000000..0a1d034b95 --- /dev/null +++ b/package/Interface/CommunityShaders/Fonts/OpenDyslexic/OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es), +with Reserved Font Name OpenDyslexic. +Copyright (c) 12/2012 - 2019 +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/package/Interface/CommunityShaders/Fonts/OpenDyslexic/OpenDyslexic3-Bold.ttf b/package/Interface/CommunityShaders/Fonts/OpenDyslexic/OpenDyslexic3-Bold.ttf new file mode 100644 index 0000000000..395dffc333 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/OpenDyslexic/OpenDyslexic3-Bold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/OpenDyslexic/OpenDyslexic3-Regular.ttf b/package/Interface/CommunityShaders/Fonts/OpenDyslexic/OpenDyslexic3-Regular.ttf new file mode 100644 index 0000000000..0ff4c0b58d Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/OpenDyslexic/OpenDyslexic3-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Roboto/OFL.txt b/package/Interface/CommunityShaders/Fonts/Roboto/OFL.txt new file mode 100644 index 0000000000..65a3057b1f --- /dev/null +++ b/package/Interface/CommunityShaders/Fonts/Roboto/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Bold.ttf b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Bold.ttf new file mode 100644 index 0000000000..4658f9a67b Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Bold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Regular.ttf b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Regular.ttf new file mode 100644 index 0000000000..7e3bb2f8ce Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-SemiBold.ttf new file mode 100644 index 0000000000..3f348341cb Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Thin.ttf b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Thin.ttf new file mode 100644 index 0000000000..6ee97b8895 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Thin.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-Light.ttf b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-Light.ttf new file mode 100644 index 0000000000..e70c357377 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-Light.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-Regular.ttf b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-Regular.ttf new file mode 100644 index 0000000000..5af42d4733 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-SemiBold.ttf new file mode 100644 index 0000000000..4297f17386 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/RobotoSlab/RobotoSlab-Light.ttf b/package/Interface/CommunityShaders/Fonts/RobotoSlab/RobotoSlab-Light.ttf new file mode 100644 index 0000000000..ee82cf71d5 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/RobotoSlab/RobotoSlab-Light.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/RobotoSlab/RobotoSlab-Regular.ttf b/package/Interface/CommunityShaders/Fonts/RobotoSlab/RobotoSlab-Regular.ttf new file mode 100644 index 0000000000..f163cfdab7 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/RobotoSlab/RobotoSlab-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/RobotoSlab/RobotoSlab-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/RobotoSlab/RobotoSlab-SemiBold.ttf new file mode 100644 index 0000000000..9d4584620f Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/RobotoSlab/RobotoSlab-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Rubik/OFL.txt b/package/Interface/CommunityShaders/Fonts/Rubik/OFL.txt new file mode 100644 index 0000000000..6d11c3af96 --- /dev/null +++ b/package/Interface/CommunityShaders/Fonts/Rubik/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/package/Interface/CommunityShaders/Fonts/Rubik/Rubik-Light.ttf b/package/Interface/CommunityShaders/Fonts/Rubik/Rubik-Light.ttf new file mode 100644 index 0000000000..8d82397b12 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Rubik/Rubik-Light.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Rubik/Rubik-Regular.ttf b/package/Interface/CommunityShaders/Fonts/Rubik/Rubik-Regular.ttf new file mode 100644 index 0000000000..e799407e13 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Rubik/Rubik-Regular.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Rubik/Rubik-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/Rubik/Rubik-SemiBold.ttf new file mode 100644 index 0000000000..b912562727 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Rubik/Rubik-SemiBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Sanguis/Sanguis OFL License.txt b/package/Interface/CommunityShaders/Fonts/Sanguis/Sanguis OFL License.txt new file mode 100644 index 0000000000..def39a2719 --- /dev/null +++ b/package/Interface/CommunityShaders/Fonts/Sanguis/Sanguis OFL License.txt @@ -0,0 +1,41 @@ +Copyright (c) 2018, mjorka (mjorka.net), +without Reserved Font Name. + +-——————————————————————— +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +-——————————————————————— + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +“Font Software” refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +“Reserved Font Name” refers to any names specified as such after the copyright statement(s). + +“Original Version” refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +“Modified Version” refers to any derivative made by adding to, deleting, or substituting – in part or in whole – any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +“Author” refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/package/Interface/CommunityShaders/Fonts/Sanguis/Sanguis.ttf b/package/Interface/CommunityShaders/Fonts/Sanguis/Sanguis.ttf new file mode 100644 index 0000000000..ecc566ecbc Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Sanguis/Sanguis.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Sovngarde/Sovngarde OFL License.txt b/package/Interface/CommunityShaders/Fonts/Sovngarde/Sovngarde OFL License.txt new file mode 100644 index 0000000000..1b363709e3 --- /dev/null +++ b/package/Interface/CommunityShaders/Fonts/Sovngarde/Sovngarde OFL License.txt @@ -0,0 +1,41 @@ +Copyright (c) 2016, mjorka (mjorka.net), +without Reserved Font Name. + +-——————————————————————— +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +-——————————————————————— + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +“Font Software” refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +“Reserved Font Name” refers to any names specified as such after the copyright statement(s). + +“Original Version” refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +“Modified Version” refers to any derivative made by adding to, deleting, or substituting – in part or in whole – any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +“Author” refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/package/Interface/CommunityShaders/Fonts/Sovngarde/SovngardeBold.ttf b/package/Interface/CommunityShaders/Fonts/Sovngarde/SovngardeBold.ttf new file mode 100644 index 0000000000..f13dd072d1 Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Sovngarde/SovngardeBold.ttf differ diff --git a/package/Interface/CommunityShaders/Fonts/Sovngarde/SovngardeLight.ttf b/package/Interface/CommunityShaders/Fonts/Sovngarde/SovngardeLight.ttf new file mode 100644 index 0000000000..bf690797cd Binary files /dev/null and b/package/Interface/CommunityShaders/Fonts/Sovngarde/SovngardeLight.ttf differ diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/clear-cache.png b/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/clear-cache.png new file mode 100644 index 0000000000..57876169cf Binary files /dev/null and b/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/clear-cache.png differ diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/load-settings.png b/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/load-settings.png new file mode 100644 index 0000000000..dee0a7072b Binary files /dev/null and b/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/load-settings.png differ diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/restore-settings.png b/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/restore-settings.png new file mode 100644 index 0000000000..029f7d0b9c Binary files /dev/null and b/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/restore-settings.png differ diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/save-settings.png b/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/save-settings.png new file mode 100644 index 0000000000..194fa7400e Binary files /dev/null and b/package/Interface/CommunityShaders/Icons/Action Icons/Monochrome/save-settings.png differ diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/discord.png b/package/Interface/CommunityShaders/Icons/Action Icons/discord.png index 670b9b4038..3654718739 100644 Binary files a/package/Interface/CommunityShaders/Icons/Action Icons/discord.png and b/package/Interface/CommunityShaders/Icons/Action Icons/discord.png differ diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/load-settings.png b/package/Interface/CommunityShaders/Icons/Action Icons/load-settings.png index 200c6b254e..4d2ad3d600 100644 Binary files a/package/Interface/CommunityShaders/Icons/Action Icons/load-settings.png and b/package/Interface/CommunityShaders/Icons/Action Icons/load-settings.png differ diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/restore-settings.png b/package/Interface/CommunityShaders/Icons/Action Icons/restore-settings.png index a87fbfa221..82b8e83fc2 100644 Binary files a/package/Interface/CommunityShaders/Icons/Action Icons/restore-settings.png and b/package/Interface/CommunityShaders/Icons/Action Icons/restore-settings.png differ diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/save-settings.png b/package/Interface/CommunityShaders/Icons/Action Icons/save-settings.png index 5f64ceaba3..cae925ef61 100644 Binary files a/package/Interface/CommunityShaders/Icons/Action Icons/save-settings.png and b/package/Interface/CommunityShaders/Icons/Action Icons/save-settings.png differ diff --git a/package/Interface/CommunityShaders/Icons/Categories/display.png b/package/Interface/CommunityShaders/Icons/Categories/display.png index 8574127193..350145c507 100644 Binary files a/package/Interface/CommunityShaders/Icons/Categories/display.png and b/package/Interface/CommunityShaders/Icons/Categories/display.png differ diff --git a/package/Interface/CommunityShaders/Icons/Community Shaders Logo/Monochrome/cs-logo.png b/package/Interface/CommunityShaders/Icons/Community Shaders Logo/Monochrome/cs-logo.png new file mode 100644 index 0000000000..56b3d908e4 Binary files /dev/null and b/package/Interface/CommunityShaders/Icons/Community Shaders Logo/Monochrome/cs-logo.png differ diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json new file mode 100644 index 0000000000..9675fa6260 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -0,0 +1,164 @@ +{ + "DisplayName": "Default Dark", + "Description": "The classic Community Shaders dark theme with modern styling", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "FontSize": 27.0, + "FontName": "Jost/Jost-Regular.ttf", + "GlobalScale": 0.0, + "FontRoles": [ + { + "Family": "Jost", + "Style": "Regular", + "File": "Jost/Jost-Regular.ttf", + "SizeScale": 1.0 + }, + { + "Family": "Jost", + "Style": "Light", + "File": "Jost/Jost-Light.ttf", + "SizeScale": 1.3 + }, + { + "Family": "Jost", + "Style": "Regular", + "File": "Jost/Jost-Regular.ttf", + "SizeScale": 1.0 + }, + { + "Family": "Jost", + "Style": "Regular", + "File": "Jost/Jost-Regular.ttf", + "SizeScale": 1.0 + } + ], + "UseSimplePalette": false, + "ShowActionIcons": true, + "ShowFooter": true, + "CenterHeader": true, + "TooltipHoverDelay": 0.5, + "BackgroundBlurEnabled": false, + "Palette": { + "Background": [0.03, 0.03, 0.03, 0.39216], + "Text": [1.0, 1.0, 1.0, 1.0], + "WindowBorder": [0.5, 0.5, 0.5, 0.8], + "FrameBorder": [0.4, 0.4, 0.4, 0.7], + "Separator": [0.5, 0.5, 0.5, 0.6], + "ResizeGrip": [0.6, 0.6, 0.6, 0.8] + }, + "StatusPalette": { + "Disable": [0.5, 0.5, 0.5, 1.0], + "Error": [1.0, 0.4, 0.4, 1.0], + "Warning": [1.0, 0.6, 0.2, 1.0], + "RestartNeeded": [0.4, 1.0, 0.4, 1.0], + "CurrentHotkey": [1.0, 1.0, 0.0, 1.0], + "SuccessColor": [0.0, 1.0, 0.0, 1.0], + "InfoColor": [0.2, 0.6, 1.0, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.8, 0.8, 0.8, 1.0], + "ColorHovered": [0.6, 0.6, 0.6, 1.0], + "MinimizedFactor": 0.7 + }, + "ScrollbarOpacity": { + "Background": 0.0, + "Thumb": 0.3, + "ThumbHovered": 0.5, + "ThumbActive": 0.8 + }, + "Style": { + "WindowBorderSize": 0.0, + "ChildBorderSize": 0.0, + "FrameBorderSize": 0.0, + "WindowPadding": [8.0, 8.0], + "WindowRounding": 12.0, + "IndentSpacing": 8.0, + "FramePadding": [4.0, 4.0], + "CellPadding": [8.0, 2.0], + "ItemSpacing": [4.0, 8.0], + "FrameRounding": 6.0, + "TabRounding": 4.0, + "ScrollbarRounding": 12.0, + "ScrollbarSize": 10.0, + "GrabRounding": 12.0, + "GrabMinSize": 8.0, + "ItemInnerSpacing": [2.0, 4.0], + "ButtonTextAlign": [0.5, 0.5], + "SelectableTextAlign": [0.0, 0.0], + "SeparatorTextAlign": [0.32, 0.5], + "SeparatorTextPadding": [20.0, 3.0], + "SeparatorTextBorderSize": 3.0, + "WindowMinSize": [32.0, 32.0], + "ChildRounding": 0.0, + "PopupRounding": 0.0, + "PopupBorderSize": 1.0, + "TabBorderSize": 0.0, + "TabBarBorderSize": 1.0, + "TabMinWidthForCloseButton": 0.0, + "ColorButtonPosition": 0, + "ColumnsMinSpacing": 6.0, + "DockingSeparatorSize": 2.0, + "LogSliderDeadzone": 4.0, + "MouseCursorScale": 1.0, + "TableAngledHeadersAngle": 0.611 + }, + "FullPalette": [ + [0.9, 0.9, 0.9, 0.9], + [0.6, 0.6, 0.6, 1.0], + [0.09, 0.09, 0.09, 0.95], + [0.0, 0.0, 0.0, 0.0], + [0.05, 0.05, 0.1, 0.85], + [0.7, 0.7, 0.7, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + [0.26, 0.26, 0.26, 0.4], + [0.4, 0.4, 0.4, 0.45], + [0.0, 0.0, 0.0, 0.83], + [0.0, 0.0, 0.0, 0.87], + [0.2, 0.2, 0.3, 0.9], + [0.02, 0.02, 0.03, 0.9], + [0.2, 0.22, 0.27, 0.9], + [0.28, 0.28, 0.28, 1.0], + [0.42, 0.42, 0.42, 1.0], + [0.56, 0.56, 0.56, 1.0], + [1.0, 1.0, 1.0, 1.0], + [0.7, 0.7, 0.7, 1.0], + [0.26, 0.26, 0.26, 1.0], + [0.26, 0.59, 0.98, 0.39], + [0.26, 0.59, 0.98, 0.2], + [0.26, 0.59, 0.98, 0.59], + [0.06, 0.53, 0.98, 0.39], + [0.26, 0.59, 0.98, 0.2], + [0.26, 0.59, 0.98, 0.59], + [0.5, 0.5, 0.5, 1.0], + [0.7, 0.6, 0.6, 1.0], + [0.9, 0.7, 0.7, 1.0], + [1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 0.6], + [1.0, 1.0, 1.0, 0.9], + [0.26, 0.59, 0.98, 0.31], + [0.26, 0.59, 0.98, 0.8], + [0.26, 0.59, 0.98, 1.0], + [0.15, 0.15, 0.15, 0.97], + [0.26, 0.59, 0.98, 1.0], + [0.7, 0.6, 0.6, 0.5], + [0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0], + [0.9, 0.7, 0.0, 1.0], + [0.9, 0.7, 0.0, 1.0], + [0.9, 0.7, 0.0, 1.0], + [0.26, 0.59, 0.98, 0.4], + [0.26, 0.26, 0.26, 1.0], + [0.19, 0.19, 0.19, 1.0], + [0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 0.06], + [0.26, 0.59, 0.98, 0.35], + [0.8, 0.5, 0.5, 1.0], + [0.26, 0.59, 0.98, 1.0], + [0.3, 0.3, 0.3, 0.56], + [0.2, 0.2, 0.2, 0.35], + [0.2, 0.2, 0.2, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json new file mode 100644 index 0000000000..aaa00ac4dc --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json @@ -0,0 +1,132 @@ +{ + "DisplayName": "Dragon Blood", + "Description": "Dark red theme inspired by dragon lore and ancient power", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "FontSize": 27.0, + "FontName": "Jost/Jost-Regular.ttf", + "GlobalScale": 0.0, + "FontRoles": [ + { + "Family": "Crimson Pro", + "Style": "Regular", + "File": "CrimsonPro/CrimsonPro-Regular.ttf", + "SizeScale": 1.0 + }, + { + "Family": "Sanguis", + "Style": "Regular", + "File": "Sanguis/Sanguis.ttf", + "SizeScale": 1.1 + }, + { + "Family": "Crimson Pro", + "Style": "SemiBold", + "File": "CrimsonPro/CrimsonPro-SemiBold.ttf", + "SizeScale": 1.0 + }, + { + "Family": "Crimson Pro", + "Style": "Light", + "File": "CrimsonPro/CrimsonPro-Light.ttf", + "SizeScale": 1.0 + } + ], + "UseSimplePalette": false, + "ShowActionIcons": true, + "UseMonochromeIcons": true, + "TooltipHoverDelay": 0.5, + "BackgroundBlurEnabled": false, + "Palette": { + "Background": [0.25, 0.05, 0.05, 0.9], + "Text": [1.0, 0.85, 0.85, 1.0], + "WindowBorder": [0.8, 0.2, 0.2, 0.9], + "FrameBorder": [0.7, 0.15, 0.15, 0.8], + "Separator": [0.8, 0.2, 0.2, 0.7], + "ResizeGrip": [0.9, 0.25, 0.25, 0.9] + }, + "StatusPalette": { + "Disable": [0.4, 0.2, 0.2, 1.0], + "Error": [1.0, 0.1, 0.1, 1.0], + "Warning": [1.0, 0.5, 0.0, 1.0], + "RestartNeeded": [0.8, 0.6, 0.2, 1.0], + "CurrentHotkey": [1.0, 0.8, 0.2, 1.0], + "SuccessColor": [0.6, 0.8, 0.2, 1.0], + "InfoColor": [0.8, 0.4, 0.6, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.9, 0.6, 0.6, 1.0], + "ColorHovered": [1.0, 0.7, 0.7, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 3.0, + "ChildBorderSize": 1.0, + "FrameBorderSize": 2.0, + "WindowPadding": [16.0, 14.0], + "WindowRounding": 8.0, + "IndentSpacing": 10.0, + "FramePadding": [8.0, 6.0], + "CellPadding": [14.0, 6.0], + "ItemSpacing": [10.0, 10.0] + }, + "FullPalette": [ + [1.0, 0.85, 0.85, 0.9], + [0.8, 0.6, 0.6, 1.0], + [0.25, 0.05, 0.05, 0.9], + [0.0, 0.0, 0.0, 0.0], + [0.2, 0.03, 0.03, 0.85], + [0.6, 0.4, 0.4, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.15, 0.0, 0.0, 1.0], + [0.4, 0.15, 0.15, 0.4], + [0.5, 0.2, 0.2, 0.45], + [0.1, 0.0, 0.0, 0.83], + [0.15, 0.0, 0.0, 0.87], + [0.3, 0.1, 0.1, 0.9], + [0.25, 0.05, 0.05, 0.9], + [0.35, 0.15, 0.15, 0.9], + [0.45, 0.15, 0.15, 1.0], + [0.6, 0.25, 0.25, 1.0], + [0.75, 0.35, 0.35, 1.0], + [1.0, 0.85, 0.85, 1.0], + [0.8, 0.6, 0.6, 1.0], + [0.4, 0.15, 0.15, 1.0], + [0.8, 0.3, 0.3, 0.4], + [0.9, 0.4, 0.4, 0.67], + [1.0, 0.5, 0.5, 1.0], + [0.85, 0.25, 0.25, 1.0], + [0.9, 0.35, 0.35, 1.0], + [0.95, 0.45, 0.45, 1.0], + [0.6, 0.3, 0.3, 1.0], + [0.8, 0.4, 0.4, 1.0], + [0.95, 0.55, 0.55, 1.0], + [1.0, 0.85, 0.85, 1.0], + [1.0, 0.85, 0.85, 0.6], + [1.0, 0.85, 0.85, 0.9], + [0.706, 0.051, 0.051, 0.314], + [0.35, 0.05, 0.05, 0.95], + [0.5, 0.08, 0.08, 1.0], + [0.18, 0.04, 0.04, 0.98], + [0.42, 0.08, 0.08, 1.0], + [0.3, 0.05, 0.05, 0.75], + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.85, 0.85, 1.0], + [1.0, 0.6, 0.2, 1.0], + [1.0, 0.6, 0.2, 1.0], + [1.0, 0.6, 0.2, 1.0], + [0.8, 0.3, 0.3, 0.4], + [0.4, 0.15, 0.15, 1.0], + [0.3, 0.1, 0.1, 1.0], + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.85, 0.85, 0.06], + [0.8, 0.3, 0.3, 0.35], + [1.0, 0.3, 0.3, 1.0], + [0.9, 0.4, 0.4, 1.0], + [0.5, 0.2, 0.2, 0.56], + [0.35, 0.15, 0.15, 0.35], + [0.35, 0.15, 0.15, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood/discord.png b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood/discord.png new file mode 100644 index 0000000000..666cb18c9b Binary files /dev/null and b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood/discord.png differ diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json new file mode 100644 index 0000000000..289b12be04 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json @@ -0,0 +1,135 @@ +{ + "DisplayName": "Dwemer Bronze", + "Description": "Ancient bronze theme inspired by lost Dwemer technology and metallic machinery", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "FontSize": 27.0, + "FontName": "Jost/Jost-Regular.ttf", + "GlobalScale": 0.0, + "FontRoles": [ + { + "Family": "Crimson Pro", + "File": "CrimsonPro/CrimsonPro-Regular.ttf", + "SizeScale": 1.0, + "Style": "Regular" + }, + { + "Family": "Sovngarde", + "File": "Sovngarde/SovngardeLight.ttf", + "SizeScale": 1.0, + "Style": "Light" + }, + { + "Family": "Roboto", + "File": "Roboto/Roboto-Regular.ttf", + "SizeScale": 1.0, + "Style": "Regular" + }, + { + "Family": "RobotoSlab", + "File": "RobotoSlab/RobotoSlab-Light.ttf", + "SizeScale": 1.0, + "Style": "Light" + } + ], + "UseSimplePalette": false, + "ShowActionIcons": true, + "UseMonochromeIcons": true, + "UseMonochromeLogo": true, + "ShowFooter": false, + "CenterHeader": true, + "TooltipHoverDelay": 0.5, + "BackgroundBlurEnabled": false, + "Palette": { + "Background": [0.15, 0.12, 0.08, 0.9], + "Text": [0.9, 0.75, 0.5, 1.0], + "WindowBorder": [0.7, 0.5, 0.3, 0.9], + "FrameBorder": [0.6, 0.4, 0.25, 0.8], + "Separator": [0.7, 0.5, 0.3, 0.7], + "ResizeGrip": [0.8, 0.6, 0.35, 0.9] + }, + "StatusPalette": { + "Disable": [0.4, 0.35, 0.25, 1.0], + "Error": [0.9, 0.3, 0.1, 1.0], + "Warning": [1.0, 0.7, 0.2, 1.0], + "RestartNeeded": [0.8, 0.8, 0.4, 1.0], + "CurrentHotkey": [1.0, 0.8, 0.3, 1.0], + "SuccessColor": [0.5, 0.7, 0.3, 1.0], + "InfoColor": [0.6, 0.7, 0.8, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.8, 0.65, 0.4, 1.0], + "ColorHovered": [0.95, 0.8, 0.55, 1.0], + "MinimizedFactor": 0.7 + }, + "Style": { + "WindowBorderSize": 1.5, + "ChildBorderSize": 2.0, + "FrameBorderSize": 1.0, + "WindowPadding": [16.0, 14.0], + "WindowRounding": 2.0, + "IndentSpacing": 10.0, + "FramePadding": [8.0, 6.0], + "CellPadding": [14.0, 6.0], + "ItemSpacing": [10.0, 9.0] + }, + "FullPalette": [ + [0.9, 0.75, 0.5, 0.9], + [0.7, 0.6, 0.4, 1.0], + [0.15, 0.12, 0.08, 0.9], + [0.0, 0.0, 0.0, 0.0], + [0.13, 0.1, 0.06, 0.85], + [0.6, 0.5, 0.35, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.1, 0.08, 0.05, 1.0], + [0.35, 0.28, 0.18, 0.4], + [0.45, 0.36, 0.23, 0.45], + [0.1, 0.08, 0.05, 0.83], + [0.13, 0.1, 0.07, 0.87], + [0.25, 0.2, 0.13, 0.9], + [0.18, 0.14, 0.09, 0.9], + [0.3, 0.24, 0.15, 0.9], + [0.4, 0.32, 0.2, 1.0], + [0.5, 0.42, 0.28, 1.0], + [0.6, 0.52, 0.38, 1.0], + [0.9, 0.75, 0.5, 1.0], + [0.7, 0.6, 0.4, 1.0], + [0.35, 0.28, 0.18, 1.0], + [0.7, 0.5, 0.3, 0.4], + [0.25, 0.18, 0.12, 0.8], + [0.35, 0.28, 0.18, 1.0], + [0.85, 0.55, 0.25, 1.0], + [0.9, 0.65, 0.35, 1.0], + [0.95, 0.75, 0.45, 1.0], + [0.65, 0.5, 0.3, 1.0], + [0.75, 0.6, 0.4, 1.0], + [0.85, 0.7, 0.5, 1.0], + [0.9, 0.75, 0.5, 1.0], + [0.9, 0.75, 0.5, 0.6], + [0.9, 0.75, 0.5, 0.9], + [0.7, 0.5, 0.3, 0.31], + [0.35, 0.22, 0.12, 0.9], + [0.45, 0.3, 0.18, 1.0], + [0.13, 0.1, 0.06, 0.97], + [0.4, 0.28, 0.16, 1.0], + [0.32, 0.22, 0.13, 0.7], + [0.0, 0.0, 0.0, 0.0], + [0.9, 0.75, 0.5, 1.0], + [0.9, 0.7, 0.3, 1.0], + [0.9, 0.7, 0.3, 1.0], + [0.9, 0.7, 0.3, 1.0], + [0.7, 0.5, 0.3, 0.4], + [0.3, 0.24, 0.15, 1.0], + [0.23, 0.18, 0.11, 1.0], + [0.0, 0.0, 0.0, 0.0], + [0.9, 0.75, 0.5, 0.06], + [0.7, 0.5, 0.3, 0.35], + [0.8, 0.45, 0.25, 1.0], + [0.8, 0.6, 0.35, 1.0], + [0.45, 0.36, 0.23, 0.56], + [0.3, 0.24, 0.15, 0.35], + [0.3, 0.24, 0.15, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze/discord.png b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze/discord.png new file mode 100644 index 0000000000..c5cdae99ee Binary files /dev/null and b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze/discord.png differ diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json new file mode 100644 index 0000000000..50c34a3983 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json @@ -0,0 +1,132 @@ +{ + "DisplayName": "CS Classic", + "Description": "A theme reminiscent of the original Community Shaders 1.0 look.", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "FontSize": 27.0, + "FontName": "Jost/Jost-Regular.ttf", + "GlobalScale": 0.0, + "FontRoles": [ + { + "Family": "Inter", + "Style": "Light", + "File": "Inter/Inter-Light.ttf", + "SizeScale": 1.0 + }, + { + "Family": "Inter", + "Style": "SemiBold", + "File": "Inter/Inter-SemiBold.ttf", + "SizeScale": 1.0 + }, + { + "Family": "Rubik", + "Style": "SemiBold", + "File": "Rubik/Rubik-SemiBold.ttf", + "SizeScale": 1.0 + }, + { + "Family": "Inter", + "Style": "Light", + "File": "Inter/Inter-Light.ttf", + "SizeScale": 1.0 + } + ], + "UseSimplePalette": false, + "ShowActionIcons": true, + "UseMonochromeIcons": false, + "TooltipHoverDelay": 0.5, + "BackgroundBlurEnabled": false, + "Palette": { + "Background": [0.0, 0.0, 0.0, 0.95], + "Text": [1.0, 1.0, 1.0, 1.0], + "WindowBorder": [1.0, 1.0, 1.0, 1.0], + "FrameBorder": [0.4, 0.4, 0.4, 0.9], + "Separator": [1.0, 1.0, 1.0, 0.8], + "ResizeGrip": [1.0, 1.0, 1.0, 1.0] + }, + "StatusPalette": { + "Disable": [0.5, 0.5, 0.5, 1.0], + "Error": [1.0, 0.0, 0.0, 1.0], + "Warning": [1.0, 1.0, 0.0, 1.0], + "RestartNeeded": [0.0, 1.0, 0.0, 1.0], + "CurrentHotkey": [0.0, 1.0, 1.0, 1.0], + "SuccessColor": [0.0, 1.0, 0.0, 1.0], + "InfoColor": [0.0, 0.5, 1.0, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [1.0, 1.0, 1.0, 1.0], + "ColorHovered": [0.8, 0.8, 0.8, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 2.0, + "ChildBorderSize": 2.0, + "FrameBorderSize": 2.0, + "WindowPadding": [18.0, 16.0], + "WindowRounding": 2.0, + "IndentSpacing": 12.0, + "FramePadding": [10.0, 6.0], + "CellPadding": [16.0, 8.0], + "ItemSpacing": [12.0, 12.0] + }, + "FullPalette": [ + [1.0, 1.0, 1.0, 0.9], + [0.8, 0.8, 0.8, 1.0], + [0.0, 0.0, 0.0, 0.95], + [1.0, 1.0, 1.0, 0.39], + [0.05, 0.05, 0.05, 0.85], + [1.0, 1.0, 1.0, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + [0.3, 0.3, 0.3, 0.4], + [0.5, 0.5, 0.5, 0.45], + [0.0, 0.0, 0.0, 0.83], + [0.0, 0.0, 0.0, 0.87], + [0.2, 0.2, 0.2, 0.9], + [0.0, 0.0, 0.0, 0.9], + [0.15, 0.15, 0.15, 0.9], + [0.3, 0.3, 0.3, 1.0], + [0.5, 0.5, 0.5, 1.0], + [0.7, 0.7, 0.7, 1.0], + [1.0, 1.0, 1.0, 1.0], + [0.8, 0.8, 0.8, 1.0], + [0.2, 0.2, 0.2, 1.0], + [0.3, 0.3, 0.3, 1.0], + [0.35, 0.35, 0.35, 1.0], + [0.4, 0.4, 0.4, 1.0], + [0.25, 0.25, 0.25, 1.0], + [0.3, 0.3, 0.3, 1.0], + [0.35, 0.35, 0.35, 1.0], + [0.3, 0.3, 0.3, 1.0], + [0.35, 0.35, 0.35, 1.0], + [0.4, 0.4, 0.4, 1.0], + [1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 0.6], + [1.0, 1.0, 1.0, 0.9], + [0.6, 0.6, 0.6, 0.31], + [0.8, 0.8, 0.8, 0.8], + [0.5, 0.5, 0.5, 1.0], + [0.55, 0.55, 0.55, 1.0], + [0.85, 0.85, 0.85, 1.0], + [0.7, 0.7, 0.7, 0.5], + [0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [0.6, 0.6, 0.6, 0.4], + [0.2, 0.2, 0.2, 1.0], + [0.15, 0.15, 0.15, 1.0], + [0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 0.06], + [0.6, 0.6, 0.6, 0.35], + [1.0, 0.0, 0.0, 1.0], + [0.8, 0.8, 0.8, 1.0], + [0.4, 0.4, 0.4, 0.56], + [0.25, 0.25, 0.25, 0.35], + [0.25, 0.25, 0.25, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast/discord.png b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast/discord.png new file mode 100644 index 0000000000..6b585b8583 Binary files /dev/null and b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast/discord.png differ diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json new file mode 100644 index 0000000000..238c2fef3d --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json @@ -0,0 +1,139 @@ +{ + "DisplayName": "Default Light", + "Description": "Sleek monochrome theme with modern minimalist aesthetic", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "FontSize": 27.0, + "FontName": "Jost/Jost-Regular.ttf", + "GlobalScale": 0.0, + "FontRoles": [ + { + "Family": "IBMPlex Sans", + "Style": "Condensed Light", + "File": "IBMPlexSans/IBMPlexSans_Condensed-Light.ttf", + "SizeScale": 1.0 + }, + { + "Family": "IBMPlex Mono", + "Style": "Regular", + "File": "IBMPlexMono/IBMPlexMono-Regular.ttf", + "SizeScale": 1.0 + }, + { + "Family": "IBMPlex Sans", + "Style": "Regular", + "File": "IBMPlexSans/IBMPlexSans-Regular.ttf", + "SizeScale": 1.0 + }, + { + "Family": "IBMPlex Serif", + "Style": "Light", + "File": "IBMPlexSerif/IBMPlexSerif-Light.ttf", + "SizeScale": 1.0 + } + ], + "UseSimplePalette": false, + "ShowActionIcons": true, + "ShowFooter": false, + "CenterHeader": true, + "TooltipHoverDelay": 0.5, + "BackgroundBlurEnabled": false, + "Palette": { + "Background": [0.98, 0.98, 0.98, 0.9], + "Text": [0.08, 0.08, 0.08, 1.0], + "WindowBorder": [0.75, 0.75, 0.75, 0.7], + "FrameBorder": [0.82, 0.82, 0.82, 0.8], + "Separator": [0.85, 0.85, 0.85, 0.7], + "ResizeGrip": [0.65, 0.65, 0.65, 0.8] + }, + "StatusPalette": { + "Disable": [0.55, 0.55, 0.55, 1.0], + "Error": [0.25, 0.25, 0.25, 1.0], + "Warning": [0.35, 0.35, 0.35, 1.0], + "RestartNeeded": [0.15, 0.15, 0.15, 1.0], + "CurrentHotkey": [0.3, 0.3, 0.3, 1.0], + "SuccessColor": [0.2, 0.2, 0.2, 1.0], + "InfoColor": [0.4, 0.4, 0.4, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.88, 0.88, 0.88, 1.0], + "ColorHovered": [0.8, 0.8, 0.8, 1.0], + "MinimizedFactor": 0.7 + }, + "Style": { + "WindowBorderSize": 1.0, + "ChildBorderSize": 0.0, + "FrameBorderSize": 1.0, + "WindowPadding": [16.0, 12.0], + "WindowRounding": 6.0, + "IndentSpacing": 8.0, + "FramePadding": [8.0, 4.0], + "CellPadding": [12.0, 4.0], + "ItemSpacing": [8.0, 6.0] + }, + "FullPalette": [ + [0.08, 0.08, 0.08, 1.0], + [0.55, 0.55, 0.55, 1.0], + [0.98, 0.98, 0.98, 0.9], + [1.0, 1.0, 1.0, 0.0], + [0.94, 0.94, 0.94, 0.85], + [0.75, 0.75, 0.75, 0.7], + [1.0, 1.0, 1.0, 0.0], + [1.0, 1.0, 1.0, 0.95], + [0.85, 0.85, 0.85, 0.7], + [0.88, 0.88, 0.88, 0.8], + [1.0, 1.0, 1.0, 0.75], + [1.0, 1.0, 1.0, 0.82], + [0.88, 0.88, 0.88, 0.9], + [0.92, 0.92, 0.92, 0.85], + [0.85, 0.85, 0.85, 0.85], + [0.82, 0.82, 0.82, 0.8], + [0.75, 0.75, 0.75, 0.85], + [0.68, 0.68, 0.68, 0.9], + [0.08, 0.08, 0.08, 1.0], + [0.15, 0.15, 0.15, 1.0], + [0.25, 0.25, 0.25, 1.0], + [0.7, 0.7, 0.7, 1.0], + [0.6, 0.6, 0.6, 1.0], + [0.5, 0.5, 0.5, 1.0], + [0.45, 0.45, 0.45, 1.0], + [0.5, 0.5, 0.5, 1.0], + [0.55, 0.55, 0.55, 1.0], + [0.68, 0.68, 0.68, 1.0], + [0.6, 0.6, 0.6, 1.0], + [0.52, 0.52, 0.52, 1.0], + [0.08, 0.08, 0.08, 1.0], + [0.08, 0.08, 0.08, 0.15], + [0.08, 0.08, 0.08, 0.3], + [0.65, 0.65, 0.65, 1.0], + [0.55, 0.55, 0.55, 1.0], + [0.45, 0.45, 0.45, 1.0], + [0.96, 0.96, 0.96, 1.0], + [0.5, 0.5, 0.5, 1.0], + [0.7, 0.7, 0.7, 0.8], + [1.0, 1.0, 1.0, 0.0], + [0.08, 0.08, 0.08, 1.0], + [0.3, 0.3, 0.3, 1.0], + [0.25, 0.25, 0.25, 1.0], + [0.2, 0.2, 0.2, 1.0], + [0.65, 0.65, 0.65, 1.0], + [0.8, 0.8, 0.8, 1.0], + [0.75, 0.75, 0.75, 1.0], + [1.0, 1.0, 1.0, 0.0], + [0.08, 0.08, 0.08, 0.08], + [0.6, 0.6, 0.6, 1.0], + [0.25, 0.25, 0.25, 1.0], + [0.5, 0.5, 0.5, 1.0], + [0.78, 0.78, 0.78, 1.0], + [0.72, 0.72, 0.72, 1.0], + [0.68, 0.68, 0.68, 1.0] + ], + "ScrollbarOpacity": { + "Background": 0.0, + "Thumb": 0.5, + "ThumbHovered": 0.7, + "ThumbActive": 0.85 + } + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light/clear-cache.png b/package/SKSE/Plugins/CommunityShaders/Themes/Light/clear-cache.png new file mode 100644 index 0000000000..b02b476df6 Binary files /dev/null and b/package/SKSE/Plugins/CommunityShaders/Themes/Light/clear-cache.png differ diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light/cs-logo.png b/package/SKSE/Plugins/CommunityShaders/Themes/Light/cs-logo.png new file mode 100644 index 0000000000..6f9084da18 Binary files /dev/null and b/package/SKSE/Plugins/CommunityShaders/Themes/Light/cs-logo.png differ diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light/discord.png b/package/SKSE/Plugins/CommunityShaders/Themes/Light/discord.png new file mode 100644 index 0000000000..6b585b8583 Binary files /dev/null and b/package/SKSE/Plugins/CommunityShaders/Themes/Light/discord.png differ diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light/load-settings.png b/package/SKSE/Plugins/CommunityShaders/Themes/Light/load-settings.png new file mode 100644 index 0000000000..a11c3e170a Binary files /dev/null and b/package/SKSE/Plugins/CommunityShaders/Themes/Light/load-settings.png differ diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light/restore-settings.png b/package/SKSE/Plugins/CommunityShaders/Themes/Light/restore-settings.png new file mode 100644 index 0000000000..122c5c143d Binary files /dev/null and b/package/SKSE/Plugins/CommunityShaders/Themes/Light/restore-settings.png differ diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light/save-settings.png b/package/SKSE/Plugins/CommunityShaders/Themes/Light/save-settings.png new file mode 100644 index 0000000000..57f7f655ff Binary files /dev/null and b/package/SKSE/Plugins/CommunityShaders/Themes/Light/save-settings.png differ diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json new file mode 100644 index 0000000000..6b25308086 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json @@ -0,0 +1,132 @@ +{ + "DisplayName": "Nordic Frost", + "Description": "Cool blue-white theme reflecting the harsh Nordic climate and icy mountain peaks", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "FontSize": 27.0, + "FontName": "Jost/Jost-Regular.ttf", + "GlobalScale": 0.0, + "FontRoles": [ + { + "Family": "Crimson Pro", + "Style": "Light", + "File": "CrimsonPro/CrimsonPro-Light.ttf", + "SizeScale": 1.0 + }, + { + "Family": "Sovngarde", + "Style": "Bold", + "File": "Sovngarde/SovngardeBold.ttf", + "SizeScale": 1.0 + }, + { + "Family": "Sovngarde", + "Style": "Light", + "File": "Sovngarde/SovngardeLight.ttf", + "SizeScale": 1.0 + }, + { + "Family": "Crimson Pro", + "Style": "Regular", + "File": "CrimsonPro/CrimsonPro-Regular.ttf", + "SizeScale": 1.0 + } + ], + "UseSimplePalette": false, + "ShowActionIcons": true, + "UseMonochromeIcons": true, + "TooltipHoverDelay": 0.5, + "BackgroundBlurEnabled": false, + "Palette": { + "Background": [0.05, 0.15, 0.25, 0.9], + "Text": [0.9, 0.95, 1.0, 1.0], + "WindowBorder": [0.7, 0.85, 0.9, 0.8], + "FrameBorder": [0.6, 0.75, 0.8, 0.7], + "Separator": [0.7, 0.85, 0.9, 0.6], + "ResizeGrip": [0.8, 0.9, 0.95, 0.8] + }, + "StatusPalette": { + "Disable": [0.4, 0.45, 0.5, 1.0], + "Error": [1.0, 0.3, 0.4, 1.0], + "Warning": [1.0, 0.8, 0.3, 1.0], + "RestartNeeded": [0.7, 0.9, 0.4, 1.0], + "CurrentHotkey": [0.5, 0.8, 1.0, 1.0], + "SuccessColor": [0.4, 0.8, 0.6, 1.0], + "InfoColor": [0.6, 0.8, 0.9, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.7, 0.85, 0.95, 1.0], + "ColorHovered": [0.85, 0.95, 1.0, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 2.0, + "ChildBorderSize": 1.0, + "FrameBorderSize": 1.0, + "WindowPadding": [12.0, 10.0], + "WindowRounding": 8.0, + "IndentSpacing": 6.0, + "FramePadding": [6.0, 4.0], + "CellPadding": [10.0, 4.0], + "ItemSpacing": [6.0, 6.0] + }, + "FullPalette": [ + [0.9, 0.95, 1.0, 0.9], + [0.7, 0.8, 0.9, 1.0], + [0.05, 0.15, 0.25, 0.9], + [0.0, 0.0, 0.0, 0.0], + [0.04, 0.12, 0.2, 0.85], + [0.5, 0.65, 0.8, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.03, 0.08, 0.15, 1.0], + [0.25, 0.35, 0.5, 0.4], + [0.3, 0.4, 0.55, 0.45], + [0.03, 0.08, 0.15, 0.83], + [0.05, 0.1, 0.18, 0.87], + [0.15, 0.25, 0.4, 0.9], + [0.08, 0.18, 0.28, 0.9], + [0.2, 0.3, 0.45, 0.9], + [0.3, 0.4, 0.55, 1.0], + [0.4, 0.5, 0.65, 1.0], + [0.5, 0.6, 0.75, 1.0], + [0.9, 0.95, 1.0, 1.0], + [0.7, 0.8, 0.9, 1.0], + [0.25, 0.35, 0.5, 1.0], + [0.5, 0.7, 0.9, 0.4], + [0.15, 0.2, 0.3, 0.8], + [0.25, 0.35, 0.5, 1.0], + [0.6, 0.8, 1.0, 1.0], + [0.7, 0.85, 1.0, 1.0], + [0.8, 0.9, 1.0, 1.0], + [0.5, 0.65, 0.8, 1.0], + [0.6, 0.75, 0.9, 1.0], + [0.8, 0.9, 1.0, 1.0], + [0.9, 0.95, 1.0, 1.0], + [0.9, 0.95, 1.0, 0.6], + [0.9, 0.95, 1.0, 0.9], + [0.5, 0.7, 0.9, 0.31], + [0.6, 0.75, 0.95, 0.8], + [0.7, 0.85, 1.0, 1.0], + [0.04, 0.12, 0.2, 0.97], + [0.65, 0.8, 0.95, 1.0], + [0.5, 0.65, 0.8, 0.5], + [0.0, 0.0, 0.0, 0.0], + [0.9, 0.95, 1.0, 1.0], + [0.5, 0.8, 1.0, 1.0], + [0.5, 0.8, 1.0, 1.0], + [0.5, 0.8, 1.0, 1.0], + [0.5, 0.7, 0.9, 0.4], + [0.2, 0.3, 0.45, 1.0], + [0.15, 0.25, 0.35, 1.0], + [0.0, 0.0, 0.0, 0.0], + [0.9, 0.95, 1.0, 0.06], + [0.5, 0.7, 0.9, 0.35], + [0.6, 0.4, 0.8, 1.0], + [0.6, 0.75, 0.95, 1.0], + [0.3, 0.4, 0.55, 0.56], + [0.2, 0.3, 0.45, 0.35], + [0.2, 0.3, 0.45, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost/discord.png b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost/discord.png new file mode 100644 index 0000000000..53705ef62c Binary files /dev/null and b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost/discord.png differ diff --git a/package/Shaders/AmbientCompositeCS.hlsl b/package/Shaders/AmbientCompositeCS.hlsl deleted file mode 100644 index 39a8a47e9f..0000000000 --- a/package/Shaders/AmbientCompositeCS.hlsl +++ /dev/null @@ -1,147 +0,0 @@ -#include "Common/Color.hlsli" -#include "Common/FrameBuffer.hlsli" -#include "Common/GBuffer.hlsli" -#include "Common/Math.hlsli" -#include "Common/SharedData.hlsli" -#include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" -#include "Common/VR.hlsli" - -Texture2D AlbedoTexture : register(t0); -Texture2D NormalRoughnessTexture : register(t1); -Texture2D DepthTexture : register(t2); - -#if defined(SKYLIGHTING) -# include "Skylighting/Skylighting.hlsli" - -Texture3D SkylightingProbeArray : register(t3); -Texture2DArray stbn_vec3_2Dx1D_128x128x64 : register(t4); - -#endif - -#if defined(SSGI) -Texture2D SsgiAoTexture : register(t5); -Texture2D SsgiYTexture : register(t6); -Texture2D SsgiCoCgTexture : register(t7); -#endif - -#if defined(IBL) -# define IBL_AMBIENTCOMPOSITE -# include "IBL/IBL.hlsli" -#endif - -RWTexture2D MainRW : register(u0); -#if defined(SSGI) -void SampleSSGI(uint2 pixCoord, float3 normalWS, out float ao, out float3 il) -{ - ao = 1 - SsgiAoTexture[pixCoord]; - float4 ssgiIlYSh = SsgiYTexture[pixCoord]; - // without ZH hallucination - // float ssgiIlY = SphericalHarmonics::FuncProductIntegral(ssgiIlYSh, SphericalHarmonics::EvaluateCosineLobe(normalWS)); - float ssgiIlY = SphericalHarmonics::SHHallucinateZH3Irradiance(ssgiIlYSh, normalWS); - float2 ssgiIlCoCg = SsgiCoCgTexture[pixCoord]; - il = max(0, Color::YCoCgToRGB(float3(ssgiIlY, ssgiIlCoCg))); -} -#endif - -[numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { - // Early exit if dispatch thread is outside screen bounds - if (any(dispatchID.xy >= uint2(SharedData::BufferDim.xy))) - return; - - float2 uv = float2(dispatchID.xy + 0.5) * SharedData::BufferDim.zw; - uv *= FrameBuffer::DynamicResolutionParams2.xy; // adjust for dynamic res - - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - uv = Stereo::ConvertFromStereoUV(uv, eyeIndex); - - float3 normalGlossiness = NormalRoughnessTexture[dispatchID.xy]; - float3 normalVS = GBuffer::DecodeNormal(normalGlossiness.xy); - - float3 diffuseColor = MainRW[dispatchID.xy].xyz; - float3 albedo = AlbedoTexture[dispatchID.xy]; - - float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(normalVS, 0)).xyz); - - float3 directionalAmbientColor = max(0, mul(SharedData::DirectionalAmbient, float4(normalWS, 1.0))); - -#if defined(IBL) - if (SharedData::iblSettings.EnableDiffuseIBL) { - directionalAmbientColor *= SharedData::iblSettings.DALCAmount; - directionalAmbientColor += Color::Saturation(ImageBasedLighting::GetDiffuseIBL(-normalWS), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; - } -#endif - - float3 linAlbedo = Color::GammaToLinear(albedo); - float3 linDirectionalAmbientColor = Color::GammaToLinear(directionalAmbientColor); - float3 linDiffuseColor = Color::GammaToLinear(diffuseColor); - float3 originalDiffuseColor = linDiffuseColor; - - float3 linAmbient = Color::GammaToLinear(albedo * directionalAmbientColor); - - float visibility = 1.0; -#if defined(SKYLIGHTING) - if (!SharedData::InInterior) { - float rawDepth = DepthTexture[dispatchID.xy]; - float4 positionCS = float4(2 * float2(uv.x, -uv.y + 1) - 1, rawDepth, 1); - float4 positionMS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionCS); - positionMS.xyz = positionMS.xyz / positionMS.w; -# if defined(VR) - positionMS.xyz += FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -# endif - float3 skylightingNormal = normalize(float3(normalWS.xy, max(0, normalWS.z))); - - sh2 skylightingSH = Skylighting::sample(SharedData::skylightingSettings, SkylightingProbeArray, stbn_vec3_2Dx1D_128x128x64, dispatchID.xy, positionMS.xyz, normalWS); - float skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(skylightingNormal)) / Math::PI; - skylightingDiffuse = saturate(skylightingDiffuse); - - skylightingDiffuse = lerp(1.0, skylightingDiffuse, Skylighting::getFadeOutFactor(positionMS.xyz)); - - skylightingDiffuse = Skylighting::mixDiffuse(SharedData::skylightingSettings, skylightingDiffuse); - - visibility = skylightingDiffuse; - } -#endif - -#if defined(SSGI) -# if defined(VR) - float3 uvF = float3((dispatchID.xy + 0.5) * SharedData::BufferDim.zw, DepthTexture[dispatchID.xy]); // calculate high precision uv of initial eye - float3 uv2 = Stereo::ConvertStereoUVToOtherEyeStereoUV(uvF, eyeIndex, false); // calculate other eye uv - float3 uv1Mono = Stereo::ConvertFromStereoUV(uvF, eyeIndex); - float3 uv2Mono = Stereo::ConvertFromStereoUV(uv2, (1 - eyeIndex)); - uint2 pixCoord2 = (uint2)(uv2.xy / SharedData::BufferDim.zw - 0.5); -# endif - - float ssgiAo; - float3 ssgiIl; - SampleSSGI(dispatchID.xy, normalWS, ssgiAo, ssgiIl); - -# if defined(VR) - float ssgiAo2; - float3 ssgiIl2; - SampleSSGI(pixCoord2, normalWS, ssgiAo2, ssgiIl2); - - float4 ssgiMixed = Stereo::BlendEyeColors(uv1Mono, float4(ssgiIl, ssgiAo), uv2Mono, float4(ssgiIl2, ssgiAo2)); - ssgiAo = ssgiMixed.a; - ssgiIl = ssgiMixed.rgb; -# endif - - visibility *= ssgiAo; - -# if defined(INTERIOR) - linDiffuseColor *= ssgiAo; -# else - linDiffuseColor *= lerp(ssgiAo, 1.0, 0.5); -# endif - - linDiffuseColor += ssgiIl * linAlbedo; -#endif - - linAmbient *= visibility; - diffuseColor = Color::LinearToGamma(linDiffuseColor); - directionalAmbientColor = Color::LinearToGamma(linDirectionalAmbientColor * visibility); - - diffuseColor = diffuseColor + directionalAmbientColor * albedo; - - - MainRW[dispatchID.xy] = float4(diffuseColor, 1); -}; \ No newline at end of file diff --git a/package/Shaders/Common/Color.hlsli b/package/Shaders/Common/Color.hlsli index 8be9da9954..c7770044fc 100644 --- a/package/Shaders/Common/Color.hlsli +++ b/package/Shaders/Common/Color.hlsli @@ -7,6 +7,21 @@ namespace Color { static float GammaCorrectionValue = 2.2; + // [Jimenez et al. 2016, "Practical Realtime Strategies for Accurate Indirect Occlusion"] + float3 MultiBounceAO(float3 baseColor, float ao) + { + float3 a = 2.0404 * baseColor - 0.3324; + float3 b = -4.7951 * baseColor + 0.6417; + float3 c = 2.7552 * baseColor + 0.6903; + return max(ao, ((ao * a + b) * ao + c) * ao); + } + + // [Lagarde et al. 2014, "Moving Frostbite to Physically Based Rendering 3.0"] + float SpecularAOLagarde(float NdotV, float ao, float roughness) + { + return saturate(pow(abs(NdotV + ao), exp2(-16.0 * roughness - 1.0)) - 1.0 + ao); + } + float RGBToLuminance(float3 color) { return dot(color, float3(0.2125, 0.7154, 0.0721)); @@ -50,17 +65,30 @@ namespace Color return color; } - // Attempt to match vanilla materials tha are a darker than PBR - const static float PBRLightingScale = 0.666; + // Attempt to match vanilla materials that are darker than PBR + const static float PBRLightingScale = 0.65; + + // Attempt to normalise reflection brightness against DALC + const static float ReflectionNormalisationScale = 0.65; + + float GammaToLinear(float color) + { + return pow(abs(color), 1.6); + } + + float LinearToGamma(float color) + { + return pow(abs(color), 1.0 / 1.6); + } float3 GammaToLinear(float3 color) { - return pow(abs(color), 1.8); + return pow(abs(color), 1.6); } float3 LinearToGamma(float3 color) { - return pow(abs(color), 1.0 / 1.8); + return pow(abs(color), 1.0 / 1.6); } float3 GammaToTrueLinear(float3 color) @@ -76,7 +104,7 @@ namespace Color float3 Diffuse(float3 color) { #if defined(TRUE_PBR) - return LinearToGamma(color); + return TrueLinearToGamma(color); #else return color; #endif diff --git a/package/Shaders/Common/DisplayMapping.hlsli b/package/Shaders/Common/DisplayMapping.hlsli index b2aa77421c..54cbd526c6 100644 --- a/package/Shaders/Common/DisplayMapping.hlsli +++ b/package/Shaders/Common/DisplayMapping.hlsli @@ -128,7 +128,7 @@ namespace DisplayMapping return XYZToRGB(col); } - float3 HuePreservingTonemap(float3 col) + float3 HuePreservingHejlBurgessDawson(float3 col, float3 bloomCol) { float3 ictcp = RGBToICtCp(col); @@ -136,7 +136,11 @@ namespace DisplayMapping float saturationAmount = pow(smoothstep(1.0, 0.3, ictcp.x), 1.3); col = ICtCpToRGB(ictcp * float3(1, saturationAmount.xx)); - col = Param.z > 0.5 ? GetTonemapFactorHejlBurgessDawson(col) : GetTonemapFactorReinhard(col); + // Non-hue preserving mapping + float3 perChannelCompressed = GetTonemapFactorHejlBurgessDawson(col); + perChannelCompressed += saturate(Param.x - perChannelCompressed) * bloomCol; + + col = perChannelCompressed; float3 ictcpMapped = RGBToICtCp(col); diff --git a/package/Shaders/Common/LightingCommon.hlsli b/package/Shaders/Common/LightingCommon.hlsli new file mode 100644 index 0000000000..d6b5a06605 --- /dev/null +++ b/package/Shaders/Common/LightingCommon.hlsli @@ -0,0 +1,112 @@ +#ifndef LIGHTING_COMMON_HLSLI +#define LIGHTING_COMMON_HLSLI + +struct DirectContext +{ + float3 worldNormal; + float3 vertexNormal; + float3 viewDir; + float3 lightDir; + float3 halfVector; + float3 lightColor; +#if defined(TRUE_PBR) + float3 coatWorldNormal; + float3 coatViewDir; + float3 coatLightDir; + float3 coatHalfVector; + float3 coatLightColor; +#elif defined(HAIR) && defined(CS_HAIR) + float hairShadow; +#endif +}; + +struct IndirectContext +{ + float3 worldNormal; + float3 vertexNormal; + float3 viewDir; +}; + +struct DirectLightingOutput +{ + float3 diffuse; + float3 specular; + float3 transmission; +#if defined(TRUE_PBR) + float3 coatDiffuse; +#endif +}; + +struct IndirectLobeWeights +{ + float3 diffuse; + float3 specular; +}; + +#if defined(TRUE_PBR) +# if defined(GLINT) +# include "Common/Glints/Glints2023.hlsli" +# else +namespace Glints +{ + typedef float GlintCachedVars; +} +# endif +#endif + +struct MaterialProperties +{ + float3 BaseColor; +#if !defined(TRUE_PBR) + float Shininess; + float Glossiness; + float3 SpecularColor; +# if (defined(RIM_LIGHTING) || defined(SOFT_LIGHTING) || defined(LOAD_SOFT_LIGHTING)) + float3 rimSoftLightColor; +# endif +# if defined(BACK_LIGHTING) + float3 backLightColor; +# endif + float Roughness; + float3 F0; +#else + float Roughness; + float Metallic; + float AO; + float3 F0; + float3 SubsurfaceColor; + float Thickness; + float3 CoatColor; + float CoatStrength; + float CoatRoughness; + float3 CoatF0; + float3 FuzzColor; + float FuzzWeight; + float GlintScreenSpaceScale; + float GlintLogMicrofacetDensity; + float GlintMicrofacetRoughness; + float GlintDensityRandomization; + Glints::GlintCachedVars GlintCache; + float Noise; +#endif +}; + +float ShininessToRoughness(float shininess) +{ + return pow(abs(2.0 / (shininess + 2.0)), 0.25); +} + +float3x3 ReconstructTBN(float3 worldPos, float3 worldNormal, float2 uv) +{ + float3 dFdx = ddx(worldPos); + float3 dFdy = ddy(worldPos); + float2 dUVdx = ddx(uv); + float2 dUVdy = ddy(uv); + float3 tangent = normalize(dFdx * dUVdy.y - dFdy * dUVdx.y); + float3 bitangent = normalize(dFdy * dUVdx.x - dFdx * dUVdy.x); + tangent = normalize(tangent - worldNormal * dot(worldNormal, tangent)); + bitangent = normalize(bitangent - worldNormal * dot(worldNormal, bitangent)); + + return float3x3(tangent, bitangent, normalize(worldNormal)); +} +#endif \ No newline at end of file diff --git a/package/Shaders/Common/LightingEval.hlsli b/package/Shaders/Common/LightingEval.hlsli new file mode 100644 index 0000000000..7bb3ca2d13 --- /dev/null +++ b/package/Shaders/Common/LightingEval.hlsli @@ -0,0 +1,230 @@ +#ifndef LIGHTING_EVAL_HLSLI +#define LIGHTING_EVAL_HLSLI +#include "Common/LightingCommon.hlsli" + +#include "Common/BRDF.hlsli" +#include "Common/Math.hlsli" +#if defined(TRUE_PBR) +# include "Common/PBR.hlsli" +#endif + +#if defined(TRUE_PBR) +DirectContext CreateDirectLightingContext(float3 worldNormal, float3 coatWorldNormal, float3 vertexNormal, float3 viewDir, float3 coatViewDir, float3 lightDir, float3 coatLightDir, float3 lightColor, float shadowFactor, float parallaxShadow) +#else +DirectContext CreateDirectLightingContext(float3 worldNormal, float3 vertexNormal, float3 viewDir, float3 lightDir, float3 lightColor, float shadowFactor, float parallaxShadow) +#endif +{ + DirectContext context = (DirectContext)0; + context.worldNormal = normalize(worldNormal); + context.vertexNormal = normalize(vertexNormal); + context.viewDir = normalize(viewDir); + context.lightDir = normalize(lightDir); + context.halfVector = normalize(context.viewDir + context.lightDir); + context.lightColor = lightColor * shadowFactor * parallaxShadow; +#if defined(TRUE_PBR) + context.coatWorldNormal = normalize(coatWorldNormal); + context.coatViewDir = normalize(coatViewDir); + context.coatLightDir = normalize(coatLightDir); + context.coatHalfVector = normalize(context.coatViewDir + context.coatLightDir); + [branch] if ((PBRFlags & PBR::Flags::InterlayerParallax) != 0) + { + context.coatLightColor = lightColor * shadowFactor; + } + else + { + context.coatLightColor = context.lightColor; + } +#endif + return context; +} + +IndirectContext CreateIndirectLightingContext(float3 worldNormal, float3 vertexNormal, float3 viewDir) +{ + IndirectContext context = (IndirectContext)0; + context.worldNormal = normalize(worldNormal); + context.vertexNormal = normalize(vertexNormal); + context.viewDir = normalize(viewDir); + return context; +} + +float3 VanillaSpecular(DirectContext context, float shininess, float2 uv) +{ + const float3 N = context.worldNormal; + const float3 G = context.vertexNormal; + float3 V = context.viewDir; + const float3 L = context.lightDir; + const float3 H = context.halfVector; + float HdotN; +# if defined(ANISO_LIGHTING) + const float3 AN = normalize(N * 0.5 + G); + float LdotAN = dot(AN, L); + float HdotAN = dot(AN, H); + HdotN = 1 - min(1, abs(LdotAN - HdotAN)); +# else + HdotN = saturate(dot(H, N)); +# endif + +# if defined(SPECULAR) + float lightColorMultiplier = exp2(shininess * log2(HdotN)); + +# elif defined(SPARKLE) + float lightColorMultiplier = 0; +# else + float lightColorMultiplier = HdotN; +# endif + +# if defined(ANISO_LIGHTING) + lightColorMultiplier *= 0.7 * max(0, L.z); +# endif + +# if defined(SPARKLE) && !defined(SNOW) + float3 sparkleUvScale = exp2(float3(1.3, 1.6, 1.9) * log2(abs(SparkleParams.x)).xxx); + + float sparkleColor1 = TexProjDetail.Sample(SampProjDetailSampler, uv * sparkleUvScale.xx).z; + float sparkleColor2 = TexProjDetail.Sample(SampProjDetailSampler, uv * sparkleUvScale.yy).z; + float sparkleColor3 = TexProjDetail.Sample(SampProjDetailSampler, uv * sparkleUvScale.zz).z; + float sparkleColor = ProcessSparkleColor(sparkleColor1) + ProcessSparkleColor(sparkleColor2) + ProcessSparkleColor(sparkleColor3); + float VdotN = dot(V, N); + V += N * -(2 * VdotN); + float sparkleMultiplier = exp2(SparkleParams.w * log2(saturate(dot(V, -L)))) * (SparkleParams.z * sparkleColor); + sparkleMultiplier = sparkleMultiplier >= 0.5 ? 1 : 0; + lightColorMultiplier += sparkleMultiplier * HdotN; +# endif + return lightColorMultiplier; +} + +void EvaluateLighting(DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv, out DirectLightingOutput lightingOutput) +{ + lightingOutput = (DirectLightingOutput)0; +#if defined(TRUE_PBR) + PBR::GetDirectLightInput(lightingOutput, context, material, tbnTr, uv); +#else +# if defined(HAIR) && defined(CS_HAIR) + if (SharedData::hairSpecularSettings.Enabled) + { + Hair::GetHairDirectLight(lightingOutput, context, material, tbnTr, uv); + return; + } +# endif + const float NdotL = dot(context.worldNormal, context.lightDir); + lightingOutput.diffuse = saturate(NdotL) * context.lightColor; +# if defined(SOFT_LIGHTING) + lightingOutput.diffuse += context.lightColor * GetSoftLightMultiplier(NdotL) * material.rimSoftLightColor; +# endif + +# if defined(RIM_LIGHTING) + lightingOutput.diffuse += context.lightColor * GetRimLightMultiplier(context.lightDir, context.viewDir, context.worldNormal) * material.rimSoftLightColor; +# endif + +# if defined(BACK_LIGHTING) + lightingOutput.diffuse += context.lightColor * saturate(-NdotL) * material.backLightColor; +# endif + lightingOutput.specular = VanillaSpecular(context, material.Shininess, uv) * material.SpecularColor * material.Glossiness * context.lightColor; +#endif +} + +void GetIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext context, MaterialProperties material, float2 uv) +{ + lobeWeights = (IndirectLobeWeights)0; +#if defined(TRUE_PBR) + PBR::GetIndirectLobeWeights(lobeWeights, context, material); +#else +# if defined(HAIR) && defined(CS_HAIR) + if (SharedData::hairSpecularSettings.Enabled) + { + Hair::GetHairIndirectLobeWeights(lobeWeights, context, material, uv); + return; + } +# endif + lobeWeights.diffuse = material.BaseColor; +# if defined(DYNAMIC_CUBEMAPS) + if (any(material.F0 > 0)) { + const float3 N = context.worldNormal; + const float3 V = context.viewDir; + const float3 VN = context.vertexNormal; + + float NdotV = saturate(dot(N, V)); + + float2 specularBRDF = BRDF::EnvBRDF(material.Roughness, NdotV); + lobeWeights.specular = material.F0 * specularBRDF.x + specularBRDF.y; + lobeWeights.specular *= 1 + material.F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); + + // Horizon specular occlusion + // https://marmosetco.tumblr.com/post/81245981087 + float3 R = reflect(-V, N); + float horizon = min(1.0 + dot(R, VN), 1.0); + horizon = horizon * horizon; + lobeWeights.specular *= horizon; + } +# endif +#endif +} + +#if defined(WETNESS_EFFECTS) +void EvaluateWetnessLighting(float3 wetnessNormal, DirectContext context, float roughness, inout DirectLightingOutput lightingOutput) +{ + const float wetnessStrength = saturate(1 - roughness); +# if defined(TRUE_PBR) + const float3 lightColor = context.coatLightColor; +# else + const float3 lightColor = context.lightColor; +# endif + + const float wetnessF0 = 0.02; + + const float3 N = wetnessNormal; + const float3 V = context.viewDir; + const float3 L = context.lightDir; + const float3 H = context.halfVector; + + float NdotL = clamp(dot(N, L), EPSILON_DOT_CLAMP, 1); + float NdotV = saturate(abs(dot(N, V)) + EPSILON_DOT_CLAMP); + float NdotH = saturate(dot(N, H)); + float VdotH = saturate(dot(V, H)); + + float D = BRDF::D_GGX(roughness, NdotH); + float G = BRDF::Vis_SmithJointApprox(roughness, NdotV, NdotL); + float3 F = BRDF::F_Schlick(wetnessF0, VdotH); + + F *= wetnessStrength; + + float3 wetnessSpecular = D * G * F * NdotL * lightColor; + +#if !defined(TRUE_PBR) + wetnessSpecular *= Math::PI * Color::PBRLightingScale; // Compensate for GGX on traditional specular +#endif + + lightingOutput.diffuse *= 1 - F; + lightingOutput.specular *= 1 - F; + lightingOutput.specular += wetnessSpecular; +} + +float3 GetWetnessIndirectLobeWeights(inout IndirectLobeWeights lobeWeights, float3 wetnessNormal, float roughness, IndirectContext context) +{ + const float wetnessF0 = 0.02; + const float wetnessStrength = saturate(1 - roughness); + + const float3 N = wetnessNormal; + const float3 V = context.viewDir; + const float3 VN = context.vertexNormal; + + float NdotV = saturate(abs(dot(N, V)) + EPSILON_DOT_CLAMP); + float2 specularBRDF = BRDF::EnvBRDF(roughness, NdotV); + float3 specularLobeWeight = wetnessF0 * specularBRDF.x + specularBRDF.y; + + specularLobeWeight *= wetnessStrength; + + lobeWeights.diffuse *= 1 - specularLobeWeight; + lobeWeights.specular *= 1 - specularLobeWeight; + + // Horizon specular occlusion + // https://marmosetco.tumblr.com/post/81245981087 + float3 R = reflect(-V, N); + float horizon = min(1.0 + dot(R, VN), 1.0); + horizon = horizon * horizon; + specularLobeWeight *= horizon; + + return specularLobeWeight; +} +#endif +#endif \ No newline at end of file diff --git a/package/Shaders/Common/PBR.hlsli b/package/Shaders/Common/PBR.hlsli index 65dd3a45d7..6608ee6dc8 100644 --- a/package/Shaders/Common/PBR.hlsli +++ b/package/Shaders/Common/PBR.hlsli @@ -1,5 +1,6 @@ #ifndef __PBR_DEPENDENCY_HLSL__ #define __PBR_DEPENDENCY_HLSL__ +#include "Common/LightingCommon.hlsli" #include "Common/BRDF.hlsli" #include "Common/Color.hlsli" @@ -59,109 +60,6 @@ namespace PBR static const float MaxGlintDensityRandomization = 5.0f; } -#if defined(GLINT) -# include "Common/Glints/Glints2023.hlsli" -#else - namespace Glints - { - typedef float GlintCachedVars; - } -#endif - - struct SurfaceProperties - { - float3 BaseColor; - float Roughness; - float Metallic; - float AO; - float3 F0; - float3 SubsurfaceColor; - float Thickness; - float3 CoatColor; - float CoatStrength; - float CoatRoughness; - float3 CoatF0; - float3 FuzzColor; - float FuzzWeight; - float GlintScreenSpaceScale; - float GlintLogMicrofacetDensity; - float GlintMicrofacetRoughness; - float GlintDensityRandomization; - Glints::GlintCachedVars GlintCache; - float Noise; - }; - - SurfaceProperties InitSurfaceProperties() - { - SurfaceProperties surfaceProperties; - - surfaceProperties.Roughness = 1; - surfaceProperties.Metallic = 0; - surfaceProperties.AO = 1; - surfaceProperties.F0 = 0; - - surfaceProperties.SubsurfaceColor = 0; - surfaceProperties.Thickness = 0; - - surfaceProperties.CoatColor = 0; - surfaceProperties.CoatStrength = 0; - surfaceProperties.CoatRoughness = 0; - surfaceProperties.CoatF0 = 0; - - surfaceProperties.FuzzColor = 0; - surfaceProperties.FuzzWeight = 0; - - surfaceProperties.GlintScreenSpaceScale = 1.5; - surfaceProperties.GlintLogMicrofacetDensity = 1.0; - surfaceProperties.GlintMicrofacetRoughness = 0.015; - surfaceProperties.GlintDensityRandomization = 2.0; - -#ifdef GLINT - surfaceProperties.GlintCache.uv = 0; - surfaceProperties.GlintCache.gridSeed = 0; - surfaceProperties.GlintCache.footprintArea = 0; - surfaceProperties.Noise = 0; -#endif - - return surfaceProperties; - } - - struct LightProperties - { - float3 LightColor; - float3 CoatLightColor; - }; - - LightProperties InitLightProperties(float3 lightColor, float3 nonParallaxShadow, float3 parallaxShadow) - { - LightProperties result; - result.LightColor = lightColor * nonParallaxShadow * parallaxShadow; - [branch] if ((PBRFlags & Flags::InterlayerParallax) != 0) - { - result.CoatLightColor = lightColor * nonParallaxShadow; - } - else - { - result.CoatLightColor = result.LightColor; - } - return result; - } - - // [Jimenez et al. 2016, "Practical Realtime Strategies for Accurate Indirect Occlusion"] - float3 MultiBounceAO(float3 baseColor, float ao) - { - float3 a = 2.0404 * baseColor - 0.3324; - float3 b = -4.7951 * baseColor + 0.6417; - float3 c = 2.7552 * baseColor + 0.6903; - return max(ao, ((ao * a + b) * ao + c) * ao); - } - - // [Lagarde et al. 2014, "Moving Frostbite to Physically Based Rendering 3.0"] - float SpecularAOLagarde(float NdotV, float ao, float roughness) - { - return saturate(pow(abs(NdotV + ao), exp2(-16.0 * roughness - 1.0)) - 1.0 + ao); - } - #if defined(GLINT) float3 GetSpecularDirectLightMultiplierMicrofacetWithGlint(float noise, float roughness, float3 specularColor, float NdotL, float NdotV, float NdotH, float VdotH, float glintH, float logDensity, float microfacetRoughness, float densityRandomization, Glints::GlintCachedVars glintCache, @@ -218,7 +116,7 @@ namespace PBR return exp(-0.5 * Theta * Theta / (B * B)) / (sqrt(Math::TAU) * B); } - float3 GetHairDiffuseColorMarschner(float3 N, float3 V, float3 L, float NdotL, float NdotV, float VdotL, float backlit, float area, SurfaceProperties surfaceProperties) + float3 GetHairDiffuseColorMarschner(float3 N, float3 V, float3 L, float NdotL, float NdotV, float VdotL, float backlit, float area, MaterialProperties material) { float3 S = 0; @@ -227,7 +125,7 @@ namespace PBR float cosThetaD = sqrt((1 + cosThetaL * cosThetaV + NdotV * NdotL) / 2.0); const float3 Lp = L - NdotL * N; - const float3 Vp = V - NdotL * N; + const float3 Vp = V - NdotV * N; const float cosPhi = dot(Lp, Vp) * rsqrt(dot(Lp, Lp) * dot(Vp, Vp) + EPSILON_DIVISION); const float cosHalfPhi = sqrt(saturate(0.5 + 0.5 * cosPhi)); @@ -240,9 +138,9 @@ namespace PBR Shift * 4 }; float B[] = { - area + surfaceProperties.Roughness, - area + surfaceProperties.Roughness / 2, - area + surfaceProperties.Roughness * 2 + area + material.Roughness, + area + material.Roughness / 2, + area + material.Roughness * 2 }; float hairIOR = HairIOR(); @@ -263,7 +161,7 @@ namespace PBR h = cosHalfPhi * (1 + a * (0.6 - 0.8 * cosPhi)); f = BRDF::F_Schlick(specularColor, cosThetaD * sqrt(saturate(1 - h * h))).x; Fp = (1 - f) * (1 - f); - Tp = pow(abs(surfaceProperties.BaseColor), 0.5 * sqrt(1 - (h * a) * (h * a)) / cosThetaD); + Tp = pow(abs(material.BaseColor), 0.5 * sqrt(1 - (h * a) * (h * a)) / cosThetaD); Np = exp(-3.65 * cosPhi - 3.98); S += (Mp * Np) * (Fp * Tp) * backlit; @@ -271,14 +169,14 @@ namespace PBR Mp = HairGaussian(B[2], ThetaH - Alpha[2]); f = BRDF::F_Schlick(specularColor, cosThetaD * 0.5f).x; Fp = (1 - f) * (1 - f) * f; - Tp = pow(abs(surfaceProperties.BaseColor), 0.8 / cosThetaD); + Tp = pow(abs(material.BaseColor), 0.8 / cosThetaD); Np = exp(17 * cosPhi - 16.78); S += (Mp * Np) * (Fp * Tp); return S; } - float3 GetHairDiffuseAttenuationKajiyaKay(float3 N, float3 V, float3 L, float NdotL, float NdotV, float shadow, SurfaceProperties surfaceProperties) + float3 GetHairDiffuseAttenuationKajiyaKay(float3 N, float3 V, float3 L, float NdotL, float NdotV, float shadow, MaterialProperties material) { float3 S = 0; @@ -288,32 +186,36 @@ namespace PBR const float wrap = 1; float wrappedNdotL = saturate((dot(fakeN, L) + wrap) / ((1 + wrap) * (1 + wrap))); float diffuseScatter = (1 / Math::PI) * lerp(wrappedNdotL, diffuseKajiya, 0.33); - float luma = Color::RGBToLuminance(surfaceProperties.BaseColor); - float3 scatterTint = pow(surfaceProperties.BaseColor / luma, 1 - shadow); - S += sqrt(surfaceProperties.BaseColor) * diffuseScatter * scatterTint; + float luma = Color::RGBToLuminance(material.BaseColor); + float3 scatterTint = pow(material.BaseColor / luma, 1 - shadow); + S += sqrt(material.BaseColor) * diffuseScatter * scatterTint; return S; } - float3 GetHairColorMarschner(float3 N, float3 V, float3 L, float NdotL, float NdotV, float VdotL, float shadow, float backlit, float area, SurfaceProperties surfaceProperties) + float3 GetHairColorMarschner(float3 N, float3 V, float3 L, float NdotL, float NdotV, float VdotL, float shadow, float backlit, float area, MaterialProperties material) { float3 color = 0; - color += GetHairDiffuseColorMarschner(N, V, L, NdotL, NdotV, VdotL, backlit, area, surfaceProperties); - color += GetHairDiffuseAttenuationKajiyaKay(N, V, L, NdotL, NdotV, shadow, surfaceProperties); + color += GetHairDiffuseColorMarschner(N, V, L, NdotL, NdotV, VdotL, backlit, area, material); + color += GetHairDiffuseAttenuationKajiyaKay(N, V, L, NdotL, NdotV, shadow, material); return color; } - void GetDirectLightInput(out float3 diffuse, out float3 coatDiffuse, out float3 transmission, out float3 specular, float3 N, float3 coatN, float3 V, float3 coatV, float3 L, float3 coatL, LightProperties lightProperties, SurfaceProperties surfaceProperties, - float3x3 tbnTr, float2 uv) + void GetDirectLightInput(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) { - diffuse = 0; - coatDiffuse = 0; - transmission = 0; - specular = 0; + lightingOutput = (DirectLightingOutput)0; - float3 H = normalize(V + L); + const float3 N = context.worldNormal; + const float3 V = context.viewDir; + const float3 L = context.lightDir; + const float3 H = context.halfVector; + + const float3 coatN = context.coatWorldNormal; + const float3 coatV = context.coatViewDir; + const float3 coatL = context.coatLightDir; + const float3 coatH = context.coatHalfVector; float NdotL = dot(N, L); float NdotV = dot(N, V); @@ -330,46 +232,44 @@ namespace PBR #if !defined(LANDSCAPE) && !defined(LODLANDSCAPE) [branch] if ((PBRFlags & Flags::HairMarschner) != 0) { - transmission += lightProperties.LightColor * GetHairColorMarschner(N, V, L, NdotL, NdotV, VdotL, 0, 1, 0, surfaceProperties); + lightingOutput.transmission += context.lightColor * GetHairColorMarschner(N, V, L, NdotL, NdotV, VdotL, 0, 1, 0, material); } else #endif { - diffuse += lightProperties.LightColor * satNdotL * BRDF::Diffuse_Lambert(); + lightingOutput.diffuse += context.lightColor * satNdotL * BRDF::Diffuse_Lambert(); float3 F; #if defined(GLINT) - specular += GetSpecularDirectLightMultiplierMicrofacetWithGlint(surfaceProperties.Noise, surfaceProperties.Roughness, surfaceProperties.F0, satNdotL, satNdotV, satNdotH, satVdotH, mul(tbnTr, H).x, - surfaceProperties.GlintLogMicrofacetDensity, surfaceProperties.GlintMicrofacetRoughness, surfaceProperties.GlintDensityRandomization, surfaceProperties.GlintCache, F) * - lightProperties.LightColor * satNdotL; + lightingOutput.specular += GetSpecularDirectLightMultiplierMicrofacetWithGlint(material.Noise, material.Roughness, material.F0, satNdotL, satNdotV, satNdotH, satVdotH, mul(tbnTr, H).x, + material.GlintLogMicrofacetDensity, material.GlintMicrofacetRoughness, material.GlintDensityRandomization, material.GlintCache, F) * + context.lightColor * satNdotL; #else - specular += GetSpecularDirectLightMultiplierMicrofacet(surfaceProperties.Roughness, surfaceProperties.F0, satNdotL, satNdotV, satNdotH, satVdotH, F) * lightProperties.LightColor * satNdotL; + lightingOutput.specular += GetSpecularDirectLightMultiplierMicrofacet(material.Roughness, material.F0, satNdotL, satNdotV, satNdotH, satVdotH, F) * context.lightColor * satNdotL; #endif - float2 specularBRDF = BRDF::EnvBRDF(surfaceProperties.Roughness, satNdotV); - specular *= 1 + surfaceProperties.F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); + float2 specularBRDF = BRDF::EnvBRDF(material.Roughness, satNdotV); + lightingOutput.specular *= 1 + material.F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); #if !defined(LANDSCAPE) && !defined(LODLANDSCAPE) [branch] if ((PBRFlags & Flags::Fuzz) != 0) { - float3 fuzzSpecular = GetSpecularDirectLightMultiplierMicroflakes(surfaceProperties.Roughness, surfaceProperties.FuzzColor, satNdotL, satNdotV, satNdotH, satVdotH) * lightProperties.LightColor * satNdotL; - fuzzSpecular *= 1 + surfaceProperties.FuzzColor * (1 / (specularBRDF.x + specularBRDF.y) - 1); + float3 fuzzSpecular = GetSpecularDirectLightMultiplierMicroflakes(material.Roughness, material.FuzzColor, satNdotL, satNdotV, satNdotH, satVdotH) * context.lightColor * satNdotL; + fuzzSpecular *= 1 + material.FuzzColor * (1 / (specularBRDF.x + specularBRDF.y) - 1); - specular = lerp(specular, fuzzSpecular, surfaceProperties.FuzzWeight); + lightingOutput.specular = lerp(lightingOutput.specular, fuzzSpecular, material.FuzzWeight); } [branch] if ((PBRFlags & Flags::Subsurface) != 0) { const float subsurfacePower = 12.234; float forwardScatter = exp2(saturate(-VdotL) * subsurfacePower - subsurfacePower); - float backScatter = saturate(satNdotL * surfaceProperties.Thickness + (1.0 - surfaceProperties.Thickness)) * 0.5; - float subsurface = lerp(backScatter, 1, forwardScatter) * (1.0 - surfaceProperties.Thickness); - transmission += surfaceProperties.SubsurfaceColor * subsurface * lightProperties.LightColor * BRDF::Diffuse_Lambert(); + float backScatter = saturate(satNdotL * material.Thickness + (1.0 - material.Thickness)) * 0.5; + float subsurface = lerp(backScatter, 1, forwardScatter) * (1.0 - material.Thickness); + lightingOutput.transmission += material.SubsurfaceColor * subsurface * context.lightColor * BRDF::Diffuse_Lambert(); } else if ((PBRFlags & Flags::TwoLayer) != 0) { - float3 coatH = normalize(coatV + coatL); - float coatNdotL = satNdotL; float coatNdotV = satNdotV; float coatNdotH = satNdotH; @@ -383,14 +283,14 @@ namespace PBR } float3 coatF; - float3 coatSpecular = GetSpecularDirectLightMultiplierMicrofacet(surfaceProperties.CoatRoughness, surfaceProperties.CoatF0, coatNdotL, coatNdotV, coatNdotH, coatVdotH, coatF) * lightProperties.CoatLightColor * coatNdotL; + float3 coatSpecular = GetSpecularDirectLightMultiplierMicrofacet(material.CoatRoughness, material.CoatF0, coatNdotL, coatNdotV, coatNdotH, coatVdotH, coatF) * context.coatLightColor * coatNdotL; - float3 layerAttenuation = 1 - coatF * surfaceProperties.CoatStrength; - diffuse *= layerAttenuation; - specular *= layerAttenuation; + float3 layerAttenuation = 1 - coatF * material.CoatStrength; + lightingOutput.diffuse *= layerAttenuation; + lightingOutput.specular *= layerAttenuation; - coatDiffuse += lightProperties.CoatLightColor * coatNdotL * BRDF::Diffuse_Lambert(); - specular += coatSpecular * surfaceProperties.CoatStrength; + lightingOutput.coatDiffuse += context.coatLightColor * coatNdotL * BRDF::Diffuse_Lambert(); + lightingOutput.specular += coatSpecular * material.CoatStrength; } #endif } @@ -413,10 +313,13 @@ namespace PBR return wetnessSpecular * wetnessStrength; } - void GetIndirectLobeWeights(out float3 diffuseLobeWeight, out float3 specularLobeWeight, float3 N, float3 V, float3 VN, float3 diffuseColor, SurfaceProperties surfaceProperties) + void GetIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext context, MaterialProperties material) { - diffuseLobeWeight = 0; - specularLobeWeight = 0; + lobeWeights = (IndirectLobeWeights)0; + + const float3 N = context.worldNormal; + const float3 V = context.viewDir; + const float3 VN = context.vertexNormal; float NdotV = saturate(dot(N, V)); @@ -426,49 +329,49 @@ namespace PBR float3 L = normalize(V - N * dot(V, N)); float NdotL = dot(N, L); float VdotL = dot(V, L); - diffuseLobeWeight = GetHairColorMarschner(N, V, L, NdotL, NdotV, VdotL, 1, 0, 0.2, surfaceProperties); + lobeWeights.diffuse = GetHairColorMarschner(N, V, L, NdotL, NdotV, VdotL, 1, 0, 0.2, material); } else #endif { - diffuseLobeWeight = diffuseColor; + lobeWeights.diffuse = material.BaseColor; #if !defined(LANDSCAPE) && !defined(LODLANDSCAPE) [branch] if ((PBRFlags & Flags::Subsurface) != 0) { - diffuseLobeWeight += surfaceProperties.SubsurfaceColor * (1 - surfaceProperties.Thickness) / Math::PI; + lobeWeights.diffuse += material.SubsurfaceColor * (1 - material.Thickness) / Math::PI; } [branch] if ((PBRFlags & Flags::Fuzz) != 0) { - diffuseLobeWeight += surfaceProperties.FuzzColor * surfaceProperties.FuzzWeight; + lobeWeights.diffuse += material.FuzzColor * material.FuzzWeight; } #endif - float2 specularBRDF = BRDF::EnvBRDF(surfaceProperties.Roughness, NdotV); - specularLobeWeight = surfaceProperties.F0 * specularBRDF.x + specularBRDF.y; + float2 specularBRDF = BRDF::EnvBRDF(material.Roughness, NdotV); + lobeWeights.specular = material.F0 * specularBRDF.x + specularBRDF.y; - diffuseLobeWeight *= (1 - specularLobeWeight); - specularLobeWeight *= 1 + surfaceProperties.F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); + lobeWeights.diffuse *= (1 - lobeWeights.specular); + lobeWeights.specular *= 1 + material.F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); #if !defined(LANDSCAPE) && !defined(LODLANDSCAPE) [branch] if ((PBRFlags & Flags::TwoLayer) != 0) { - float2 coatSpecularBRDF = BRDF::EnvBRDF(surfaceProperties.CoatRoughness, NdotV); - float3 coatSpecularLobeWeight = surfaceProperties.CoatF0 * coatSpecularBRDF.x + coatSpecularBRDF.y; - coatSpecularLobeWeight *= 1 + surfaceProperties.CoatF0 * (1 / (coatSpecularBRDF.x + coatSpecularBRDF.y) - 1); + float2 coatSpecularBRDF = BRDF::EnvBRDF(material.CoatRoughness, NdotV); + float3 coatSpecularLobeWeight = material.CoatF0 * coatSpecularBRDF.x + coatSpecularBRDF.y; + coatSpecularLobeWeight *= 1 + material.CoatF0 * (1 / (coatSpecularBRDF.x + coatSpecularBRDF.y) - 1); - float3 coatF = BRDF::F_Schlick(surfaceProperties.CoatF0, NdotV); + float3 coatF = BRDF::F_Schlick(material.CoatF0, NdotV); - float3 layerAttenuation = 1 - coatF * surfaceProperties.CoatStrength; - diffuseLobeWeight *= layerAttenuation; - specularLobeWeight *= layerAttenuation; + float3 layerAttenuation = 1 - coatF * material.CoatStrength; + lobeWeights.diffuse *= layerAttenuation; + lobeWeights.specular *= layerAttenuation; [branch] if ((PBRFlags & Flags::ColoredCoat) != 0) { - float3 coatDiffuseLobeWeight = surfaceProperties.CoatColor * (1 - coatSpecularLobeWeight); - diffuseLobeWeight += coatDiffuseLobeWeight * surfaceProperties.CoatStrength; + float3 coatDiffuseLobeWeight = material.CoatColor * (1 - coatSpecularLobeWeight); + lobeWeights.diffuse += coatDiffuseLobeWeight * material.CoatStrength; } - specularLobeWeight += coatSpecularLobeWeight * surfaceProperties.CoatStrength; + lobeWeights.specular += coatSpecularLobeWeight * material.CoatStrength; } #endif } @@ -478,16 +381,16 @@ namespace PBR float3 R = reflect(-V, N); float horizon = min(1.0 + dot(R, VN), 1.0); horizon = horizon * horizon; - specularLobeWeight *= horizon; + lobeWeights.specular *= horizon; - float3 diffuseAO = surfaceProperties.AO; - float3 specularAO = SpecularAOLagarde(NdotV, surfaceProperties.AO, surfaceProperties.Roughness); + float3 diffuseAO = material.AO; + float3 specularAO = Color::SpecularAOLagarde(NdotV, material.AO, material.Roughness); - diffuseAO = MultiBounceAO(diffuseColor, diffuseAO.x).y; - specularAO = MultiBounceAO(surfaceProperties.F0, specularAO.x).y; + diffuseAO = Color::MultiBounceAO(material.BaseColor, diffuseAO.x).y; + specularAO = Color::MultiBounceAO(material.F0, specularAO.x).y; - diffuseLobeWeight *= diffuseAO * Color::PBRLightingScale; - specularLobeWeight *= specularAO; + lobeWeights.diffuse *= diffuseAO; + lobeWeights.specular *= specularAO; } float3 GetWetnessIndirectSpecularLobeWeight(float3 N, float3 V, float3 VN, float roughness) diff --git a/package/Shaders/Common/Permutation.hlsli b/package/Shaders/Common/Permutation.hlsli index b197c1f2b6..5cd4babe9c 100644 --- a/package/Shaders/Common/Permutation.hlsli +++ b/package/Shaders/Common/Permutation.hlsli @@ -72,6 +72,7 @@ namespace Permutation static const int THLand3HasDisplacement = (1 << 3); static const int THLand4HasDisplacement = (1 << 4); static const int THLand5HasDisplacement = (1 << 5); + static const int THLandHasDisplacement = (1 << 9); } cbuffer PerShader : register(b4) diff --git a/package/Shaders/Common/ShadowSampling.hlsli b/package/Shaders/Common/ShadowSampling.hlsli index e688537f92..000bfdb5cf 100644 --- a/package/Shaders/Common/ShadowSampling.hlsli +++ b/package/Shaders/Common/ShadowSampling.hlsli @@ -149,7 +149,7 @@ namespace ShadowSampling float worldShadow = 1.0; #if defined(TERRAIN_SHADOWS) - worldShadow = lerp(TerrainShadows::GetTerrainShadow(positionWS + offset, LinearSampler), 1.0, 0.1); + worldShadow = TerrainShadows::GetTerrainShadow(positionWS + offset, LinearSampler); #endif #if defined(CLOUD_SHADOWS) diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 7e423f60d0..2c684c65b2 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -143,6 +143,10 @@ namespace SharedData float LODObjectBrightness; float LODObjectSnowBrightness; bool DisableTerrainVertexColors; + float LODTerrainGamma; + float LODObjectGamma; + float LODObjectSnowGamma; + float pad0; }; struct HairSpecularSettings @@ -179,11 +183,11 @@ namespace SharedData uint EnableDiffuseIBL; uint PreserveFogLuminance; uint UseStaticIBL; + uint EnableInterior; float DiffuseIBLScale; float DALCAmount; float IBLSaturation; float FogAmount; - float DynamicCubemapsAmount; }; struct ExtendedTranslucencySettings @@ -212,7 +216,7 @@ namespace SharedData float3 masserColor; float apTrMix; // float3 secundaDir; - float _pad3; // + float sunDiskCos; // float3 secundaColor; // GENERAL diff --git a/package/Shaders/DeferredCompositeCS.hlsl b/package/Shaders/DeferredCompositeCS.hlsl index 18e4d57149..2f6e8d58ea 100644 --- a/package/Shaders/DeferredCompositeCS.hlsl +++ b/package/Shaders/DeferredCompositeCS.hlsl @@ -10,7 +10,7 @@ Texture2D SpecularTexture : register(t0); Texture2D AlbedoTexture : register(t1); Texture2D NormalRoughnessTexture : register(t2); -Texture2D MasksTexture : register(t3); +Texture2D MasksTexture : register(t3); RWTexture2D MainRW : register(u0); RWTexture2D NormalTAAMaskSpecularMaskRW : register(u1); @@ -39,21 +39,26 @@ Texture2D SsgiYTexture : register(t11); Texture2D SsgiCoCgTexture : register(t12); Texture2D SsgiSpecularTexture : register(t13); -void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, out float ao, out float3 il, in float3 normal, in float3 view) +void SampleSSGI(uint2 pixCoord, float3 normalWS, out float ao, out float3 il) +{ + ao = 1 - SsgiAoTexture[pixCoord]; + float4 ssgiIlYSh = SsgiYTexture[pixCoord]; + // without ZH hallucination + // float ssgiIlY = SphericalHarmonics::FuncProductIntegral(ssgiIlYSh, SphericalHarmonics::EvaluateCosineLobe(normalWS)); + float ssgiIlY = SphericalHarmonics::SHHallucinateZH3Irradiance(ssgiIlYSh, normalWS); + float2 ssgiIlCoCg = SsgiCoCgTexture[pixCoord]; + il = max(0, Color::YCoCgToRGB(float3(ssgiIlY, ssgiIlCoCg))); +} + +void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, out float ao, out float3 il, in float3 normal, in float3 view, in float roughness) { - // https://www.iryoku.com/stare-into-the-future/ ao = 1 - SsgiAoTexture[pixCoord].x; - const float SpecularPow = 8.0; float NdotV = dot(normal, view); - float s = saturate(-0.3 + NdotV * NdotV); - ao = lerp(pow(ao, SpecularPow), 1.0, s); + ao = Color::SpecularAOLagarde(saturate(NdotV), ao, roughness); float4 ssgiIlYSh = SsgiYTexture[pixCoord]; float ssgiIlY = SphericalHarmonics::FuncProductIntegral(ssgiIlYSh, lobe); float2 ssgiIlCoCg = SsgiCoCgTexture[pixCoord].xy; - // specular is a bit too saturated, because CoCg are average over hemisphere - // we just cheese this bit - ssgiIlCoCg *= 0.8; // pi to compensate for the /pi in specularLobe // i don't think there really should be a 1/PI but without it the specular is too strong @@ -67,6 +72,15 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, out float ao, out float3 il, i } #endif +#if defined(IBL) +# if !defined(DYNAMIC_CUBEMAPS) +# undef IBL +# else +# define IBL_DEFERRED +# include "IBL/IBL.hlsli" +# endif +#endif + #if defined(PHYSICAL_SKY) # define PS_DEFERRED_RSRCS # define PS_DEFERRED_SAMPLERS @@ -103,21 +117,58 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, out float ao, out float3 il, i float glossiness = normalGlossiness.z; - float3 color = Color::GammaToLinear(diffuseColor) + specularColor; + float3 linDiffuseColor = Color::GammaToLinear(diffuseColor); + float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(normalVS, 0)).xyz); -#if defined(DYNAMIC_CUBEMAPS) +#if defined(SSGI) - float3 reflectance = ReflectanceTexture[dispatchID.xy]; + float ssgiAo; + float3 ssgiIl; + SampleSSGI(dispatchID.xy, normalWS, ssgiAo, ssgiIl); - if (reflectance.x > 0.0 || reflectance.y > 0.0 || reflectance.z > 0.0) { - float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(normalVS, 0)).xyz); + float3 directionalAmbientColor = max(0, mul(SharedData::DirectionalAmbient, float4(normalWS, 1.0))); + directionalAmbientColor *= albedo; + + directionalAmbientColor = Color::RGBToYCoCg(directionalAmbientColor); + directionalAmbientColor.x = MasksTexture[dispatchID.xy].z; + directionalAmbientColor = Color::YCoCgToRGB(directionalAmbientColor); + directionalAmbientColor = max(0, directionalAmbientColor); - float wetnessMask = MasksTexture[dispatchID.xy].z * 2.0 - 1.0; + float maxScale = 1.0; + if (directionalAmbientColor.x > 0.0) + maxScale = min(maxScale, diffuseColor.x / directionalAmbientColor.x); + if (directionalAmbientColor.y > 0.0) + maxScale = min(maxScale, diffuseColor.y / directionalAmbientColor.y); + if (directionalAmbientColor.z > 0.0) + maxScale = min(maxScale, diffuseColor.z / directionalAmbientColor.z); + directionalAmbientColor *= maxScale; - normalWS.z = wetnessMask; - float xyLength = sqrt(max(0.0, 1.0 - wetnessMask * wetnessMask)); - normalWS.xy = normalize(normalWS.xy) * xyLength; + diffuseColor = max(0.0, diffuseColor - directionalAmbientColor); + + linDiffuseColor = Color::GammaToLinear(diffuseColor); + + float3 linAlbedo = Color::GammaToLinear(albedo / Color::PBRLightingScale); + + float3 multiBounceAO = Color::MultiBounceAO(linAlbedo, ssgiAo); + + linDiffuseColor *= sqrt(multiBounceAO); + + diffuseColor = Color::LinearToGamma(linDiffuseColor); + + diffuseColor += Color::LinearToGamma(Color::GammaToLinear(directionalAmbientColor) * multiBounceAO); + + linDiffuseColor = Color::GammaToLinear(diffuseColor); + + linDiffuseColor += ssgiIl * linAlbedo; +#endif + + float3 color = linDiffuseColor + specularColor; + +#if defined(DYNAMIC_CUBEMAPS) + float3 reflectance = ReflectanceTexture[dispatchID.xy]; + + if (reflectance.x > 0.0 || reflectance.y > 0.0 || reflectance.z > 0.0) { float3 V = normalize(positionWS.xyz); float3 R = reflect(V, normalWS); @@ -128,10 +179,25 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, out float ao, out float3 il, i float3 finalIrradiance = 0; + float directionalAmbientColorSpecular = Color::RGBToLuminance(max(0, mul(SharedData::DirectionalAmbient, float4(R, 1.0)))) * Color::ReflectionNormalisationScale; + # if defined(INTERIOR) - float3 specularIrradiance = Color::GammaToLinear(EnvTexture.SampleLevel(LinearSampler, R, level)); + float3 specularIrradiance = EnvTexture.SampleLevel(LinearSampler, R, level); + + float specularIrradianceLuminance = Color::RGBToLuminance(EnvTexture.SampleLevel(LinearSampler, R, 15)); + +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL && SharedData::iblSettings.EnableInterior) { + directionalAmbientColorSpecular *= SharedData::iblSettings.DALCAmount; + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-R), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; + float iblColorLuminance = Color::RGBToLuminance(Color::LinearToGamma(iblColor)); + directionalAmbientColorSpecular += iblColorLuminance; + } +# endif + specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; - finalIrradiance += specularIrradiance; + finalIrradiance = Color::GammaToLinear(specularIrradiance); # elif defined(SKYLIGHTING) # if defined(VR) float3 positionMS = positionWS.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; @@ -142,48 +208,79 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, out float ao, out float3 il, i sh2 skylighting = Skylighting::sample(SharedData::skylightingSettings, SkylightingProbeArray, stbn_vec3_2Dx1D_128x128x64, dispatchID.xy, positionMS.xyz, R); float skylightingSpecular = SphericalHarmonics::FuncProductIntegral(skylighting, specularLobe); + skylightingSpecular = saturate(skylightingSpecular); skylightingSpecular = Skylighting::mixSpecular(SharedData::skylightingSettings, skylightingSpecular); - float3 specularIrradiance = 1; +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL) { + directionalAmbientColorSpecular *= SharedData::iblSettings.DALCAmount; + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-R, skylightingSpecular), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; + float iblColorLuminance = Color::RGBToLuminance(Color::LinearToGamma(iblColor)); + directionalAmbientColorSpecular += iblColorLuminance; + } +# endif + + float3 specularIrradianceReflections = 0.0; + + if (skylightingSpecular > 0.0){ + specularIrradianceReflections = EnvReflectionsTexture.SampleLevel(LinearSampler, R, level); + + float specularIrradianceLuminance = Color::RGBToLuminance(EnvReflectionsTexture.SampleLevel(LinearSampler, R, 15)); + + specularIrradianceReflections = (specularIrradianceReflections / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; + + specularIrradianceReflections = Color::GammaToLinear(specularIrradianceReflections); + + } - if (skylightingSpecular < 1.0) - specularIrradiance = Color::GammaToLinear(EnvTexture.SampleLevel(LinearSampler, R, level)); + float3 specularIrradiance = 0.0; - float3 specularIrradianceReflections = 1.0; + if (skylightingSpecular < 1.0){ + specularIrradiance = EnvTexture.SampleLevel(LinearSampler, R, level); - if (skylightingSpecular > 0.0) - specularIrradianceReflections = Color::GammaToLinear(EnvReflectionsTexture.SampleLevel(LinearSampler, R, level)); + float specularIrradianceLuminance = Color::RGBToLuminance(EnvTexture.SampleLevel(LinearSampler, R, 15)); + + directionalAmbientColorSpecular = Color::GammaToLinear(directionalAmbientColorSpecular); + directionalAmbientColorSpecular *= skylightingSpecular; + directionalAmbientColorSpecular = Color::LinearToGamma(directionalAmbientColorSpecular); + + specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; + + specularIrradiance = Color::GammaToLinear(specularIrradiance); + } finalIrradiance = lerp(specularIrradiance, specularIrradianceReflections, skylightingSpecular); # else - float3 specularIrradianceReflections = Color::GammaToLinear(EnvReflectionsTexture.SampleLevel(LinearSampler, R, level)); +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL) { + directionalAmbientColorSpecular *= SharedData::iblSettings.DALCAmount; + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-R), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; + float iblColorLuminance = Color::RGBToLuminance(Color::LinearToGamma(iblColor)); + directionalAmbientColorSpecular += iblColorLuminance; + } +# endif + float3 specularIrradianceReflections = EnvReflectionsTexture.SampleLevel(LinearSampler, R, level); + + float specularIrradianceReflectionsLuminance = Color::RGBToLuminance(EnvReflectionsTexture.SampleLevel(LinearSampler, R, 15)); + + specularIrradianceReflections = (specularIrradianceReflections / max(specularIrradianceReflectionsLuminance, 0.001)) * directionalAmbientColorSpecular; - finalIrradiance += specularIrradianceReflections; + finalIrradiance = Color::GammaToLinear(specularIrradianceReflections); # endif # if defined(SSGI) -# if defined(VR) - float3 uvF = float3((dispatchID.xy + 0.5) * SharedData::BufferDim.zw, DepthTexture[dispatchID.xy]); // calculate high precision uv of initial eye - float3 uv2 = Stereo::ConvertStereoUVToOtherEyeStereoUV(uvF, eyeIndex, false); // calculate other eye uv - float3 uv1Mono = Stereo::ConvertFromStereoUV(uvF, eyeIndex); - float3 uv2Mono = Stereo::ConvertFromStereoUV(uv2, (1 - eyeIndex)); - uint2 pixCoord2 = (uint2)(uv2.xy / SharedData::BufferDim.zw - 0.5); -# endif - float ssgiAo; float3 ssgiIlSpecular; - SampleSSGISpecular(dispatchID.xy, specularLobe, ssgiAo, ssgiIlSpecular, normalWS, V); + SampleSSGISpecular(dispatchID.xy, specularLobe, ssgiAo, ssgiIlSpecular, normalWS, V, roughness); -# if defined(VR) - float ssgiAo2; - float3 ssgiIlSpecular2; - SampleSSGISpecular(pixCoord2, specularLobe, ssgiAo2, ssgiIlSpecular2, normalWS, V); - float4 ssgiMixed = Stereo::BlendEyeColors(uv1Mono, float4(ssgiIlSpecular, ssgiAo), uv2Mono, float4(ssgiIlSpecular2, ssgiAo2)); - ssgiAo = ssgiMixed.a; - ssgiIlSpecular = ssgiMixed.rgb; -# endif + finalIrradiance = (finalIrradiance * ssgiAo); + + ssgiIlSpecular = Color::RGBToYCoCg(ssgiIlSpecular); + ssgiIlSpecular = max(0, Color::YCoCgToRGB(float3(ssgiIlSpecular.x, lerp(ssgiIlSpecular.yz, Color::RGBToYCoCg(finalIrradiance).yz, 0.5)))); - finalIrradiance = (finalIrradiance * ssgiAo) + ssgiIlSpecular; + finalIrradiance += ssgiIlSpecular; # endif color += reflectance * finalIrradiance; diff --git a/package/Shaders/DistantTree.hlsl b/package/Shaders/DistantTree.hlsl index 5ff26a98d5..75983b376d 100644 --- a/package/Shaders/DistantTree.hlsl +++ b/package/Shaders/DistantTree.hlsl @@ -6,6 +6,10 @@ #include "Common/SharedData.hlsli" #include "Common/VR.hlsli" +#if !defined(DYNAMIC_CUBEMAPS) && defined(IBL) +# undef IBL +#endif + struct VS_INPUT { float3 Position : POSITION0; @@ -206,7 +210,7 @@ PS_OUTPUT main(PS_INPUT input) discard; } - float alpha = TexDiffuse.Sample(SampDiffuse, input.TexCoord.xy).w; + float alpha = TexDiffuse.SampleBias(SampDiffuse, input.TexCoord.xy, SharedData::MipBias).w; if ((alpha - AlphaTestRefRS) < 0) { discard; @@ -215,7 +219,7 @@ PS_OUTPUT main(PS_INPUT input) psout.Diffuse.xyz = input.Depth.xxx / input.Depth.yyy; psout.Diffuse.w = 0; # else - float4 baseColor = TexDiffuse.Sample(SampDiffuse, input.TexCoord.xy); + float4 baseColor = TexDiffuse.SampleBias(SampDiffuse, input.TexCoord.xy, SharedData::MipBias); if ((baseColor.w - AlphaTestRefRS) < 0) { discard; @@ -246,16 +250,20 @@ PS_OUTPUT main(PS_INPUT input) float3 ddy = ddy_coarse(input.WorldPosition.xyz); float3 normal = -normalize(cross(ddx, ddy)); -# if !defined(SSGI) float3 directionalAmbientColor = max(0, mul(SharedData::DirectionalAmbient, float4(normal, 1.0))); -# if defined(IBL) +# if defined(IBL) + float3 iblColor = 0; if (SharedData::iblSettings.EnableDiffuseIBL) { directionalAmbientColor *= SharedData::iblSettings.DALCAmount; - directionalAmbientColor += Color::Saturation(ImageBasedLighting::GetDiffuseIBL(-normal), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# if defined(SKYLIGHTING) + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-normal, 1.0), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# else + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-normal), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# endif + directionalAmbientColor += Color::LinearToGamma(iblColor); } -# endif - diffuseColor += directionalAmbientColor; # endif + diffuseColor += directionalAmbientColor; psout.Diffuse.xyz = diffuseColor * baseColor.xyz; psout.Diffuse.w = 1; @@ -290,9 +298,15 @@ PS_OUTPUT main(PS_INPUT input) float3 directionalAmbientColor = mul(SharedData::DirectionalAmbient, float4(normal, 1.0)); # if defined(IBL) + float3 iblColor = 0; if (SharedData::iblSettings.EnableDiffuseIBL) { directionalAmbientColor *= SharedData::iblSettings.DALCAmount; - directionalAmbientColor += Color::Saturation(ImageBasedLighting::GetDiffuseIBL(-normal), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# if defined(SKYLIGHTING) + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-normal, 1.0), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# else + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-normal), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# endif + directionalAmbientColor += Color::LinearToGamma(iblColor); } # endif diffuseColor += directionalAmbientColor; diff --git a/package/Shaders/Effect.hlsl b/package/Shaders/Effect.hlsl index deedfc4a11..ee029ecef3 100644 --- a/package/Shaders/Effect.hlsl +++ b/package/Shaders/Effect.hlsl @@ -11,6 +11,10 @@ #define EFFECT +#if !defined(DYNAMIC_CUBEMAPS) && defined(IBL) +# undef IBL +#endif + struct VS_INPUT { float4 Position : POSITION0; @@ -427,9 +431,7 @@ struct PS_OUTPUT # elif defined(NORMALS) float4 NormalGlossiness : SV_Target2; # endif -# if defined(MULTBLEND) || defined(MULTBLEND_DECAL) float4 Albedo : SV_Target3; -# endif float4 Specular : SV_Target4; float4 Reflectance : SV_Target5; float4 Masks : SV_Target6; @@ -562,9 +564,8 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float4 screenPo float3 ambientColor = max(0, mul(SharedData::DirectionalAmbient, float4(0, 0, 1, 1))); # if defined(IBL) - if (SharedData::iblSettings.EnableDiffuseIBL && !SharedData::InInterior) { + if (SharedData::iblSettings.EnableDiffuseIBL && (!SharedData::InInterior || SharedData::iblSettings.EnableInterior)) { ambientColor *= SharedData::iblSettings.DALCAmount; - ambientColor += Color::Saturation(ImageBasedLighting::GetDiffuseIBL(float3(0, 0, -1)), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; } # endif @@ -588,6 +589,21 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float4 screenPo color = Color::LinearToGamma(color); # endif +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL) { + if (!SharedData::InInterior || SharedData::iblSettings.EnableInterior) + { +# if defined(SKYLIGHTING) + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(float3(0, 0, -1), skylightingDiffuse), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# else + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(float3(0, 0, -1)), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# endif + color += Color::LinearToGamma(iblColor); + } + } +# endif + if (!SharedData::InInterior){ bool isWorldShadow = false; float shadow = ShadowSampling::GetEffectShadow(worldPosition.xyz, normalize(worldPosition.xyz), screenPosition.xy, eyeIndex, isWorldShadow); @@ -871,6 +887,7 @@ PS_OUTPUT main(PS_INPUT input) psout.Reflectance = float4(psout.Diffuse.xyz, finalColor.w); psout.Masks = float4(Color::RGBToLuminance(psout.Diffuse.xyz).xxx, finalColor.w); #else + psout.Albedo = float4(0, 0, 0, finalColor.w); psout.Specular = float4(0, 0, 0, finalColor.w); psout.Reflectance = float4(0, 0, 0, finalColor.w); psout.Masks = float4(0, 0, 0, finalColor.w); diff --git a/package/Shaders/ISHDR.hlsl b/package/Shaders/ISHDR.hlsl index 324937b907..3e008c8574 100644 --- a/package/Shaders/ISHDR.hlsl +++ b/package/Shaders/ISHDR.hlsl @@ -38,24 +38,11 @@ cbuffer PerGeometry : register(b2) float4 BlurOffsets[16] : packoffset(c7); }; -float GetTonemapFactorReinhard(float luminance) -{ - return (luminance * (luminance * Param.y + 1)) / (luminance + 1); -} - float3 GetTonemapFactorReinhard(float3 luminance) { return (luminance * (luminance * Param.y + 1)) / (luminance + 1); } -float GetTonemapFactorHejlBurgessDawson(float luminance) -{ - float tmp = max(0, luminance - 0.004); - return Param.y * - pow(((tmp * 6.2 + 0.5) * tmp) / (tmp * (tmp * 6.2 + 1.7) + 0.06), Color::GammaCorrectionValue); -} - - float3 GetTonemapFactorHejlBurgessDawson(float3 luminance) { float3 tmp = max(0, luminance - 0.004); @@ -97,7 +84,7 @@ PS_OUTPUT main(PS_INPUT input) # elif defined(BLEND) float2 uv = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(input.TexCoord); - float3 hdrColor = BlendTex.Sample(BlendSampler, uv).xyz; + float3 inputColor = BlendTex.Sample(BlendSampler, uv).xyz; float3 bloomColor = 0; if (Flags.x > 0.5) { @@ -108,18 +95,41 @@ PS_OUTPUT main(PS_INPUT input) float2 avgValue = AvgTex.Sample(AvgSampler, input.TexCoord.xy).xy; - hdrColor *= avgValue.y / avgValue.x; + // Vanilla tonemapping and post-processing + float3 gameSdrColor = 0.0; + float3 ppColor = 0.0; + { + if (avgValue.x != 0 && avgValue.y != 0) + inputColor *= avgValue.y / avgValue.x; - hdrColor += DisplayMapping::RangeCompress(max(0, Param.x - hdrColor)) * bloomColor; + inputColor = max(0, inputColor); - float3 contrastOriginal = lerp(avgValue.x, hdrColor, Cinematic.z); - float3 contrastShadows = pow(abs(hdrColor) / avgValue.x, Cinematic.z) * avgValue.x * sign(hdrColor); - hdrColor = contrastOriginal < hdrColor ? contrastShadows : contrastOriginal; + float3 blendedColor; + [branch] if (Param.z > 0.5) + { + blendedColor = DisplayMapping::HuePreservingHejlBurgessDawson(inputColor, bloomColor); + } + else + { + float maxCol = Color::RGBToLuminance(inputColor); + float mappedMax = GetTonemapFactorReinhard(maxCol).x; + float3 compressedHuePreserving = inputColor * mappedMax / maxCol; + blendedColor = compressedHuePreserving; + blendedColor += saturate(Param.x - blendedColor) * bloomColor; + } + + gameSdrColor = blendedColor; - float hdrLuminance = Color::RGBToLuminance(hdrColor); - hdrColor = Cinematic.w * lerp(lerp(hdrLuminance, hdrColor, Cinematic.x), lerp(hdrColor, hdrLuminance, saturate(hdrLuminance)) * Tint.xyz, Tint.w).xyz; + float blendedLuminance = Color::RGBToLuminance(blendedColor); + + float3 linearColor = Cinematic.w * lerp(lerp(blendedLuminance, blendedColor, Cinematic.x), blendedLuminance * Tint.xyz, Tint.w).xyz; + + linearColor = lerp(avgValue.x, linearColor, Cinematic.z); + + ppColor = max(0, linearColor); + } - float3 srgbColor = DisplayMapping::HuePreservingTonemap(hdrColor); + float3 srgbColor = ppColor; # if defined(FADE) srgbColor = lerp(srgbColor, Fade.xyz, Fade.w); diff --git a/package/Shaders/ISWaterBlend.hlsl b/package/Shaders/ISWaterBlend.hlsl index bf6101261a..4f1c7a4b19 100644 --- a/package/Shaders/ISWaterBlend.hlsl +++ b/package/Shaders/ISWaterBlend.hlsl @@ -28,6 +28,22 @@ cbuffer PerGeometry : register(b2) float4 NearFar_Menu_DistanceFactor : packoffset(c0); }; +float3 LogToLinear(float3 logColor) +{ + const float linearRange = 14.0f; + const float linearGrey = 0.18f; + const float exposureGrey = 444.0f; + return exp2((logColor - exposureGrey / 1023.0) * linearRange) * linearGrey; +} + +float3 LinearToLog(float3 linearColor) +{ + const float linearRange = 14.0f; + const float linearGrey = 0.18f; + const float exposureGrey = 444.0f; + return saturate(log2(linearColor) / linearRange - log2(linearGrey) / linearRange + exposureGrey / 1023.0f); +} + PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; @@ -45,6 +61,7 @@ PS_OUTPUT main(PS_INPUT input) FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(motionScreenPosition); float4 waterHistory = waterHistoryTex.Sample(waterHistorySampler, motionAdjustedScreenPosition).xyzw; + waterHistory.xyz = LogToLinear(waterHistory.xyz) - LogToLinear(0); float3 finalColor = sourceColor; if ( @@ -67,7 +84,7 @@ PS_OUTPUT main(PS_INPUT input) finalColor = lerp(sourceColor, waterHistory.xyz, historyFactor); } - psout.Color1 = float4(finalColor, 1); + psout.Color1 = float4(LinearToLog(finalColor + LogToLinear(0)), 1); psout.Color = finalColor; return psout; diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index e4ae206dfb..34d46c4bfb 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -15,8 +15,8 @@ # define SKIN #endif -#if defined(HAIR) && defined(CS_HAIR) -# define DYNAMIC_CUBEMAPS +#if !defined(DYNAMIC_CUBEMAPS) && defined(IBL) +# undef IBL #endif #if (defined(TREE_ANIM) || defined(LANDSCAPE)) && !defined(VC) @@ -289,7 +289,8 @@ VS_OUTPUT main(VS_INPUT input) vsout.LandBlendWeights2.w = 1 - saturate(0.000375600968 * (9625.59961 - length(gridOffset))); vsout.LandBlendWeights2.xyz = input.LandBlendWeights2.xyz; # elif defined(PROJECTED_UV) && !defined(SKINNED) - vsout.TexProj = TextureProj[eyeIndex][2].xyz; + float3x3 texProjWorld3x3 = float3x3(World[eyeIndex][0].xyz, World[eyeIndex][1].xyz, World[eyeIndex][2].xyz); + vsout.TexProj = mul(texProjWorld3x3, TextureProj[eyeIndex][2].xyz); # endif # if defined(EYE) @@ -350,10 +351,6 @@ struct PS_OUTPUT { float4 Diffuse : SV_Target0; float4 MotionVectors : SV_Target1; - float4 ScreenSpaceNormals : SV_Target2; -# if defined(SNOW) - float4 Parameters : SV_Target3; -# endif }; #endif @@ -665,48 +662,6 @@ float ProcessSparkleColor(float color) } # endif -float3 GetLightSpecularInput(PS_INPUT input, float3 L, float3 V, float3 N, float3 lightColor, float shininess, float2 uv) -{ - float3 H = normalize(V + L); - float HdotN = 1.0; -# if defined(ANISO_LIGHTING) - float3 AN = normalize(N * 0.5 + float3(input.TBN0.z, input.TBN1.z, input.TBN2.z)); - float LdotAN = dot(AN, L); - float HdotAN = dot(AN, H); - HdotN = 1 - min(1, abs(LdotAN - HdotAN)); -# else - HdotN = saturate(dot(H, N)); -# endif - -# if defined(SPECULAR) - float lightColorMultiplier = exp2(shininess * log2(HdotN)); - -# elif defined(SPARKLE) - float lightColorMultiplier = 0; -# else - float lightColorMultiplier = HdotN; -# endif - -# if defined(ANISO_LIGHTING) - lightColorMultiplier *= 0.7 * max(0, L.z); -# endif - -# if defined(SPARKLE) && !defined(SNOW) - float3 sparkleUvScale = exp2(float3(1.3, 1.6, 1.9) * log2(abs(SparkleParams.x)).xxx); - - float sparkleColor1 = TexProjDetail.Sample(SampProjDetailSampler, uv * sparkleUvScale.xx).z; - float sparkleColor2 = TexProjDetail.Sample(SampProjDetailSampler, uv * sparkleUvScale.yy).z; - float sparkleColor3 = TexProjDetail.Sample(SampProjDetailSampler, uv * sparkleUvScale.zz).z; - float sparkleColor = ProcessSparkleColor(sparkleColor1) + ProcessSparkleColor(sparkleColor2) + ProcessSparkleColor(sparkleColor3); - float VdotN = dot(V, N); - V += N * -(2 * VdotN); - float sparkleMultiplier = exp2(SparkleParams.w * log2(saturate(dot(V, -L)))) * (SparkleParams.z * sparkleColor); - sparkleMultiplier = sparkleMultiplier >= 0.5 ? 1 : 0; - lightColorMultiplier += sparkleMultiplier * HdotN; -# endif - return lightColor * lightColorMultiplier; -} - float3 TransformNormal(float3 normal) { return normal * 2 + -1.0.xxx; @@ -890,6 +845,8 @@ float GetSnowParameterY(float texProjTmp, float alpha) # undef SKYLIGHTING # endif +# include "Common/LightingCommon.hlsli" + # if defined(WATER_EFFECTS) # include "WaterEffects/WaterCaustics.hlsli" # endif @@ -983,6 +940,8 @@ float GetSnowParameterY(float texProjTmp, float alpha) # include "IBL/IBL.hlsli" # endif +# include "Common/LightingEval.hlsli" + PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) { PS_OUTPUT psout; @@ -1024,8 +983,10 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if !defined(TRUE_PBR) # if defined(LANDSCAPE) float shininess = dot(input.LandBlendWeights1, LandscapeTexture1to4IsSpecPower) + input.LandBlendWeights2.x * LandscapeTexture5to6IsSpecPower.x + input.LandBlendWeights2.y * LandscapeTexture5to6IsSpecPower.y; -# else +# elif defined(SPECULAR) float shininess = SpecularColor.w; +# else + float shininess = 0.0; # endif // defined (LANDSCAPE) # endif @@ -1239,7 +1200,11 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif # if defined(EMAT) - if (SharedData::extendedMaterialSettings.EnableTerrainParallax) { +# if defined(TRUE_PBR) + if (SharedData::extendedMaterialSettings.EnableParallax) { +# else + if (SharedData::extendedMaterialSettings.EnableTerrainParallax || (SharedData::extendedMaterialSettings.EnableParallax && Permutation::ExtraFeatureDescriptor & Permutation::ExtraFeatureFlags::THLandHasDisplacement)) { +# endif mipLevels[0] = ExtendedMaterials::GetMipLevel(uv, TexColorSampler); mipLevels[1] = ExtendedMaterials::GetMipLevel(uv, TexLandColor2Sampler); mipLevels[2] = ExtendedMaterials::GetMipLevel(uv, TexLandColor3Sampler); @@ -1350,7 +1315,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(TRUE_PBR) [branch] if ((PBRFlags & PBR::TerrainFlags::LandTile0PBR) == 0) { - landColorRGB1 = landColorRGB1 / Color::PBRLightingScale; + landColorRGB1 = Color::GammaToTrueLinear(landColorRGB1 / Color::PBRLightingScale); } # endif float landAlpha1 = landColor1.a; @@ -1431,7 +1396,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(TRUE_PBR) [branch] if ((PBRFlags & PBR::TerrainFlags::LandTile1PBR) == 0) { - landColorRGB2 = landColorRGB2 / Color::PBRLightingScale; + landColorRGB2 = Color::GammaToTrueLinear(landColorRGB2 / Color::PBRLightingScale); } # endif float landAlpha2 = landColor2.a; @@ -1511,7 +1476,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(TRUE_PBR) [branch] if ((PBRFlags & PBR::TerrainFlags::LandTile2PBR) == 0) { - landColorRGB3 = landColorRGB3 / Color::PBRLightingScale; + landColorRGB3 = Color::GammaToTrueLinear(landColorRGB3 / Color::PBRLightingScale); } # endif float landAlpha3 = landColor3.a; @@ -1591,7 +1556,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(TRUE_PBR) [branch] if ((PBRFlags & PBR::TerrainFlags::LandTile3PBR) == 0) { - landColorRGB4 = landColorRGB4 / Color::PBRLightingScale; + landColorRGB4 = Color::GammaToTrueLinear(landColorRGB4 / Color::PBRLightingScale); } # endif float landAlpha4 = landColor4.a; @@ -1671,7 +1636,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(TRUE_PBR) [branch] if ((PBRFlags & PBR::TerrainFlags::LandTile4PBR) == 0) { - landColorRGB5 = landColorRGB5 / Color::PBRLightingScale; + landColorRGB5 = Color::GammaToTrueLinear(landColorRGB5 / Color::PBRLightingScale); } # endif float landAlpha5 = landColor5.a; @@ -1752,7 +1717,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(TRUE_PBR) [branch] if ((PBRFlags & PBR::TerrainFlags::LandTile5PBR) == 0) { - landColorRGB6 = landColorRGB6 / Color::PBRLightingScale; + landColorRGB6 = Color::GammaToTrueLinear(landColorRGB6 / Color::PBRLightingScale); } # endif float landAlpha6 = landColor6.a; @@ -1833,7 +1798,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(LOD_BLENDING) # if defined(LODOBJECTS) || defined(LODOBJECTSHD) - baseColor.xyz *= SharedData::lodBlendingSettings.LODObjectBrightness; + baseColor.xyz = pow(abs(baseColor.xyz), SharedData::lodBlendingSettings.LODObjectGamma) * SharedData::lodBlendingSettings.LODObjectBrightness; # elif defined(LODLANDSCAPE) // First apply terrain variation if enabled # if defined(TERRAIN_VARIATION) @@ -1847,7 +1812,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) baseColor.xyz = Color::Diffuse(lodStochasticColor.rgb); } # endif - baseColor.xyz *= SharedData::lodBlendingSettings.LODTerrainBrightness; + baseColor.xyz = pow(abs(baseColor.xyz), SharedData::lodBlendingSettings.LODTerrainGamma) * SharedData::lodBlendingSettings.LODTerrainBrightness; # endif # endif // LOD_BLENDING @@ -1949,7 +1914,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif # if defined(LOD_BLENDING) - lodLandColor.xyz *= SharedData::lodBlendingSettings.LODTerrainBrightness; + lodLandColor.xyz = pow(abs(lodLandColor.xyz), SharedData::lodBlendingSettings.LODTerrainGamma) * SharedData::lodBlendingSettings.LODTerrainBrightness; # endif // LOD_BLENDING float lodBlendParameter = GetLodLandBlendParameter(lodLandColor.xyz); float lodBlendMask = TexLandLodBlend2Sampler.Sample(SampLandLodBlend2Sampler, 3.0.xx * input.TexCoord0.zw).x; @@ -1986,6 +1951,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(MODELSPACENORMALS) && !defined(SKINNED) float3 worldNormal = normal.xyz; + float3x3 tbnTr = ReconstructTBN(input.WorldPosition.xyz, worldNormal, screenUV); # else float3 worldNormal = normalize(mul(tbn, normal.xyz)); @@ -2046,7 +2012,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } glintParameters = lerp(glintParameters, projectedGlintParameters, projectedMaterialWeight); # elif defined(LOD_BLENDING) && (defined(LODOBJECTS) || defined(LODOBJECTSHD)) - projBaseColor.xyz *= SharedData::lodBlendingSettings.LODObjectSnowBrightness; + projBaseColor.xyz = pow(abs(projBaseColor.xyz), SharedData::lodBlendingSettings.LODObjectSnowGamma) * SharedData::lodBlendingSettings.LODObjectSnowBrightness; # endif // TRUE_PBR normal.xyz = lerp(normal.xyz, finalProjNormal, projectedMaterialWeight); baseColor.xyz = lerp(baseColor.xyz, projBaseColor, projectedMaterialWeight); @@ -2102,59 +2068,62 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif hairT = Hair::ReorientTangent(hairT, worldNormal); - if (SharedData::hairSpecularSettings.Enabled && SharedData::hairSpecularSettings.EnableTangentShift) { - float3 shiftedNormal = Hair::ShiftWorldNormal(hairT, worldNormal, 0, uv); - screenSpaceNormal = normalize(FrameBuffer::WorldToView(shiftedNormal, false, eyeIndex)); + if (SharedData::hairSpecularSettings.Enabled) { + if (SharedData::hairSpecularSettings.EnableTangentShift && SharedData::hairSpecularSettings.HairMode != 1) { + float3 shiftedNormal = Hair::ShiftWorldNormal(hairT, worldNormal, 0, uv); + screenSpaceNormal = normalize(FrameBuffer::WorldToView(shiftedNormal, false, eyeIndex)); + } } - - float3 transmissionColor = 0; # endif -# if defined(TRUE_PBR) - PBR::SurfaceProperties pbrSurfaceProperties = PBR::InitSurfaceProperties(); + MaterialProperties material = (MaterialProperties)0; - pbrSurfaceProperties.Noise = screenNoise; + material.F0 = 0; + material.Roughness = 1; - pbrSurfaceProperties.Roughness = clamp(rawRMAOS.x, PBR::Constants::MinRoughness, PBR::Constants::MaxRoughness); - pbrSurfaceProperties.Metallic = saturate(rawRMAOS.y); - pbrSurfaceProperties.AO = rawRMAOS.z; - pbrSurfaceProperties.F0 = lerp(saturate(rawRMAOS.w), Color::GammaToLinear(baseColor.xyz), pbrSurfaceProperties.Metallic); +# if defined(TRUE_PBR) + material.Noise = screenNoise; + + material.Roughness = clamp(rawRMAOS.x, PBR::Constants::MinRoughness, PBR::Constants::MaxRoughness); + material.Metallic = saturate(rawRMAOS.y); + material.AO = rawRMAOS.z; + material.F0 = lerp(saturate(rawRMAOS.w), Color::GammaToTrueLinear(baseColor.xyz), material.Metallic); - pbrSurfaceProperties.GlintScreenSpaceScale = max(1, glintParameters.x); - pbrSurfaceProperties.GlintLogMicrofacetDensity = clamp(PBR::Constants::MaxGlintDensity - glintParameters.y, PBR::Constants::MinGlintDensity, PBR::Constants::MaxGlintDensity); - pbrSurfaceProperties.GlintMicrofacetRoughness = clamp(glintParameters.z, PBR::Constants::MinGlintRoughness, PBR::Constants::MaxGlintRoughness); - pbrSurfaceProperties.GlintDensityRandomization = clamp(glintParameters.w, PBR::Constants::MinGlintDensityRandomization, PBR::Constants::MaxGlintDensityRandomization); + material.GlintScreenSpaceScale = max(1, glintParameters.x); + material.GlintLogMicrofacetDensity = clamp(PBR::Constants::MaxGlintDensity - glintParameters.y, PBR::Constants::MinGlintDensity, PBR::Constants::MaxGlintDensity); + material.GlintMicrofacetRoughness = clamp(glintParameters.z, PBR::Constants::MinGlintRoughness, PBR::Constants::MaxGlintRoughness); + material.GlintDensityRandomization = clamp(glintParameters.w, PBR::Constants::MinGlintDensityRandomization, PBR::Constants::MaxGlintDensityRandomization); # if defined(GLINT) float glintNoise = Random::R1Modified(float(SharedData::FrameCount), (Random::pcg2d(uint2(input.Position.xy)) / 4294967296.0).x); - PBR::Glints::PrecomputeGlints(glintNoise, uvOriginal, ddx(uvOriginal), ddy(uvOriginal), pbrSurfaceProperties.GlintScreenSpaceScale, pbrSurfaceProperties.GlintCache); + Glints::PrecomputeGlints(glintNoise, uvOriginal, ddx(uvOriginal), ddy(uvOriginal), material.GlintScreenSpaceScale, material.GlintCache); # endif - baseColor.xyz *= 1 - pbrSurfaceProperties.Metallic; + baseColor.xyz *= 1 - material.Metallic; - pbrSurfaceProperties.BaseColor = baseColor.xyz; + material.BaseColor = baseColor.xyz; float3 coatWorldNormal = worldNormal; # if !defined(LANDSCAPE) && !defined(LODLANDSCAPE) [branch] if ((PBRFlags & PBR::Flags::Subsurface) != 0) { - pbrSurfaceProperties.SubsurfaceColor = PBRParams2.xyz; - pbrSurfaceProperties.Thickness = PBRParams2.w; + material.SubsurfaceColor = PBRParams2.xyz; + material.Thickness = PBRParams2.w; [branch] if ((PBRFlags & PBR::Flags::HasFeatureTexture0) != 0) { float4 sampledSubsurfaceProperties = TexRimSoftLightWorldMapOverlaySampler.Sample(SampRimSoftLightWorldMapOverlaySampler, uv); - pbrSurfaceProperties.SubsurfaceColor *= Color::Diffuse(sampledSubsurfaceProperties.xyz); - pbrSurfaceProperties.Thickness *= sampledSubsurfaceProperties.w; + material.SubsurfaceColor *= Color::Diffuse(sampledSubsurfaceProperties.xyz); + material.Thickness *= sampledSubsurfaceProperties.w; } - pbrSurfaceProperties.Thickness = lerp(pbrSurfaceProperties.Thickness, 1, projectedMaterialWeight); + material.Thickness = lerp(material.Thickness, 1, projectedMaterialWeight); } else if ((PBRFlags & PBR::Flags::TwoLayer) != 0) { - pbrSurfaceProperties.CoatColor = sampledCoatColor.xyz; - pbrSurfaceProperties.CoatStrength = sampledCoatColor.w; - pbrSurfaceProperties.CoatRoughness = MultiLayerParallaxData.x; - pbrSurfaceProperties.CoatF0 = MultiLayerParallaxData.y; + material.CoatColor = sampledCoatColor.xyz; + material.CoatStrength = sampledCoatColor.w; + material.CoatRoughness = MultiLayerParallaxData.x; + material.CoatF0 = MultiLayerParallaxData.y; float2 coatUv = uv; [branch] if ((PBRFlags & PBR::Flags::InterlayerParallax) != 0) @@ -2164,34 +2133,139 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) [branch] if ((PBRFlags & PBR::Flags::HasFeatureTexture1) != 0) { float4 sampledCoatProperties = TexBackLightSampler.Sample(SampBackLightSampler, coatUv); - pbrSurfaceProperties.CoatRoughness *= sampledCoatProperties.w; + material.CoatRoughness *= sampledCoatProperties.w; [branch] if ((PBRFlags & PBR::Flags::CoatNormal) != 0) { coatWorldNormal = normalize(mul(tbn, TransformNormal(sampledCoatProperties.xyz))); } } - pbrSurfaceProperties.CoatStrength = lerp(pbrSurfaceProperties.CoatStrength, 0, projectedMaterialWeight); + material.CoatStrength = lerp(material.CoatStrength, 0, projectedMaterialWeight); } [branch] if ((PBRFlags & PBR::Flags::Fuzz) != 0) { - pbrSurfaceProperties.FuzzColor = MultiLayerParallaxData.xyz; - pbrSurfaceProperties.FuzzWeight = MultiLayerParallaxData.w; + material.FuzzColor = MultiLayerParallaxData.xyz; + material.FuzzWeight = MultiLayerParallaxData.w; [branch] if ((PBRFlags & PBR::Flags::HasFeatureTexture1) != 0) { float4 sampledFuzzProperties = TexBackLightSampler.Sample(SampBackLightSampler, uv); - pbrSurfaceProperties.FuzzColor *= Color::Diffuse(sampledFuzzProperties.xyz); - pbrSurfaceProperties.FuzzWeight *= sampledFuzzProperties.w; + material.FuzzColor *= Color::Diffuse(sampledFuzzProperties.xyz); + material.FuzzWeight *= sampledFuzzProperties.w; } - pbrSurfaceProperties.FuzzWeight = lerp(pbrSurfaceProperties.FuzzWeight, 0, projectedMaterialWeight); + material.FuzzWeight = lerp(material.FuzzWeight, 0, projectedMaterialWeight); } # endif +# else + material.BaseColor = baseColor.xyz; +# if defined(SPECULAR) + material.Shininess = shininess; + material.Glossiness = glossiness; + material.SpecularColor = SpecularColor.xyz; +# else + material.Shininess = 0; + material.Glossiness = 0; + material.SpecularColor = 0; +# endif +# if (defined(RIM_LIGHTING) || defined(SOFT_LIGHTING) || defined(LOAD_SOFT_LIGHTING)) + material.rimSoftLightColor = rimSoftLightColor.xyz; +# endif +# if defined(BACK_LIGHTING) + material.backLightColor = backLightColor.xyz; +# endif +# endif // TRUE_PBR - float3 specularColorPBR = 0; - float3 transmissionColor = 0; +# if defined(CS_HAIR) && defined(HAIR) + if (SharedData::hairSpecularSettings.Enabled) { + material.Shininess = SharedData::hairSpecularSettings.HairGlossiness; + material.F0 = Hair::HairF0(); + if (SharedData::hairSpecularSettings.HairMode == 1) { + material.Roughness = 1; + } else { + material.Roughness = ShininessToRoughness(material.Shininess * 0.75); + } + } +# endif - float pbrGlossiness = 1 - pbrSurfaceProperties.Roughness; -# endif // TRUE_PBR + bool dynamicCubemap = false; + +# if defined(ENVMAP) || defined(MULTI_LAYER_PARALLAX) || defined(EYE) + float envMask = EnvmapData.x * MaterialData.x; + + float viewNormalAngle = dot(worldNormal.xyz, viewDirection); + float3 envSamplingPoint = (viewNormalAngle * 2) * worldNormal.xyz - viewDirection; + + if (envMask > 0.0) { + if (EnvmapData.y) { + envMask *= TexEnvMaskSampler.Sample(SampEnvMaskSampler, uv).x; + } else { + envMask *= material.Glossiness; + } + } + + float3 envColor = 0.0; + + if (envMask > 0.0) { +# if defined(DYNAMIC_CUBEMAPS) + uint2 envSize; + TexEnvSampler.GetDimensions(envSize.x, envSize.y); + +# if defined(EMAT) + if (envSize.x == 1 && envSize.y == 1 || complexMaterial) { +# else + if (envSize.x == 1 && envSize.y == 1) { +# endif + + dynamicCubemap = true; + +# if defined(EMAT) + if (!complexMaterial) +# endif + { + // Dynamic Cubemap Creator sets this value to black, if it is anything but black it is wrong + float3 envColorTest = TexEnvSampler.SampleLevel(SampEnvSampler, float3(0.0, 1.0, 0.0), 15).xyz; + dynamicCubemap = all(envColorTest == 0.0); + } + +# if defined(CREATOR) + if (SharedData::cubemapCreatorSettings.Enabled) { + dynamicCubemap = true; + } +# endif + + if (dynamicCubemap) { + float4 envColorBase = TexEnvSampler.SampleLevel(SampEnvSampler, float3(1.0, 0.0, 0.0), 15); + + if (envColorBase.a < 1.0) { + material.F0 = Color::GammaToLinear(envColorBase.rgb); + material.Roughness = envColorBase.a; + } else { + material.F0 = 1.0; + material.Roughness = 1.0 / 7.0; + } + +# if defined(CREATOR) + if (SharedData::cubemapCreatorSettings.Enabled) { + material.F0 = SharedData::cubemapCreatorSettings.CubemapColor.rgb; + material.Roughness = SharedData::cubemapCreatorSettings.CubemapColor.a; + } +# endif + +# if defined(EMAT) + float complexMaterialRoughness = 1.0 - complexMaterialColor.y; + material.Roughness = lerp(material.Roughness, complexMaterialRoughness, complexMaterial); + material.F0 = lerp(material.F0, complexSpecular, complexMaterial); +# endif + } + } +# endif + + if (!dynamicCubemap) { + float3 envColorBase = Color::GammaToLinear(TexEnvSampler.Sample(SampEnvSampler, envSamplingPoint).xyz); + envColor = envColorBase.xyz * envMask; + } + } + +# endif // defined (ENVMAP) || defined (MULTI_LAYER_PARALLAX) || defined(EYE) float porosity = 1.0; @@ -2201,9 +2275,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # else float3 positionMSSkylight = input.WorldPosition.xyz; # endif - - float3 skylightingNormal = normalize(float3(worldNormal.xy, max(0, worldNormal.z))); - # if defined(DEFERRED) sh2 skylightingSH = Skylighting::sample(SharedData::skylightingSettings, Skylighting::SkylightingProbeArray, Skylighting::stbn_vec3_2Dx1D_128x128x64, input.Position.xy, positionMSSkylight, worldNormal); # else @@ -2220,7 +2291,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(WETNESS_EFFECTS) // Initialize wetness parameters float wetness = 0.0; - float3 wetnessSpecular = 0.0; float3 wetnessNormal = worldNormal; // Calculate shore wetness factors @@ -2298,7 +2368,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 rippleNormal = normalize(lerp(float3(0, 0, 1), raindropInfo.xyz, lerp(flatnessAmount, 1.0, 0.5))); wetnessNormal = WetnessEffects::ReorientNormal(rippleNormal, wetnessNormal); - waterRoughnessSpecular = 1.0 - wetnessGlossinessSpecular; + waterRoughnessSpecular = saturate(1.0 - wetnessGlossinessSpecular); # endif float3 dirLightColor = Color::Light(DirLightColor.xyz); @@ -2355,8 +2425,11 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) { float3 dirLightDirectionTS = mul(refractedDirLightDirection, tbn).xyz; # if defined(LANDSCAPE) - [branch] if (SharedData::extendedMaterialSettings.EnableTerrainParallax) - { +# if defined(TRUE_PBR) + if (SharedData::extendedMaterialSettings.EnableParallax){ +# else + if (SharedData::extendedMaterialSettings.EnableTerrainParallax || (SharedData::extendedMaterialSettings.EnableParallax && Permutation::ExtraFeatureDescriptor & Permutation::ExtraFeatureFlags::THLandHasDisplacement)){ +# endif # if defined(TERRAIN_VARIATION) float weights[6]; // Initialize weights array @@ -2388,8 +2461,16 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) dirLightColorMultiplier *= dirShadow; +# if defined(CS_HAIR) && defined(HAIR) + if (SharedData::hairSpecularSettings.Enabled) { + vertexNormal.xyz = worldNormal.xyz; + worldNormal.xyz = hairT; + } +# endif + float3 diffuseColor = 0.0.xxx; float3 specularColor = 0.0.xxx; + float3 transmissionColor = 0.0.xxx; float3 lightsDiffuseColor = 0.0.xxx; float3 coatLightsDiffuseColor = 0.0.xxx; @@ -2397,70 +2478,36 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 lodLandDiffuseColor = 0; + // Directiontal Lighting + DirectContext dirLightContext; + DirectLightingOutput dirLightOutput; # if defined(TRUE_PBR) - { - PBR::LightProperties lightProperties = PBR::InitLightProperties(dirLightColor, dirLightColorMultiplier * dirDetailShadow, parallaxShadow); - float3 dirDiffuseColor, coatDirDiffuseColor, dirTransmissionColor, dirSpecularColor; - PBR::GetDirectLightInput(dirDiffuseColor, coatDirDiffuseColor, dirTransmissionColor, dirSpecularColor, worldNormal.xyz, coatWorldNormal, refractedViewDirection, viewDirection, refractedDirLightDirection, DirLightDirection, lightProperties, pbrSurfaceProperties, tbnTr, uvOriginal); - lightsDiffuseColor += dirDiffuseColor; - coatLightsDiffuseColor += coatDirDiffuseColor; - transmissionColor += dirTransmissionColor; - specularColorPBR += dirSpecularColor * !SharedData::InInterior; -# if defined(LOD_LAND_BLEND) - lodLandDiffuseColor += dirLightColor / Math::PI * saturate(dirLightAngle) * dirLightColorMultiplier * dirDetailShadow * parallaxShadow; -# endif -# if defined(WETNESS_EFFECTS) - if (waterRoughnessSpecular < 1.0) - specularColorPBR += PBR::GetWetnessDirectLightSpecularInput(wetnessNormal, viewDirection, DirLightDirection, lightProperties.CoatLightColor, waterRoughnessSpecular) * wetnessGlossinessSpecular; -# endif - } + dirLightContext = CreateDirectLightingContext(worldNormal.xyz, coatWorldNormal, vertexNormal.xyz, refractedViewDirection, viewDirection, refractedDirLightDirection, DirLightDirection, dirLightColor * dirLightColorMultiplier, dirDetailShadow, parallaxShadow); # else - dirDetailShadow *= parallaxShadow; - dirLightColor *= dirLightColorMultiplier; - - float3 dirDiffuseColor = dirLightColor * saturate(dirLightAngle) * dirDetailShadow; - -# if defined(SOFT_LIGHTING) - lightsDiffuseColor += dirLightColor * GetSoftLightMultiplier(dirLightAngle) * rimSoftLightColor.xyz; -# endif - -# if defined(RIM_LIGHTING) - lightsDiffuseColor += dirLightColor * GetRimLightMultiplier(DirLightDirection, viewDirection, worldNormal.xyz) * rimSoftLightColor.xyz; -# endif - -# if defined(BACK_LIGHTING) - lightsDiffuseColor += dirLightColor * saturate(-dirLightAngle) * backLightColor.xyz; -# endif - - if (useSnowSpecular && useSnowDecalSpecular) { -# if defined(SNOW) - lightsSpecularColor += GetSnowSpecularColor(input, worldNormal.xyz, viewDirection); -# endif - } else { + dirLightContext = CreateDirectLightingContext(worldNormal.xyz, vertexNormal.xyz, viewDirection, DirLightDirection, dirLightColor * dirLightColorMultiplier, dirDetailShadow, parallaxShadow); # if defined(HAIR) && defined(CS_HAIR) - if (SharedData::hairSpecularSettings.Enabled) { - float3 dirTransmissionColor = 0.0; - float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, DirLightDirection, screenNoise, eyeIndex); - Hair::GetHairDirectLight(dirDiffuseColor, lightsSpecularColor, dirTransmissionColor, hairT, DirLightDirection, viewDirection, worldNormal.xyz, vertexNormal.xyz, dirLightColor.xyz * dirDetailShadow, SharedData::hairSpecularSettings.HairGlossiness, hairShadow, uv, baseColor.xyz); - transmissionColor += dirTransmissionColor; - } - else { -# if defined(SPECULAR) - lightsSpecularColor = GetLightSpecularInput(input, DirLightDirection, viewDirection, worldNormal.xyz, dirLightColor.xyz * dirDetailShadow, shininess, uv); -# endif - } -# elif defined(SPECULAR) || defined(SPARKLE) - lightsSpecularColor = GetLightSpecularInput(input, DirLightDirection, viewDirection, worldNormal.xyz, dirLightColor.xyz * dirDetailShadow, shininess, uv); -# endif + if (SharedData::hairSpecularSettings.Enabled) { + float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, DirLightDirection, screenNoise, eyeIndex); + dirLightContext.hairShadow = hairShadow; } +# endif +# endif - lightsDiffuseColor += dirDiffuseColor; + EvaluateLighting(dirLightContext, material, tbnTr, uvOriginal, dirLightOutput); +# if defined(WETNESS_EFFECTS) + if (waterRoughnessSpecular < 1) + EvaluateWetnessLighting(wetnessNormal, dirLightContext, waterRoughnessSpecular, dirLightOutput); +# endif -# if defined(WETNESS_EFFECTS) - if (waterRoughnessSpecular < 1.0) - wetnessSpecular += WetnessEffects::GetWetnessSpecular(wetnessNormal, DirLightDirection, viewDirection, dirLightColor * dirDetailShadow, waterRoughnessSpecular); + lightsDiffuseColor += dirLightOutput.diffuse; + lightsSpecularColor += dirLightOutput.specular; +# if defined(TRUE_PBR) + coatLightsDiffuseColor += dirLightOutput.coatDiffuse; +# if defined(LOD_LAND_BLEND) + lodLandDiffuseColor += dirLightColor / Math::PI * saturate(dirLightAngle) * dirLightColorMultiplier * dirDetailShadow * parallaxShadow; # endif # endif + transmissionColor += dirLightOutput.transmission; # if !defined(LOD) # if !defined(LIGHT_LIMIT_FIX) @@ -2483,9 +2530,10 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 normalizedLightDirection = normalize(lightDirection); + DirectContext pointLightContext; + DirectLightingOutput pointLightOutput; # if defined(TRUE_PBR) { - float3 pointDiffuseColor, coatPointDiffuseColor, pointTransmissionColor, pointSpecularColor; float3 refractedLightDirection = normalizedLightDirection; # if !defined(LANDSCAPE) && !defined(LODLANDSCAPE) [branch] if ((PBRFlags & PBR::Flags::InterlayerParallax) != 0) @@ -2494,48 +2542,28 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) refractedLightDirection = -refract(-normalizedLightDirection, coatWorldNormal, eta); } # endif - PBR::LightProperties lightProperties = PBR::InitLightProperties(lightColor, lightShadow, 1); - PBR::GetDirectLightInput(pointDiffuseColor, coatPointDiffuseColor, pointTransmissionColor, pointSpecularColor, worldNormal.xyz, coatWorldNormal, refractedViewDirection, viewDirection, refractedLightDirection, normalizedLightDirection, lightProperties, pbrSurfaceProperties, tbnTr, uvOriginal); - lightsDiffuseColor += pointDiffuseColor; - coatLightsDiffuseColor += coatPointDiffuseColor; - transmissionColor += pointTransmissionColor; - specularColorPBR += pointSpecularColor; + pointLightContext = CreateDirectLightingContext(worldNormal.xyz, coatWorldNormal, vertexNormal.xyz, refractedViewDirection, viewDirection, refractedLightDirection, normalizedLightDirection, lightColor, lightShadow, 1); } # else - lightColor *= lightShadow; - float lightAngle = dot(worldNormal.xyz, normalizedLightDirection.xyz); - float3 lightDiffuseColor = lightColor * saturate(lightAngle.xxx); - -# if defined(SOFT_LIGHTING) - lightDiffuseColor += lightColor * GetSoftLightMultiplier(lightAngle) * rimSoftLightColor.xyz; -# endif // SOFT_LIGHTING - -# if defined(RIM_LIGHTING) - lightDiffuseColor += lightColor * GetRimLightMultiplier(normalizedLightDirection, viewDirection, worldNormal.xyz) * rimSoftLightColor.xyz; -# endif // RIM_LIGHTING - -# if defined(BACK_LIGHTING) - lightDiffuseColor += lightColor * saturate(-lightAngle) * backLightColor.xyz; -# endif // BACK_LIGHTING + pointLightContext = CreateDirectLightingContext(worldNormal.xyz, vertexNormal.xyz, viewDirection, normalizedLightDirection, lightColor, lightShadow, 1); # if defined(HAIR) && defined(CS_HAIR) if (SharedData::hairSpecularSettings.Enabled) { - float3 lightSpecularColor = 0; - float3 lightTransmissionColor = 0; float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, normalizedLightDirection, screenNoise, eyeIndex); - Hair::GetHairDirectLight(lightDiffuseColor, lightSpecularColor, lightTransmissionColor, hairT, normalizedLightDirection, viewDirection, worldNormal.xyz, vertexNormal.xyz, lightColor, SharedData::hairSpecularSettings.HairGlossiness, hairShadow, uv, baseColor.xyz); - lightsSpecularColor += lightSpecularColor; - transmissionColor += lightTransmissionColor; - } else { -# if defined(SPECULAR) - lightsSpecularColor += GetLightSpecularInput(input, normalizedLightDirection, viewDirection, worldNormal.xyz, lightColor, shininess, uv); -# endif + pointLightContext.hairShadow = hairShadow; } -# elif defined(SPECULAR) || (defined(SPARKLE) && !defined(SNOW)) - lightsSpecularColor += GetLightSpecularInput(input, normalizedLightDirection, viewDirection, worldNormal.xyz, lightColor, shininess, uv); -# endif // defined (SPECULAR) || (defined (SPARKLE) && !defined(SNOW)) - - lightsDiffuseColor += lightDiffuseColor; +# endif +# endif + EvaluateLighting(pointLightContext, material, tbnTr, uvOriginal, pointLightOutput); +# if defined(WETNESS_EFFECTS) + if (waterRoughnessSpecular < 1) + EvaluateWetnessLighting(wetnessNormal, pointLightContext, waterRoughnessSpecular, pointLightOutput); +# endif + lightsDiffuseColor += pointLightOutput.diffuse; + lightsSpecularColor += pointLightOutput.specular; +# if defined(TRUE_PBR) + coatLightsDiffuseColor += pointLightOutput.coatDiffuse; # endif + transmissionColor += pointLightOutput.transmission; } # else @@ -2614,7 +2642,11 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) [branch] if (SharedData::extendedMaterialSettings.EnableParallax) parallaxShadow = ExtendedMaterials::GetParallaxSoftShadowMultiplier(uv, mipLevel, lightDirectionTS, sh0, TexParallaxSampler, SampParallaxSampler, 0, parallaxShadowQuality, screenNoise, displacementParams); # elif defined(LANDSCAPE) - [branch] if (SharedData::extendedMaterialSettings.EnableTerrainParallax) +# if defined(TRUE_PBR) + if (SharedData::extendedMaterialSettings.EnableParallax) +# else + if (SharedData::extendedMaterialSettings.EnableTerrainParallax || (SharedData::extendedMaterialSettings.EnableParallax && Permutation::ExtraFeatureDescriptor & Permutation::ExtraFeatureFlags::THLandHasDisplacement)) +# endif # if defined(TERRAIN_VARIATION) parallaxShadow = ExtendedMaterials::GetParallaxSoftShadowMultiplierTerrain(input, uv, mipLevels, lightDirectionTS, sh0, parallaxShadowQuality, screenNoise, displacementParams, sharedOffset, dx, dy); # else @@ -2630,62 +2662,31 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } # endif + DirectContext pointLightContext; + DirectLightingOutput pointLightOutput; # if defined(TRUE_PBR) - { - PBR::LightProperties lightProperties = PBR::InitLightProperties(lightColor, lightShadow, parallaxShadow); - float3 pointDiffuseColor, coatPointDiffuseColor, pointTransmissionColor, pointSpecularColor; - PBR::GetDirectLightInput(pointDiffuseColor, coatPointDiffuseColor, pointTransmissionColor, pointSpecularColor, worldNormal.xyz, coatWorldNormal, refractedViewDirection, viewDirection, refractedLightDirection, normalizedLightDirection, lightProperties, pbrSurfaceProperties, tbnTr, uvOriginal); - lightsDiffuseColor += pointDiffuseColor; - coatLightsDiffuseColor += coatPointDiffuseColor; - transmissionColor += pointTransmissionColor; - specularColorPBR += pointSpecularColor; -# if defined(WETNESS_EFFECTS) - if (waterRoughnessSpecular < 1.0) - specularColorPBR += PBR::GetWetnessDirectLightSpecularInput(wetnessNormal, viewDirection, normalizedLightDirection, lightProperties.CoatLightColor, waterRoughnessSpecular) * wetnessGlossinessSpecular; -# endif - } + pointLightContext = CreateDirectLightingContext(worldNormal.xyz, coatWorldNormal, vertexNormal.xyz, refractedViewDirection, viewDirection, refractedLightDirection, normalizedLightDirection, lightColor, lightShadow, parallaxShadow); # else - lightColor *= lightShadow; - - float3 lightDiffuseColor = lightColor * parallaxShadow * saturate(lightAngle.xxx); - float lightBacklighting = 1.0 + saturate(dot(normalizedLightDirection.xyz, viewDirection)); - -# if defined(SOFT_LIGHTING) - lightDiffuseColor += lightBacklighting * lightColor * GetSoftLightMultiplier(lightAngle) * rimSoftLightColor.xyz; -# endif - -# if defined(RIM_LIGHTING) - lightDiffuseColor += lightBacklighting * lightColor * GetRimLightMultiplier(normalizedLightDirection, viewDirection, worldNormal.xyz) * rimSoftLightColor.xyz; -# endif - -# if defined(BACK_LIGHTING) - lightDiffuseColor += lightBacklighting * lightColor * saturate(-lightAngle) * backLightColor.xyz; -# endif - -# if defined(HAIR) && defined(CS_HAIR) && (defined(SKINNED) || !defined(MODELSPACENORMALS)) + pointLightContext = CreateDirectLightingContext(worldNormal.xyz, vertexNormal.xyz, viewDirection, normalizedLightDirection, lightColor, lightShadow, parallaxShadow); +# if defined(HAIR) && defined(CS_HAIR) if (SharedData::hairSpecularSettings.Enabled) { float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, normalizedLightDirection, screenNoise, eyeIndex); - float3 lightSpecularColor = 0; - float3 lightTransmissionColor = 0; - Hair::GetHairDirectLight(lightDiffuseColor, lightSpecularColor, lightTransmissionColor, hairT, normalizedLightDirection, viewDirection, worldNormal.xyz, vertexNormal.xyz, lightColor, SharedData::hairSpecularSettings.HairGlossiness, hairShadow, uv, baseColor.xyz); - lightsSpecularColor += lightSpecularColor; - transmissionColor += lightTransmissionColor; - } else { -# if defined(SPECULAR) - lightsSpecularColor += GetLightSpecularInput(input, normalizedLightDirection, viewDirection, worldNormal.xyz, lightColor, shininess, uv); -# endif + pointLightContext.hairShadow = hairShadow; } -# elif defined(SPECULAR) || (defined(SPARKLE) && !defined(SNOW)) - lightsSpecularColor += GetLightSpecularInput(input, normalizedLightDirection, viewDirection, worldNormal.xyz, lightColor, shininess, uv); # endif - - lightsDiffuseColor += lightDiffuseColor; # endif - + EvaluateLighting(pointLightContext, material, tbnTr, uvOriginal, pointLightOutput); # if defined(WETNESS_EFFECTS) - if (waterRoughnessSpecular < 1.0) - wetnessSpecular += WetnessEffects::GetWetnessSpecular(wetnessNormal, normalizedLightDirection, viewDirection, lightColor, waterRoughnessSpecular); + if (waterRoughnessSpecular < 1) + EvaluateWetnessLighting(wetnessNormal, pointLightContext, waterRoughnessSpecular, pointLightOutput); +# endif + + lightsDiffuseColor += pointLightOutput.diffuse; + lightsSpecularColor += pointLightOutput.specular; +# if defined(TRUE_PBR) + coatLightsDiffuseColor += pointLightOutput.coatDiffuse; # endif + transmissionColor += pointLightOutput.transmission; } # endif # endif @@ -2722,15 +2723,28 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) diffuseColor += emitColor.xyz; # endif - float3 directionalAmbientColor = max(0, mul(DirectionalAmbient, float4(worldNormal, 1.0))); + IndirectContext indirectContext = (IndirectContext)0; + IndirectLobeWeights indirectLobeWeights; + + float3 ambientNormal = worldNormal.xyz; +# if defined(HAIR) && defined(CS_HAIR) + if (SharedData::hairSpecularSettings.Enabled) { + if (SharedData::hairSpecularSettings.HairMode == 1) + ambientNormal = normalize(viewDirection - hairT * dot(viewDirection, hairT)); + else + ambientNormal = vertexNormal.xyz; + screenSpaceNormal = normalize(FrameBuffer::WorldToView(ambientNormal, false, eyeIndex)); + } +# endif + + float3 directionalAmbientColor = max(0, mul(DirectionalAmbient, float4(ambientNormal, 1.0))); # if defined(IBL) if (SharedData::iblSettings.EnableDiffuseIBL) { if (SharedData::iblSettings.UseStaticIBL && !inWorld && !inReflection) { - directionalAmbientColor = ImageBasedLighting::GetStaticDiffuseIBL(worldNormal, SampColorSampler); - } else if (!SharedData::InInterior) { + directionalAmbientColor = ImageBasedLighting::GetStaticDiffuseIBL(ambientNormal, SampColorSampler); + } else if (!SharedData::InInterior || SharedData::iblSettings.EnableInterior) { directionalAmbientColor *= SharedData::iblSettings.DALCAmount; - directionalAmbientColor += Color::Saturation(ImageBasedLighting::GetDiffuseIBL(-worldNormal), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; } } # endif @@ -2740,129 +2754,34 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float skylightingFadeOutFactor = 1.0; if (!SharedData::InInterior) { skylightingFadeOutFactor = Skylighting::getFadeOutFactor(input.WorldPosition.xyz); - - skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(skylightingNormal)) / Math::PI; + skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(ambientNormal)) / Math::PI; skylightingDiffuse = saturate(skylightingDiffuse); - skylightingDiffuse = lerp(1.0, skylightingDiffuse, skylightingFadeOutFactor); - skylightingDiffuse = Skylighting::mixDiffuse(SharedData::skylightingSettings, skylightingDiffuse); - - directionalAmbientColor = Color::GammaToLinear(directionalAmbientColor); - directionalAmbientColor *= skylightingDiffuse; - directionalAmbientColor = Color::LinearToGamma(directionalAmbientColor); } # endif - float3 reflectionDiffuseColor = diffuseColor + directionalAmbientColor; - -# if defined(TRUE_PBR) && defined(LOD_LAND_BLEND) && !defined(DEFERRED) - lodLandDiffuseColor += directionalAmbientColor; -# endif - -# if !defined(TRUE_PBR) -# if defined(DEFERRED) && defined(SSGI) -# elif defined(HAIR) && defined(CS_HAIR) - if (!SharedData::hairSpecularSettings.Enabled) - diffuseColor += directionalAmbientColor; +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL) { + if ((!SharedData::InInterior || SharedData::iblSettings.EnableInterior) && !(SharedData::iblSettings.UseStaticIBL && !inWorld && !inReflection)) + { +# if defined(SKYLIGHTING) + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-ambientNormal, skylightingDiffuse), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; # else - diffuseColor += directionalAmbientColor; + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-ambientNormal), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; # endif -# endif - -# if defined(ENVMAP) || defined(MULTI_LAYER_PARALLAX) || defined(EYE) - float envMask = EnvmapData.x * MaterialData.x; - - float viewNormalAngle = dot(worldNormal.xyz, viewDirection); - float3 envSamplingPoint = (viewNormalAngle * 2) * worldNormal.xyz - viewDirection; - - if (envMask > 0.0) { - if (EnvmapData.y) { - envMask *= TexEnvMaskSampler.Sample(SampEnvMaskSampler, uv).x; - } else { - envMask *= glossiness; + iblColor = Color::LinearToGamma(iblColor); + directionalAmbientColor += iblColor; } } +# endif - float3 envColor = 0.0; - bool dynamicCubemap = false; - -# if defined(DYNAMIC_CUBEMAPS) - float3 F0 = 0.0; - float envRoughness = 1.0; -# endif - - if (envMask > 0.0) { -# if defined(DYNAMIC_CUBEMAPS) - uint2 envSize; - TexEnvSampler.GetDimensions(envSize.x, envSize.y); - -# if defined(EMAT) - if (envSize.x == 1 && envSize.y == 1 || complexMaterial) { -# else - if (envSize.x == 1 && envSize.y == 1) { -# endif - - dynamicCubemap = true; - -# if defined(EMAT) - if (!complexMaterial) -# endif - { - // Dynamic Cubemap Creator sets this value to black, if it is anything but black it is wrong - float3 envColorTest = TexEnvSampler.SampleLevel(SampEnvSampler, float3(0.0, 1.0, 0.0), 15).xyz; - dynamicCubemap = all(envColorTest == 0.0); - } - -# if defined(CREATOR) - if (SharedData::cubemapCreatorSettings.Enabled) { - dynamicCubemap = true; - } -# endif - - if (dynamicCubemap) { - float4 envColorBase = TexEnvSampler.SampleLevel(SampEnvSampler, float3(1.0, 0.0, 0.0), 15); - - if (envColorBase.a < 1.0) { - F0 = Color::GammaToLinear(envColorBase.rgb); - envRoughness = envColorBase.a; - } else { - F0 = 1.0; - envRoughness = 1.0 / 7.0; - } - -# if defined(CREATOR) - if (SharedData::cubemapCreatorSettings.Enabled) { - F0 = SharedData::cubemapCreatorSettings.CubemapColor.rgb; - envRoughness = SharedData::cubemapCreatorSettings.CubemapColor.a; - } -# endif - -# if defined(EMAT) - float complexMaterialRoughness = 1.0 - complexMaterialColor.y; - envRoughness = lerp(envRoughness, complexMaterialRoughness, complexMaterial); - F0 = lerp(F0, complexSpecular, complexMaterial); -# endif - - if (any(F0 > 0.0)) -# if defined(SKYLIGHTING) - envColor = DynamicCubemaps::GetDynamicCubemap(worldNormal, vertexNormal, viewDirection, envRoughness, F0, skylightingSH) * envMask; -# else - envColor = DynamicCubemaps::GetDynamicCubemap(worldNormal, vertexNormal, viewDirection, envRoughness, F0) * envMask; -# endif - else - envColor = 0.0; - } - } -# endif - - if (!dynamicCubemap) { - float3 envColorBase = Color::GammaToLinear(TexEnvSampler.Sample(SampEnvSampler, envSamplingPoint).xyz); - envColor = envColorBase.xyz * envMask; - } - } + float3 reflectionDiffuseColor = diffuseColor + directionalAmbientColor; -# endif // defined (ENVMAP) || defined (MULTI_LAYER_PARALLAX) || defined(EYE) +# if defined(TRUE_PBR) && defined(LOD_LAND_BLEND) && !defined(DEFERRED) + lodLandDiffuseColor += directionalAmbientColor; +# endif float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); @@ -2877,41 +2796,22 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) else # endif { - porosity = lerp(porosity, 0.0, saturate(sqrt(pbrSurfaceProperties.Metallic))); + porosity = lerp(porosity, 0.0, saturate(sqrt(material.Metallic))); } # elif defined(ENVMAP) || defined(MULTI_LAYER_PARALLAX) porosity = lerp(porosity, 0.0, saturate(sqrt(envMask))); # endif float wetnessDarkeningAmount = porosity * wetnessGlossinessAlbedo; - baseColor.xyz = lerp(baseColor.xyz, pow(abs(baseColor.xyz), 1.0 + wetnessDarkeningAmount), 0.5); -# endif - -# if defined(DYNAMIC_CUBEMAPS) -# if defined(SKYLIGHTING) - float3 wetnessReflectance = DynamicCubemaps::GetDynamicCubemap(wetnessNormal, vertexNormal, viewDirection, waterRoughnessSpecular, 0.02, skylightingSH) * sqrt(wetnessGlossinessSpecular); -# else - float3 wetnessReflectance = DynamicCubemaps::GetDynamicCubemap(wetnessNormal, vertexNormal, viewDirection, waterRoughnessSpecular, 0.02) * sqrt(wetnessGlossinessSpecular); -# endif -# else - float3 wetnessReflectance = 0.0; -# endif - -# if !defined(DEFERRED) - wetnessSpecular += wetnessReflectance; + material.BaseColor = lerp(material.BaseColor, pow(abs(material.BaseColor), 1.0 + wetnessDarkeningAmount), 0.5); # endif # endif # if defined(HAIR) float3 vertexColor = lerp(1, TintColor.xyz, input.Color.y); # if defined(CS_HAIR) - float3 indirectDiffuseLobeWeight, indirectSpecularLobeWeightPrim, indirectSpecularLobeWeightSec; if (SharedData::hairSpecularSettings.Enabled) vertexColor = 1; - Hair::GetHairIndirectSpecularLobeWeights(indirectDiffuseLobeWeight, indirectSpecularLobeWeightPrim, indirectSpecularLobeWeightSec, hairT, worldNormal.xyz, viewDirection, vertexNormal, SharedData::hairSpecularSettings.HairGlossiness, uv, baseColor.xyz); - indirectDiffuseLobeWeight *= SharedData::hairSpecularSettings.DiffuseIndirectMult; - indirectSpecularLobeWeightPrim *= SharedData::hairSpecularSettings.SpecularIndirectMult; - indirectSpecularLobeWeightSec *= SharedData::hairSpecularSettings.SpecularIndirectMult; -# endif // CS_HAIR +# endif # elif defined(SKYLIGHTING) float3 vertexColor = input.Color.xyz; float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); @@ -2946,84 +2846,55 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } # else float3 vertexColor = input.Color.xyz; +# if defined(LANDSCAPE) && defined(LOD_BLENDING) + vertexColor = lerp(vertexColor, 1, SharedData::lodBlendingSettings.DisableTerrainVertexColors); +# endif // LOD_BLENDING # endif // defined (HAIR) float4 color = 0; + indirectContext = CreateIndirectLightingContext(ambientNormal, vertexNormal.xyz, viewDirection); + + GetIndirectLobeWeights(indirectLobeWeights, indirectContext, material, uvOriginal); + +# if defined(WETNESS_EFFECTS) +# if defined(DYNAMIC_CUBEMAPS) + float3 wetnessReflectance = GetWetnessIndirectLobeWeights(indirectLobeWeights, wetnessNormal, waterRoughnessSpecular, indirectContext); +# else + float3 wetnessReflectance = 0.0; +# endif +# endif +# if defined(ENVMAP) || defined(MULTI_LAYER_PARALLAX) || defined(EYE) + indirectLobeWeights.specular *= envMask; +# endif + +# if defined(SPECULAR) && !defined(TRUE_PBR) + indirectLobeWeights.specular *= MaterialData.yyy; + specularColor *= MaterialData.yyy; +# endif + # if defined(TRUE_PBR) { - float3 directLightsDiffuseInput = diffuseColor * baseColor.xyz; + float3 directLightsDiffuseInput = diffuseColor * material.BaseColor; [branch] if ((PBRFlags & PBR::Flags::ColoredCoat) != 0) { - directLightsDiffuseInput = lerp(directLightsDiffuseInput, pbrSurfaceProperties.CoatColor * coatLightsDiffuseColor, pbrSurfaceProperties.CoatStrength); + directLightsDiffuseInput = lerp(directLightsDiffuseInput, material.CoatColor * coatLightsDiffuseColor, material.CoatStrength); } color.xyz += directLightsDiffuseInput; } - float3 indirectDiffuseLobeWeight, indirectSpecularLobeWeight; - PBR::GetIndirectLobeWeights(indirectDiffuseLobeWeight, indirectSpecularLobeWeight, worldNormal.xyz, viewDirection, vertexNormal, baseColor.xyz, pbrSurfaceProperties); -# if defined(WETNESS_EFFECTS) - if (waterRoughnessSpecular < 1.0) - indirectSpecularLobeWeight += PBR::GetWetnessIndirectSpecularLobeWeight(wetnessNormal, viewDirection, vertexNormal, waterRoughnessSpecular) * wetnessGlossinessSpecular; -# endif - -# if defined(DEFERRED) && defined(SSGI) -# else - color.xyz += indirectDiffuseLobeWeight * directionalAmbientColor; -# endif - -# if !defined(DEFERRED) -# if defined(DYNAMIC_CUBEMAPS) -# if defined(SKYLIGHTING) - specularColorPBR += indirectSpecularLobeWeight * DynamicCubemaps::GetDynamicCubemapSpecularIrradiance(screenUV, worldNormal, vertexNormal, viewDirection, pbrSurfaceProperties.Roughness, skylightingSH); -# else - specularColorPBR += indirectSpecularLobeWeight * DynamicCubemaps::GetDynamicCubemapSpecularIrradiance(screenUV, worldNormal, vertexNormal, viewDirection, pbrSurfaceProperties.Roughness); -# endif -# else - specularColorPBR += indirectSpecularLobeWeight * directionalAmbientColor; -# endif -# else - indirectDiffuseLobeWeight *= vertexColor; -# endif - // Fixes white items in UI for VR [branch] if ((PBRFlags & PBR::Flags::HasEmissive) != 0) { color.xyz += emitColor.xyz; } - color.xyz += transmissionColor; -# elif defined(HAIR) && defined(CS_HAIR) - color.xyz += diffuseColor * baseColor.xyz; - if (SharedData::hairSpecularSettings.Enabled) { -# if defined(DEFERRED) && defined(SSGI) -# else - color.xyz += indirectDiffuseLobeWeight * directionalAmbientColor; -# endif - color.xyz += transmissionColor; - } # else - color.xyz += diffuseColor * baseColor.xyz; + color.xyz += diffuseColor * material.BaseColor; # endif -# if defined(HAIR) && defined(CS_HAIR) -# if !defined(DEFERRED) -# if defined(DYNAMIC_CUBEMAPS) - if (SharedData::hairSpecularSettings.Enabled) -# if defined(SKYLIGHTING) - { - float3 indirectSpecular = Hair::GetHairDynamicCubemapSpecularIrradiance(uv, screenUV, hairT, worldNormal, vertexNormal, viewDirection, SharedData::hairSpecularSettings.HairGlossiness, indirectSpecularLobeWeightPrim, indirectSpecularLobeWeightSec, skylightingSH); - color.xyz += indirectSpecular; - } -# else - { - float3 indirectSpecular = Hair::GetHairDynamicCubemapSpecularIrradiance(uv, screenUV, hairT, worldNormal, vertexNormal, viewDirection, SharedData::hairSpecularSettings.HairGlossiness, indirectSpecularLobeWeightPrim, indirectSpecularLobeWeightSec); - color.xyz += indirectSpecular; - } -# endif -# endif -# endif -# endif + color.xyz += indirectLobeWeights.diffuse * directionalAmbientColor; + color.xyz += transmissionColor; color.xyz *= vertexColor; @@ -3039,26 +2910,13 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float mlpBlendFactor = saturate(viewNormalAngle) * (1.0 - baseColor.w); -# if defined(DEFERRED) && defined(SSGI) - color.xyz = lerp(color.xyz, (diffuseColor + directionalAmbientColor) * vertexColor * layerColor, mlpBlendFactor); -# else color.xyz = lerp(color.xyz, diffuseColor * vertexColor * layerColor, mlpBlendFactor); -# endif # if defined(DEFERRED) - baseColor.xyz *= 1.0 - mlpBlendFactor; + indirectLobeWeights.diffuse *= 1.0 - mlpBlendFactor; # endif # endif // MULTI_LAYER_PARALLAX -# if defined(SPECULAR) -# if defined(HAIR) && defined(CS_HAIR) - if (!SharedData::hairSpecularSettings.Enabled) -# endif - specularColor = (specularColor * glossiness * MaterialData.yyy) * SpecularColor.xyz; -# elif defined(SPARKLE) - specularColor *= glossiness; -# endif // SPECULAR - # if defined(SNOW) if (useSnowSpecular) specularColor = 0; @@ -3079,33 +2937,62 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) specularColor *= complexSpecular; # endif // defined (EMAT) && defined(ENVMAP) -# if !defined(DEFERRED) && defined(DYNAMIC_CUBEMAPS) && (defined(ENVMAP) || defined(MULTI_LAYER_PARALLAX) || defined(EYE)) - if (dynamicCubemap) - specularColor += envColor; -# endif - -# if defined(WETNESS_EFFECTS) && !defined(TRUE_PBR) - specularColor += wetnessSpecular * wetnessGlossinessSpecular; -# endif - # if defined(LOD_LAND_BLEND) && defined(TRUE_PBR) { -# if defined(DEFERRED) && defined(SSGI) -# else lodLandDiffuseColor += directionalAmbientColor; -# endif float3 litLodLandColor = vertexColor * lodLandColor.xyz * lodLandFadeFactor * lodLandDiffuseColor; color.xyz = lerp(color.xyz * Color::PBRLightingScale, litLodLandColor, lodLandBlendFactor); - specularColor = lerp(specularColorPBR * Color::PBRLightingScale, 0, lodLandBlendFactor); - indirectDiffuseLobeWeight = lerp(indirectDiffuseLobeWeight, vertexColor * lodLandColor.xyz * lodLandFadeFactor, lodLandBlendFactor); - indirectSpecularLobeWeight = lerp(indirectSpecularLobeWeight, 0, lodLandBlendFactor); - pbrGlossiness = lerp(pbrGlossiness, 0, lodLandBlendFactor); + specularColor = lerp(specularColor * Color::PBRLightingScale, 0, lodLandBlendFactor); + indirectLobeWeights.diffuse = lerp(indirectLobeWeights.diffuse * Color::PBRLightingScale, vertexColor * lodLandColor.xyz * lodLandFadeFactor, lodLandBlendFactor); + indirectLobeWeights.specular = lerp(indirectLobeWeights.specular, 0, lodLandBlendFactor); + material.Roughness = lerp(material.Roughness, 1, lodLandBlendFactor); } # elif defined(TRUE_PBR) color.xyz *= Color::PBRLightingScale; - specularColorPBR *= Color::PBRLightingScale; - specularColor = specularColorPBR; + specularColor *= Color::PBRLightingScale; + indirectLobeWeights.diffuse *= Color::PBRLightingScale; +# endif + +# if !defined(DEFERRED) + if (any(indirectLobeWeights.specular > 0) +# if defined(WETNESS_EFFECTS) + || any(wetnessReflectance > 0) +# endif + ) +# if defined(DYNAMIC_CUBEMAPS) +# if defined(SKYLIGHTING) + color.xyz += indirectLobeWeights.specular * DynamicCubemaps::GetDynamicCubemapSpecularIrradiance(screenUV, worldNormal, vertexNormal, viewDirection, material.Roughness, skylightingSH); +# if defined(WETNESS_EFFECTS) + if (waterRoughnessSpecular < 1) + color.xyz += wetnessReflectance * DynamicCubemaps::GetDynamicCubemapSpecularIrradiance(screenUV, wetnessNormal, vertexNormal, viewDirection, waterRoughnessSpecular, skylightingSH); +# endif +# else + color.xyz += indirectLobeWeights.specular * DynamicCubemaps::GetDynamicCubemapSpecularIrradiance(screenUV, worldNormal, vertexNormal, viewDirection, material.Roughness); +# if defined(WETNESS_EFFECTS) + if (waterRoughnessSpecular < 1) + color.xyz += wetnessReflectance * DynamicCubemaps::GetDynamicCubemapSpecularIrradiance(screenUV, wetnessNormal, vertexNormal, viewDirection, waterRoughnessSpecular); +# endif +# endif +# else + color.xyz += indirectLobeWeights.specular * directionalAmbientColor; +# endif +# endif + + float3 outputAlbedo = indirectLobeWeights.diffuse * vertexColor.xyz; + +# if defined(IBL) && defined(SKYLIGHTING) + directionalAmbientColor -= iblColor; +# endif + + directionalAmbientColor *= outputAlbedo; + +# if defined(SKYLIGHTING) + Skylighting::applySkylighting(color.xyz, directionalAmbientColor, outputAlbedo, skylightingDiffuse); +# endif + +# if defined(IBL) && defined(SKYLIGHTING) + directionalAmbientColor += iblColor * outputAlbedo; # endif # if !defined(DEFERRED) @@ -3133,7 +3020,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) diffuseColor = 0.0; dynamicCubemap = true; envColor = 1.0; - envRoughness = 0.0; + material.Roughness = 0.0; color.xyz = 0; # endif @@ -3242,8 +3129,8 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // ANISOTROPIC_ALPHA psout.Diffuse.w = alpha; - # endif + # if defined(LIGHT_LIMIT_FIX) && defined(LLFDEBUG) if (SharedData::lightLimitFixSettings.EnableLightsVisualisation) { if (SharedData::lightLimitFixSettings.LightsVisualisationMode == 0) { @@ -3263,35 +3150,10 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Diffuse.xyz = color.xyz; # endif // defined(LIGHT_LIMIT_FIX) -# if defined(SNOW) -# if defined(TRUE_PBR) - psout.Parameters.x = Color::RGBToLuminanceAlternative(specularColor); - psout.Parameters.y = 0; -# else - psout.Parameters.x = Color::RGBToLuminanceAlternative(lightsSpecularColor); -# endif -# endif // SNOW && !PBR - - psout.MotionVectors.xy = SSRParams.z > 1e-5 ? float2(1, 0) : screenMotionVector.xy; - psout.MotionVectors.zw = float2(0, 1); - -# if !defined(DEFERRED) - float ssrMask = glossiness; -# if defined(TRUE_PBR) - ssrMask = Color::RGBToLuminanceAlternative(pbrSurfaceProperties.F0); -# endif - psout.ScreenSpaceNormals.w = smoothstep(-1e-5 + SSRParams.x, SSRParams.y, ssrMask) * SSRParams.w; - - // Green reflections fix - if (FrameBuffer::FrameParams.z) - psout.ScreenSpaceNormals.w = 0.0; - - screenSpaceNormal.z = max(0.001, sqrt(8 + -8 * screenSpaceNormal.z)); - screenSpaceNormal.xy /= screenSpaceNormal.zz; - psout.ScreenSpaceNormals.xy = screenSpaceNormal.xy + 0.5.xx; - psout.ScreenSpaceNormals.z = 0; + psout.MotionVectors.xy = screenMotionVector.xy; + psout.MotionVectors.zw = float2(0, psout.Diffuse.w); -# else +# if defined(DEFERRED) # if defined(TERRAIN_BLENDING) psout.Diffuse.w = blendFactorTerrain; @@ -3299,99 +3161,37 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.MotionVectors.zw = float2(0.0, psout.Diffuse.w); psout.Specular = float4(specularColor, psout.Diffuse.w); - - float3 outputAlbedo = baseColor.xyz * vertexColor; -# if defined(TRUE_PBR) - outputAlbedo = indirectDiffuseLobeWeight; -# endif - -# if defined(HAIR) && defined(CS_HAIR) - if (SharedData::hairSpecularSettings.Enabled) { - outputAlbedo = indirectDiffuseLobeWeight; - } -# endif - psout.Albedo = float4(outputAlbedo, psout.Diffuse.w); - const float wetnessGlossinessGain = 0.65; - float outGlossiness = saturate(glossiness * SSRParams.w); - -# if defined(HAIR) && defined(CS_HAIR) - if (SharedData::hairSpecularSettings.Enabled) { - outGlossiness = 1.0 - (SharedData::hairSpecularSettings.HairMode == 1 ? 1.0 : pow(abs(2.0 / (glossiness * 0.5 + 2.0)), 0.25)); +# if defined(WETNESS_EFFECTS) + indirectLobeWeights.specular += wetnessReflectance; + if (waterRoughnessSpecular < 1) { + screenSpaceNormal = lerp(screenSpaceNormal, normalize(FrameBuffer::WorldToView(wetnessNormal, false, eyeIndex)), saturate(wetnessGlossinessSpecular)); + material.Roughness = lerp(material.Roughness, waterRoughnessSpecular, wetnessReflectance.x); } # endif -# if defined(TRUE_PBR) - psout.Reflectance = float4(indirectSpecularLobeWeight, psout.Diffuse.w); -# if defined(WETNESS_EFFECTS) - psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), lerp(pbrGlossiness, saturate(pbrGlossiness + wetnessGlossinessGain), wetnessGlossinessSpecular), psout.Diffuse.w); -# else - psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), pbrGlossiness, psout.Diffuse.w); -# endif -# elif defined(HAIR) && defined(CS_HAIR) - if (SharedData::hairSpecularSettings.Enabled) { -# if defined(WETNESS_EFFECTS) - psout.Reflectance = float4(indirectSpecularLobeWeightPrim + indirectSpecularLobeWeightSec + wetnessReflectance, psout.Diffuse.w); - psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), lerp(outGlossiness, saturate(outGlossiness + wetnessGlossinessGain), wetnessGlossinessSpecular), psout.Diffuse.w); -# else - psout.Reflectance = float4(indirectSpecularLobeWeightPrim + indirectSpecularLobeWeightSec, psout.Diffuse.w); - psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), outGlossiness, psout.Diffuse.w); -# endif - } else { -# if defined(WETNESS_EFFECTS) - psout.Reflectance = float4(wetnessReflectance, psout.Diffuse.w); - psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), lerp(outGlossiness, saturate(outGlossiness + wetnessGlossinessGain), wetnessGlossinessSpecular), psout.Diffuse.w); -# else - psout.Reflectance = float4(0.0.xxx, psout.Diffuse.w); - psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), outGlossiness, psout.Diffuse.w); -# endif - } -# elif defined(WETNESS_EFFECTS) - psout.Reflectance = float4(wetnessReflectance, psout.Diffuse.w); - psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), lerp(outGlossiness, saturate(outGlossiness + wetnessGlossinessGain), wetnessGlossinessSpecular), psout.Diffuse.w); -# else - psout.Reflectance = float4(0.0.xxx, psout.Diffuse.w); - psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), outGlossiness, psout.Diffuse.w); -# endif + psout.Reflectance = float4(indirectLobeWeights.specular, psout.Diffuse.w); + psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), saturate(1.0 - material.Roughness), psout.Diffuse.w); # if defined(SNOW) - psout.Parameters.w = psout.Diffuse.w; -# endif - -# if (defined(ENVMAP) || defined(MULTI_LAYER_PARALLAX) || defined(EYE)) -# if defined(DYNAMIC_CUBEMAPS) - if (dynamicCubemap) { -# if defined(WETNESS_EFFECTS) - psout.Reflectance.xyz = max(envColor, wetnessReflectance); - psout.NormalGlossiness.z = lerp(1.0 - envRoughness, saturate(1.0 - envRoughness + wetnessGlossinessGain), wetnessGlossinessSpecular); -# else - psout.Reflectance.xyz = envColor; - psout.NormalGlossiness.z = 1.0 - envRoughness; -# endif - } +# if defined(TRUE_PBR) + psout.Parameters.x = Color::RGBToLuminanceAlternative(specularColor); + psout.Parameters.y = 0; +# else + psout.Parameters.x = Color::RGBToLuminanceAlternative(lightsSpecularColor); # endif + psout.Parameters.w = psout.Diffuse.w; # endif # if defined(SSS) && defined(SKIN) -# if defined(WETNESS_EFFECTS) - psout.Masks = float4(saturate(baseColor.a), !(Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::IsBeastRace), wetnessNormal.z * 0.5 + 0.5, psout.Diffuse.w); -# else - psout.Masks = float4(saturate(baseColor.a), !(Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::IsBeastRace), 0, psout.Diffuse.w); -# endif -# elif defined(WETNESS_EFFECTS) - psout.Masks = float4(0, 0, wetnessNormal.z * 0.5 + 0.5, psout.Diffuse.w); + psout.Masks = float4(saturate(baseColor.a), !(Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::IsBeastRace), Color::RGBToYCoCg(directionalAmbientColor).x, psout.Diffuse.w); # else - psout.Masks = float4(0, 0, 0, psout.Diffuse.w); + psout.Masks = float4(0, 0, Color::RGBToYCoCg(directionalAmbientColor).x, psout.Diffuse.w); # endif -# if defined(TERRAIN_BLENDING) - float stochasticBlend = (screenNoise * screenNoise) < blendFactorTerrain ? 1.0 : 0.0; - stochasticBlend = lerp(stochasticBlend, blendFactorTerrain, 0.1); + float stochasticBlend = (screenNoise * screenNoise) < psout.Diffuse.w ? 1.0 : 0.0; psout.NormalGlossiness.w = stochasticBlend; - psout.Albedo.w = stochasticBlend; -# endif - # endif return psout; diff --git a/package/Shaders/Menu/BackgroundBlurHorizontal.hlsl b/package/Shaders/Menu/BackgroundBlurHorizontal.hlsl new file mode 100644 index 0000000000..4c941f4350 --- /dev/null +++ b/package/Shaders/Menu/BackgroundBlurHorizontal.hlsl @@ -0,0 +1,69 @@ +// Horizontal Blur Pass Shader +// Part of the BackgroundBlur system - separable Gaussian blur implementation + +cbuffer BlurBuffer : register(b0) +{ + float4 TexelSize; // x = 1/width, y = 1/height, z = blur strength, w = unused + int4 BlurParams; // x = samples, y = unused, z = unused, w = unused +}; + +SamplerState LinearSampler : register(s0); +Texture2D InputTexture : register(t0); + +struct VS_OUTPUT +{ + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; +}; + +VS_OUTPUT VS_Main(uint vertexID : SV_VertexID) +{ + VS_OUTPUT output; + output.TexCoord = float2((vertexID << 1) & 2, vertexID & 2); + output.Position = float4(output.TexCoord * 2.0f - 1.0f, 0.0f, 1.0f); + output.Position.y = -output.Position.y; + return output; +} + +// Precomputed normalized Gaussian weights (sigma = 2.0) +static const float WEIGHTS[8] = { + 0.1760327f, // offset 0 (center) + 0.1658591f, // offset ±1 + 0.1403215f, // offset ±2 + 0.1069852f, // offset ±3 + 0.0732894f, // offset ±4 + 0.0451904f, // offset ±5 + 0.0248657f, // offset ±6 + 0.0122423f // offset ±7 +}; + +float4 PS_Main(VS_OUTPUT input) : SV_TARGET +{ + const int samples = min(BlurParams.x, 15); + const int halfSamples = samples / 2; + + // Compute normalization factor for actual weights used + float weightSum = WEIGHTS[0]; + [unroll(7)] + for (int j = 1; j <= halfSamples; ++j) + { + weightSum += 2.0f * WEIGHTS[min(j, 7)]; + } + const float normalization = 1.0f / weightSum; + + // Sample center pixel + float4 result = InputTexture.Sample(LinearSampler, input.TexCoord) * (WEIGHTS[0] * normalization); + + // Sample symmetric pairs + [unroll(7)] + for (int i = 1; i <= halfSamples; ++i) + { + float weight = WEIGHTS[min(i, 7)] * normalization; + float offset = i * TexelSize.x; + + result += InputTexture.Sample(LinearSampler, input.TexCoord + float2(offset, 0.0f)) * weight; + result += InputTexture.Sample(LinearSampler, input.TexCoord - float2(offset, 0.0f)) * weight; + } + + return result; +} diff --git a/package/Shaders/Menu/BackgroundBlurVertical.hlsl b/package/Shaders/Menu/BackgroundBlurVertical.hlsl new file mode 100644 index 0000000000..b851f23392 --- /dev/null +++ b/package/Shaders/Menu/BackgroundBlurVertical.hlsl @@ -0,0 +1,69 @@ +// Vertical Blur Pass Shader +// Part of the BackgroundBlur system - separable Gaussian blur implementation + +cbuffer BlurBuffer : register(b0) +{ + float4 TexelSize; // x = 1/width, y = 1/height, z = blur strength, w = unused + int4 BlurParams; // x = samples, y = unused, z = unused, w = unused +}; + +SamplerState LinearSampler : register(s0); +Texture2D InputTexture : register(t0); + +struct VS_OUTPUT +{ + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; +}; + +VS_OUTPUT VS_Main(uint vertexID : SV_VertexID) +{ + VS_OUTPUT output; + output.TexCoord = float2((vertexID << 1) & 2, vertexID & 2); + output.Position = float4(output.TexCoord * 2.0f - 1.0f, 0.0f, 1.0f); + output.Position.y = -output.Position.y; + return output; +} + +// Precomputed normalized Gaussian weights (sigma = 2.0) +static const float WEIGHTS[8] = { + 0.1760327f, // offset 0 (center) + 0.1658591f, // offset ±1 + 0.1403215f, // offset ±2 + 0.1069852f, // offset ±3 + 0.0732894f, // offset ±4 + 0.0451904f, // offset ±5 + 0.0248657f, // offset ±6 + 0.0122423f // offset ±7 +}; + +float4 PS_Main(VS_OUTPUT input) : SV_TARGET +{ + const int samples = min(BlurParams.x, 15); + const int halfSamples = samples / 2; + + // Compute normalization factor for actual weights used + float weightSum = WEIGHTS[0]; + [unroll(7)] + for (int j = 1; j <= halfSamples; ++j) + { + weightSum += 2.0f * WEIGHTS[min(j, 7)]; + } + const float normalization = 1.0f / weightSum; + + // Sample center pixel + float4 result = InputTexture.Sample(LinearSampler, input.TexCoord) * (WEIGHTS[0] * normalization); + + // Sample symmetric pairs + [unroll(7)] + for (int i = 1; i <= halfSamples; ++i) + { + float weight = WEIGHTS[min(i, 7)] * normalization; + float offset = i * TexelSize.y; + + result += InputTexture.Sample(LinearSampler, input.TexCoord + float2(0.0f, offset)) * weight; + result += InputTexture.Sample(LinearSampler, input.TexCoord - float2(0.0f, offset)) * weight; + } + + return result; +} diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index 70fa3d2c4c..87a37430f0 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -11,6 +11,10 @@ # define GRASS #endif // GRASS_LIGHTING +#if !defined(DYNAMIC_CUBEMAPS) && defined(IBL) +# undef IBL +#endif + struct VS_INPUT { float4 Position : POSITION0; @@ -136,11 +140,8 @@ cbuffer cb8 : register(b8) float4 cb8[240]; } -# ifdef GRASS_LIGHTING -float4 GetMSPosition(VS_INPUT input, float windTimer, float3x3 world3x3) -# else -float4 GetMSPosition(VS_INPUT input, float windTimer) -# endif +// Calculate wind displacement for a grass vertex +float3 CalculateWindDisplacement(VS_INPUT input, float windTimer) { float windAngle = 0.4 * ((input.InstanceData1.x + input.InstanceData1.y) * -0.0078125 + windTimer); float windAngleSin, windAngleCos; @@ -152,14 +153,22 @@ float4 GetMSPosition(VS_INPUT input, float windTimer) float windPower = WindVector.z * (((windTmp1 + windTmp2) * 0.3 + windTmp3) * (0.5 * (input.Color.w * input.Color.w))); + return float3(WindVector.xy, 0) * windPower; +} + +#ifdef GRASS_LIGHTING +float4 GetMSPosition(VS_INPUT input, float3x3 world3x3) +#else +float4 GetMSPosition(VS_INPUT input) +#endif +{ float3 inputPosition = input.Position.xyz * (input.InstanceData4.yyy * ScaleMask.xyz + float3(1, 1, 1)); - float3 windVector = float3(WindVector.xy, 0); -# ifdef GRASS_LIGHTING - float3 InstanceData4 = mul(world3x3, inputPosition); +#ifdef GRASS_LIGHTING + float3 transformedPosition = mul(world3x3, inputPosition); float4 msPosition; - msPosition.xyz = input.InstanceData1.xyz + (windVector * windPower + InstanceData4); -# else + msPosition.xyz = input.InstanceData1.xyz + transformedPosition; +#else float3 instancePosition; instancePosition.z = dot( float3(input.InstanceData4.x, input.InstanceData2.w, input.InstanceData3.w), inputPosition); @@ -167,8 +176,8 @@ float4 GetMSPosition(VS_INPUT input, float windTimer) instancePosition.y = dot(input.InstanceData3.xyz, inputPosition); float4 msPosition; - msPosition.xyz = input.InstanceData1.xyz + (windVector * windPower + instancePosition); -# endif + msPosition.xyz = input.InstanceData1.xyz + instancePosition; +#endif msPosition.w = 1; return msPosition; @@ -186,13 +195,19 @@ VS_OUTPUT main(VS_INPUT input) ); float3x3 world3x3 = float3x3(input.InstanceData2.xyz, input.InstanceData3.xyz, float3(input.InstanceData4.x, input.InstanceData2.w, input.InstanceData3.w)); - float4 msPosition = GetMSPosition(input, WindTimer, world3x3); + float4 msPosition = GetMSPosition(input, world3x3); + + float3 windDisplacement = CalculateWindDisplacement(input, WindTimer); + float3 previousWindDisplacement = CalculateWindDisplacement(input, PreviousWindTimer); # ifdef GRASS_COLLISION - float3 displacement = GrassCollision::GetDisplacedPosition(input, msPosition.xyz, eyeIndex); + float3 displacement, previousDisplacement; + GrassCollision::GetDisplacedPosition(input, msPosition.xyz, displacement, previousDisplacement); msPosition.xyz += displacement; # endif // GRASS_COLLISION + msPosition.xyz += windDisplacement; + float4 projSpacePosition = mul(WorldViewProj[eyeIndex], msPosition); # if !defined(VR) vsout.HPosition = projSpacePosition; @@ -221,12 +236,14 @@ VS_OUTPUT main(VS_INPUT input) vsout.ViewSpacePosition = mul(WorldView[eyeIndex], msPosition).xyz; vsout.WorldPosition = mul(World[eyeIndex], msPosition); - float4 previousMsPosition = GetMSPosition(input, PreviousWindTimer, world3x3); + float4 previousMsPosition = GetMSPosition(input, world3x3); # ifdef GRASS_COLLISION - previousMsPosition.xyz += displacement; + previousMsPosition.xyz += previousDisplacement; # endif // GRASS_COLLISION + previousMsPosition.xyz += previousWindDisplacement; + vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], previousMsPosition); # if defined(VR) Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(projSpacePosition, eyeIndex); @@ -252,13 +269,19 @@ VS_OUTPUT main(VS_INPUT input) # endif // VR ); - float4 msPosition = GetMSPosition(input, WindTimer); + float4 msPosition = GetMSPosition(input); + + float3 windDisplacement = CalculateWindDisplacement(input, WindTimer); + float3 previousWindDisplacement = CalculateWindDisplacement(input, PreviousWindTimer); # ifdef GRASS_COLLISION - float3 displacement = GrassCollision::GetDisplacedPosition(input, msPosition.xyz, eyeIndex); + float3 displacement, previousDisplacement; + GrassCollision::GetDisplacedPosition(input, msPosition.xyz, displacement, previousDisplacement); msPosition.xyz += displacement; # endif // GRASS_COLLISION + msPosition.xyz += windDisplacement; + float4 projSpacePosition = mul(WorldViewProj[eyeIndex], msPosition); # if !defined(VR) vsout.HPosition = projSpacePosition; @@ -293,7 +316,7 @@ VS_OUTPUT main(VS_INPUT input) vsout.ViewSpacePosition = mul(WorldView[eyeIndex], msPosition).xyz; vsout.WorldPosition = mul(World[eyeIndex], msPosition); - float4 previousMsPosition = GetMSPosition(input, PreviousWindTimer); + float4 previousMsPosition = GetMSPosition(input); # if defined(VR) Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(projSpacePosition, eyeIndex); vsout.HPosition = VRout.VRPosition; @@ -302,9 +325,11 @@ VS_OUTPUT main(VS_INPUT input) # endif // !VR # ifdef GRASS_COLLISION - previousMsPosition.xyz += displacement; + previousMsPosition.xyz += previousDisplacement; # endif // GRASS_COLLISION + previousMsPosition.xyz += previousWindDisplacement; + vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], previousMsPosition); return vsout; @@ -462,7 +487,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 complexTest = TexBaseSampler.Load(int3(0, int(y) - 1, 0)).xyz * 2.0 - 1.0; float complexLength = length(complexTest); - bool complex = abs(complexLength - 1.0) < 0.02; + bool complex = abs(complexLength - 1.0) < 0.03; # endif // !TRUE_PBR float4 baseColor; @@ -626,7 +651,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 sss = dirLightColor * saturate(-dirLightAngle); if (complex) - lightsSpecularColor += GrassLighting::GetLightSpecularInput(DirLightDirection, viewDirection, normal, dirLightColor, SharedData::grassLightingSettings.Glossiness); + lightsSpecularColor += GrassLighting::GetLightSpecularInput(SharedData::DirLightDirection.xyz, viewDirection, normal, dirLightColor, SharedData::grassLightingSettings.Glossiness); # endif # if defined(LIGHT_LIMIT_FIX) @@ -714,42 +739,61 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 directionalAmbientColor = max(0, mul(SharedData::DirectionalAmbient, float4(normal, 1.0))); # if defined(IBL) - if (SharedData::iblSettings.EnableDiffuseIBL && !SharedData::InInterior) { + if (SharedData::iblSettings.EnableDiffuseIBL && (!SharedData::InInterior || SharedData::iblSettings.EnableInterior)) { directionalAmbientColor *= SharedData::iblSettings.DALCAmount; - directionalAmbientColor += Color::Saturation(ImageBasedLighting::GetDiffuseIBL(-normal), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; } # endif // IBL # if defined(SKYLIGHTING) + float skylightingDiffuse = 1.0; if (!SharedData::InInterior) { - float3 skylightingNormal = normal; - # if defined(VR) float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; # else float3 positionMSSkylight = input.WorldPosition.xyz; # endif - sh2 skylightingSH = Skylighting::sample(SharedData::skylightingSettings, Skylighting::SkylightingProbeArray, Skylighting::stbn_vec3_2Dx1D_128x128x64, input.HPosition.xy, positionMSSkylight, normal); - float skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(skylightingNormal)) / Math::PI; + skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(normal)) / Math::PI; skylightingDiffuse = saturate(skylightingDiffuse); - skylightingDiffuse = lerp(1.0, skylightingDiffuse, skylightingFadeOutFactor); skylightingDiffuse = Skylighting::mixDiffuse(SharedData::skylightingSettings, skylightingDiffuse); - - directionalAmbientColor = Color::GammaToLinear(directionalAmbientColor); - directionalAmbientColor *= skylightingDiffuse; - directionalAmbientColor = Color::LinearToGamma(directionalAmbientColor); } # endif // SKYLIGHTING -# if !defined(SSGI) - diffuseColor += directionalAmbientColor; +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL) { + if (!SharedData::InInterior || SharedData::iblSettings.EnableInterior) + { +# if defined(SKYLIGHTING) + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-normal, skylightingDiffuse), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# else + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-normal), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# endif + iblColor = Color::LinearToGamma(iblColor); + directionalAmbientColor += iblColor; + } + } # endif + diffuseColor += directionalAmbientColor; + +# if defined(IBL) && defined(SKYLIGHTING) + directionalAmbientColor -= iblColor; +# endif diffuseColor *= albedo; diffuseColor += max(0, sss * subsurfaceColor * SharedData::grassLightingSettings.SubsurfaceScatteringAmount); + directionalAmbientColor *= albedo; + +# if defined(SKYLIGHTING) + Skylighting::applySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); +# endif + +# if defined(IBL) && defined(SKYLIGHTING) + directionalAmbientColor += iblColor * albedo; +# endif + specularColor += lightsSpecularColor; specularColor *= specColor.w * SharedData::grassLightingSettings.SpecularStrength; specularColor = Color::GammaToLinear(specularColor); @@ -783,7 +827,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif psout.Specular = float4(specularColor, 1); - psout.Masks = float4(0, 0, 0, 0); + psout.Masks = float4(0, 0, Color::RGBToYCoCg(directionalAmbientColor).x, 0); # endif return psout; } @@ -903,41 +947,62 @@ PS_OUTPUT main(PS_INPUT input) float3 directionalAmbientColor = max(0, mul(SharedData::DirectionalAmbient, float4(normal, 1.0))); # if defined(IBL) - if (SharedData::iblSettings.EnableDiffuseIBL && !SharedData::InInterior) { + if (SharedData::iblSettings.EnableDiffuseIBL && (!SharedData::InInterior || SharedData::iblSettings.EnableInterior)) { directionalAmbientColor *= SharedData::iblSettings.DALCAmount; - directionalAmbientColor += Color::Saturation(ImageBasedLighting::GetDiffuseIBL(-normal), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; } # endif // IBL # if defined(SKYLIGHTING) + float skylightingDiffuse = 1.0; if (!SharedData::InInterior) { - float3 skylightingNormal = normal; - # if defined(VR) float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; # else float3 positionMSSkylight = input.WorldPosition.xyz; # endif - sh2 skylightingSH = Skylighting::sample(SharedData::skylightingSettings, Skylighting::SkylightingProbeArray, Skylighting::stbn_vec3_2Dx1D_128x128x64, input.HPosition.xy, positionMSSkylight, normal); - float skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(skylightingNormal)) / Math::PI; + skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(normal)) / Math::PI; skylightingDiffuse = saturate(skylightingDiffuse); - skylightingDiffuse = lerp(1.0, skylightingDiffuse, skylightingFadeOutFactor); skylightingDiffuse = Skylighting::mixDiffuse(SharedData::skylightingSettings, skylightingDiffuse); - - directionalAmbientColor = Color::GammaToLinear(directionalAmbientColor); - directionalAmbientColor *= skylightingDiffuse; - directionalAmbientColor = Color::LinearToGamma(directionalAmbientColor); } # endif // SKYLIGHTING -# if !defined(SSGI) - diffuseColor += directionalAmbientColor; +# if defined(IBL) + float3 iblColor = 0; + if (SharedData::iblSettings.EnableDiffuseIBL) { + if (!SharedData::InInterior || SharedData::iblSettings.EnableInterior) + { +# if defined(SKYLIGHTING) + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-normal, skylightingDiffuse), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# else + iblColor += Color::Saturation(ImageBasedLighting::GetIBLColor(-normal), SharedData::iblSettings.IBLSaturation) * SharedData::iblSettings.DiffuseIBLScale; +# endif + iblColor = Color::LinearToGamma(iblColor); + directionalAmbientColor += iblColor; + } + } # endif + diffuseColor += directionalAmbientColor; + float3 albedo = baseColor.xyz * vertexColor; - psout.Diffuse.xyz = diffuseColor * albedo; + + diffuseColor *= albedo; +# if defined(IBL) && defined(SKYLIGHTING) + directionalAmbientColor -= iblColor; +# endif + directionalAmbientColor *= albedo; + +# if defined(SKYLIGHTING) + Skylighting::applySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); +# endif + +# if defined(IBL) && defined(SKYLIGHTING) + directionalAmbientColor += iblColor * albedo; +# endif + + psout.Diffuse.xyz = diffuseColor; psout.Diffuse.w = 1; @@ -953,7 +1018,7 @@ PS_OUTPUT main(PS_INPUT input) psout.Normal.zw = 0; psout.Albedo = float4(albedo, 1); - psout.Masks = float4(0, 0, 0, 0); + psout.Masks = float4(0, 0, Color::RGBToYCoCg(directionalAmbientColor).x, 0); # endif return psout; diff --git a/package/Shaders/Sky.hlsl b/package/Shaders/Sky.hlsl index 31426ecb35..a0c52cae7f 100644 --- a/package/Shaders/Sky.hlsl +++ b/package/Shaders/Sky.hlsl @@ -1,3 +1,4 @@ +#include "Common/FastMath.hlsli" #include "Common/FrameBuffer.hlsli" #include "Common/Random.hlsli" #include "Common/VR.hlsli" @@ -276,8 +277,21 @@ PS_OUTPUT main(PS_INPUT input) # elif defined(PS_CLOUDS) float4 apColor = PhysSky::SampleAp(viewDir, input.Position.xy, psCloudDist, PhysSky::SampSv); psout.Color.xyz = psout.Color.xyz * apColor.a + apColor.rgb; -# else - // discard; // TODO: REMOVE +# elif defined(DEFERRED) && defined(TEX) + float3 sunDir = normalize(SharedData::physSkyData.sunDir); + float cosTheta = saturate(dot(normalize(input.WorldPosition.xyz), sunDir)); + if (cosTheta > SharedData::physSkyData.sunDiskCos && SharedData::physSkyData.sunDiskCos > 0.0) + { + float sunDiskSin = sqrt(1.0 - SharedData::physSkyData.sunDiskCos * SharedData::physSkyData.sunDiskCos); + float tanTheta = sqrt(1.0 - cosTheta * cosTheta) / cosTheta; + float normDist = tanTheta * SharedData::physSkyData.sunDiskCos * rcp(sunDiskSin); + float3 limbFactor = PhysSky::LimbDarkenHestroffer(normDist); + + float3 dirLightColor = SharedData::physSkyData.sunlightColor * limbFactor; + dirLightColor *= PhysSky::SampleTr(normalize(input.WorldPosition.xyz), SampBaseSampler); + psout.Color.xyz += dirLightColor; + psout.Color.w = 1.0; + } # endif } #endif diff --git a/package/Shaders/Water.hlsl b/package/Shaders/Water.hlsl index b60bd8fc46..6da26ef848 100644 --- a/package/Shaders/Water.hlsl +++ b/package/Shaders/Water.hlsl @@ -88,7 +88,7 @@ struct VS_OUTPUT float4 TexCoord3 : TEXCOORD3; # endif # if defined(FLOWMAP) - nointerpolation float TexCoord4 : TEXCOORD4; + nointerpolation float2 TexCoord4 : TEXCOORD4; # endif # if NUM_SPECULAR_LIGHTS == 0 float4 MPosition : TEXCOORD5; @@ -453,7 +453,7 @@ struct FlowmapData FlowmapData GetFlowmapDataTextureSpace(PS_INPUT input, float2 uvShift) { FlowmapData data; - data.color = FlowMapTex.Sample(FlowMapSampler, input.TexCoord2.zw + uvShift); + data.color = FlowMapTex.SampleLevel(FlowMapSampler, input.TexCoord2.zw + uvShift, 0); data.flowVector = (64 * input.TexCoord3.xy) * sqrt(1.01 - data.color.z); // NOTE: flowVector is NOT transformed yet - this is the raw vector before rotation matrix return data; @@ -486,23 +486,90 @@ FlowmapData GetFlowmapDataUV(PS_INPUT input, float2 uvShift) data.flowVector = mul(transpose(flowRotationMatrix), data.flowVector); return data; } +// ---------------------------------------------------------------- +// Flowmap Parallax Functions +// ---------------------------------------------------------------- /** - * Generates flowmap-based normal perturbation for water surface + * Samples height from flowmap texture using the same 4-sample blend as flowmap normals + * This ensures height transitions match the normal transitions exactly * - * @param input Pixel shader input containing texture coordinates and world position - * @param uvShift UV offset for flowmap sampling (used for animation phases) - * @param multiplier Intensity multiplier for the flow effect - * @param offset Base UV offset for the normal texture sampling - * @return float3 Normal perturbation (XY=normal offset, Z=flow strength mask) - * - * @details This function uses flowmap data to: - * - Calculate flow-displaced UV coordinates for normal texture sampling - * - Apply flow-based animation to water normal textures - * - Return both the normal perturbation and flow strength information - * - * @note The returned Z component contains the original flowmap strength value - * which can be used for blending between flow and non-flow normals + * @param input PS_INPUT for flowmap coordinate access + * @param normalMul The blend weights from the flowmap system (same as used for normals) + * @param uvShift The UV shift value (1 / (128 * flowmapDimensions)) + * @param mipLevel Mip level for texture sampling + */ +float GetFlowmapHeightBlended(PS_INPUT input, float2 normalMul, float2 uvShift, float mipLevel) +{ + // Sample height using the EXACT same UV computation as GetFlowmapNormal + // This ensures the height blending matches the normal blending perfectly + + // Sample 0: uvShift, multiplier=9.92, offset=0 + FlowmapData flowData0 = GetFlowmapDataUV(input, uvShift); + float2 uv0 = 0 + (flowData0.flowVector - float2(9.92 * ((0.001 * ReflectionColor.w) * flowData0.color.w), 0)); + float height0 = FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, uv0, mipLevel).w; + + // Sample 1: float2(0, uvShift.y), multiplier=10.64, offset=0.27 + FlowmapData flowData1 = GetFlowmapDataUV(input, float2(0, uvShift.y)); + float2 uv1 = 0.27 + (flowData1.flowVector - float2(10.64 * ((0.001 * ReflectionColor.w) * flowData1.color.w), 0)); + float height1 = FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, uv1, mipLevel).w; + + // Sample 2: 0.0.xx, multiplier=8, offset=0 + FlowmapData flowData2 = GetFlowmapDataUV(input, 0.0.xx); + float2 uv2 = 0 + (flowData2.flowVector - float2(8 * ((0.001 * ReflectionColor.w) * flowData2.color.w), 0)); + float height2 = FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, uv2, mipLevel).w; + + // Sample 3: float2(uvShift.x, 0), multiplier=8.48, offset=0.62 + FlowmapData flowData3 = GetFlowmapDataUV(input, float2(uvShift.x, 0)); + float2 uv3 = 0.62 + (flowData3.flowVector - float2(8.48 * ((0.001 * ReflectionColor.w) * flowData3.color.w), 0)); + float height3 = FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, uv3, mipLevel).w; + + // Use the EXACT same blending formula as flowmap normals + float blendedHeight = + normalMul.y * (normalMul.x * height2 + (1 - normalMul.x) * height3) + + (1 - normalMul.y) * (normalMul.x * height1 + (1 - normalMul.x) * height0); + + return blendedHeight; +} + +// Keep this for compatibility - just forwards to the proper function +float GetFlowmapHeightBarycentric(PS_INPUT input, float2 flowmapDimensions, float2 baseUV, float mipLevel) +{ + // This is now unused - we use GetFlowmapHeightBlended directly + return FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, baseUV, mipLevel).w; +} + +/** + * Computes mip level for flowmap texture sampling + */ +float GetFlowmapMipLevel(float2 flowmapUV) +{ + float2 textureDims; + FlowMapNormalsTex.GetDimensions(textureDims.x, textureDims.y); + +#if defined(VR) + textureDims /= 16.0; +#else + textureDims /= 8.0; +#endif + + float2 texCoordsPerSize = flowmapUV * textureDims; + float2 dxSize = ddx(texCoordsPerSize); + float2 dySize = ddy(texCoordsPerSize); + float2 dTexCoords = dxSize * dxSize + dySize * dySize; + float minTexCoordDelta = max(dTexCoords.x, dTexCoords.y); + return max(0.5 * log2(minTexCoordDelta), 0); +} + +/** + * Samples height from flowmap texture (riverflow.dds alpha channel) + * Uses the same UV calculation as GetFlowmapNormal for consistency + */ + + +/** + * Generates flowmap-based normal (no parallax - flowmap normals are not parallax-shifted) + * Uses mip clamping to preserve detail at distance and prevent over-blurring */ float3 GetFlowmapNormal(PS_INPUT input, float2 uvShift, float multiplier, float offset) { @@ -588,14 +655,34 @@ WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float norma # endif # if defined(FLOWMAP) - float2 normalMul = - 0.5 + -(-0.5 + abs(frac(input.TexCoord2.zw * (64 * input.TexCoord4)) * 2 - 1)); - float uvShift = 1 / (128 * input.TexCoord4); + # if defined(UNIFIED_WATER) + float2 flowmapDimensions = input.TexCoord4.xy; +# else + float2 flowmapDimensions = input.TexCoord4.xx; +# endif + float2 uvShift = 1 / (128 * flowmapDimensions); + + // Compute flowmap parallax and create parallaxed input for normal sampling + PS_INPUT flowmapInput = input; + float2 flowmapParallaxOffset = float2(0, 0); +# if defined(WATER_PARALLAX) && !defined(LOD) + float parallaxAmount = WaterEffects::GetFlowmapParallaxAmount(input, flowmapDimensions, viewDirection); + float2 parallaxDir = viewDirection.xy / -viewDirection.z; + parallaxDir.y = -parallaxDir.y; + float viewDotUp = -viewDirection.z; + parallaxDir *= 0.008 * saturate(viewDotUp * 2.0); + flowmapInput.TexCoord3.xy = input.TexCoord3.xy + parallaxAmount * parallaxDir; + flowmapParallaxOffset = WaterEffects::GetFlowmapParallaxOffset(input, flowmapDimensions, viewDirection, normalScalesRcp); +# endif - float3 flowmapNormal0 = GetFlowmapNormal(input, uvShift.xx, 9.92, 0); - float3 flowmapNormal1 = GetFlowmapNormal(input, float2(0, uvShift), 10.64, 0.27); - float3 flowmapNormal2 = GetFlowmapNormal(input, 0.0.xx, 8, 0); - float3 flowmapNormal3 = GetFlowmapNormal(input, float2(uvShift, 0), 8.48, 0.62); + // Calculate cell blend weights using parallaxed input + float2 normalMul = 0.5 + -(-0.5 + abs(frac(flowmapInput.TexCoord2.zw * (64 * flowmapDimensions)) * 2 - 1)); + + // Sample flowmap normals with parallax applied + float3 flowmapNormal0 = GetFlowmapNormal(flowmapInput, uvShift, 9.92, 0); + float3 flowmapNormal1 = GetFlowmapNormal(flowmapInput, float2(0, uvShift.y), 10.64, 0.27); + float3 flowmapNormal2 = GetFlowmapNormal(flowmapInput, 0.0.xx, 8, 0); + float3 flowmapNormal3 = GetFlowmapNormal(flowmapInput, float2(uvShift.x, 0), 8.48, 0.62); float2 flowmapNormalWeighted = normalMul.y * (normalMul.x * flowmapNormal2.xy + (1 - normalMul.x) * flowmapNormal3.xy) + @@ -608,14 +695,21 @@ WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float norma 0); flowmapNormal.z = sqrt(1 - flowmapNormal.x * flowmapNormal.x - flowmapNormal.y * flowmapNormal.y); -# endif - + float2 baseNormalUv = input.TexCoord1.xy; # if defined(WATER_PARALLAX) - float3 normals1 = Normals01Tex.SampleBias(Normals01Sampler, input.TexCoord1.xy + parallaxOffset.xy * normalScalesRcp.x, SharedData::MipBias).xyz * 2.0 + float3(-1, -1, -2); -# else - float3 normals1 = Normals01Tex.SampleBias(Normals01Sampler, input.TexCoord1.xy, SharedData::MipBias).xyz * 2.0 + float3(-1, -1, -2); + // Use flowmap-derived parallax offset for base normals + baseNormalUv += flowmapParallaxOffset.xy * normalScalesRcp.x; # endif - + float3 normals1 = Normals01Tex.SampleBias(Normals01Sampler, baseNormalUv, SharedData::MipBias).xyz * 2.0 + float3(-1, -1, -2); + # endif // End of FLOWMAP block + + # if !defined(FLOWMAP) + # if defined(WATER_PARALLAX) + float3 normals1 = Normals01Tex.SampleBias(Normals01Sampler, input.TexCoord1.xy + parallaxOffset.xy * normalScalesRcp.x, SharedData::MipBias).xyz * 2.0 + float3(-1, -1, -2); + # else + float3 normals1 = Normals01Tex.SampleBias(Normals01Sampler, input.TexCoord1.xy, SharedData::MipBias).xyz * 2.0 + float3(-1, -1, -2); + # endif + # endif // End of !FLOWMAP block # if defined(FLOWMAP) && !defined(BLEND_NORMALS) # ifdef DISABLE_FLOWMAP_NORMALS // FLOWMAP NORMALS DISABLED: Using only base normals (flow system still active for ripples/splashes) @@ -1173,7 +1267,7 @@ PS_OUTPUT main(PS_INPUT input) # endif # endif - psout.Lighting = saturate(float4(finalColor, isSpecular)); + psout.Lighting = float4(finalColor, isSpecular); # endif # if defined(STENCIL) diff --git a/src/Buffer.h b/src/Buffer.h index dd4e064a79..05e53f43a2 100644 --- a/src/Buffer.h +++ b/src/Buffer.h @@ -8,6 +8,9 @@ #include #include +#define STATIC_ASSERT_ALIGNAS_16(structName) \ + static_assert(sizeof(structName) % 16 == 0, #structName " is not a multiple of 16."); + template D3D11_BUFFER_DESC StructuredBufferDesc(uint64_t count, bool uav = true, bool dynamic = false) { diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 7e62a4eaa3..ae3067fa60 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -99,7 +99,7 @@ void Deferred::SetupResources() // TEMPORAL_AA_WATER_2 // Albedo - SetupRenderTarget(ALBEDO, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R8G8B8A8_UNORM, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); + SetupRenderTarget(ALBEDO, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R10G10B10A2_UNORM, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); // Specular SetupRenderTarget(SPECULAR, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R11G11B10_FLOAT, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); // Reflectance @@ -107,7 +107,7 @@ void Deferred::SetupResources() // Normal + Roughness SetupRenderTarget(NORMALROUGHNESS, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R10G10B10A2_UNORM, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); // Masks - SetupRenderTarget(MASKS, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R8G8B8A8_UNORM, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); + SetupRenderTarget(MASKS, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R11G11B10_FLOAT, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); } { @@ -289,25 +289,6 @@ void Deferred::PrepassPasses() auto context = globals::d3d::context; context->OMSetRenderTargets(0, nullptr, nullptr); // Unbind all bound render targets - globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); // Run OMSetRenderTargets again - - { - ID3D11Buffer* buffers[1] = { *globals::game::perFrame.get() }; - - ID3D11Buffer* vrBuffer = nullptr; - - if (REL::Module::IsVR()) { - static REL::Relocation VRValues{ REL::Offset(0x3180688) }; - vrBuffer = *VRValues.get(); - } - if (vrBuffer) { - context->CSSetConstantBuffers(12, 1, buffers); - context->CSSetConstantBuffers(13, 1, &vrBuffer); - } else { - context->CSSetConstantBuffers(12, 1, buffers); - } - } - globals::truePBR->PrePass(); for (auto* feature : Feature::GetFeatureList()) { if (feature->loaded) { @@ -422,50 +403,8 @@ void Deferred::DeferredPasses() auto [ssgi_ao, ssgi_y, ssgi_cocg, ssgi_gi_spec] = ssgi.GetOutputTextures(); bool ssgi_hq_spec = ssgi.settings.EnableExperimentalSpecularGI; - auto& ibl = globals::features::ibl; - auto dispatchCount = Util::GetScreenDispatchCount(true); - if (ssgi.loaded) { - // Ambient Composite - { - TracyD3D11Zone(globals::state->tracyCtx, "Ambient Composite"); - - ID3D11ShaderResourceView* srvs[9]{ - albedo.SRV, - normalRoughness.SRV, - skylighting.loaded || REL::Module::IsVR() ? depth.depthSRV : nullptr, - skylighting.loaded ? skylighting.texProbeArray->srv.get() : nullptr, - skylighting.loaded ? skylighting.stbn_vec3_2Dx1D_128x128x64.get() : nullptr, - ssgi_ao, - ssgi_y, - ssgi_cocg, - ibl.loaded ? ibl.diffuseIBLTexture->srv.get() : nullptr, - }; - - context->CSSetShaderResources(0, ARRAYSIZE(srvs), srvs); - - ID3D11UnorderedAccessView* uavs[1]{ main.UAV }; - context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); - - auto shader = interior ? GetComputeAmbientCompositeInterior() : GetComputeAmbientComposite(); - context->CSSetShader(shader, nullptr, 0); - - context->Dispatch(dispatchCount.x, dispatchCount.y, 1); - } - - // Clear - { - ID3D11ShaderResourceView* views[9]{ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; - context->CSSetShaderResources(0, ARRAYSIZE(views), views); - - ID3D11UnorderedAccessView* uavs[2]{ nullptr, nullptr }; - context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); - - context->CSSetShader(nullptr, nullptr, 0); - } - } - auto& sss = globals::features::subsurfaceScattering; if (sss.loaded) sss.DrawSSS(); @@ -476,6 +415,8 @@ void Deferred::DeferredPasses() auto& terrainBlending = globals::features::terrainBlending; + auto& ibl = globals::features::ibl; + auto& physSky = globals::features::physicalSky; // Deferred Composite @@ -498,6 +439,7 @@ void Deferred::DeferredPasses() ssgi_hq_spec ? nullptr : ssgi_cocg, ssgi_hq_spec ? ssgi_gi_spec : nullptr, ibl.loaded ? ibl.diffuseIBLTexture->srv.get() : nullptr, + ibl.loaded ? ibl.diffuseSkyIBLTexture->srv.get() : nullptr, physSky.loaded ? physSky.texApLut->srv.get() : nullptr, physSky.loaded ? physSky.texApShadow->srv.get() : nullptr, }; @@ -521,7 +463,7 @@ void Deferred::DeferredPasses() // Clear { - ID3D11ShaderResourceView* views[17]{ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; + ID3D11ShaderResourceView* views[18]{ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; context->CSSetShaderResources(0, ARRAYSIZE(views), views); ID3D11UnorderedAccessView* uavs[3]{ nullptr, nullptr, nullptr }; @@ -663,14 +605,6 @@ void Deferred::ResetBlendStates() void Deferred::ClearShaderCache() { - if (ambientCompositeCS) { - ambientCompositeCS->Release(); - ambientCompositeCS = nullptr; - } - if (ambientCompositeInteriorCS) { - ambientCompositeInteriorCS->Release(); - ambientCompositeInteriorCS = nullptr; - } if (mainCompositeCS) { mainCompositeCS->Release(); mainCompositeCS = nullptr; @@ -681,49 +615,6 @@ void Deferred::ClearShaderCache() } } -ID3D11ComputeShader* Deferred::GetComputeAmbientComposite() -{ - if (!ambientCompositeCS) { - logger::debug("Compiling AmbientCompositeCS"); - - std::vector> defines; - - if (globals::features::skylighting.loaded) - defines.push_back({ "SKYLIGHTING", nullptr }); - - if (globals::features::screenSpaceGI.loaded) - defines.push_back({ "SSGI", nullptr }); - - if (REL::Module::IsVR()) - defines.push_back({ "FRAMEBUFFER", nullptr }); - - if (globals::features::ibl.loaded) - defines.push_back({ "IBL", nullptr }); - - ambientCompositeCS = static_cast(Util::CompileShader(L"Data\\Shaders\\AmbientCompositeCS.hlsl", defines, "cs_5_0")); - } - return ambientCompositeCS; -} - -ID3D11ComputeShader* Deferred::GetComputeAmbientCompositeInterior() -{ - if (!ambientCompositeInteriorCS) { - logger::debug("Compiling AmbientCompositeCS INTERIOR"); - - std::vector> defines; - defines.push_back({ "INTERIOR", nullptr }); - - if (globals::features::screenSpaceGI.loaded) - defines.push_back({ "SSGI", nullptr }); - - if (REL::Module::IsVR()) - defines.push_back({ "FRAMEBUFFER", nullptr }); - - ambientCompositeInteriorCS = static_cast(Util::CompileShader(L"Data\\Shaders\\AmbientCompositeCS.hlsl", defines, "cs_5_0")); - } - return ambientCompositeInteriorCS; -} - ID3D11ComputeShader* Deferred::GetComputeMainComposite() { if (!mainCompositeCS) { @@ -768,6 +659,9 @@ ID3D11ComputeShader* Deferred::GetComputeMainCompositeInterior() if (globals::features::screenSpaceGI.loaded) defines.push_back({ "SSGI", nullptr }); + if (globals::features::ibl.loaded) + defines.push_back({ "IBL", nullptr }); + if (REL::Module::IsVR()) defines.push_back({ "FRAMEBUFFER", nullptr }); diff --git a/src/Deferred.h b/src/Deferred.h index 9f1c01b0f4..8c920f3593 100644 --- a/src/Deferred.h +++ b/src/Deferred.h @@ -1,5 +1,7 @@ #pragma once +#include "Buffer.h" + #define ALBEDO RE::RENDER_TARGETS::kINDIRECT #define SPECULAR RE::RENDER_TARGETS::kINDIRECT_DOWNSCALED #define REFLECTANCE RE::RENDER_TARGETS::kRAWINDIRECT @@ -29,10 +31,8 @@ class Deferred void PrepassPasses(); void ClearShaderCache(); - ID3D11ComputeShader* GetComputeAmbientComposite(); - ID3D11ComputeShader* GetComputeAmbientCompositeInterior(); - ID3D11ComputeShader* GetComputeMainComposite(); + ID3D11ComputeShader* GetComputeMainComposite(); ID3D11ComputeShader* GetComputeMainCompositeInterior(); ID3D11BlendState* deferredBlendStates[7][2][13][2]; @@ -40,9 +40,6 @@ class Deferred RE::RENDER_TARGET forwardRenderTargets[4]; - ID3D11ComputeShader* ambientCompositeCS = nullptr; - ID3D11ComputeShader* ambientCompositeInteriorCS = nullptr; - ID3D11ComputeShader* mainCompositeCS = nullptr; ID3D11ComputeShader* mainCompositeInteriorCS = nullptr; @@ -67,6 +64,7 @@ class Deferred DirectX::XMFLOAT4X3 ShadowMapProj[2][3]; DirectX::XMFLOAT4X3 CameraViewProjInverse[2]; }; + STATIC_ASSERT_ALIGNAS_16(PerGeometry); ID3D11ComputeShader* copyShadowCS = nullptr; Buffer* perShadow = nullptr; diff --git a/src/Feature.cpp b/src/Feature.cpp index ac57299471..f43d5edd0d 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -16,6 +16,7 @@ #include "Features/LightLimitFix.h" #include "Features/PerformanceOverlay.h" #include "Features/PhysicalSky.h" +#include "Features/RenderDoc.h" #include "Features/ScreenSpaceGI.h" #include "Features/ScreenSpaceShadows.h" #include "Features/SkySync.h" @@ -227,6 +228,7 @@ const std::vector& Feature::GetFeatureList() &globals::features::ibl, &globals::features::extendedTranslucency, &globals::features::upscaling, + &globals::features::renderDoc, &globals::features::physicalSky }; diff --git a/src/FeatureIssues.cpp b/src/FeatureIssues.cpp index 45c7de67e4..4c67821e24 100644 --- a/src/FeatureIssues.cpp +++ b/src/FeatureIssues.cpp @@ -1436,7 +1436,7 @@ namespace FeatureIssues auto* menu = Menu::GetSingleton(); const auto& themeSettings = menu->GetTheme(); - if (ImGui::CollapsingHeader("Testing", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + if (ImGui::CollapsingHeader("Testing", ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { { auto sectionWrapper = Util::SectionWrapper("Feature Issue Testing", "These tools create test INI files to trigger all known feature issue types for testing purposes.", @@ -1459,7 +1459,7 @@ namespace FeatureIssues { auto disableGuard = Util::DisableGuard(hasActiveTests); auto buttonStyle = Util::StyledButtonWrapper( - themeSettings.Palette.Border, + themeSettings.Palette.FrameBorder, themeSettings.StatusPalette.RestartNeeded, themeSettings.StatusPalette.CurrentHotkey); @@ -1482,7 +1482,7 @@ namespace FeatureIssues { auto disableGuard = Util::DisableGuard(!hasActiveTests); auto buttonStyle = Util::StyledButtonWrapper( - themeSettings.Palette.Border, + themeSettings.Palette.FrameBorder, themeSettings.StatusPalette.Error, themeSettings.StatusPalette.CurrentHotkey); diff --git a/src/Features/DynamicCubemaps.cpp b/src/Features/DynamicCubemaps.cpp index 9b18ed69c1..eb11fc5e60 100644 --- a/src/Features/DynamicCubemaps.cpp +++ b/src/Features/DynamicCubemaps.cpp @@ -473,6 +473,19 @@ void DynamicCubemaps::Irradiance(bool a_reflections) void DynamicCubemaps::UpdateCubemap() { TracyD3D11Zone(globals::state->tracyCtx, "Cubemap Update"); + + // Reset capture when game time jumps (wait menu, timescale changes, console commands) + if (auto calendar = RE::Calendar::GetSingleton()) { + float currentHoursPassed = calendar->GetHoursPassed(); + float hoursPassedDiff = std::abs(currentHoursPassed - previousHoursPassed); + previousHoursPassed = currentHoursPassed; + + if (hoursPassedDiff >= 0.01f) { // ~36 seconds game time + resetCapture[0] = true; + resetCapture[1] = true; + } + } + if (recompileFlag) { logger::debug("Recompiling for Dynamic Cubemaps"); auto shaderCache = globals::shaderCache; diff --git a/src/Features/DynamicCubemaps.h b/src/Features/DynamicCubemaps.h index 9b7f0e5be6..62ba301037 100644 --- a/src/Features/DynamicCubemaps.h +++ b/src/Features/DynamicCubemaps.h @@ -1,5 +1,7 @@ #pragma once +#include "Buffer.h" + class MenuOpenCloseEventHandler : public RE::BSTEventSink { public: @@ -21,6 +23,7 @@ struct DynamicCubemaps : Feature float roughness; float pad[3]; }; + STATIC_ASSERT_ALIGNAS_16(SpecularMapFilterSettingsCB); ID3D11ComputeShader* specularIrradianceCS = nullptr; ConstantBuffer* spmapCB = nullptr; @@ -36,6 +39,7 @@ struct DynamicCubemaps : Feature float3 CameraPreviousPosAdjust; uint pad0; }; + STATIC_ASSERT_ALIGNAS_16(UpdateCubemapCB); ID3D11ComputeShader* updateCubemapCS = nullptr; ID3D11ComputeShader* updateCubemapReflectionsCS = nullptr; @@ -64,6 +68,7 @@ struct DynamicCubemaps : Feature bool resetCapture[2] = { true, true }; bool recompileFlag = false; + float previousHoursPassed = 0.0f; enum class NextTask { diff --git a/src/Features/ExtendedMaterials.cpp b/src/Features/ExtendedMaterials.cpp index 03c254e4ed..5bab1f88f2 100644 --- a/src/Features/ExtendedMaterials.cpp +++ b/src/Features/ExtendedMaterials.cpp @@ -44,7 +44,7 @@ void ExtendedMaterials::DrawSettings() ImGui::Text("Enables parallax on standard meshes made for parallax."); } - if (ImGui::Checkbox("Enable Terrain", (bool*)&settings.EnableTerrain)) { + if (ImGui::Checkbox("Enable Legacy Terrain", (bool*)&settings.EnableTerrain)) { if (settings.EnableTerrain) { DataLoaded(); } diff --git a/src/Features/ExtendedMaterials.h b/src/Features/ExtendedMaterials.h index 604d234540..2a559912e8 100644 --- a/src/Features/ExtendedMaterials.h +++ b/src/Features/ExtendedMaterials.h @@ -1,5 +1,7 @@ #pragma once +#include "Buffer.h" + struct ExtendedMaterials : Feature { virtual inline std::string GetName() override { return "Extended Materials"; } @@ -36,6 +38,7 @@ struct ExtendedMaterials : Feature float pad[1]; }; + STATIC_ASSERT_ALIGNAS_16(Settings); Settings settings; diff --git a/src/Features/GrassCollision.cpp b/src/Features/GrassCollision.cpp index aa465868af..e5ddc12e93 100644 --- a/src/Features/GrassCollision.cpp +++ b/src/Features/GrassCollision.cpp @@ -1,38 +1,28 @@ #include "GrassCollision.h" -#include "../Utils/ActorUtils.h" + #include "State.h" +#include "Utils/ActorUtils.h" + +static const uint MAX_BOUNDING_BOXES = 64; +static const uint MAX_COLLISIONS_PER_BOUNDING_BOX = 64; +static const uint MAX_COLLISIONS = MAX_BOUNDING_BOXES * MAX_COLLISIONS_PER_BOUNDING_BOX; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( GrassCollision::Settings, EnableGrassCollision, TrackRagdolls) -struct ActorRow -{ - RE::TESObjectREFR* actor; - std::vector row; - float sqDist; -}; - void GrassCollision::DrawSettings() { if (ImGui::TreeNodeEx("Grass Collision", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Checkbox("Enable Grass Collision", (bool*)&settings.EnableGrassCollision); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Allows player collision to modify grass position."); - } - ImGui::Checkbox("Track Ragdolls", &settings.TrackRagdolls); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("If enabled, dead actors (ragdolls) will be tracked."); - } ImGui::TreePop(); } } void GrassCollision::UpdateCollisions(PerFrame& perFrameData) { - actorList.clear(); - std::vector actorDisplayInfos; + eastl::vector actorList{}; // Actor query code from po3 under MIT // https://github.com/powerof3/PapyrusExtenderSSE/blob/7a73b47bc87331bec4e16f5f42f2dbc98b66c3a7/include/Papyrus/Functions/Faction.h#L24C7-L46 @@ -42,9 +32,8 @@ void GrassCollision::UpdateCollisions(PerFrame& perFrameData) for (auto array : actors) { for (auto& actorHandle : *array) { auto actorPtr = actorHandle.get(); - if (actorPtr && actorPtr.get() && actorPtr.get()->Is3DLoaded()) { + if (actorPtr && actorPtr.get()) { actorList.push_back(actorPtr.get()); - totalActorCount++; } } } @@ -53,80 +42,173 @@ void GrassCollision::UpdateCollisions(PerFrame& perFrameData) if (auto player = RE::PlayerCharacter::GetSingleton()) actorList.push_back(player); - RE::NiPoint3 cameraPosition = Util::GetAverageEyePosition(); + RE::NiPoint3 cameraPosition = Util::GetEyePosition(0); - for (const auto actor : actorList) { - Util::ActorDisplayInfo info; - if (!Util::GetActorDisplayInfo(actor, cameraPosition, settings.TrackRagdolls, info)) - continue; - actorDisplayInfos.push_back(info); - } + // Sort actors by distance to eye, closest first + std::sort(actorList.begin(), actorList.end(), [&cameraPosition](RE::Actor* a, RE::Actor* b) { + float distA = cameraPosition.GetSquaredDistance(a->GetPosition()); + float distB = cameraPosition.GetSquaredDistance(b->GetPosition()); + return distA < distB; + }); + + eastl::vector boundingBoxData{}; + boundingBoxData.reserve(MAX_BOUNDING_BOXES); + + eastl::vector collisionsData{}; + collisionsData.reserve(MAX_COLLISIONS); - for (const auto& info : actorDisplayInfos) { - if (currentCollisionCount == 256) - break; - auto actor = static_cast(info.actor); + uint collisionIndexExtent = 0; + + for (const auto actor : actorList) { if (actor && actor->Is3DLoaded()) { auto root = actor->Get3D(false); if (!root) continue; - float distance = cameraPosition.GetDistance(info.pos); + + float distance = cameraPosition.GetDistance(actor->GetPosition()); if (distance > 2048.0f) continue; - activeActorCount++; + + eastl::vector collisionShapes{}; + RE::BSVisit::TraverseScenegraphCollision(root, [&](RE::bhkNiCollisionObject* a_object) -> RE::BSVisit::BSVisitControl { RE::NiPoint3 centerPos; float radius; if (Util::GetShapeBound(a_object, centerPos, radius)) { - if (radius < distance * 0.01f) + // Cull extremely small collisions + if (radius < distance * 0.001f) return RE::BSVisit::BSVisitControl::kContinue; - radius *= 2.0f; - CollisionData data{}; - RE::NiPoint3 eyePosition{}; - for (int eyeIndex = 0; eyeIndex < eyeCount; eyeIndex++) { - eyePosition = Util::GetEyePosition(eyeIndex); - data.centre[eyeIndex].x = centerPos.x - eyePosition.x; - data.centre[eyeIndex].y = centerPos.y - eyePosition.y; - data.centre[eyeIndex].z = centerPos.z - eyePosition.z; - } - data.centre[0].w = radius; - perFrameData.collisionData[currentCollisionCount] = data; - currentCollisionCount++; - if (currentCollisionCount == 256) - return RE::BSVisit::BSVisitControl::kStop; + + centerPos -= cameraPosition; + + float4 data{}; + data.x = centerPos.x; + data.y = centerPos.y; + data.z = centerPos.z; + data.w = radius; + + collisionShapes.push_back(data); } return RE::BSVisit::BSVisitControl::kContinue; }); + + std::sort(collisionShapes.begin(), collisionShapes.end(), [](const float4& a, const float4& b) { + return a.w > b.w; + }); + + BoundingBoxPacked boundingBox; + + boundingBox.IndexStart = collisionIndexExtent; + boundingBox.IndexEnd = collisionIndexExtent; + + uint boundingBoxCollisions = 0; + + for (const auto& data : collisionShapes) { + collisionsData.push_back(data); + + float2 pointMin(data.x - data.w, data.y - data.w); + float2 pointMax(data.x + data.w, data.y + data.w); + + boundingBox.MinExtent.x = std::min(boundingBox.MinExtent.x, pointMin.x); + boundingBox.MinExtent.y = std::min(boundingBox.MinExtent.y, pointMin.y); + + boundingBox.MaxExtent.x = std::max(boundingBox.MaxExtent.x, pointMax.x); + boundingBox.MaxExtent.y = std::max(boundingBox.MaxExtent.y, pointMax.y); + + boundingBox.IndexEnd++; + + boundingBoxCollisions++; + + if (boundingBoxCollisions == MAX_COLLISIONS_PER_BOUNDING_BOX) + break; + } + + if (boundingBox.IndexStart != boundingBox.IndexEnd) + boundingBoxData.push_back(boundingBox); + + collisionIndexExtent = boundingBox.IndexEnd; } } - perFrameData.numCollisions = currentCollisionCount; + + perFrameData.BoundingBoxCount = std::min((uint)boundingBoxData.size(), MAX_BOUNDING_BOXES); + + auto context = globals::d3d::context; + + if (collisionIndexExtent > 0) { + D3D11_MAPPED_SUBRESOURCE mapped; + DX::ThrowIfFailed(context->Map(collisionInstances->resource.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); + size_t bytes = sizeof(float4) * collisionIndexExtent; + memcpy_s(mapped.pData, bytes, collisionsData.data(), bytes); + context->Unmap(collisionInstances->resource.get(), 0); + } + + if (perFrameData.BoundingBoxCount > 0) { + D3D11_MAPPED_SUBRESOURCE mapped; + DX::ThrowIfFailed(context->Map(collisionBoundingBoxes->resource.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); + size_t bytes = sizeof(BoundingBoxPacked) * perFrameData.BoundingBoxCount; + memcpy_s(mapped.pData, bytes, boundingBoxData.data(), bytes); + context->Unmap(collisionBoundingBoxes->resource.get(), 0); + } } void GrassCollision::Update() { - if (updatePerFrame) { + static Util::FrameChecker frameChecker; + if (frameChecker.IsNewFrame()) { PerFrame perFrameData{}; - perFrameData.numCollisions = 0; - currentCollisionCount = 0; - totalActorCount = 0; - activeActorCount = 0; + perFrameData.BoundingBoxCount = 0; + + static float2 prevCellID = { 0, 0 }; + + auto eyePosNI = Util::GetEyePosition(0); + static auto prevEyePosNI = eyePosNI; + + auto eyePos = float2{ eyePosNI.x, eyePosNI.y }; + + float worldSize = 4096.0f; + uint textureArrayDims = 512; + + float cellSize = worldSize / textureArrayDims; + + auto cellID = eyePos / cellSize; + cellID = { round(cellID.x), round(cellID.y) }; + auto cellOrigin = cellID * cellSize; + + float2 cellIDDiff = prevCellID - cellID; + prevCellID = cellID; + + perFrameData.PosOffset = cellOrigin - eyePos; + + perFrameData.ArrayOrigin = { + ((int)cellID.x - textureArrayDims / 2) % textureArrayDims, + ((int)cellID.y - textureArrayDims / 2) % textureArrayDims + }; + + perFrameData.ValidMargin = { (int)cellIDDiff.x, (int)cellIDDiff.y }; + + perFrameData.TimeDelta = *globals::game::deltaTime * !globals::game::ui->GameIsPaused(); + + perFrameData.CameraHeightDelta = prevEyePosNI.z - eyePosNI.z; if (settings.EnableGrassCollision) UpdateCollisions(perFrameData); perFrame->Update(perFrameData); - updatePerFrame = false; - } + UpdateCollisionTexture(); - auto context = globals::d3d::context; + prevCellID = cellID; + prevEyePosNI = eyePosNI; + + auto context = globals::d3d::context; - static Util::FrameChecker frameChecker; - if (frameChecker.IsNewFrame()) { ID3D11Buffer* buffers[1]; buffers[0] = perFrame->CB(); context->VSSetConstantBuffers(5, ARRAYSIZE(buffers), buffers); + + ID3D11ShaderResourceView* srvs[] = { collisionTexture->srv.get() }; + context->VSSetShaderResources(100, ARRAYSIZE(srvs), srvs); } } @@ -153,11 +235,73 @@ void GrassCollision::PostPostLoad() void GrassCollision::SetupResources() { perFrame = new ConstantBuffer(ConstantBufferDesc()); -} -void GrassCollision::Reset() -{ - updatePerFrame = true; + { + D3D11_TEXTURE2D_DESC texDesc = { + .Width = 512, + .Height = 512, + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_R16G16B16A16_UNORM, + .SampleDesc = { .Count = 1 }, + .Usage = D3D11_USAGE_DEFAULT, + .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS + }; + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { + .MostDetailedMip = 0, + .MipLevels = 1 } + }; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + collisionTexture = new Texture2D(texDesc); + collisionTexture->CreateSRV(srvDesc); + collisionTexture->CreateUAV(uavDesc); + } + + { + D3D11_BUFFER_DESC sbDesc{}; + sbDesc.Usage = D3D11_USAGE_DYNAMIC; + sbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + sbDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + sbDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; + sbDesc.StructureByteStride = sizeof(BoundingBoxPacked); + sbDesc.ByteWidth = sizeof(BoundingBoxPacked) * MAX_BOUNDING_BOXES; + collisionBoundingBoxes = eastl::make_unique(sbDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc; + srvDesc.Format = DXGI_FORMAT_UNKNOWN; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER; + srvDesc.Buffer.FirstElement = 0; + srvDesc.Buffer.NumElements = MAX_BOUNDING_BOXES; + collisionBoundingBoxes->CreateSRV(srvDesc); + } + + { + D3D11_BUFFER_DESC sbDesc{}; + sbDesc.Usage = D3D11_USAGE_DYNAMIC; + sbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + sbDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + sbDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; + sbDesc.StructureByteStride = sizeof(float4); + sbDesc.ByteWidth = sizeof(float4) * MAX_COLLISIONS; + collisionInstances = eastl::make_unique(sbDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc; + srvDesc.Format = DXGI_FORMAT_UNKNOWN; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER; + srvDesc.Buffer.FirstElement = 0; + srvDesc.Buffer.NumElements = MAX_COLLISIONS; + collisionInstances->CreateSRV(srvDesc); + } } bool GrassCollision::HasShaderDefine(RE::BSShader::Type shaderType) @@ -175,3 +319,72 @@ void GrassCollision::Hooks::BSGrassShader_SetupGeometry::thunk(RE::BSShader* Thi globals::features::grassCollision.Update(); func(This, Pass, RenderFlags); } + +void GrassCollision::ClearShaderCache() +{ + if (collisionUpdateCS) + collisionUpdateCS->Release(); + collisionUpdateCS = nullptr; +} + +ID3D11ComputeShader* GrassCollision::GetCollisionUpdateCS() +{ + if (!collisionUpdateCS) { + logger::debug("Compiling CollisionUpdateCS"); + collisionUpdateCS = static_cast(Util::CompileShader(L"Data\\Shaders\\GrassCollision\\CollisionUpdateCS.hlsl", {}, "cs_5_0")); + } + return collisionUpdateCS; +} + +void GrassCollision::UpdateCollisionTexture() +{ + auto context = globals::d3d::context; + + if (!settings.EnableGrassCollision) { + float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + context->ClearUnorderedAccessViewFloat(collisionTexture->uav.get(), clearColor); + return; + } + + { + ID3D11Buffer* buffers[1] = { *globals::game::perFrame }; + ID3D11Buffer* vrBuffer = nullptr; + + if (REL::Module::IsVR()) { + static REL::Relocation VRValues{ REL::Offset(0x3180688) }; + vrBuffer = *VRValues.get(); + } + if (vrBuffer) { + context->CSSetConstantBuffers(12, 1, buffers); + context->CSSetConstantBuffers(13, 1, &vrBuffer); + } else { + context->CSSetConstantBuffers(12, 1, buffers); + } + } + + { + ID3D11Buffer* buffers[1] = { perFrame->CB() }; + context->CSSetConstantBuffers(0, 1, buffers); + + ID3D11ShaderResourceView* srvs[] = { + collisionBoundingBoxes->srv.get(), + collisionInstances->srv.get(), + }; + + context->CSSetShaderResources(0, ARRAYSIZE(srvs), srvs); + + ID3D11UnorderedAccessView* uavs[] = { collisionTexture->uav.get() }; + context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); + + context->CSSetShader(GetCollisionUpdateCS(), nullptr, 0); + context->Dispatch(512 / 8, 512 / 8, 1); + } + + context->CSSetShader(nullptr, nullptr, 0); + + ID3D11Buffer* null_buffer = nullptr; + context->CSSetConstantBuffers(0, 1, &null_buffer); + + ID3D11UnorderedAccessView* null_uavs[1] = { nullptr }; + context->CSSetUnorderedAccessViews(0, 1, null_uavs, nullptr); +} diff --git a/src/Features/GrassCollision.h b/src/Features/GrassCollision.h index 1d671431c0..2ec9e612c5 100644 --- a/src/Features/GrassCollision.h +++ b/src/Features/GrassCollision.h @@ -1,5 +1,7 @@ #pragma once +#include "Buffer.h" + struct GrassCollision : Feature { private: @@ -26,38 +28,54 @@ struct GrassCollision : Feature bool HasShaderDefine(RE::BSShader::Type shaderType) override; + void UpdateCollisionTexture(); + struct Settings { bool EnableGrassCollision = 1; bool TrackRagdolls = 1; + bool EnableBlur = 1; }; - struct alignas(16) CollisionData + struct alignas(16) BoundingBoxPacked { - float4 centre[2]; + float2 MinExtent = { 0, 0 }; + float2 MaxExtent = { 0, 0 }; + uint IndexStart = 0; + uint IndexEnd = 0; + float2 pad0; }; + STATIC_ASSERT_ALIGNAS_16(BoundingBoxPacked); struct alignas(16) PerFrame { - CollisionData collisionData[256]; - uint numCollisions; - uint pad0[3]; - }; + float2 PosOffset; // cell origin in camera model space + DirectX::XMUINT2 ArrayOrigin; // xy: array origin (clipmap wrapping) - std::uint32_t totalActorCount = 0; - std::uint32_t activeActorCount = 0; - std::uint32_t currentCollisionCount = 0; - std::vector actorList{}; - std::uint32_t colllisionCount = 0; + DirectX::XMINT2 ValidMargin; + float TimeDelta; + uint BoundingBoxCount; + + float CameraHeightDelta; + float3 pad0; + }; + STATIC_ASSERT_ALIGNAS_16(PerFrame); Settings settings; - bool updatePerFrame = false; ConstantBuffer* perFrame = nullptr; - int eyeCount = !REL::Module::IsVR() ? 1 : 2; + + eastl::unique_ptr collisionBoundingBoxes = nullptr; + eastl::unique_ptr collisionInstances = nullptr; + + virtual void ClearShaderCache() override; + + ID3D11ComputeShader* GetCollisionUpdateCS(); + ID3D11ComputeShader* collisionUpdateCS; + + Texture2D* collisionTexture = nullptr; virtual void SetupResources() override; - virtual void Reset() override; virtual void DrawSettings() override; void UpdateCollisions(PerFrame& perFrame); diff --git a/src/Features/GrassLighting.h b/src/Features/GrassLighting.h index 152a6c6155..461703b531 100644 --- a/src/Features/GrassLighting.h +++ b/src/Features/GrassLighting.h @@ -1,5 +1,7 @@ #pragma once +#include "Buffer.h" + struct GrassLighting : Feature { private: @@ -35,6 +37,7 @@ struct GrassLighting : Feature float BasicGrassBrightness = 1.0f; uint pad[3]; }; + STATIC_ASSERT_ALIGNAS_16(Settings); Settings settings; diff --git a/src/Features/HairSpecular.h b/src/Features/HairSpecular.h index ac8666dd13..3fa3d2a798 100644 --- a/src/Features/HairSpecular.h +++ b/src/Features/HairSpecular.h @@ -40,13 +40,13 @@ struct HairSpecular : Feature float HairSaturation = 1.0f; float SpecularIndirectMult = 1.0f; float DiffuseIndirectMult = 1.0f; - float BaseColorMult = 1.0f; + float BaseColorMult = 1.5f; float Transmission = 1.0f; uint EnableSelfShadow = true; float SelfShadowStrength = 1.0f; float SelfShadowExponent = 0.1f; float SelfShadowScale = 2.5f; - uint HairMode = 0; // 0: Kajiya-Kay, 1: Marschner + uint HairMode = 1; // 0: Kajiya-Kay, 1: Marschner uint pad[3]; } settings; diff --git a/src/Features/IBL.cpp b/src/Features/IBL.cpp index e8422d5f88..9d090d9e27 100644 --- a/src/Features/IBL.cpp +++ b/src/Features/IBL.cpp @@ -13,11 +13,11 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( EnableDiffuseIBL, PreserveFogLuminance, UseStaticIBL, + EnableInterior, DiffuseIBLScale, DALCAmount, IBLSaturation, - FogAmount, - DynamicCubemapsAmount) + FogAmount) void IBL::DrawSettings() { @@ -25,19 +25,13 @@ void IBL::DrawSettings() ImGui::SliderFloat("Diffuse IBL Scale", &settings.DiffuseIBLScale, 0.0f, 10.0f, "%.2f"); ImGui::SliderFloat("Diffuse IBL Saturation", &settings.IBLSaturation, 0.0f, 2.0f, "%.2f"); ImGui::SliderFloat("DALC Amount", &settings.DALCAmount, 0.0f, 1.0f, "%.2f"); + ImGui::Checkbox("Enable Interior", (bool*)&settings.EnableInterior); ImGui::Checkbox("Use Static IBL For Out-of-World Objects", (bool*)&settings.UseStaticIBL); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Enables the use of static IBL textures for objects that are not in the world (e.g. inventory items)."); } ImGui::SliderFloat("Fog Mix", &settings.FogAmount, 0.0f, 1.0f, "%.2f"); ImGui::Checkbox("Preserve Fog Luminance", (bool*)&settings.PreserveFogLuminance); - ImGui::SliderFloat("Dynamic Cubemaps Amount", &settings.DynamicCubemapsAmount, 0.0f, 1.0f, "%.2f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Samples from dynamic cubemaps.\n" - "Requires the Dynamic Cubemaps feature to be enabled.\n" - "May cause dynamic cubemaps sampling accumulation issues under certain conditions."); - } } void IBL::LoadSettings(json& o_json) @@ -62,12 +56,13 @@ void IBL::EarlyPrepass() // Set PS shader resource { - std::array srvs = { + std::array srvs = { diffuseIBLTexture->srv.get(), + diffuseSkyIBLTexture->srv.get(), staticDiffuseIBLTexture->srv.get(), staticSpecularIBLTexture->srv.get() }; - context->PSSetShaderResources(76, 3, srvs.data()); + context->PSSetShaderResources(76, 4, srvs.data()); } } } @@ -75,29 +70,24 @@ void IBL::EarlyPrepass() void IBL::Prepass() { auto context = globals::d3d::context; - - auto renderer = globals::game::renderer; - auto& reflections = renderer->GetRendererData().cubemapRenderTargets[RE::RENDER_TARGET_CUBEMAP::kREFLECTIONS]; + auto state = globals::state; auto& dynamicCubemaps = globals::features::dynamicCubemaps; auto& envTexture = dynamicCubemaps.envTexture; auto& envReflectionsTexture = dynamicCubemaps.envReflectionsTexture; - std::array srvs = { - reflections.SRV, - (dynamicCubemaps.loaded && envTexture) ? envTexture->srv.get() : nullptr, - (dynamicCubemaps.loaded && envReflectionsTexture) ? envReflectionsTexture->srv.get() : nullptr - }; - std::array uavs = { diffuseIBLTexture->uav.get() }; - std::array samplers = { Deferred::GetSingleton()->linearSampler }; - // Unset PS shader resource { - ID3D11ShaderResourceView* srv = nullptr; - context->PSSetShaderResources(76, 1, &srv); + ID3D11ShaderResourceView* views[2]{ nullptr, nullptr }; + context->PSSetShaderResources(76, 2, views); } + state->BeginPerfEvent("IBL"); + std::array srvs = { (dynamicCubemaps.loaded && envTexture) ? envTexture->srv.get() : nullptr }; + std::array uavs = { diffuseIBLTexture->uav.get() }; + std::array samplers = { Deferred::GetSingleton()->linearSampler }; + // IBL { samplers[0] = Deferred::GetSingleton()->linearSampler; @@ -109,6 +99,16 @@ void IBL::Prepass() context->Dispatch(1, 1, 1); } + // IBL with sky + { + srvs.at(0) = (dynamicCubemaps.loaded && envReflectionsTexture) ? envReflectionsTexture->srv.get() : nullptr; + uavs.at(0) = diffuseSkyIBLTexture->uav.get(); + + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->Dispatch(1, 1, 1); + } + // Reset { srvs.fill(nullptr); @@ -120,11 +120,12 @@ void IBL::Prepass() context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(nullptr, nullptr, 0); } + state->EndPerfEvent(); // Set PS shader resource { - ID3D11ShaderResourceView* srv = diffuseIBLTexture->srv.get(); - context->PSSetShaderResources(76, 1, &srv); + ID3D11ShaderResourceView* views[2]{ diffuseIBLTexture->srv.get(), diffuseSkyIBLTexture->srv.get() }; + context->PSSetShaderResources(76, 2, views); } } @@ -161,6 +162,9 @@ void IBL::SetupResources() diffuseIBLTexture = new Texture2D(texDesc); diffuseIBLTexture->CreateSRV(srvDesc); diffuseIBLTexture->CreateUAV(uavDesc); + diffuseSkyIBLTexture = new Texture2D(texDesc); + diffuseSkyIBLTexture->CreateSRV(srvDesc); + diffuseSkyIBLTexture->CreateUAV(uavDesc); } auto device = globals::d3d::device; diff --git a/src/Features/IBL.h b/src/Features/IBL.h index 5b78d50785..7e19f33717 100644 --- a/src/Features/IBL.h +++ b/src/Features/IBL.h @@ -25,6 +25,7 @@ struct IBL : Feature bool HasShaderDefine(RE::BSShader::Type) override { return true; }; Texture2D* diffuseIBLTexture = nullptr; + Texture2D* diffuseSkyIBLTexture = nullptr; ID3D11ComputeShader* diffuseIBLCS = nullptr; virtual void RestoreDefaultSettings() override; @@ -38,16 +39,16 @@ struct IBL : Feature virtual void SetupResources() override; virtual void ClearShaderCache() override; - struct alignas(16) Settings + struct Settings { uint EnableDiffuseIBL = 1; uint PreserveFogLuminance = 0; uint UseStaticIBL = 1; + uint EnableInterior = 0; float DiffuseIBLScale = 1.0f; float DALCAmount = 0.33f; float IBLSaturation = 1.0f; float FogAmount = 0.0f; - float DynamicCubemapsAmount = 0.0f; } settings; eastl::unique_ptr staticDiffuseIBLTexture = nullptr; diff --git a/src/Features/InteriorSun.cpp b/src/Features/InteriorSun.cpp index 8114da4794..691ec8b00e 100644 --- a/src/Features/InteriorSun.cpp +++ b/src/Features/InteriorSun.cpp @@ -18,7 +18,8 @@ void InteriorSun::DrawSettings() } if (ImGui::SliderFloat("Interior Shadow Distance", &settings.InteriorShadowDistance, 1000.0f, 8000.0f)) { *gInteriorShadowDistance = settings.InteriorShadowDistance; - SetShadowDistance(globals::game::tes && globals::game::tes->interiorCell); + auto tes = RE::TES::GetSingleton(); + SetShadowDistance(tes && tes->interiorCell); } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( @@ -44,6 +45,8 @@ void InteriorSun::RestoreDefaultSettings() void InteriorSun::PostPostLoad() { + stl::write_thunk_call(REL::RelocationID(100852, 107642).address() + REL::Relocate(0x29E, 0x28F)); + // Hooks and patch to enable directional lighting for interiors stl::write_thunk_call(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x399, 0x37D, 0x639)); stl::write_thunk_call(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x3AE, 0x392, 0x64E)); @@ -72,7 +75,7 @@ void InteriorSun::PostPostLoad() void InteriorSun::EarlyPrepass() { - isInteriorWithSun = IsInteriorWithSun(globals::game::tes->interiorCell); + isInteriorWithSun = IsInteriorWithSun(RE::TES::GetSingleton()->interiorCell); } inline bool InteriorSun::IsInteriorWithSun(const RE::TESObjectCELL* cell) @@ -102,7 +105,7 @@ RE::TESWorldSpace* InteriorSun::disableInteriorSun = [] { void InteriorSun::DirShadowLightCulling::thunk(RE::BSShadowDirectionalLight* dirLight, RE::BSTArray>>& jobArrays, RE::BSTArray>& nodes) { auto& singleton = globals::features::interiorSun; - const auto cell = globals::game::tes->interiorCell; + const auto cell = RE::TES::GetSingleton()->interiorCell; auto* passedJobArrays = &jobArrays; if (cell && singleton.isInteriorWithSun) { @@ -122,6 +125,12 @@ void InteriorSun::DirShadowLightCulling::thunk(RE::BSShadowDirectionalLight* dir func(dirLight, *passedJobArrays, nodes); } +void InteriorSun::BSBatchRenderer_RenderPassImmediately::thunk(RE::BSRenderPass* a_pass, uint32_t a_technique, bool a_alphaTest, uint32_t a_renderFlags) +{ + globals::features::interiorSun.UpdateRasterStateCullMode(a_pass, a_technique); + func(a_pass, a_technique, a_alphaTest, a_renderFlags); +} + void InteriorSun::ClearArrays() { currentCellRoomsAndPortals.clear(); diff --git a/src/Features/InteriorSun.h b/src/Features/InteriorSun.h index fb74e3473a..353c962da9 100644 --- a/src/Features/InteriorSun.h +++ b/src/Features/InteriorSun.h @@ -50,6 +50,12 @@ struct InteriorSun : Feature static inline REL::Relocation func; }; + struct BSBatchRenderer_RenderPassImmediately + { + static void thunk(RE::BSRenderPass* a_pass, uint32_t a_technique, bool a_alphaTest, uint32_t a_renderFlags); + static inline REL::Relocation func; + }; + void UpdateRasterStateCullMode(const RE::BSRenderPass* pass, const uint32_t technique) const { if (isInteriorWithSun && settings.ForceDoubleSidedRendering && technique & static_cast(SIE::ShaderCache::UtilityShaderFlags::RenderShadowmap)) { diff --git a/src/Features/LODBlending.cpp b/src/Features/LODBlending.cpp index 62d73c5987..c1e0e5b91b 100644 --- a/src/Features/LODBlending.cpp +++ b/src/Features/LODBlending.cpp @@ -5,13 +5,19 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( LODTerrainBrightness, LODObjectBrightness, LODObjectSnowBrightness, - DisableTerrainVertexColors) + DisableTerrainVertexColors, + LODTerrainGamma, + LODObjectGamma, + LODObjectSnowGamma) void LODBlending::DrawSettings() { - ImGui::SliderFloat("LOD Terrain Brightness", &settings.LODTerrainBrightness, 0.01f, 2.f, "%.2f"); - ImGui::SliderFloat("LOD Object Brightness", &settings.LODObjectBrightness, 0.01f, 2.f, "%.2f"); - ImGui::SliderFloat("LOD Object Snow Brightness", &settings.LODObjectSnowBrightness, 0.01f, 2.f, "%.2f"); + ImGui::SliderFloat("LOD Terrain Brightness", &settings.LODTerrainBrightness, 0.01f, 5.f, "%.2f"); + ImGui::SliderFloat("LOD Object Brightness", &settings.LODObjectBrightness, 0.01f, 5.f, "%.2f"); + ImGui::SliderFloat("LOD Object Snow Brightness", &settings.LODObjectSnowBrightness, 0.01f, 5.f, "%.2f"); + ImGui::SliderFloat("LOD Terrain Gamma", &settings.LODTerrainGamma, 0.1f, 3.f, "%.2f"); + ImGui::SliderFloat("LOD Object Gamma", &settings.LODObjectGamma, 0.1f, 3.f, "%.2f"); + ImGui::SliderFloat("LOD Object Snow Gamma", &settings.LODObjectSnowGamma, 0.1f, 3.f, "%.2f"); ImGui::Checkbox("Disable Terrain Vertex Colors", (bool*)&settings.DisableTerrainVertexColors); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( diff --git a/src/Features/LODBlending.h b/src/Features/LODBlending.h index dac24b1cc2..6fe1129797 100644 --- a/src/Features/LODBlending.h +++ b/src/Features/LODBlending.h @@ -25,6 +25,10 @@ struct LODBlending : Feature float LODObjectBrightness = 1; float LODObjectSnowBrightness = 1; uint DisableTerrainVertexColors = false; + float LODTerrainGamma = 1; + float LODObjectGamma = 1; + float LODObjectSnowGamma = 1; + float pad; }; Settings settings; diff --git a/src/Features/LightLimitFIx/ParticleLights.cpp b/src/Features/LightLimitFIx/ParticleLights.cpp deleted file mode 100644 index fdd810acb6..0000000000 --- a/src/Features/LightLimitFIx/ParticleLights.cpp +++ /dev/null @@ -1,136 +0,0 @@ -#include "Features/LightLimitFix/ParticleLights.h" - -#include - -void ParticleLights::GetConfigs() -{ - if (std::filesystem::exists("Data\\ParticleLights")) { - logger::info("[LLF] Loading particle lights configs"); - - auto configs = clib_util::distribution::get_configs("Data\\ParticleLights", "", ".ini"); - - if (configs.empty()) { - logger::warn("[LLF] No .ini files were found within the Data\\ParticleLights folder, aborting..."); - return; - } - - logger::info("[LLF] {} matching inis found", configs.size()); - - for (auto& path : configs) { - logger::info("[LLF] loading ini : {}", path); - - CSimpleIniA ini; - ini.SetUnicode(); - ini.SetMultiKey(); - - if (const auto rc = ini.LoadFile(path.c_str()); rc < 0) { - logger::error("\t\t[LLF] couldn't read INI"); - continue; - } - - Config data{}; - particleLightConfigs.insert({ "default", data }); - - data.cull = ini.GetBoolValue("Light", "Cull", false); - data.colorMult.red = (float)ini.GetDoubleValue("Light", "ColorMultRed", 1.0); - data.colorMult.green = (float)ini.GetDoubleValue("Light", "ColorMultGreen", 1.0); - data.colorMult.blue = (float)ini.GetDoubleValue("Light", "ColorMultBlue", 1.0); - data.radiusMult = (float)ini.GetDoubleValue("Light", "RadiusMult", 1.0); - data.saturationMult = (float)ini.GetDoubleValue("Light", "SaturationMult", 1.0); - - auto lastSeparatorPos = path.find_last_of("\\/"); - if (lastSeparatorPos != std::string::npos) { - std::string filename = path.substr(lastSeparatorPos + 1); - if (filename.size() < 4) { - logger::error("[LLF] Path too short"); - continue; - } - - filename.erase(filename.length() - 4); // Remove ".ini" - std::transform(filename.begin(), filename.end(), filename.begin(), [](auto c) { return (char)::tolower(c); }); - - logger::debug("[LLF] Inserting {}", filename); - - particleLightConfigs.insert({ filename, data }); - } else { - logger::error("[LLF] Path incomplete"); - } - } - } - - if (std::filesystem::exists("Data\\ParticleLights\\Gradients")) { - logger::info("[LLF] Loading particle lights gradients configs"); - - auto configs = clib_util::distribution::get_configs("Data\\ParticleLights\\Gradients", "", ".ini"); - - if (configs.empty()) { - logger::warn("[LLF] No .ini files were found within the Data\\ParticleLights\\Gradients folder, aborting..."); - return; - } - - logger::info("[LLF] {} matching inis found", configs.size()); - - for (auto& path : configs) { - logger::info("[LLF] loading ini : {}", path); - - CSimpleIniA ini; - ini.SetUnicode(); - ini.SetMultiKey(); - - if (const auto rc = ini.LoadFile(path.c_str()); rc < 0) { - logger::error("\t\t[LLF] couldn't read INI"); - continue; - } - - GradientConfig data{}; - const char* value = nullptr; - constexpr std::string_view prefix1 = "0x"; - constexpr std::string_view prefix2 = "#"; - constexpr std::string_view cset = "0123456789ABCDEFabcdef"; - - value = ini.GetValue("Gradient", "Color"); - if (value && strcmp(value, "") != 0) { - std::string_view str = value; - - if (str.starts_with(prefix1)) { - str.remove_prefix(prefix1.size()); - } - - if (str.starts_with(prefix2)) { - str.remove_prefix(prefix2.size()); - } - - bool matches = std::strspn(str.data(), cset.data()) == str.size(); - - if (matches) { - uint32_t color = std::stoi(str.data(), 0, 16); - data.color = color; - } else { - logger::error("[LLF] invalid color"); - continue; - } - } else { - logger::error("[LLF] missing color"); - continue; - } - - auto lastSeparatorPos = path.find_last_of("\\/"); - if (lastSeparatorPos != std::string::npos) { - std::string filename = path.substr(lastSeparatorPos + 1); - if (filename.size() < 4) { - logger::error("[LLF] Path too short"); - continue; - } - - filename.erase(filename.length() - 4); // Remove ".ini" - std::transform(filename.begin(), filename.end(), filename.begin(), [](auto c) { return (char)::tolower(c); }); - - logger::debug("[LLF] Inserting {}", filename); - - particleLightGradientConfigs.insert({ filename, data }); - } else { - logger::error("[LLF] Path incomplete"); - } - } - } -} diff --git a/src/Features/LightLimitFIx/ParticleLights.h b/src/Features/LightLimitFIx/ParticleLights.h deleted file mode 100644 index 2b33785855..0000000000 --- a/src/Features/LightLimitFIx/ParticleLights.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -class ParticleLights -{ -public: - struct Config - { - bool cull = false; - RE::NiColor colorMult{ 1.0f, 1.0f, 1.0f }; - float radiusMult = 1.0f; - float saturationMult = 1.0f; - }; - - struct GradientConfig - { - RE::NiColor color; - }; - - ankerl::unordered_dense::map particleLightConfigs; - ankerl::unordered_dense::map particleLightGradientConfigs; - - void GetConfigs(); -}; \ No newline at end of file diff --git a/src/Features/LightLimitFix.cpp b/src/Features/LightLimitFix.cpp index 135105265d..23c7f083aa 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -4,65 +4,28 @@ #include "Shadercache.h" #include "State.h" -static constexpr uint CLUSTER_MAX_LIGHTS = 256; +static constexpr uint CLUSTER_MAX_LIGHTS = 128; static constexpr uint MAX_LIGHTS = 1024; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( LightLimitFix::Settings, - EnableParticleLights, - EnableParticleLightsCulling, - EnableParticleLightsDetection, - ParticleLightsSaturation, - EnableParticleLightsOptimization, - ParticleBrightness, - ParticleRadius, - BillboardBrightness, - BillboardRadius) + EnableContactShadows, + LightsVisualisationMode) void LightLimitFix::DrawSettings() { - if (ImGui::TreeNodeEx("Particle Lights", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Enable Particle Lights", &settings.EnableParticleLights); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enables Particle Lights."); - } - - ImGui::Checkbox("Enable Culling", &settings.EnableParticleLightsCulling); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Significantly improves performance by not rendering empty textures. Only disable if you are encountering issues."); - } - - ImGui::Checkbox("Enable Detection", &settings.EnableParticleLightsDetection); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Adds particle lights to the player light level, so that NPCs can detect them for stealth and gameplay."); - } - - ImGui::Checkbox("Enable Optimization", &settings.EnableParticleLightsOptimization); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Merges vertices which are close enough to each other to improve performance."); - } - - ImGui::Spacing(); - ImGui::Spacing(); + auto shaderCache = globals::shaderCache; - ImGui::TextWrapped("Particle Lights Customisation"); - ImGui::SliderFloat("Saturation", &settings.ParticleLightsSaturation, 1.0, 2.0, "%.2f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Particle light saturation."); - } - ImGui::SliderFloat("Particle Brightness", &settings.ParticleBrightness, 0.0, 10.0, "%.2f"); - ImGui::SliderFloat("Particle Radius", &settings.ParticleRadius, 0.0, 10.0, "%.2f"); - ImGui::SliderFloat("Billboard Brightness", &settings.BillboardBrightness, 0.0, 10.0, "%.2f"); - ImGui::SliderFloat("Billboard Radius", &settings.BillboardRadius, 0.0, 10.0, "%.2f"); + if (ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text(std::format("Clustered Light Count : {}", lightCount).c_str()); - ImGui::Spacing(); - ImGui::Spacing(); ImGui::TreePop(); } - auto shaderCache = globals::shaderCache; + /////////////////////////////// + ImGui::SeparatorText("Debug"); - if (ImGui::TreeNodeEx("Light Limit Visualization", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::TreeNode("Light Limit Visualization")) { ImGui::Checkbox("Enable Lights Visualisation", &settings.EnableLightsVisualisation); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Enables visualization of the light limit\n"); @@ -86,15 +49,6 @@ void LightLimitFix::DrawSettings() previousEnableLightsVisualisation = currentEnableLightsVisualisation; } - ImGui::Spacing(); - ImGui::Spacing(); - ImGui::TreePop(); - } - - if (ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text(std::format("Clustered Light Count : {}", lightCount).c_str()); - ImGui::Text(std::format("Particle Lights Count : {}", currentParticleLights.size()).c_str()); - ImGui::TreePop(); } } @@ -108,11 +62,6 @@ LightLimitFix::PerFrame LightLimitFix::GetCommonBufferData() return perFrame; } -void LightLimitFix::CleanupParticleLights(RE::NiNode* a_node) -{ - particleLightsReferences.erase(a_node); -} - void LightLimitFix::SetupResources() { auto screenSize = globals::state->screenSize; @@ -210,22 +159,6 @@ void LightLimitFix::SetupResources() } } -void LightLimitFix::Reset() -{ - for (auto& particleLight : currentParticleLights) { - if (!particleLight.billboard) { - if (const auto particleSystem = static_cast(particleLight.node)) { - if (auto particleData = particleSystem->GetParticleRuntimeData().particleData.get()) { - particleData->DecRefCount(); - } - } - } - particleLight.node->DecRefCount(); - } - currentParticleLights.clear(); - std::swap(currentParticleLights, queuedParticleLights); -} - void LightLimitFix::LoadSettings(json& o_json) { settings = o_json; @@ -377,39 +310,6 @@ void LightLimitFix::SetLightPosition(LightLimitFix::LightData& a_light, RE::NiPo } } -float LightLimitFix::CalculateLuminance(CachedParticleLight& light, RE::NiPoint3& point) -{ - // See BSLight::CalculateLuminance_14131D3D0 - // Performs lighting on the CPU which is identical to GPU code - - auto lightDirection = light.position - point; - float lightDist = lightDirection.Length(); - float intensityFactor = std::clamp(lightDist / light.radius, 0.0f, 1.0f); - float intensityMultiplier = 1 - intensityFactor * intensityFactor; - - return light.grey * intensityMultiplier; -} - -void LightLimitFix::AddParticleLightLuminance(RE::NiPoint3& targetPosition, int& numHits, float& lightLevel) -{ - auto shaderCache = globals::shaderCache; - - if (!shaderCache->IsEnabled()) - return; - - std::lock_guard lk{ cachedParticleLightsMutex }; - int particleLightsDetectionHits = 0; - if (settings.EnableParticleLightsDetection) { - for (auto& light : cachedParticleLights) { - auto luminance = CalculateLuminance(light, targetPosition); - lightLevel += luminance; - if (luminance > 0.0) - particleLightsDetectionHits++; - } - } - numHits += particleLightsDetectionHits; -} - void LightLimitFix::Prepass() { auto context = globals::d3d::context; @@ -438,217 +338,8 @@ bool LightLimitFix::IsGlobalLight(RE::BSLight* a_light) return !(a_light->portalStrict || !a_light->portalGraph); } -struct VertexColor -{ - std::uint8_t data[4]; -}; - -struct VertexPosition -{ - std::uint8_t data[3]; -}; - -std::string ExtractTextureStem(std::string_view a_path) -{ - if (a_path.size() < 1) - return {}; - - auto lastSeparatorPos = a_path.find_last_of("\\/"); - if (lastSeparatorPos == std::string::npos) - return {}; - - a_path = a_path.substr(lastSeparatorPos + 1); - a_path.remove_suffix(4); // Remove ".dds" - - auto textureNameView = a_path | std::views::transform([](auto c) { return (char)::tolower(c); }); - std::string textureName = { textureNameView.begin(), textureNameView.end() }; - - return textureName; -} - -LightLimitFix::ParticleLightReference LightLimitFix::GetParticleLightConfigs(RE::BSRenderPass* a_pass) -{ - auto& particleLights = globals::features::llf::particleLights; - - // see https://www.nexusmods.com/skyrimspecialedition/articles/1391 - if (settings.EnableParticleLights) { - if (auto shaderProperty = a_pass->shaderProperty->GetRTTI() == globals::rtti::BSEffectShaderPropertyRTTI.get() ? static_cast(a_pass->shaderProperty) : nullptr) { - if (!shaderProperty->lightData) { - if (auto material = shaderProperty->GetMaterial()) { - // Check if it's a valid particle light - bool billboard = false; - if (a_pass->geometry->GetRTTI() != globals::rtti::NiParticleSystemRTTI.get()) { - if (auto parent = a_pass->geometry->parent) { - if (auto billboardNode = parent->GetRTTI() == globals::rtti::NiBillboardNodeRTTI.get() ? static_cast(parent) : nullptr) { - billboard = true; - } else { - return { false }; - } - } else { - return { false }; - } - } - - // Already scanned - { - auto it = particleLightsReferences.find(reinterpret_cast(a_pass->geometry)); - if (it != particleLightsReferences.end()) - return (*it).second; - } - - // Not scanned, scan now - - if (!material->sourceTexturePath.empty()) { - std::string textureName = ExtractTextureStem(material->sourceTexturePath.c_str()); - if (textureName.size() < 1) { - particleLightsReferences.insert({ (RE::NiNode*)a_pass->geometry, { false } }); - return { false }; - } - - auto& configs = particleLights.particleLightConfigs; - auto it = configs.find(textureName); - if (it == configs.end()) { - particleLightsReferences.insert({ (RE::NiNode*)a_pass->geometry, { false } }); - return { false }; - } - - ParticleLights::Config* config = &it->second; - ParticleLights::GradientConfig* gradientConfig = nullptr; - if (!material->greyscaleTexturePath.empty()) { - textureName = ExtractTextureStem(material->greyscaleTexturePath.c_str()); - if (textureName.size() < 1) { - particleLightsReferences.insert({ (RE::NiNode*)a_pass->geometry, { false } }); - return { false }; - } - - auto& gradientConfigs = particleLights.particleLightGradientConfigs; - auto itGradient = gradientConfigs.find(textureName); - if (itGradient == gradientConfigs.end()) { - particleLightsReferences.insert({ (RE::NiNode*)a_pass->geometry, { false } }); - return { false }; - } - gradientConfig = &itGradient->second; - } - - ParticleLightReference reference{ true }; - reference.billboard = billboard; - reference.config = config; - reference.gradientConfig = gradientConfig; - reference.baseColor = { 1, 1, 1, 1 }; - - if (billboard) { - if (auto rendererData = a_pass->geometry->GetGeometryRuntimeData().rendererData) { - if (auto triShape = a_pass->geometry->AsTriShape()) { - uint32_t vertexSize = rendererData->vertexDesc.GetSize(); - if (rendererData->vertexDesc.HasFlag(RE::BSGraphics::Vertex::Flags::VF_COLORS)) { - uint32_t offset = rendererData->vertexDesc.GetAttributeOffset(RE::BSGraphics::Vertex::Attribute::VA_COLOR); - - uint8_t maxAlpha = 0u; - VertexColor* vertexColor = nullptr; - - for (int v = 0; v < triShape->GetTrishapeRuntimeData().vertexCount; v++) { - if (VertexColor* vertex = reinterpret_cast(&rendererData->rawVertexData[vertexSize * v + offset])) { - uint8_t alpha = vertex->data[3]; - if (alpha > maxAlpha) { - maxAlpha = alpha; - vertexColor = vertex; - } - } - } - - if (vertexColor) { - reference.baseColor.red *= vertexColor->data[0] / 255.f; - reference.baseColor.green *= vertexColor->data[1] / 255.f; - reference.baseColor.blue *= vertexColor->data[2] / 255.f; - if (shaderProperty->flags.any(RE::BSShaderProperty::EShaderPropertyFlag::kVertexAlpha)) { - reference.baseColor.alpha *= vertexColor->data[3] / 255.f; - } - } - } - } - } - } - - particleLightsReferences.insert({ reinterpret_cast(a_pass->geometry), reference }); - return reference; - } - } - } - } - } - return { false }; -} - -bool LightLimitFix::CheckParticleLights(RE::BSRenderPass* a_pass, uint32_t) -{ - auto shaderCache = globals::shaderCache; - - if (!shaderCache->IsEnabled()) - return true; - - auto reference = GetParticleLightConfigs(a_pass); - if (reference.valid) { - if (AddParticleLight(a_pass, reference)) { - return !(settings.EnableParticleLightsCulling && reference.config->cull); - } - } - return true; -} - -bool LightLimitFix::AddParticleLight(RE::BSRenderPass* a_pass, ParticleLightReference a_reference) -{ - auto shaderProperty = static_cast(a_pass->shaderProperty); - auto material = shaderProperty->GetMaterial(); - auto config = a_reference.config; - auto gradientConfig = a_reference.gradientConfig; - - a_pass->geometry->IncRefCount(); - - if (!a_reference.billboard) { - if (auto particleSystem = static_cast(a_pass->geometry)) { - if (auto particleData = particleSystem->GetParticleRuntimeData().particleData.get()) { - particleData->IncRefCount(); - } - } - } - - RE::NiColorA color = a_reference.baseColor; - color.red *= material->baseColor.red * material->baseColorScale; - color.green *= material->baseColor.green * material->baseColorScale; - color.blue *= material->baseColor.blue * material->baseColorScale; - color.alpha *= material->baseColor.alpha * shaderProperty->alpha; - - if (auto emittance = shaderProperty->unk88) { - color.red *= emittance->red; - color.green *= emittance->green; - color.blue *= emittance->blue; - } - - if (gradientConfig) { - auto grey = float3(config->colorMult.red, config->colorMult.green, config->colorMult.blue).Dot(float3(0.3f, 0.59f, 0.11f)); - color.red *= grey * gradientConfig->color.red; - color.green *= grey * gradientConfig->color.green; - color.blue *= grey * gradientConfig->color.blue; - } else { - color.red *= config->colorMult.red; - color.green *= config->colorMult.green; - color.blue *= config->colorMult.blue; - } - - color.alpha = config->radiusMult; - - ParticleLightInfo info; - info.billboard = a_reference.billboard; - info.node = a_pass->geometry; - info.color = color; - - queuedParticleLights.push_back(info); - return true; -} - void LightLimitFix::PostPostLoad() { - globals::features::llf::particleLights.GetConfigs(); Hooks::Install(); } @@ -673,52 +364,6 @@ void LightLimitFix::ClearShaderCache() clusterCullingCS = (ID3D11ComputeShader*)Util::CompileShader(L"Data\\Shaders\\LightLimitFix\\ClusterCullingCS.hlsl", {}, "cs_5_0"); } -float LightLimitFix::CalculateLightDistance(float3 a_lightPosition, float a_radius) -{ - return (a_lightPosition.x * a_lightPosition.x) + (a_lightPosition.y * a_lightPosition.y) + (a_lightPosition.z * a_lightPosition.z) - (a_radius * a_radius); -} - -void LightLimitFix::AddCachedParticleLights(eastl::vector& lightsData, LightLimitFix::LightData& light) -{ - static float& lightFadeStart = *reinterpret_cast(REL::RelocationID(527668, 414582).address()); - static float& lightFadeEnd = *reinterpret_cast(REL::RelocationID(527669, 414583).address()); - - float distance = CalculateLightDistance(light.positionWS[0].data, light.radius); - - float dimmer = 0.0f; - - if (distance < lightFadeStart || lightFadeEnd == 0.0f) { - dimmer = 1.0f; - } else if (distance <= lightFadeEnd) { - dimmer = 1.0f - ((distance - lightFadeStart) / (lightFadeEnd - lightFadeStart)); - } else { - dimmer = 0.0f; - } - - light.color *= dimmer; - - if ((light.color.x + light.color.y + light.color.z) > 1e-4 && light.radius > 1e-4) { - light.invRadius = 1.f / light.radius; - lightsData.push_back(light); - - CachedParticleLight cachedParticleLight{}; - cachedParticleLight.grey = float3(light.color.x, light.color.y, light.color.z).Dot(float3(0.3f, 0.59f, 0.11f)); - cachedParticleLight.radius = light.radius; - cachedParticleLight.position = { light.positionWS[0].data.x + eyePositionCached[0].x, light.positionWS[0].data.y + eyePositionCached[0].y, light.positionWS[0].data.z + eyePositionCached[0].z }; - - cachedParticleLights.push_back(cachedParticleLight); - } -} - -float3 LightLimitFix::Saturation(float3 color, float saturation) -{ - float grey = color.Dot(float3(0.3f, 0.59f, 0.11f)); - color.x = std::max(std::lerp(grey, color.x, saturation), 0.0f); - color.y = std::max(std::lerp(grey, color.y, saturation), 0.0f); - color.z = std::max(std::lerp(grey, color.z, saturation), 0.0f); - return color; -} - namespace RE { class BSMultiBoundRoom : public NiNode @@ -815,133 +460,6 @@ void LightLimitFix::UpdateLights() addLight(e); } - { - std::lock_guard lk{ cachedParticleLightsMutex }; - cachedParticleLights.clear(); - - LightData clusteredLight{}; - uint32_t clusteredLights = 0; - - auto eyePositionOffset = eyePositionCached[0] - eyePositionCached[1]; - - for (const auto& particleLight : currentParticleLights) { - if (!particleLight.billboard) { - auto particleSystem = static_cast(particleLight.node); - if (particleSystem && particleSystem->GetParticleRuntimeData().particleData.get()) { - // Process BSGeometry - auto particleData = particleSystem->GetParticleRuntimeData().particleData.get(); - auto& particleSystemRuntimeData = particleSystem->GetParticleSystemRuntimeData(); - auto& particleRuntimeData = particleData->GetParticlesRuntimeData(); - - auto numVertices = particleData->GetActiveVertexCount(); - for (std::uint32_t p = 0; p < numVertices; p++) { - float radius = particleRuntimeData.radii[p] * particleRuntimeData.sizes[p]; - - auto initialPosition = particleRuntimeData.positions[p]; - if (!particleSystemRuntimeData.isWorldspace) { - // Detect first-person meshes - if ((particleLight.node->GetModelData().modelBound.radius * particleLight.node->world.scale) != particleLight.node->worldBound.radius) - initialPosition += particleLight.node->worldBound.center; - else - initialPosition += particleLight.node->world.translate; - } - - RE::NiPoint3 positionWS = initialPosition - eyePositionCached[0]; - - if (clusteredLights) { - auto averageRadius = clusteredLight.radius / (float)clusteredLights; - float radiusDiff = abs(averageRadius - radius); - - auto averagePosition = clusteredLight.positionWS[0].data / (float)clusteredLights; - float positionDiff = positionWS.GetDistance({ averagePosition.x, averagePosition.y, averagePosition.z }); - - if ((radiusDiff + positionDiff) > 32.0f || !settings.EnableParticleLightsOptimization) { - clusteredLight.radius /= (float)clusteredLights; - clusteredLight.positionWS[0].data /= (float)clusteredLights; - clusteredLight.positionWS[1].data = clusteredLight.positionWS[0].data; - if (eyeCount == 2) { - clusteredLight.positionWS[1].data.x += eyePositionOffset.x / (float)clusteredLights; - clusteredLight.positionWS[1].data.y += eyePositionOffset.y / (float)clusteredLights; - clusteredLight.positionWS[1].data.z += eyePositionOffset.z / (float)clusteredLights; - } - - clusteredLight.lightFlags.set(LightFlags::Simple); - - AddCachedParticleLights(lightsData, clusteredLight); - - clusteredLights = 0; - clusteredLight.color = { 0, 0, 0 }; - clusteredLight.radius = 0; - clusteredLight.positionWS[0].data = { 0, 0, 0 }; - } - } - - if (particleRuntimeData.color) { - float alpha = particleLight.color.alpha * particleRuntimeData.color[p].alpha; - - float3 color; - color.x = particleLight.color.red * particleRuntimeData.color[p].red; - color.y = particleLight.color.green * particleRuntimeData.color[p].green; - color.z = particleLight.color.blue * particleRuntimeData.color[p].blue; - - clusteredLight.color += Saturation(color, settings.ParticleLightsSaturation) * alpha * settings.ParticleBrightness; - } else { - float alpha = particleLight.color.alpha; - - float3 color; - color.x = particleLight.color.red; - color.y = particleLight.color.green; - color.z = particleLight.color.blue; - - clusteredLight.color += Saturation(color, settings.ParticleLightsSaturation) * alpha * settings.ParticleBrightness; - } - - clusteredLight.radius += radius * particleLight.color.alpha * settings.ParticleRadius; - - clusteredLight.positionWS[0].data.x += positionWS.x; - clusteredLight.positionWS[0].data.y += positionWS.y; - clusteredLight.positionWS[0].data.z += positionWS.z; - - clusteredLights++; - } - } - } else { - // Process billboard - LightData light{}; - - light.color.x = particleLight.color.red; - light.color.y = particleLight.color.green; - light.color.z = particleLight.color.blue; - - light.color = Saturation(light.color, settings.ParticleLightsSaturation); - - light.color *= particleLight.color.alpha * settings.BillboardBrightness; - light.radius = particleLight.node->worldBound.radius * particleLight.color.alpha * settings.BillboardRadius * 0.5f; - - auto position = particleLight.node->world.translate; - - SetLightPosition(light, position); // Light is complete for both eyes by now - - light.lightFlags.set(LightFlags::Simple); - - AddCachedParticleLights(lightsData, light); - } - } - - if (clusteredLights) { - clusteredLight.radius /= (float)clusteredLights; - clusteredLight.positionWS[0].data /= (float)clusteredLights; - clusteredLight.positionWS[1].data = clusteredLight.positionWS[0].data; - if (eyeCount == 2) { - clusteredLight.positionWS[1].data.x += eyePositionOffset.x / (float)clusteredLights; - clusteredLight.positionWS[1].data.y += eyePositionOffset.y / (float)clusteredLights; - clusteredLight.positionWS[1].data.z += eyePositionOffset.z / (float)clusteredLights; - } - clusteredLight.lightFlags.set(LightFlags::Simple); - AddCachedParticleLights(lightsData, clusteredLight); - } - } - auto context = globals::d3d::context; lightCount = std::min((uint)lightsData.size(), MAX_LIGHTS); @@ -1048,16 +566,3 @@ void LightLimitFix::Hooks::BSWaterShader_SetupGeometry::thunk(RE::BSShader* This singleton.BSLightingShader_SetupGeometry_Before(Pass); singleton.BSLightingShader_SetupGeometry_After(Pass); }; - -float LightLimitFix::Hooks::AIProcess_CalculateLightValue_GetLuminance::thunk(RE::ShadowSceneNode* shadowSceneNode, RE::NiPoint3& targetPosition, int& numHits, float& sunLightLevel, float& lightLevel, RE::NiLight& refLight, int32_t shadowBitMask) -{ - auto ret = func(shadowSceneNode, targetPosition, numHits, sunLightLevel, lightLevel, refLight, shadowBitMask); - globals::features::lightLimitFix.AddParticleLightLuminance(targetPosition, numHits, ret); - return ret; -} - -void LightLimitFix::Hooks::NiNode_Destroy::thunk(RE::NiNode* This) -{ - globals::features::lightLimitFix.CleanupParticleLights(This); - func(This); -} \ No newline at end of file diff --git a/src/Features/LightLimitFix.h b/src/Features/LightLimitFix.h index 6def6e1bc5..6b92097bbe 100644 --- a/src/Features/LightLimitFix.h +++ b/src/Features/LightLimitFix.h @@ -1,6 +1,6 @@ #pragma once -#include "Features/LightLimitFix/ParticleLights.h" +#include "Buffer.h" struct LightLimitFix : Feature { @@ -23,7 +23,7 @@ struct LightLimitFix : Feature "Unlimited dynamic lights", "Improved lighting quality", "Enhanced visual realism", - "Support for particle lights" } + "Enhanced visual realism" } }; } @@ -61,6 +61,7 @@ struct LightLimitFix : Feature uint pad0; uint pad1; }; + STATIC_ASSERT_ALIGNAS_16(LightData); struct ClusterAABB { @@ -74,6 +75,7 @@ struct LightLimitFix : Feature uint lightCount; uint pad0[2]; }; + STATIC_ASSERT_ALIGNAS_16(LightGrid); struct alignas(16) LightBuildingCB { @@ -82,6 +84,7 @@ struct LightLimitFix : Feature uint pad0[2]; uint ClusterSize[4]; }; + STATIC_ASSERT_ALIGNAS_16(LightBuildingCB); struct alignas(16) LightCullingCB { @@ -89,6 +92,7 @@ struct LightLimitFix : Feature uint pad[3]; uint ClusterSize[4]; }; + STATIC_ASSERT_ALIGNAS_16(LightCullingCB); struct alignas(16) PerFrame { @@ -97,6 +101,7 @@ struct LightLimitFix : Feature float pad0[2]; uint ClusterSize[4]; }; + STATIC_ASSERT_ALIGNAS_16(PerFrame); PerFrame GetCommonBufferData(); @@ -108,16 +113,10 @@ struct LightLimitFix : Feature uint pad0; LightData StrictLights[15]; }; + STATIC_ASSERT_ALIGNAS_16(StrictLightDataCB); StrictLightDataCB strictLightDataTemp; - struct CachedParticleLight - { - float grey; - RE::NiPoint3 position; - float radius; - }; - ConstantBuffer* strictLightDataCB = nullptr; int eyeCount = !REL::Module::IsVR() ? 1 : 2; @@ -140,28 +139,6 @@ struct LightLimitFix : Feature float lightsNear = 1; float lightsFar = 16384; - struct ParticleLightInfo - { - bool billboard; - RE::BSGeometry* node; - RE::NiColorA color; - }; - - struct ParticleLightReference - { - bool valid; - bool billboard; - ParticleLights::Config* config; - ParticleLights::GradientConfig* gradientConfig; - RE::NiColorA baseColor; - }; - - eastl::hash_map particleLightsReferences; - eastl::vector queuedParticleLights; - eastl::vector currentParticleLights; - - void CleanupParticleLights(RE::NiNode* a_node); - RE::NiPoint3 eyePositionCached[2]{}; bool wasEmpty = false; bool wasWorld = false; @@ -171,7 +148,6 @@ struct LightLimitFix : Feature Util::FrameChecker frameChecker; virtual void SetupResources() override; - virtual void Reset() override; virtual void LoadSettings(json& o_json) override; virtual void SaveSettings(json& o_json) override; @@ -185,7 +161,6 @@ struct LightLimitFix : Feature virtual void ClearShaderCache() override; float CalculateLightDistance(float3 a_lightPosition, float a_radius); - void AddCachedParticleLights(eastl::vector& lightsData, LightLimitFix::LightData& light); void SetLightPosition(LightLimitFix::LightData& a_light, RE::NiPoint3 a_initialPosition, bool a_cached = true); void UpdateLights(); void UpdateStructure(); @@ -200,39 +175,20 @@ struct LightLimitFix : Feature bool EnableContactShadows = false; bool EnableLightsVisualisation = false; uint LightsVisualisationMode = 0; - bool EnableParticleLights = true; - bool EnableParticleLightsCulling = true; - bool EnableParticleLightsDetection = true; - float ParticleLightsSaturation = 1.0f; - float ParticleBrightness = 1.0f; - float ParticleRadius = 1.0f; - float BillboardBrightness = 1.0f; - float BillboardRadius = 1.0f; - bool EnableParticleLightsOptimization = true; }; uint clusterSize[3] = { 16 }; Settings settings; - ParticleLightReference GetParticleLightConfigs(RE::BSRenderPass* a_pass); - bool AddParticleLight(RE::BSRenderPass* a_pass, ParticleLightReference a_reference); - bool CheckParticleLights(RE::BSRenderPass* a_pass, uint32_t a_technique); - void BSLightingShader_SetupGeometry_Before(RE::BSRenderPass* a_pass); void BSLightingShader_SetupGeometry_GeometrySetupConstantPointLights(RE::BSRenderPass* a_pass); void BSLightingShader_SetupGeometry_After(RE::BSRenderPass* a_pass); - std::shared_mutex cachedParticleLightsMutex; - eastl::vector cachedParticleLights; - eastl::hash_map roomNodes; - float CalculateLuminance(CachedParticleLight& light, RE::NiPoint3& point); - void AddParticleLightLuminance(RE::NiPoint3& targetPosition, int& numHits, float& lightLevel); - struct Hooks { struct BSLightingShader_SetupGeometry @@ -253,18 +209,6 @@ struct LightLimitFix : Feature static inline REL::Relocation func; }; - struct AIProcess_CalculateLightValue_GetLuminance - { - static float thunk(RE::ShadowSceneNode* shadowSceneNode, RE::NiPoint3& targetPosition, int& numHits, float& sunLightLevel, float& lightLevel, RE::NiLight& refLight, int32_t shadowBitMask); - static inline REL::Relocation func; - }; - - struct NiNode_Destroy - { - static void thunk(RE::NiNode* This); - static inline REL::Relocation func; - }; - template struct ValidLight { @@ -281,14 +225,10 @@ struct LightLimitFix : Feature static void Install() { - stl::write_thunk_call(REL::RelocationID(38900, 39946).address() + REL::Relocate(0x1C9, 0x1D3)); - stl::write_vfunc<0x6, BSLightingShader_SetupGeometry>(RE::VTABLE_BSLightingShader[0]); stl::write_vfunc<0x6, BSEffectShader_SetupGeometry>(RE::VTABLE_BSEffectShader[0]); stl::write_vfunc<0x6, BSWaterShader_SetupGeometry>(RE::VTABLE_BSWaterShader[0]); - stl::detour_thunk(REL::RelocationID(68937, 70288)); - stl::write_thunk_call(REL::RelocationID(100994, 107781).address() + 0x92); stl::write_thunk_call(REL::RelocationID(100997, 107784).address() + REL::Relocate(0x139, 0x12A)); stl::write_thunk_call(REL::RelocationID(101296, 108283).address() + REL::Relocate(0xB7, 0x7E)); @@ -298,6 +238,7 @@ struct LightLimitFix : Feature }; virtual bool SupportsVR() override { return true; }; + virtual bool IsCore() const override { return true; } }; template <> @@ -315,7 +256,7 @@ struct fmt::formatter // Check if reached the end of the range: if (it != end && *it != '}') - throw_format_error("invalid format"); + throw format_error("invalid format"); // Return an iterator past the end of the parsed range: return it; diff --git a/src/Features/PerformanceOverlay.cpp b/src/Features/PerformanceOverlay.cpp index 0cbc489e83..590e4e0cbb 100644 --- a/src/Features/PerformanceOverlay.cpp +++ b/src/Features/PerformanceOverlay.cpp @@ -39,7 +39,7 @@ #include #include #include -#include +#include #include #include @@ -164,51 +164,92 @@ void PerformanceOverlay::DrawSettings() ImGui::Indent(); // Display options - if (ImGui::CollapsingHeader("Display Options", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Indent(); + ImGui::TextUnformatted("Display Options"); + ImGui::Separator(); - ImGui::Checkbox("Show FPS Counter", &this->settings.ShowFPS); - ImGui::Checkbox("Show Draw Calls", &this->settings.ShowDrawCalls); - ImGui::Checkbox("Show VRAM Usage", &this->settings.ShowVRAM); + ImGui::Checkbox("Show FPS Counter", &this->settings.ShowFPS); + ImGui::Checkbox("Show Draw Calls", &this->settings.ShowDrawCalls); + ImGui::Checkbox("Show VRAM Usage", &this->settings.ShowVRAM); - bool isFrameGenerationActive = globals::features::upscaling.IsFrameGenerationActive(); - if (this->settings.ShowFPS && isFrameGenerationActive) { - ImGui::Checkbox("Show Pre-FG Frametime Graph", &this->settings.ShowPreFGFrameTimeGraph); + bool isFrameGenerationActive = globals::features::upscaling.IsFrameGenerationActive(); + if (this->settings.ShowFPS && isFrameGenerationActive) { + ImGui::Checkbox("Show Pre-FG Frametime Graph", &this->settings.ShowPreFGFrameTimeGraph); - ImGui::Checkbox("Show Post-FG Frametime Graph", &this->settings.ShowPostFGFrameTimeGraph); - if (ImGui::IsItemHovered()) { - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("FSR Frame Generation uses calculated timing data (2x Pre-FG).\nDLSS Frame Generation provides measured timing data."); - } + ImGui::Checkbox("Show Post-FG Frametime Graph", &this->settings.ShowPostFGFrameTimeGraph); + if (ImGui::IsItemHovered()) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("FSR Frame Generation uses calculated timing data (2x Pre-FG).\nDLSS Frame Generation provides measured timing data."); } - } else if (this->settings.ShowFPS) { - ImGui::Checkbox("Show Frametime Graph", &this->settings.ShowPreFGFrameTimeGraph); } - - ImGui::Unindent(); + } else if (this->settings.ShowFPS) { + ImGui::Checkbox("Show Frametime Graph", &this->settings.ShowPreFGFrameTimeGraph); } + ImGui::Spacing(); + ImGui::Spacing(); + // Appearance settings - if (ImGui::CollapsingHeader("Appearance", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Indent(); + ImGui::TextUnformatted("Appearance"); + ImGui::Separator(); + + ImGui::SliderFloat("Text Size", &this->settings.TextSize, 0.8f, 1.2f, "%.2f"); + ImGui::SliderFloat("Background Opacity", &this->settings.BackgroundOpacity, 0.0f, 1.0f, "%.2f"); + ImGui::Checkbox("Show Border", &this->settings.ShowBorder); + ImGui::SliderFloat("Update Interval", &this->settings.UpdateInterval, 0.001f, PerformanceOverlay::Settings::kMaxUpdateInterval, "%.2f seconds"); + ImGui::SliderInt("Frame History Size", &this->settings.FrameHistorySize, + this->settings.kMinFrameHistorySize, this->settings.kMaxFrameHistorySize); + + ImGui::Separator(); + ImGui::Text("Position:"); + if (ImGui::Button("Reset Position")) { + this->settings.PositionSet = false; + } + ImGui::SameLine(); + if (ImGui::Button("Restore Defaults")) { + RestoreDefaultSettings(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::TextUnformatted("Restores Performance Overlay settings to defaults, including graphs, appearance, and update intervals."); + } - ImGui::SliderFloat("Text Size", &this->settings.TextSize, 0.8f, 1.2f, "%.2f"); - ImGui::SliderFloat("Background Opacity", &this->settings.BackgroundOpacity, 0.0f, 1.0f, "%.2f"); - ImGui::Checkbox("Show Border", &this->settings.ShowBorder); - ImGui::SliderFloat("Update Interval", &this->settings.UpdateInterval, 0.001f, PerformanceOverlay::Settings::kMaxUpdateInterval, "%.2f seconds"); - ImGui::SliderInt("Frame History Size", &this->settings.FrameHistorySize, - this->settings.kMinFrameHistorySize, this->settings.kMaxFrameHistorySize); + ImGui::Unindent(); + } +} - ImGui::Separator(); - ImGui::Text("Position:"); - if (ImGui::Button("Reset Position")) { - this->settings.PositionSet = false; - } +void PerformanceOverlay::SaveSettings(json& j) +{ + // Persist all overlay settings to JSON + j = this->settings; // uses NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT +} - ImGui::Unindent(); - } - ImGui::Unindent(); +void PerformanceOverlay::LoadSettings(json& j) +{ + try { + // Load all settings from JSON (missing fields use defaults) + this->settings = j.get(); + } catch (...) { + // Fallback to defaults if JSON is invalid + this->settings = PerformanceOverlay::Settings{}; } + // Ensure history buffers match loaded size + this->state.frameTimeHistory.Resize(this->settings.FrameHistorySize); + this->state.postFGFrameTimeHistory.Resize(this->settings.FrameHistorySize); +} + +void PerformanceOverlay::RestoreDefaultSettings() +{ + this->settings = PerformanceOverlay::Settings{}; + // Reset runtime buffers/state to match defaults + this->state.frameTimeHistory.Resize(this->settings.FrameHistorySize); + this->state.postFGFrameTimeHistory.Resize(this->settings.FrameHistorySize); + this->state.smoothFps = 0.0f; + this->state.smoothFrameTimeMs = 0.0f; + this->state.postFGSmoothFps = 0.0f; + this->state.postFGSmoothFrameTimeMs = 0.0f; + this->state.minFrameTime = 1000.0f; + this->state.maxFrameTime = 0.0f; + this->state.smoothedMinFrameTime = 0.0f; + this->state.smoothedMaxFrameTime = 50.0f; } void PerformanceOverlay::DataLoaded() @@ -257,7 +298,8 @@ void PerformanceOverlay::DrawOverlay() windowFlags |= ImGuiWindowFlags_NoBackground; } else { windowFlags &= ~ImGuiWindowFlags_NoDecoration; - windowFlags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; + windowFlags &= ~ImGuiWindowFlags_AlwaysAutoResize; + windowFlags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse; } // Set background opacity @@ -331,47 +373,26 @@ void PerformanceOverlay::DrawOverlay() // Update graph values this->UpdateGraphValues(); - // Check if we should show collapsible sections (should swallow input only) - bool showCollapsibleSections = Menu::GetSingleton()->ShouldSwallowInput(); - // Show FPS counter if enabled if (this->settings.ShowFPS) { - static bool fpsExpanded = true; - if (showCollapsibleSections) { - Util::DrawSectionHeader("FPS & Frame Time", false, true, &fpsExpanded); - } - if (fpsExpanded) { - DrawFPS(); - } + DrawFPS(); } // Show Draw Calls if enabled if (this->settings.ShowDrawCalls) { - static bool drawCallsExpanded = true; - if (showCollapsibleSections) { - Util::DrawSectionHeader("Draw Calls & Shader Performance", false, true, &drawCallsExpanded); - } - if (drawCallsExpanded) { - DrawDrawCallsTable(mainRows, summaryRows); - } + DrawDrawCallsTable(mainRows, summaryRows); } // VRAM & GPU Usage if (this->settings.ShowVRAM && menu->GetDXGIAdapter3()) { - static bool vramExpanded = true; - if (showCollapsibleSections) { - Util::DrawSectionHeader("VRAM Usage", false, true, &vramExpanded); - } - if (vramExpanded) { - DrawVRAM(); - } + DrawVRAM(); } ImGui::PopStyleVar(); // ItemSpacing ImGui::SetWindowFontScale(1.0f); // Reset font scale // --- A/B Test Section --- - DrawABTestSection(allRows, showCollapsibleSections); + DrawABTestSection(allRows); ImGui::End(); ImGui::PopStyleVar(); // WindowBorderSize @@ -1116,9 +1137,8 @@ std::vector PerformanceOverlay::BuildABTestResultsTableColumns(con * - A/B test controls (clear results, show/hide settings diff) * * @param allRows The current draw call rows for data collection - * @param showCollapsibleSections Whether to show collapsible section headers */ -void PerformanceOverlay::DrawABTestSection(const std::vector& allRows, bool showCollapsibleSections) +void PerformanceOverlay::DrawABTestSection(const std::vector& allRows) { auto* menu = Menu::GetSingleton(); auto* abTestingManager = ABTestingManager::GetSingleton(); @@ -1161,125 +1181,113 @@ void PerformanceOverlay::DrawABTestSection(const std::vector& allRo // Display A/B test results if available if (aggregator.HasResults()) { - static bool abResultsExpanded = true; - if (showCollapsibleSections) { - Util::DrawSectionHeader("Aggregated A/B Test Results", false, true, &abResultsExpanded); + this->DrawABTestResultsTable(); + ImGui::Separator(); + // --- A/B Results Controls --- + static bool showSettingsDiff = false; + ImGui::BeginGroup(); + if (ImGui::Button(showSettingsDiff ? "Hide Settings Diff" : "Show Settings Diff")) { + showSettingsDiff = !showSettingsDiff; } - if (abResultsExpanded) { - this->DrawABTestResultsTable(); + ImGui::SameLine(); + if (ImGui::Button("Clear A/B Test Results")) { + aggregator.Clear(); + this->settingsDiff.clear(); + this->settingsDiffLoaded = false; + showSettingsDiff = false; + ImGui::EndGroup(); ImGui::Separator(); - // --- A/B Results Controls --- - static bool showSettingsDiff = false; - ImGui::BeginGroup(); - if (ImGui::Button(showSettingsDiff ? "Hide Settings Diff" : "Show Settings Diff")) { - showSettingsDiff = !showSettingsDiff; - } - ImGui::SameLine(); - if (ImGui::Button("Clear A/B Test Results")) { - aggregator.Clear(); - this->settingsDiff.clear(); - this->settingsDiffLoaded = false; - showSettingsDiff = false; - ImGui::EndGroup(); - ImGui::Separator(); - return; + return; + } + ImGui::EndGroup(); + // --- Settings diff section (inline, toggled) --- + if (showSettingsDiff) { + if (!this->settingsDiffLoaded) { + std::filesystem::path userPath = Util::PathHelpers::GetDataPath() / "SKSE/Plugins/CommunityShaders/SettingsUser.json"; + std::filesystem::path testPath = Util::PathHelpers::GetDataPath() / "SKSE/Plugins/CommunityShaders/SettingsTest.json"; + this->settingsDiff = Util::FileSystem::LoadJsonDiff(userPath, testPath); + this->settingsDiffLoaded = true; } - ImGui::EndGroup(); - // --- Settings diff section (inline, toggled) --- - if (showSettingsDiff) { - if (!this->settingsDiffLoaded) { - std::filesystem::path userPath = Util::PathHelpers::GetDataPath() / "SKSE/Plugins/CommunityShaders/SettingsUser.json"; - std::filesystem::path testPath = Util::PathHelpers::GetDataPath() / "SKSE/Plugins/CommunityShaders/SettingsTest.json"; - this->settingsDiff = Util::FileSystem::LoadJsonDiff(userPath, testPath); - this->settingsDiffLoaded = true; - } - static bool settingsDiffExpanded = true; - if (showCollapsibleSections) { - Util::DrawSectionHeader("A/B Test Settings Differences", false, true, &settingsDiffExpanded); - } - if (settingsDiffExpanded) { - ImGui::TextUnformatted("Differences between USER (A) and TEST (B) configs:"); - if (this->settingsDiff.empty()) { - ImGui::TextUnformatted("No setting changes detected between USER (A) and TEST (B) configs."); - } else if (ImGui::BeginTable("ABSettingsDiffTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable)) { - ImGui::TableSetupColumn("Setting Path", ImGuiTableColumnFlags_DefaultSort); - ImGui::TableSetupColumn("A Value"); - ImGui::TableSetupColumn("B Value"); - ImGui::TableHeadersRow(); - - // Determine which variant performed better based on Total row - bool variantABetter = false; - bool variantBBetter = false; - auto results = aggregator.GetAggregatedResults(); - for (const auto& stat : results) { - auto maybeSpecialType = magic_enum::enum_cast(stat.shaderType); - if (maybeSpecialType.has_value() && *maybeSpecialType == SpecialShaderType::Total) { // Total row - if (stat.meanA < stat.meanB) { - variantABetter = true; // A has lower frame time (better) - } else if (stat.meanB < stat.meanA) { - variantBBetter = true; // B has lower frame time (better) - } - break; - } + ImGui::TextUnformatted("Differences between USER (A) and TEST (B) configs:"); + if (this->settingsDiff.empty()) { + ImGui::TextUnformatted("No setting changes detected between USER (A) and TEST (B) configs."); + } else if (ImGui::BeginTable("ABSettingsDiffTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable)) { + ImGui::TableSetupColumn("Setting Path", ImGuiTableColumnFlags_DefaultSort); + ImGui::TableSetupColumn("A Value"); + ImGui::TableSetupColumn("B Value"); + ImGui::TableHeadersRow(); + + // Determine which variant performed better based on Total row + bool variantABetter = false; + bool variantBBetter = false; + auto results = aggregator.GetAggregatedResults(); + for (const auto& stat : results) { + auto maybeSpecialType = magic_enum::enum_cast(stat.shaderType); + if (maybeSpecialType.has_value() && *maybeSpecialType == SpecialShaderType::Total) { // Total row + if (stat.meanA < stat.meanB) { + variantABetter = true; // A has lower frame time (better) + } else if (stat.meanB < stat.meanA) { + variantBBetter = true; // B has lower frame time (better) } + break; + } + } - // Get theme for color coding - const auto& theme = menu->GetTheme(); - - // Sort the settings diff if needed - std::vector sortedDiff = this->settingsDiff; - if (const ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs()) { - if (sortSpecs->SpecsCount > 0) { - int sortCol = sortSpecs->Specs->ColumnIndex; - bool sortAsc = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; - std::sort(sortedDiff.begin(), sortedDiff.end(), [sortCol, sortAsc](const SettingsDiffEntry& a, const SettingsDiffEntry& b) { - if (sortCol == 0) - return sortAsc ? (a.path < b.path) : (a.path > b.path); - if (sortCol == 1) - return sortAsc ? (a.aValue < b.aValue) : (a.aValue > b.aValue); - if (sortCol == 2) - return sortAsc ? (a.bValue < b.bValue) : (a.bValue > b.bValue); - return false; - }); - } - } - for (const auto& entry : sortedDiff) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(entry.path.c_str()); - // Only show the path as text, no custom tooltip guessing - ImGui::TableSetColumnIndex(1); - // Color A value based on performance - if (variantABetter) { - ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.SuccessColor); - ImGui::TextUnformatted(entry.aValue.c_str()); - ImGui::PopStyleColor(); - } else if (variantBBetter) { - ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.Error); - ImGui::TextUnformatted(entry.aValue.c_str()); - ImGui::PopStyleColor(); - } else { - ImGui::TextUnformatted(entry.aValue.c_str()); - } - ImGui::TableSetColumnIndex(2); - // Color B value based on performance - if (variantBBetter) { - ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.SuccessColor); - ImGui::TextUnformatted(entry.bValue.c_str()); - ImGui::PopStyleColor(); - } else if (variantABetter) { - ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.Error); - ImGui::TextUnformatted(entry.bValue.c_str()); - ImGui::PopStyleColor(); - } else { - ImGui::TextUnformatted(entry.bValue.c_str()); - } - } - ImGui::EndTable(); + // Get theme for color coding + const auto& theme = menu->GetTheme(); + + // Sort the settings diff if needed + std::vector sortedDiff = this->settingsDiff; + if (const ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs()) { + if (sortSpecs->SpecsCount > 0) { + int sortCol = sortSpecs->Specs->ColumnIndex; + bool sortAsc = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; + std::sort(sortedDiff.begin(), sortedDiff.end(), [sortCol, sortAsc](const SettingsDiffEntry& a, const SettingsDiffEntry& b) { + if (sortCol == 0) + return sortAsc ? (a.path < b.path) : (a.path > b.path); + if (sortCol == 1) + return sortAsc ? (a.aValue < b.aValue) : (a.aValue > b.aValue); + if (sortCol == 2) + return sortAsc ? (a.bValue < b.bValue) : (a.bValue > b.bValue); + return false; + }); } } - ImGui::Separator(); + for (const auto& entry : sortedDiff) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(entry.path.c_str()); + // Only show the path as text, no custom tooltip guessing + ImGui::TableSetColumnIndex(1); + // Color A value based on performance + if (variantABetter) { + ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.SuccessColor); + ImGui::TextUnformatted(entry.aValue.c_str()); + ImGui::PopStyleColor(); + } else if (variantBBetter) { + ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.Error); + ImGui::TextUnformatted(entry.aValue.c_str()); + ImGui::PopStyleColor(); + } else { + ImGui::TextUnformatted(entry.aValue.c_str()); + } + ImGui::TableSetColumnIndex(2); + // Color B value based on performance + if (variantBBetter) { + ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.SuccessColor); + ImGui::TextUnformatted(entry.bValue.c_str()); + ImGui::PopStyleColor(); + } else if (variantABetter) { + ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.Error); + ImGui::TextUnformatted(entry.bValue.c_str()); + ImGui::PopStyleColor(); + } else { + ImGui::TextUnformatted(entry.bValue.c_str()); + } + } + ImGui::EndTable(); } + ImGui::Separator(); } } } diff --git a/src/Features/PerformanceOverlay.h b/src/Features/PerformanceOverlay.h index 083d1724fd..d29fc6bebd 100644 --- a/src/Features/PerformanceOverlay.h +++ b/src/Features/PerformanceOverlay.h @@ -129,6 +129,10 @@ struct PerformanceOverlay : OverlayFeature virtual void DrawSettings() override; virtual void DataLoaded() override; void DrawOverlay() override; + // Settings persistence and defaults + void SaveSettings(json& j) override; + void LoadSettings(json& j) override; + void RestoreDefaultSettings() override; // ============================================================================ // CORE PERFORMANCE DISPLAY FUNCTIONS @@ -159,7 +163,7 @@ struct PerformanceOverlay : OverlayFeature // ============================================================================ // A/B TESTING FUNCTIONS // ============================================================================ - void DrawABTestSection(const std::vector& allRows, bool showCollapsibleSections); + void DrawABTestSection(const std::vector& allRows); void DrawABTestResultsTable(); void DrawABTestStatisticalValidity(const Menu::ThemeSettings& theme, const ABTestAggregator& aggregator) const; void ConvertABTestResultsToRows(const std::vector& results, std::vector& mainRows, std::vector& summaryRows) const; diff --git a/src/Features/PhysicalSky.cpp b/src/Features/PhysicalSky.cpp index 19c9150426..e831948805 100644 --- a/src/Features/PhysicalSky.cpp +++ b/src/Features/PhysicalSky.cpp @@ -17,14 +17,19 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( trMix, apLumMix, apTrMix, + cloudShadowRemapRange, sunlightColor, masserColor, secundaColor, + proceduralSun, + sunDiskRad, adaptationStart, adaptationEnd, dayExposure, nightExposure, groundAlbedo, + planetRadius, + atmosphereRadius, rayleighFalloff, rayleighScatter, aerosolFalloff, @@ -186,6 +191,10 @@ void PhysicalSky::SettingsCelestials() ImGui::ColorEdit3("Light Color", &settings.sunlightColor.x, ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_Float | ImGuiColorEditFlags_HDR); if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text(lightColorHint); + ImGui::Checkbox("Procedural Sun", &settings.proceduralSun); + ImGui::SliderAngle("Sun Disk Angular Radius", &settings.sunDiskRad, 0.f, 5.f, "%.2f deg", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Real world sun disk angular radius is about 0.27 degrees."); ImGui::PopID(); } @@ -261,6 +270,17 @@ void PhysicalSky::SettingsAtmosphere() ImGui::DragFloat("Layer Thickness", &settings.ozoneThickness, .1f, 0.f, 50.f, "%.3f km"); ImGui::PopID(); } + + ImGui::SeparatorText("Planetary Parameters"); + { + ImGui::InputFloat("Planet Radius", &settings.planetRadius, 1.f, 100000.f, "%.1f km"); + ImGui::InputFloat("Atmosphere Radius", &settings.atmosphereRadius, 1.f, 100000.f, "%.1f km"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + "Planet radius is the distance from the planet center to sea level.\n" + "Atmosphere radius is the distance from the planet center to the top of atmosphere.\n" + "On Earth, they are about 6360 km and 6420 km respectively."); + } } void PhysicalSky::SettingsClouds() @@ -515,13 +535,14 @@ void PhysicalSky::Reset() .masserColor = settings.masserColor * exposure, .apTrMix = settings.apTrMix, .secundaDir = { secundaDir.x, secundaDir.y, secundaDir.z }, + .sunDiskCos = cos(settings.sunDiskRad) * (settings.proceduralSun ? 1.f : 0.f), .secundaColor = settings.secundaColor * exposure, .enabled = allGood, .tonemapper = settings.tonemapper, .vanillaMix = settings.vanillaMix, .zBottom = worldspaceInfo.zBottom, - .rPlanet = 6.36e3f / Util::Units::GAME_UNIT_TO_KM, - .rAtmosphere = 6.42e3f / Util::Units::GAME_UNIT_TO_KM, + .rPlanet = settings.planetRadius / Util::Units::GAME_UNIT_TO_KM, + .rAtmosphere = settings.atmosphereRadius / Util::Units::GAME_UNIT_TO_KM, .groundAlbedo = settings.groundAlbedo, .cloudShadowRemapRange = settings.cloudShadowRemapRange, .aerosolFalloff = settings.aerosolFalloff * Util::Units::GAME_UNIT_TO_KM, diff --git a/src/Features/PhysicalSky.h b/src/Features/PhysicalSky.h index ac27da7f1b..f08fde78f7 100644 --- a/src/Features/PhysicalSky.h +++ b/src/Features/PhysicalSky.h @@ -90,6 +90,9 @@ struct PhysicalSky final : public Feature float3 masserColor = float3{ 1.0f, 0.6f, 0.6f } * 5e-3f; float3 secundaColor = float3{ 0.8f, 1.0f, 1.0f } * 5e-3f; + bool proceduralSun = true; + float sunDiskRad = DirectX::XMConvertToRadians(0.53f); + float adaptationStart = DirectX::XMConvertToRadians(-2); float adaptationEnd = DirectX::XMConvertToRadians(-15); float dayExposure = 1e-2f; @@ -108,6 +111,9 @@ struct PhysicalSky final : public Feature }; float3 groundAlbedo = { .2f, .2f, .2f }; + float planetRadius = 6.36e3f; // in km + float atmosphereRadius = 6.42e3f; // in km + float rayleighFalloff = 1 / 8.69645f; // in km^-1 float3 rayleighScatter = { 6.6049f, 12.345f, 29.413f }; // in megameter^-1 float aerosolFalloff = 1 / 1.2f; @@ -141,7 +147,7 @@ struct PhysicalSky final : public Feature float3 masserColor; float apTrMix; // float3 secundaDir; - float _pad3; // + float sunDiskCos; // float3 secundaColor; // GENERAL diff --git a/src/Features/RenderDoc.cpp b/src/Features/RenderDoc.cpp new file mode 100644 index 0000000000..950ccf7954 --- /dev/null +++ b/src/Features/RenderDoc.cpp @@ -0,0 +1,841 @@ +// RenderDoc feature implementation providing in-application graphics debugging capabilities +#include "Features/RenderDoc.h" + +#include "Globals.h" +#include "Utils/FileSystem.h" +#include "Utils/Format.h" +// Include additional core headers required by the feature implementation +#include "Menu.h" +#include "Plugin.h" +#include "State.h" +#include "Utils/UI.h" +#include + +// Include the real RenderDoc API and Windows headers only in the implementation +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Global feature instance implementation +RenderDoc* RenderDoc::GetSingleton() +{ + return &globals::features::renderDoc; +} + +void RenderDoc::Load() +{ + // Only load RenderDoc if the user has enabled capture + if (!enableRenderDocCapture) { + logger::debug("[RenderDoc] RenderDoc capture disabled, skipping initialization"); + return; + } + + // Load RenderDoc only from our expected location. Do not fall back to system PATH. + std::filesystem::path renderdocPath = GetRenderDocDllPath(); + if (renderdocPath.empty() || !std::filesystem::exists(renderdocPath)) { + logger::debug("[RenderDoc] renderdoc.dll not found at expected path: {}", renderdocPath.string()); + return; + } + + std::wstring widePath = renderdocPath.wstring(); + HMODULE moduleHandle = LoadLibraryW(widePath.c_str()); + logger::debug("[RenderDoc] Attempting to load renderdoc.dll from {}", renderdocPath.string()); + + renderDocModule = moduleHandle; + if (!renderDocModule) { + logger::debug("[RenderDoc] Failed to load renderdoc.dll from {}", renderdocPath.string()); + return; + } + + // Log RenderDoc DLL version if possible. Prefer the actual loaded module path + try { + std::wstring dllPathW; + // If we had a computed path and it exists, prefer that; otherwise ask the OS for the loaded module filename + if (!renderdocPath.empty() && std::filesystem::exists(renderdocPath)) { + dllPathW = renderdocPath.wstring(); + } else { + wchar_t buf[MAX_PATH]{ 0 }; + if (GetModuleFileNameW((HMODULE)renderDocModule, buf, (DWORD)std::size(buf)) != 0) { + dllPathW = std::wstring(buf); + } + } + + if (!dllPathW.empty()) { + auto ver = Util::GetDllVersion(dllPathW); + if (ver.has_value()) { + logger::info("[RenderDoc] Loaded renderdoc.dll version {} (from {})", Util::GetFormattedVersion(*ver), Util::WStringToString(dllPathW)); + } else { + logger::info("[RenderDoc] Loaded renderdoc.dll (version unknown) from {}", Util::WStringToString(dllPathW)); + } + } else { + logger::info("[RenderDoc] Loaded renderdoc.dll (module path unknown)"); + } + } catch (...) { + logger::info("[RenderDoc] Loaded renderdoc.dll (version lookup failed)"); + } + + // Get the API function pointer + auto RENDERDOC_GetAPI = (pRENDERDOC_GetAPI)GetProcAddress((HMODULE)renderDocModule, "RENDERDOC_GetAPI"); + if (!RENDERDOC_GetAPI) { + logger::warn("[RenderDoc] Failed to get RENDERDOC_GetAPI function"); + FreeLibrary((HMODULE)renderDocModule); + renderDocModule = nullptr; + return; + } + + // Get the API interface + int ret = RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_6_0, (void**)&renderDocApi); + if (ret != 1 || !renderDocApi) { + logger::warn("[RenderDoc] Failed to get API interface"); + FreeLibrary((HMODULE)renderDocModule); + renderDocModule = nullptr; + renderDocApi = nullptr; + return; + } + + // Build a base capture file path template including the Skyrim runtime and version so captures are easy to identify + try { + auto capturesDir = GetCapturesPath(); + Util::FileHelpers::EnsureDirectoryExists(capturesDir); + + // Format runtime + game version into filename base + auto runtimeName = std::string{ magic_enum::enum_name(REL::Module::GetRuntime()) }; + auto gameVersion = Util::GetFormattedVersion(REL::Module::get().version()); + + // Build the path using std::filesystem so we don't hardcode separators + std::filesystem::path fileBase = capturesDir / std::format("Skyrim_{}_{}", runtimeName, gameVersion); + renderDocApi->SetCaptureFilePathTemplate(fileBase.string().c_str()); + } catch (const std::exception& e) { + logger::warn("[RenderDoc] Failed to prepare capture directory/template: {}", e.what()); + } + + renderDocApi->MaskOverlayBits(eRENDERDOC_Overlay_None, eRENDERDOC_Overlay_None); + + // Initialize capture count tracking + lastCaptureCount = renderDocApi->GetNumCaptures(); + + logger::info("[RenderDoc] Successfully initialized"); +} + +void RenderDoc::DrawSettings() +{ + // Track section visibility for intelligent cache refreshing + bool isSectionVisible = false; + + // Include enable toggle and annotation forcing logic here + bool prevRenderDocCapture = enableRenderDocCapture; + if (ImGui::Checkbox("Enable RenderDoc Capture", &enableRenderDocCapture)) { + if (enableRenderDocCapture && !prevRenderDocCapture) { + globals::state->useFrameAnnotations = globals::state->frameAnnotations; + globals::state->frameAnnotations = true; + } + if (!enableRenderDocCapture && prevRenderDocCapture) { + globals::state->frameAnnotations = globals::state->useFrameAnnotations; + } + } + + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Enable RenderDoc frame capture for providing debug captures to the Community Shaders team."); + ImGui::Text("Enabling capture will force-enable frame annotations for easier debugging and will restore the previous setting when disabled."); + } + + // The rest of the UI renders only when capture is active + bool renderDocCaptureEnabled = enableRenderDocCapture; + bool renderDocActive = IsAvailable(); + + const auto& themeSettings = Menu::GetSingleton()->GetTheme(); + + if (renderDocCaptureEnabled && !renderDocActive) { + ImGui::TextColored(themeSettings.StatusPalette.RestartNeeded, "Requires restart to enable RenderDoc capture."); + return; + } + + if (!renderDocCaptureEnabled && renderDocActive) { + ImGui::TextColored(themeSettings.StatusPalette.Warning, "Requires restart to disable RenderDoc capture, performance will be severely impacted."); + return; + } + + if (renderDocCaptureEnabled && renderDocActive) { + isSectionVisible = true; + // Capture Control Section + { + auto captureSection = Util::SectionWrapper("Capture Control", "Manual capture creation and basic controls"); + if (captureSection) { + ImGui::TextColored(themeSettings.StatusPalette.InfoColor, "RenderDoc capture is active."); + ImGui::SameLine(); + + std::string enabledFeaturesPreview; + for (auto* feat : Feature::GetFeatureList()) { + if (!feat->loaded) + continue; + + std::string ver = feat->version.empty() ? std::string("") : feat->version; + if (!enabledFeaturesPreview.empty()) + enabledFeaturesPreview += '\n'; + enabledFeaturesPreview += std::format("- {} ({})", feat->GetShortName(), ver); + } + + // Comments input for next capture + static char commentsBuffer[kCommentsBufferSize] = { 0 }; + + ImGui::InputTextWithHint("##CaptureComments", "Additional comments for next capture (optional)", commentsBuffer, sizeof(commentsBuffer)); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Additional comments will be appended to automatic metadata and embedded in the .rdc file"); + } + + if (ImGui::Button("Create Capture")) { + // Check available disk space before allowing capture + try { + auto capturesDir = GetCapturesDirectory(); + std::error_code ec; + uint64_t freeSpace = std::filesystem::space(capturesDir, ec).available; + if (ec) { + logger::warn("[RenderDoc] Failed to check available space in '{}': {}", capturesDir, ec.message()); + freeSpace = 0; + } + + if (freeSpace < kMinCaptureSpaceBytes) { + ImGui::OpenPopup("Not enough disk space##RenderDoc"); + } else { + // Set comments if provided + if (strlen(commentsBuffer) > 0) { + // Build complete comments with automatic metadata plus user input + std::string userComments = std::string(commentsBuffer); + + std::string completeComments = BuildAutomaticCaptureComments(userComments); + + SetPendingCaptureComments(completeComments); + memset(commentsBuffer, 0, sizeof(commentsBuffer)); // Clear the buffer + } + + // Actual capture logic + logger::info("[RenderDoc] Manual capture triggered by user"); + TriggerCapture(); + } + } catch (const std::exception& e) { + logger::error("[RenderDoc] Exception during capture logic: {}", e.what()); + } + } + + if (ImGui::BeginPopup("Not enough disk space##RenderDoc")) { + ImGui::Text("Not enough free disk space to create a capture."); + ImGui::Text("At least {} MB of free space is required.", kMinCaptureSpaceBytes / (1024 * 1024)); + if (ImGui::Button("OK")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button("Open Capture Directory")) { + // Open the directory where captures are saved + try { + auto capturesDir = GetCapturesDirectory(); + ShellExecuteA(nullptr, "open", capturesDir.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } catch (const std::exception& e) { + logger::error("[RenderDoc] Exception while trying to open captures directory: {}", e.what()); + } + } + + ImGui::TextDisabled("Capture Directory: %s", GetCapturesDirectory().c_str()); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Right-click to copy the directory path."); + } + + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("Copy Directory Path")) { + // Copy the captures directory path to clipboard + try { + auto capturesDir = GetCapturesDirectory(); + ImGui::SetClipboardText(capturesDir.c_str()); + logger::info("[RenderDoc] Copied captures directory path to clipboard: {}", capturesDir); + } catch (const std::exception& e) { + logger::error("[RenderDoc] Exception while copying directory path: {}", e.what()); + } + } + ImGui::EndPopup(); + } + } + } + + // Disk Usage Section + { + auto diskSection = Util::SectionWrapper("Disk Usage", "Monitor capture storage usage"); + if (diskSection) { + uint32_t diskUsageMB = CalculateCapturesDiskUsage(); + float diskUsageGB = static_cast(diskUsageMB) / 1024.0f; + + // Use color-coded value display for disk usage + Util::ColorCodedValueConfig diskUsageConfig = Util::ColorCodedValueConfig::HighIsBad(0.1f, 1.0f, 5.0f); + diskUsageConfig.tooltipText = "Total size of all capture files in the captures directory"; + + Util::DrawColorCodedValue("Capture Size", diskUsageGB, std::format("{:.2f} GB", diskUsageGB), diskUsageConfig); + + if (diskUsageMB > 0) { + ImGui::SameLine(); + if (ImGui::Button("Clear All Captures")) { + ImGui::OpenPopup("Confirm Clear Captures##RenderDoc"); + } + } + + if (ImGui::BeginPopup("Confirm Clear Captures##RenderDoc")) { + ImGui::Text("Are you sure you want to delete all capture files?"); + ImGui::Text("This will permanently remove %u MB of capture data.", diskUsageMB); + ImGui::Separator(); + + if (ImGui::Button("Yes, Delete All")) { + ClearFrameCaptures(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + } + + // Capture Files Section + { + auto filesSection = Util::SectionWrapper("Capture Files", "View and manage individual capture files"); + if (filesSection) { + // Get cached capture files (auto-refreshes every 5 seconds) + const auto& captureFiles = GetCachedCaptureFiles(); + + // Refresh button + if (ImGui::Button("Refresh List")) { + ClearFailedDeletions(); + RefreshCaptureFileCache(); + } + + ImGui::SameLine(); + ImGui::TextDisabled("(%zu files)", captureFiles.size()); + + if (captureFiles.empty()) { + ImGui::TextDisabled("No capture files found."); + } else { + // Display custom table with double-click and hover support + if (ImGui::BeginTable("##RenderDocCaptures", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable | ImGuiTableFlags_SortTristate)) { + // Setup headers + ImGui::TableSetupColumn("Filename", ImGuiTableColumnFlags_DefaultSort); + ImGui::TableSetupColumn("Size"); + ImGui::TableSetupColumn("Created", ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_PreferSortDescending); + ImGui::TableHeadersRow(); + + // Create a sorted copy of the capture files for display + static std::vector sortedCaptureFiles; + static std::chrono::steady_clock::time_point sortedCacheLastUpdate = std::chrono::steady_clock::time_point::min(); + static ImGuiTableSortSpecs* sortSpecs = nullptr; + + // Update sorted copy if cache has been refreshed or sorting specs changed + bool needsSortUpdate = (cacheLastUpdate != sortedCacheLastUpdate) || (sortedCaptureFiles.size() != cachedCaptureFiles.size()); + + // Handle sorting + if (ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs()) { + sortSpecs = specs; + if (specs->SpecsDirty || needsSortUpdate) { + // Copy the current cache and sort it + sortedCaptureFiles = cachedCaptureFiles; + std::sort(sortedCaptureFiles.begin(), sortedCaptureFiles.end(), + [specs](const CaptureFileInfo& a, const CaptureFileInfo& b) { + for (int i = 0; i < specs->SpecsCount; ++i) { + const ImGuiTableColumnSortSpecs* spec = &specs->Specs[i]; + int col = spec->ColumnIndex; + bool ascending = spec->SortDirection == ImGuiSortDirection_Ascending; + + int cmp = 0; + switch (col) { + case 0: // Filename + cmp = a.filename.compare(b.filename); + break; + case 1: // Size + cmp = (a.fileSize < b.fileSize) ? -1 : (a.fileSize > b.fileSize) ? 1 : + 0; + break; + case 2: // Created (time) + cmp = (a.lastWriteTime < b.lastWriteTime) ? -1 : (a.lastWriteTime > b.lastWriteTime) ? 1 : + 0; + break; + } + + if (cmp != 0) { + return ascending ? (cmp < 0) : (cmp > 0); + } + } + return false; + }); + + specs->SpecsDirty = false; + sortedCacheLastUpdate = cacheLastUpdate; + } + } else if (needsSortUpdate) { + // No sorting specs, just copy the cache (newest first by default) + sortedCaptureFiles = cachedCaptureFiles; + sortedCacheLastUpdate = cacheLastUpdate; + } + + // Display rows from the sorted copy + for (size_t i = 0; i < sortedCaptureFiles.size(); ++i) { + const auto& file = sortedCaptureFiles[i]; + ImGui::TableNextRow(); + + // Filename column with double-click and hover + ImGui::TableSetColumnIndex(0); + bool isSelected = false; + + // Push red color for failed deletions + if (file.deletionFailed) { + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 100, 100, 255)); // Red text for failed deletions + } + + if (ImGui::Selectable(file.filename.c_str(), isSelected, ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + // Double-clicked - open the file + try { + ShellExecuteW(nullptr, L"open", file.fullPath.wstring().c_str(), nullptr, nullptr, SW_SHOWNORMAL); + logger::info("[RenderDoc] Opened capture file: {}", file.fullPath.string()); + } catch (const std::exception& e) { + logger::error("[RenderDoc] Failed to open capture file '{}': {}", file.fullPath.string(), e.what()); + } + } + } + + // Pop color if we pushed it + if (file.deletionFailed) { + ImGui::PopStyleColor(); + } + + // Hover tooltip for filename + if (ImGui::IsItemHovered()) { + // Calculate time ago dynamically for tooltip + std::string currentTimeAgo = Util::FormatTimeAgo(file.lastWriteTime); + std::string tooltip = std::format("File: {}\nSize: {}\nCreated: {}", + file.fullPath.string(), file.sizeStr, currentTimeAgo); + + // Add deletion error message if applicable + if (file.deletionFailed && !file.deletionErrorMessage.empty()) { + tooltip += std::format("\n\nDeletion Failed: {}", file.deletionErrorMessage); + } + + ImGui::SetTooltip("%s", tooltip.c_str()); + } + + // Size column + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(file.sizeStr.c_str()); + + // Created column - calculate time ago dynamically + ImGui::TableSetColumnIndex(2); + std::string currentTimeAgo = Util::FormatTimeAgo(file.lastWriteTime); + ImGui::TextUnformatted(currentTimeAgo.c_str()); + } + + ImGui::EndTable(); + } + + ImGui::TextDisabled("Double-click a filename to open the capture file"); + ImGui::TextDisabled("Hover over filenames for file details"); + } + } + } + + // Intelligent cache refreshing: refresh when section becomes visible + if (isSectionVisible && !wasSectionVisible) { + InvalidateCaptureCache(); + } + wasSectionVisible = isSectionVisible; + } +} + +std::string RenderDoc::GetCapturesDirectory() const +{ + auto path = GetCapturesPath(); + Util::FileHelpers::EnsureDirectoryExists(path); + return path.string(); +} + +uint32_t RenderDoc::CalculateCapturesDiskUsage() +{ + try { + auto path = GetCapturesPath(); + // Accumulate the total size of all capture files in the directory + uint64_t totalSize = 0; + std::error_code ec; + for (const auto& entry : std::filesystem::directory_iterator(path, ec)) { + if (ec) { + logger::warn("[RenderDoc] Failed to iterate captures directory '{}': {}", path.string(), ec.message()); + break; + } + + if (entry.is_regular_file()) { + totalSize += entry.file_size(ec); + if (ec) { + logger::warn("[RenderDoc] Failed to get file size for '{}': {}", entry.path().string(), ec.message()); + } + } + } + + return static_cast(totalSize / (1024 * 1024)); // Return size in MB + } catch (const std::exception& e) { + logger::error("[RenderDoc] Exception in CalculateCapturesDiskUsage: {}", e.what()); + return 0; + } +} + +void RenderDoc::ClearFrameCaptures() +{ + try { + auto path = GetCapturesPath(); + + // Clear previous failed deletion tracking for a fresh start + failedDeletions.clear(); + + // Remove all files in the captures directory using safe deletion + std::error_code ec; + for (const auto& entry : std::filesystem::directory_iterator(path, ec)) { + if (ec) { + logger::warn("[RenderDoc] Failed to iterate captures directory '{}': {}", path.string(), ec.message()); + break; + } + + if (entry.is_regular_file()) { + // Use safe delete with proper error handling and logging + auto deleteResult = Util::FileHelpers::SafeDelete(entry.path().string(), "capture file"); + if (!deleteResult.success) { + logger::warn("[RenderDoc] Failed to delete capture file '{}': {}", entry.path().string(), deleteResult.errorMessage); + + // Track this file as failed deletion + failedDeletions[entry.path()] = deleteResult.errorMessage; + } + } + } + + // Invalidate cache to refresh the UI and show failed deletions + InvalidateCaptureCache(); + } catch (const std::exception& e) { + logger::error("[RenderDoc] Exception in ClearFrameCaptures: {}", e.what()); + } +} + +std::filesystem::path RenderDoc::GetCapturesPath() const +{ + return Util::PathHelpers::GetCommunityShaderPath() / "Captures"; +} + +std::filesystem::path RenderDoc::GetRenderDocDllPath() const +{ + // RenderDoc DLL should be in Data/Renderdoc/ + return Util::PathHelpers::GetDataPath() / "Renderdoc" / "renderdoc.dll"; +} + +void RenderDoc::SetupResources() +{ + // RenderDoc doesn't need any D3D resources +} + +void RenderDoc::SaveSettings(json& o_json) +{ + o_json["Enable RenderDoc Capture"] = enableRenderDocCapture; +} + +void RenderDoc::LoadSettings(json& o_json) +{ + if (o_json.contains("Enable RenderDoc Capture") && o_json["Enable RenderDoc Capture"].is_boolean()) { + enableRenderDocCapture = o_json["Enable RenderDoc Capture"]; + } +} + +void RenderDoc::RestoreDefaultSettings() +{ + enableRenderDocCapture = false; +} + +void RenderDoc::ClearShaderCache() +{ + // RenderDoc doesn't have shaders to clear +} + +void RenderDoc::RefreshCaptureFileCache() +{ + cachedCaptureFiles.clear(); + cacheValid = false; + + try { + auto capturesPath = GetCapturesPath(); + std::error_code ec; + for (const auto& entry : std::filesystem::directory_iterator(capturesPath, ec)) { + if (ec || !entry.is_regular_file()) + continue; + + CaptureFileInfo info; + info.fullPath = entry.path(); + info.filename = entry.path().filename().string(); + info.fileSize = entry.file_size(ec); + if (ec) + continue; + + info.sizeStr = Util::FormatFileSize(info.fileSize); + + info.lastWriteTime = entry.last_write_time(ec); + + // Check if this file previously failed to delete + auto failedIt = failedDeletions.find(info.fullPath); + if (failedIt != failedDeletions.end()) { + info.deletionFailed = true; + info.deletionErrorMessage = failedIt->second; + } + + cachedCaptureFiles.push_back(info); + } + + // Sort by modification time (newest first) + std::sort(cachedCaptureFiles.begin(), cachedCaptureFiles.end(), + [](const CaptureFileInfo& a, const CaptureFileInfo& b) { + return a.lastWriteTime > b.lastWriteTime; + }); + + cacheLastUpdate = std::chrono::steady_clock::now(); + cacheValid = true; + + // Apply automatic comments to any new captures + ApplyAutomaticCommentsToNewCaptures(); + } catch (const std::exception& e) { + logger::warn("[RenderDoc] Failed to refresh capture file cache: {}", e.what()); + cacheValid = false; + } +} + +const std::vector& RenderDoc::GetCachedCaptureFiles() +{ + // Check if cache needs refresh (invalidate after 5 seconds or if not valid) + auto now = std::chrono::steady_clock::now(); + auto cacheAge = now - cacheLastUpdate; + auto maxAge = std::chrono::seconds(kCacheRefreshIntervalSeconds); + + if (!cacheValid || cacheAge > maxAge) { + RefreshCaptureFileCache(); + } + + return cachedCaptureFiles; +} + +void RenderDoc::InvalidateCaptureCache() +{ + cacheValid = false; + cacheLastUpdate = std::chrono::steady_clock::time_point::min(); +} + +void RenderDoc::TriggerCapture() +{ + if (!renderDocApi) { + logger::warn("[RenderDoc] Cannot trigger capture - RenderDoc API not available"); + return; + } + + logger::info("[RenderDoc] Triggering immediate capture"); + renderDocApi->TriggerCapture(); + + // Invalidate cache so it refreshes when next accessed (capture should appear in list) + InvalidateCaptureCache(); +} + +void RenderDoc::SetCaptureFilePathTemplate(const std::string& a_template) +{ + if (!renderDocApi) { + logger::warn("[RenderDoc] Cannot set capture template - RenderDoc API not available"); + return; + } + + renderDocApi->SetCaptureFilePathTemplate(a_template.c_str()); + logger::info("[RenderDoc] Set capture file path template to: {}", a_template); +} + +bool RenderDoc::IsCapturing() const +{ + if (!renderDocApi) + return false; + + // RenderDoc API doesn't have a direct IsCapturing method, but we can check if captures are enabled + return enableRenderDocCapture && renderDocApi != nullptr; +} + +std::string RenderDoc::GetCapturePath(uint32_t a_index) +{ + if (!renderDocApi) { + logger::warn("[RenderDoc] Cannot get capture path - RenderDoc API not available"); + return ""; + } + + uint32_t numCaptures = renderDocApi->GetNumCaptures(); + if (a_index >= numCaptures) { + logger::warn("[RenderDoc] Capture index {} out of range ({} captures available)", a_index, numCaptures); + return ""; + } + + // Get the required buffer size first + uint32_t pathLength = 0; + uint32_t result = renderDocApi->GetCapture(a_index, nullptr, &pathLength, nullptr); + if (result == 0 || pathLength == 0) { + logger::warn("[RenderDoc] Failed to get capture path length for index {}", a_index); + return ""; + } + + // Allocate buffer and get the path + std::vector pathBuffer(pathLength + 1, 0); + result = renderDocApi->GetCapture(a_index, pathBuffer.data(), &pathLength, nullptr); + if (result == 0) { + logger::warn("[RenderDoc] Failed to get capture path for index {}", a_index); + return ""; + } + + return std::string(pathBuffer.data()); +} + +uint32_t RenderDoc::GetNumCaptures() const +{ + if (!renderDocApi) + return 0; + return renderDocApi->GetNumCaptures(); +} + +void RenderDoc::SetPendingCaptureComments(std::string a_comments) +{ + std::lock_guard lock(pendingCommentsMutex); + pendingCaptureComments = std::move(a_comments); + logger::info("[RenderDoc] Set pending capture comments: {}", pendingCaptureComments); +} + +void RenderDoc::ApplyPendingCaptureComments(uint32_t a_index) +{ + if (!renderDocApi) { + logger::warn("[RenderDoc] Cannot apply capture comments - RenderDoc API not available"); + return; + } + + std::lock_guard lock(pendingCommentsMutex); + if (pendingCaptureComments.empty()) { + return; + } + + uint32_t numCaptures = renderDocApi->GetNumCaptures(); + if (a_index >= numCaptures) { + logger::warn("[RenderDoc] Cannot apply comments to capture {} - index out of range ({} captures available)", a_index, numCaptures); + return; + } + + // Get the capture file path + std::string capturePath = GetCapturePath(a_index); + if (capturePath.empty()) { + logger::warn("[RenderDoc] Cannot get capture path for index {}", a_index); + return; + } + + // Use RenderDoc's built-in SetCaptureFileComments API to embed comments in the .rdc file + renderDocApi->SetCaptureFileComments(capturePath.c_str(), pendingCaptureComments.c_str()); + logger::info("[RenderDoc] Applied comments to capture {}: {}", a_index, pendingCaptureComments); + + pendingCaptureComments.clear(); +} + +std::string RenderDoc::BuildAutomaticCaptureComments(const std::string& userComments) const +{ + std::string comments; + + // Runtime information + auto runtime = REL::Module::GetRuntime(); + auto runtimeName = std::string{ magic_enum::enum_name(runtime) }; + auto gameVersion = Util::GetFormattedVersion(REL::Module::get().version()); + comments += std::format("Skyrim {} {}\n", runtimeName, gameVersion); + + // Plugin version + auto pluginVersion = Util::GetFormattedVersion(Plugin::VERSION); + comments += std::format("Community Shaders {}\n", pluginVersion); + + // Enabled features + const auto& features = Feature::GetFeatureList(); + std::vector enabledFeatures; + for (auto* feature : features) { + if (feature->loaded) { + std::string featVersion = feature->version.empty() ? "unknown" : feature->version; + enabledFeatures.push_back(std::format("{} ({})", feature->GetShortName(), featVersion)); + } + } + + // Sort features alphabetically for easier comparison + std::sort(enabledFeatures.begin(), enabledFeatures.end()); + + if (!enabledFeatures.empty()) { + comments += "Enabled Features:\n"; + for (const auto& feature : enabledFeatures) { + comments += std::format("- {}\n", feature); + } + } + + // Add user comments if provided + if (!userComments.empty()) { + comments += "\nUser Comments:\n"; + comments += userComments; + } + + return comments; +} + +void RenderDoc::ApplyAutomaticCommentsToNewCaptures() +{ + if (!renderDocApi) { + return; + } + + uint32_t numCaptures = renderDocApi->GetNumCaptures(); + if (numCaptures <= lastCaptureCount) { + return; // No new captures + } + + // Check for pending user comments and apply them to the first new capture + std::string userComments; + { + std::lock_guard lock(pendingCommentsMutex); + if (!pendingCaptureComments.empty()) { + userComments = std::move(pendingCaptureComments); + pendingCaptureComments.clear(); + } + } + + // Build automatic comments for new captures + std::string automaticComments = BuildAutomaticCaptureComments(""); + + // Apply comments to new captures: user comments to first capture, automatic to others + for (uint32_t i = lastCaptureCount; i < numCaptures; ++i) { + std::string capturePath = GetCapturePath(i); + if (!capturePath.empty()) { + // Use user comments for the first new capture, automatic for subsequent ones + const std::string& commentsToApply = (i == lastCaptureCount && !userComments.empty()) ? userComments : automaticComments; + renderDocApi->SetCaptureFileComments(capturePath.c_str(), commentsToApply.c_str()); + logger::info("[RenderDoc] Applied comments to capture {}: {}", i, commentsToApply); + } + } + + lastCaptureCount = numCaptures; +} + +std::string RenderDoc::GetOverlayWarningMessage() const +{ + return "WARNING: RenderDoc capture is active, performance will be severely impacted.\n" + "Upscaling and Framegeneration may be incompatible.\n" + "Press F12, Print Screen or press the Capture button in the RenderDoc feature settings.\n" + "Disable RenderDoc capture in the RenderDoc feature settings."; +} + +void RenderDoc::ClearFailedDeletions() +{ + failedDeletions.clear(); + logger::info("[RenderDoc] Cleared failed deletion tracking"); +} diff --git a/src/Features/RenderDoc.h b/src/Features/RenderDoc.h new file mode 100644 index 0000000000..c661dc65b5 --- /dev/null +++ b/src/Features/RenderDoc.h @@ -0,0 +1,131 @@ +#pragma once + +#include "Feature.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +// Forward declarations +struct ID3D11Device; +struct RENDERDOC_API_1_6_0; + +// Structure to hold capture file information for UI display +struct CaptureFileInfo +{ + std::string filename; + std::string sizeStr; + uint64_t fileSize; + std::filesystem::file_time_type lastWriteTime; + std::filesystem::path fullPath; + bool deletionFailed = false; + std::string deletionErrorMessage; +}; + +class RenderDoc : public Feature +{ +public: + static RenderDoc* GetSingleton(); + + // Core RenderDoc functionality + bool IsAvailable() const { return renderDocApi != nullptr; } + void TriggerCapture(); + void SetCaptureFilePathTemplate(const std::string& a_template); + std::string GetCapturesDirectory() const; + bool IsCapturing() const; + std::string GetCapturePath(uint32_t a_index); + uint32_t GetNumCaptures() const; + uint32_t CalculateCapturesDiskUsage(); + void ClearFrameCaptures(); + void SetPendingCaptureComments(std::string a_comments); + void ApplyPendingCaptureComments(uint32_t a_index); + + // Feature overrides + std::string GetName() override { return "RenderDoc"; } + std::string GetShortName() override { return "RenderDoc"; } + std::string_view GetCategory() const override { return "Debug"; } + bool IsCore() const override { return true; } + bool IsInMenu() const override { return true; } + std::pair> GetFeatureSummary() override + { + return { "In-application RenderDoc capture support and convenience UI.", { "Attach comments to captures that appear in RenderDoc UI", "Open captures folder", "Capture file management" } }; + } + bool SupportsVR() override { return true; } + std::string_view GetShaderDefineName() override { return ""; } + bool HasShaderDefine(RE::BSShader::Type) override { return false; }; + + // Settings & UI + void DrawSettings() override; + void RestoreDefaultSettings() override; + void LoadSettings(json& o_json) override; + void SaveSettings(json& o_json) override; + + // Resources + void SetupResources() override; + void ClearShaderCache() override; + + // Lifecycle + void Load() override; + + // Helper methods + std::filesystem::path GetCapturesPath() const; + std::filesystem::path GetRenderDocDllPath() const; + std::string BuildAutomaticCaptureComments(const std::string& userComments) const; + void ApplyAutomaticCommentsToNewCaptures(); + void ClearFailedDeletions(); + + /** + * Gets the warning message to display when RenderDoc capture is active + * @return Formatted warning message for overlay display + */ + std::string GetOverlayWarningMessage() const; + +private: + // Cache management for capture files + void RefreshCaptureFileCache(); + const std::vector& GetCachedCaptureFiles(); + void InvalidateCaptureCache(); // Force cache refresh + +public: + RenderDoc() = default; + ~RenderDoc() = default; + + // Delete copy/move operations + RenderDoc(const RenderDoc&) = delete; + RenderDoc& operator=(const RenderDoc&) = delete; + RenderDoc(RenderDoc&&) = delete; + RenderDoc& operator=(RenderDoc&&) = delete; + + // RenderDoc library and API + void* renderDocModule = nullptr; + RENDERDOC_API_1_6_0* renderDocApi = nullptr; + + // Pending comments to attach to the next capture (applied when a new capture is detected) + std::string pendingCaptureComments; + mutable std::mutex pendingCommentsMutex; + + // RenderDoc capture enable setting + bool enableRenderDocCapture = false; + + // Track the last capture count we've processed for automatic comments + uint32_t lastCaptureCount = 0; + + // Cache for capture file information to avoid frequent filesystem access + std::vector cachedCaptureFiles; + std::chrono::steady_clock::time_point cacheLastUpdate; + bool cacheValid = false; + bool wasSectionVisible = false; // Track if RenderDoc section was visible last frame + + // Track files that failed to delete for UI feedback + std::unordered_map failedDeletions; + + static constexpr uint64_t kMinCaptureSpaceBytes = 100ULL * 1024ULL * 1024ULL; // 100 MB minimum free space + static constexpr uint32_t kCacheRefreshIntervalSeconds = 5; // Cache refresh interval + static constexpr size_t kCommentsBufferSize = 1024; // Size of comments input buffer +}; diff --git a/src/Features/ScreenSpaceGI.h b/src/Features/ScreenSpaceGI.h index 34d7446495..58ab5643b3 100644 --- a/src/Features/ScreenSpaceGI.h +++ b/src/Features/ScreenSpaceGI.h @@ -1,5 +1,7 @@ #pragma once +#include "Buffer.h" + struct ScreenSpaceGI : Feature { private: @@ -11,14 +13,7 @@ struct ScreenSpaceGI : Feature virtual inline std::string GetName() override { return "Screen Space GI"; } virtual inline std::string GetShortName() override { return "ScreenSpaceGI"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } - virtual inline std::string_view GetShaderDefineName() override { return "SSGI"; } virtual std::string_view GetCategory() const override { return "Lighting"; } - virtual inline bool HasShaderDefine(RE::BSShader::Type t) override - { - return t == RE::BSShader::Type::Lighting || - t == RE::BSShader::Type::Grass || - t == RE::BSShader::Type::DistantTree; - }; virtual std::pair> GetFeatureSummary() override { @@ -78,7 +73,7 @@ struct ScreenSpaceGI : Feature float Thickness = 32.f; float2 DepthFadeRange = { 4e4, 5e4 }; // gi - float GISaturation = 0.9f; + float GISaturation = 0.8f; float GIDistanceCompensation = 0.f; // mix float AOPower = 1.0f; @@ -133,6 +128,7 @@ struct ScreenSpaceGI : Feature float2 pad; }; + STATIC_ASSERT_ALIGNAS_16(SSGICB); eastl::unique_ptr ssgiCB; eastl::unique_ptr texNoise = nullptr; diff --git a/src/Features/ScreenSpaceShadows.h b/src/Features/ScreenSpaceShadows.h index cf34c56bee..189d0ae805 100644 --- a/src/Features/ScreenSpaceShadows.h +++ b/src/Features/ScreenSpaceShadows.h @@ -1,5 +1,7 @@ #pragma once +#include "Buffer.h" + struct ScreenSpaceShadows : Feature { private: @@ -62,6 +64,7 @@ struct ScreenSpaceShadows : Feature BendSettings settings; }; + STATIC_ASSERT_ALIGNAS_16(RaymarchCB); ID3D11SamplerState* pointBorderSampler = nullptr; diff --git a/src/Features/SubsurfaceScattering.h b/src/Features/SubsurfaceScattering.h index 8196a08756..edc66144a2 100644 --- a/src/Features/SubsurfaceScattering.h +++ b/src/Features/SubsurfaceScattering.h @@ -1,5 +1,7 @@ #pragma once +#include "Buffer.h" + #define SSSS_N_SAMPLES 21 struct SubsurfaceScattering : Feature @@ -36,6 +38,7 @@ struct SubsurfaceScattering : Feature { float4 Sample[SSSS_N_SAMPLES]; }; + STATIC_ASSERT_ALIGNAS_16(Kernel); struct alignas(16) BlurCB { @@ -49,6 +52,7 @@ struct SubsurfaceScattering : Feature float4 MeanFreePathBase; float4 MeanFreePathHuman; }; + STATIC_ASSERT_ALIGNAS_16(BlurCB); ConstantBuffer* blurCB = nullptr; BlurCB blurCBData{}; diff --git a/src/Features/TerrainHelper.cpp b/src/Features/TerrainHelper.cpp index 883076be4e..9463984bb5 100644 --- a/src/Features/TerrainHelper.cpp +++ b/src/Features/TerrainHelper.cpp @@ -177,12 +177,15 @@ void TerrainHelper::BSLightingShader_SetupMaterial(RE::BSLightingShaderMaterialB const auto state = globals::state; const auto& stateData = globals::game::graphicsState->GetRuntimeData(); + state->permutationData.ExtraFeatureDescriptor &= ~uint(State::ExtraFeatureDescriptors::THLandHasDisplacement); + // Populate extended slots - // Please update bits allocation in ExtraFeatureDescriptor/Permutation.hlsli and other feature code if you need to change the constant 6 + // Bits 0-5 track individual texture displacement; THLandHasDisplacement (bit 9) tracks if any texture has displacement for (uint32_t textureI = 0; textureI < 6; ++textureI) { if (materialBase.parallax[textureI] != nullptr && materialBase.parallax[textureI] != stateData.defaultTextureNormalMap) { thExtendedRendererState.SetPSTexture(textureI, materialBase.parallax[textureI]->rendererTexture); state->permutationData.ExtraFeatureDescriptor |= 1 << textureI; + state->permutationData.ExtraFeatureDescriptor |= uint(State::ExtraFeatureDescriptors::THLandHasDisplacement); } else { thExtendedRendererState.SetPSTexture(textureI, nullptr); state->permutationData.ExtraFeatureDescriptor &= ~(1 << textureI); diff --git a/src/Features/TerrainShadows.cpp b/src/Features/TerrainShadows.cpp index b36d1e01ee..b115b56a38 100644 --- a/src/Features/TerrainShadows.cpp +++ b/src/Features/TerrainShadows.cpp @@ -27,7 +27,7 @@ void TerrainShadows::DrawSettings() if (ImGui::CollapsingHeader("Debug")) { std::string curr_worldspace = "N/A"; std::string curr_worldspace_name = "N/A"; - auto tes = globals::game::tes; + auto tes = RE::TES::GetSingleton(); if (tes) { auto worldspace = tes->GetRuntimeData2().worldSpace; if (worldspace) { @@ -171,7 +171,7 @@ void TerrainShadows::CompileComputeShaders() bool TerrainShadows::IsHeightMapReady() { - if (auto tes = globals::game::tes) + if (auto tes = RE::TES::GetSingleton()) if (auto worldspace = tes->GetRuntimeData2().worldSpace) return cachedHeightmap && cachedHeightmap->worldspace == worldspace->GetFormEditorID(); return false; @@ -197,7 +197,7 @@ TerrainShadows::PerFrame TerrainShadows::GetCommonBufferData() void TerrainShadows::LoadHeightmap() { - auto tes = globals::game::tes; + auto tes = RE::TES::GetSingleton(); if (!tes) return; auto worldspace = tes->GetRuntimeData2().worldSpace; diff --git a/src/Features/TerrainShadows.h b/src/Features/TerrainShadows.h index f4d9e0c5fa..c5add44144 100644 --- a/src/Features/TerrainShadows.h +++ b/src/Features/TerrainShadows.h @@ -1,5 +1,6 @@ #pragma once +#include "Buffer.h" #include struct TerrainShadows : public Feature @@ -65,6 +66,7 @@ struct TerrainShadows : public Feature float2 ZRange; float2 Offset; }; + STATIC_ASSERT_ALIGNAS_16(PerFrame); PerFrame GetCommonBufferData(); diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 2e0f717828..6cd4ab770f 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -6,9 +6,11 @@ #include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" #include "Upscaling/Streamline.h" -#include "Upscaling/XeSS.h" +#include "VR.h" #include +#include #include +#include NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Upscaling::Settings, @@ -18,7 +20,10 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( frameLimitMode, frameGenerationMode, frameGenerationForceEnable, - streamlineLogLevel); + streamlineLogLevel, + sharpnessFSR, + sharpnessDLSS, + DLSSPreset); decltype(&D3D11CreateDeviceAndSwapChain) ptrD3D11CreateDeviceAndSwapChainUpscaling; @@ -53,10 +58,8 @@ HRESULT WINAPI hk_D3D11CreateDeviceAndSwapChainUpscaling( if (upscaling.IsBackendInitialized()) upscaling.CheckBackendFeatures(pAdapter); - if (!globals::game::isVR) { - // Use better swap effect to prevent tearing and improve performance - pSwapChainDesc->SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; - } + // Use better swap effect to prevent tearing and improve performance + pSwapChainDesc->SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; bool shouldProxy = !globals::game::isVR; if (shouldProxy) @@ -83,8 +86,6 @@ HRESULT WINAPI hk_D3D11CreateDeviceAndSwapChainUpscaling( const D3D_FEATURE_LEVEL featureLevel = D3D_FEATURE_LEVEL_11_1; - upscaling.CreateSharedD3D12Device(pAdapter); - if (shouldProxy) { logger::info("[Frame Generation] Frame Generation enabled, using D3D12 proxy"); @@ -108,7 +109,7 @@ HRESULT WINAPI hk_D3D11CreateDeviceAndSwapChainUpscaling( *ppSwapChain = upscaling.GetProxySwapChain(); - upscaling.d3d12Interop = true; + upscaling.d3d12SwapChainActive = true; if (upscaling.IsBackendInitialized()) { upscaling.UpgradeBackendInterface((void**)&(*ppDevice)); @@ -149,59 +150,78 @@ HRESULT WINAPI hk_D3D11CreateDeviceAndSwapChainUpscaling( void Upscaling::DrawSettings() { - // Display upscaling options in the UI - build labels with version info + // Display upscaling options in the UI std::vector upscaleModes = { "None", "TAA" }; - std::string fsrLabel = "AMD FSR"; - if (!fidelityFX.versionInfo.empty()) { - fsrLabel += " " + fidelityFX.versionInfo; - } + std::string fsrLabel = "AMD FSR 3.1"; upscaleModes.push_back(fsrLabel); - std::string xessLabel = "Intel XeSS"; - if (!xess.versionInfo.empty()) { - xessLabel += " " + xess.versionInfo; - } - upscaleModes.push_back(xessLabel); - - std::string dlssLabel = "NVIDIA DLSS 4 Preset K"; + std::string dlssLabel = "NVIDIA DLSS"; upscaleModes.push_back(dlssLabel); // Determine available modes bool featureDLSS = streamline.featureDLSS; - - uint* currentUpscaleMode = &settings.upscaleMethod; - uint availableModes = 4; - - if (featureDLSS) { - // All modes available including DLSS - } else { + bool featureFSR = true; // FSR is always available + + uint32_t* currentUpscaleMode = &settings.upscaleMethod; + uint32_t availableModes = 1; // Start with TAA + if (featureFSR) + availableModes = 2; // Add FSR + if (featureDLSS) + availableModes = 3; // Add DLSS if available + else currentUpscaleMode = &settings.upscaleMethodNoDLSS; - availableModes = 3; - } // Slider for method selection - std::string currentLabel = upscaleModes[(uint)*currentUpscaleMode]; + // Clamp the index used to read from the built label vector to avoid OOB if the stored value is stale + uint32_t modeLabelIndex = std::min(*currentUpscaleMode, static_cast(upscaleModes.size() - 1)); + std::string currentLabel = upscaleModes[modeLabelIndex]; ImGui::SliderInt("Method", (int*)currentUpscaleMode, 0, availableModes, currentLabel.c_str()); - *currentUpscaleMode = std::min(availableModes, (uint)*currentUpscaleMode); + *currentUpscaleMode = std::min(availableModes, *currentUpscaleMode); // Check the current upscale method auto upscaleMethod = GetUpscaleMethod(); // Display upscaling settings if applicable - if (!globals::game::isVR) { - if (upscaleMethod != UpscaleMethod::kNONE && upscaleMethod != UpscaleMethod::kTAA) { - const char* upscalePresetsDLSS[] = { "Ultra Performance", "Performance", "Balanced", "Quality", "DLAA" }; - const char* upscalePresets[] = { "Ultra Performance", "Performance", "Balanced", "Quality", "Native AA" }; + if (upscaleMethod != UpscaleMethod::kNONE && upscaleMethod != UpscaleMethod::kTAA) { + const char* upscalePresetsDLSS[] = { "Ultra Performance", "Performance", "Balanced", "Quality", "DLAA" }; + const char* upscalePresets[] = { "Ultra Performance", "Performance", "Balanced", "Quality", "Native AA" }; - if (upscaleMethod == UpscaleMethod::kDLSS) - ImGui::SliderInt("Upscale Preset", (int*)&settings.qualityMode, 0, 4, std::format("{}", upscalePresetsDLSS[4 - settings.qualityMode]).c_str()); - else - ImGui::SliderInt("Upscale Preset", (int*)&settings.qualityMode, 0, 4, std::format("{}", upscalePresets[4 - settings.qualityMode]).c_str()); + // Compute a safe preset index (4 - qualityMode) clamped to [0,4] to avoid negative/overflow indexing + int presetIndex = 0; + if (settings.qualityMode <= 4) + presetIndex = 4 - static_cast(settings.qualityMode); + presetIndex = std::clamp(presetIndex, 0, 4); + + // Choose preset name set and the corresponding scales once, then show a + // single SliderInt to avoid duplicated calls. + const char* baseLabel = nullptr; + + if (upscaleMethod == UpscaleMethod::kFSR) { + baseLabel = upscalePresets[presetIndex]; + } else if (upscaleMethod == UpscaleMethod::kDLSS) { + baseLabel = upscalePresetsDLSS[presetIndex]; + } + + if (baseLabel) { + // Format the label with preset name and resolution scale + std::string labelWithScale = std::format("{} ( {:.2f}x )", baseLabel, (resolutionScale.x + resolutionScale.y) * 0.5f); + + ImGui::SliderInt("Upscale Preset", (int*)&settings.qualityMode, 0, 4, labelWithScale.c_str()); + } + + if (upscaleMethod == UpscaleMethod::kFSR) { + ImGui::SliderFloat("Sharpness", &settings.sharpnessFSR, 0.0f, 1.0f, "%.1f"); + } else if (upscaleMethod == UpscaleMethod::kDLSS) { + ImGui::SliderFloat("Sharpness", &settings.sharpnessDLSS, 0.0f, 1.0f, "%.1f"); + + // VR DLSS preset selection + if (globals::game::isVR) { + const char* presets[] = { "F (Fast)", "J (Quality)", "K (Ultra)" }; + ImGui::SliderInt("DLSS Preset", (int*)&settings.DLSSPreset, 0, 2, presets[settings.DLSSPreset]); + } } - } else { - ImGui::Text("Upscaling from lower resolutions is not currently available for VR"); } if (!globals::game::isVR) { @@ -239,25 +259,30 @@ void Upscaling::DrawSettings() onlyRequiresRestart = false; } - if (onlyRequiresRestart && settings.frameGenerationMode && !d3d12Interop) { + if (onlyRequiresRestart && settings.frameGenerationMode && !d3d12SwapChainActive) { ImGui::PushStyleColor(ImGuiCol_Text, Util::Colors::GetWarning()); ImGui::Text("Warning: Requires restart"); ImGui::PopStyleColor(); } - std::string backendLabel = fidelityFX.isFrameGenActive ? "FSR3" : "None"; - std::string enabledLabel = "Enabled (" + backendLabel + ")"; + if (!settings.frameGenerationMode && d3d12SwapChainActive) { + ImGui::PushStyleColor(ImGuiCol_Text, Util::Colors::GetWarning()); + ImGui::Text("Warning: Requires restart"); + ImGui::PopStyleColor(); + } + + std::string enabledLabel = "Enabled"; const char* toggleModes[] = { "Disabled", "Enabled" }; const char* toggleModesFG[] = { "Disabled", enabledLabel.c_str() }; ImGui::SliderInt("Frame Generation", (int*)&settings.frameGenerationMode, 0, 1, toggleModesFG[settings.frameGenerationMode]); - if (!d3d12Interop) + if (!d3d12SwapChainActive) ImGui::BeginDisabled(); ImGui::SliderInt("Frame Limit (Variable Refresh Rate)", (int*)&settings.frameLimitMode, 0, 1, std::format("{}", toggleModes[settings.frameLimitMode]).c_str()); - if (!d3d12Interop) + if (!d3d12SwapChainActive) ImGui::EndDisabled(); ImGui::Text("Allows frame generation to function on low refresh rate monitors"); @@ -335,6 +360,17 @@ void Upscaling::SaveSettings(json& o_json) void Upscaling::LoadSettings(json& o_json) { settings = o_json; + + // Sanitize loaded settings to ensure enum indices are valid + constexpr auto enumCount = 4; // UpscaleMethod has 4 values: kNONE, kTAA, kFSR, kDLSS + if (settings.upscaleMethod >= static_cast(enumCount)) { + logger::warn("[Upscaling] Loaded upscaleMethod {} out of range, clamping to {}", settings.upscaleMethod, enumCount ? enumCount - 1 : 0); + settings.upscaleMethod = enumCount ? enumCount - 1 : 0; + } + if (settings.upscaleMethodNoDLSS >= static_cast(enumCount)) { + logger::warn("[Upscaling] Loaded upscaleMethodNoDLSS {} out of range, clamping to {}", settings.upscaleMethodNoDLSS, enumCount ? enumCount - 1 : 0); + settings.upscaleMethodNoDLSS = enumCount ? enumCount - 1 : 0; + } auto iniSettingCollection = globals::game::iniPrefSettingCollection; if (iniSettingCollection) { auto setting = iniSettingCollection->GetSetting("bUseTAA:Display"); @@ -349,6 +385,12 @@ void Upscaling::RestoreDefaultSettings() settings = {}; } +void Upscaling::DataLoaded() +{ + // Fix screenshots fix from Engine Fixes + RE::GetINISetting("bUseTAA:Display")->data.b = false; +} + void Upscaling::Load() { *(uintptr_t*)&ptrD3D11CreateDeviceAndSwapChainUpscaling = SKSE::PatchIAT(hk_D3D11CreateDeviceAndSwapChainUpscaling, "d3d11.dll", "D3D11CreateDeviceAndSwapChain"); @@ -380,40 +422,33 @@ void Upscaling::PostPostLoad() // Performs upscaling in between volumetric lighting and post processing stl::write_thunk_call(REL::RelocationID(100430, 107148).address() + REL::Relocate(0x1F0, 0x1E7, 0x206)); - if (!REL::Module::IsVR()) { - // Patches RSSetScissorRect calls to use dynamic resolution - // This is a PC-specific function hence it was missing + // Patches RSSetScissorRect calls to use dynamic resolution + // This is a PC-specific function hence it was missing + if (!globals::game::isVR) stl::detour_thunk(REL::RelocationID(75564, 77365)); - // Patches facegen texture generation to not use dynamic resolution - stl::detour_thunk(REL::RelocationID(26455, 27041)); + // Patches facegen texture generation to not use dynamic resolution + stl::detour_thunk(REL::RelocationID(26455, 27041)); - // Patches precipitation camera to not use dynamic resolution - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x3A1, 0x3A1, 0x2FA)); + // Patches precipitation camera to not use dynamic resolution + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x3A1, 0x3A1, 0x2FA)); - // Forces FXAA off - stl::detour_thunk(REL::RelocationID(98974, 105626)); - } + // Forces FXAA off + stl::detour_thunk(REL::RelocationID(98974, 105626)); logger::info("[Upscaling] Installed hooks"); } Upscaling::UpscaleMethod Upscaling::GetUpscaleMethod() { - if (streamline.featureDLSS) { - settings.upscaleMethod = std::clamp(settings.upscaleMethod, (uint)UpscaleMethod::kNONE, (uint)UpscaleMethod::kDLSS); - settings.qualityMode = std::clamp(settings.qualityMode, 0u, 4u); + if (streamline.featureDLSS) return (UpscaleMethod)settings.upscaleMethod; - } - - settings.upscaleMethodNoDLSS = std::clamp(settings.upscaleMethodNoDLSS, (uint)UpscaleMethod::kNONE, (uint)UpscaleMethod::kXESS); - settings.qualityMode = std::clamp(settings.qualityMode, 0u, 4u); return (UpscaleMethod)settings.upscaleMethodNoDLSS; } void Upscaling::CreateUpscalingTextureResources(UpscaleMethod a_upscalemethod) { - logger::debug("[Upscaling] Creating texture resources for method {}", (int)a_upscalemethod); + logger::debug("[Upscaling] Creating texture resources for method {} ({})", static_cast(a_upscalemethod), magic_enum::enum_name(a_upscalemethod)); auto renderer = globals::game::renderer; auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; @@ -421,18 +456,17 @@ void Upscaling::CreateUpscalingTextureResources(UpscaleMethod a_upscalemethod) D3D11_TEXTURE2D_DESC texDesc{}; D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; - main.texture->GetDesc(&texDesc); main.SRV->GetDesc(&srvDesc); main.UAV->GetDesc(&uavDesc); texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; - texDesc.Format = DXGI_FORMAT_R8_UNORM; - srvDesc.Format = texDesc.Format; - uavDesc.Format = texDesc.Format; - // DLSS uses D3D11 textures (not shared D3D12) - if (a_upscalemethod == UpscaleMethod::kDLSS) { + if (a_upscalemethod == UpscaleMethod::kDLSS || a_upscalemethod == UpscaleMethod::kFSR) { + texDesc.Format = DXGI_FORMAT_R8_UNORM; + srvDesc.Format = texDesc.Format; + uavDesc.Format = texDesc.Format; + if (!reactiveMaskTexture) { reactiveMaskTexture = new Texture2D(texDesc); reactiveMaskTexture->CreateSRV(srvDesc); @@ -444,7 +478,10 @@ void Upscaling::CreateUpscalingTextureResources(UpscaleMethod a_upscalemethod) transparencyCompositionMaskTexture->CreateSRV(srvDesc); transparencyCompositionMaskTexture->CreateUAV(uavDesc); } + } + // Motion vector copy texture is only needed for DLSS + if (a_upscalemethod == UpscaleMethod::kDLSS) { if (!motionVectorCopyTexture) { auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; @@ -455,20 +492,41 @@ void Upscaling::CreateUpscalingTextureResources(UpscaleMethod a_upscalemethod) srvDesc.Format = texDesc.Format; uavDesc.Format = texDesc.Format; - motionVectorCopyTexture = new Texture2D(texDesc); + motionVectorCopyTexture = new Texture2D(motionTexDesc); motionVectorCopyTexture->CreateSRV(srvDesc); motionVectorCopyTexture->CreateUAV(uavDesc); } + + // RCAS sharpener texture - matches kMAIN format for HDR sharpening + if (!sharpenerTexture) { + main.texture->GetDesc(&texDesc); + main.SRV->GetDesc(&srvDesc); + + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + + srvDesc.Format = texDesc.Format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MostDetailedMip = 0; + srvDesc.Texture2D.MipLevels = 1; + + uavDesc.Format = texDesc.Format; + uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; + uavDesc.Texture2D.MipSlice = 0; + + sharpenerTexture = new Texture2D(texDesc); + sharpenerTexture->CreateSRV(srvDesc); + sharpenerTexture->CreateUAV(uavDesc); + } } } void Upscaling::DestroyUpscalingTextureResources(UpscaleMethod a_upscalemethod) { - logger::debug("[Upscaling] Destroying texture resources for method {}", (int)a_upscalemethod); + logger::debug("[Upscaling] Destroying texture resources for method {} ({})", static_cast(a_upscalemethod), magic_enum::enum_name(a_upscalemethod)); // Clean up D3D11 textures that are no longer needed - // Only destroy DLSS textures when switching away from DLSS - if (a_upscalemethod != UpscaleMethod::kDLSS) { + // Only destroy textures when switching away from methods that use them + if (a_upscalemethod != UpscaleMethod::kDLSS && a_upscalemethod != UpscaleMethod::kFSR) { if (reactiveMaskTexture) { reactiveMaskTexture->srv = nullptr; reactiveMaskTexture->uav = nullptr; @@ -486,7 +544,10 @@ void Upscaling::DestroyUpscalingTextureResources(UpscaleMethod a_upscalemethod) delete transparencyCompositionMaskTexture; transparencyCompositionMaskTexture = nullptr; } + } + // Motion vector copy texture is only needed for DLSS - destroy when switching away from DLSS + if (a_upscalemethod != UpscaleMethod::kDLSS) { if (motionVectorCopyTexture) { motionVectorCopyTexture->srv = nullptr; motionVectorCopyTexture->uav = nullptr; @@ -495,6 +556,14 @@ void Upscaling::DestroyUpscalingTextureResources(UpscaleMethod a_upscalemethod) delete motionVectorCopyTexture; motionVectorCopyTexture = nullptr; } + if (sharpenerTexture) { + sharpenerTexture->srv = nullptr; + sharpenerTexture->uav = nullptr; + sharpenerTexture->resource = nullptr; + + delete sharpenerTexture; + sharpenerTexture = nullptr; + } } } @@ -503,53 +572,38 @@ void Upscaling::CheckResources(UpscaleMethod a_upscalemethod) static auto previousUpscaleMode = UpscaleMethod::kTAA; static bool previousFrameGenMode = false; - bool frameGenModeChanged = (settings.frameGenerationMode && d3d12Interop) != previousFrameGenMode; + bool frameGenModeCurrent = (settings.frameGenerationMode && d3d12SwapChainActive); + bool frameGenModeChanged = frameGenModeCurrent != previousFrameGenMode; bool upscaleModeChanged = (previousUpscaleMode != a_upscalemethod); if (upscaleModeChanged || frameGenModeChanged) { - logger::debug("[Upscaling] Resource change detected - Upscale: {} -> {}, FrameGen: {} -> {}", - (int)previousUpscaleMode, (int)a_upscalemethod, previousFrameGenMode, (settings.frameGenerationMode && d3d12Interop)); - - // Synchronise all pending GPU work before destroying contexts - // Otherwise resources will be destroyed whilst in use, causing the device to crash - if (previousUpscaleMode == UpscaleMethod::kFSR || previousUpscaleMode == UpscaleMethod::kXESS) { - UINT64 fenceValue = sharedInteropFenceValue++; - DX::ThrowIfFailed(sharedD3D12CommandQueue->Signal(sharedD3D12Fence.get(), fenceValue)); - if (sharedD3D12Fence->GetCompletedValue() < fenceValue) { - sharedD3D12Fence->SetEventOnCompletion(fenceValue, sharedFenceEvent); - WaitForSingleObject(sharedFenceEvent, INFINITE); - } - } + logger::debug("[Upscaling] Resource change detected - Upscale: {} ({}) -> {} ({}), FrameGen: {} -> {} (d3d12Active={})", + static_cast(previousUpscaleMode), magic_enum::enum_name(previousUpscaleMode), static_cast(a_upscalemethod), magic_enum::enum_name(a_upscalemethod), previousFrameGenMode, frameGenModeCurrent, d3d12SwapChainActive); - // Destroy previous upscaling method resources (this will intelligently clean up based on what's still needed) + // Destroy previous upscaling method resources (only if they were actually active) if (upscaleModeChanged) { DestroyUpscalingTextureResources(a_upscalemethod); - if (previousUpscaleMode == UpscaleMethod::kDLSS) - streamline.DestroyDLSSResources(); - else if (previousUpscaleMode == UpscaleMethod::kFSR) - fidelityFX.DestroyFSRResources(); - else if (previousUpscaleMode == UpscaleMethod::kXESS) - xess.DestroyXeSSResources(); - } - - // Handle shared resource changes - if (frameGenModeChanged || upscaleModeChanged) { - UpdateSharedResources(); + // Only destroy SDK resources if the previous method was actually performing upscaling + if (previousUpscalingWasActive) { + if (previousUpscaleMode == UpscaleMethod::kDLSS) + streamline.DestroyDLSSResources(); + else if (previousUpscaleMode == UpscaleMethod::kFSR) + fidelityFX.DestroyFSRResources(); + } + if (a_upscalemethod == UpscaleMethod::kFSR) + fidelityFX.CreateFSRResources(); } // Create new upscaling method resources if (upscaleModeChanged) { CreateUpscalingTextureResources(a_upscalemethod); - - if (a_upscalemethod == UpscaleMethod::kFSR) - fidelityFX.CreateFSRResources(); - else if (a_upscalemethod == UpscaleMethod::kXESS) - xess.CreateXeSSResources(); } + // Update tracking for next call previousUpscaleMode = a_upscalemethod; - previousFrameGenMode = (settings.frameGenerationMode && d3d12Interop); + previousFrameGenMode = (settings.frameGenerationMode && d3d12SwapChainActive); + previousUpscalingWasActive = IsUpscalingActive(); } } @@ -565,15 +619,12 @@ ID3D11ComputeShader* Upscaling::GetEncodeTexturesCS() // Add upscale method define switch (upscaleMethod) { - case UpscaleMethod::kFSR: - defines.push_back({ "FSR", "" }); - break; - case UpscaleMethod::kXESS: - defines.push_back({ "XESS", "" }); - break; case UpscaleMethod::kDLSS: defines.push_back({ "DLSS", "" }); break; + case UpscaleMethod::kFSR: + defines.push_back({ "FSR", "" }); + break; default: // No define for NONE or TAA break; @@ -646,19 +697,10 @@ void GetJitterOffset(float* outX, float* outY, int32_t index, int32_t phaseCount *outY = y; } -void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) +void Upscaling::ConfigureTAA() { auto upscaleMethod = GetUpscaleMethod(); - // Delete or create resources as necessary - CheckResources(upscaleMethod); - - // The game defaults this to a non-zero value - if (!globals::game::isVR) { - auto fDRClampOffset = RE::GetINISetting("fDRClampOffset:Display"); - fDRClampOffset->data.f = 0.0f; - } - auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); GET_INSTANCE_MEMBER(BSImagespaceShaderISTemporalAA, imageSpaceManager); @@ -668,6 +710,18 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) // Force enable TAA if needed BSImagespaceShaderISTemporalAA->taaEnabled = upscaleMethod != UpscaleMethod::kNONE; +} + +void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) +{ + auto upscaleMethod = GetUpscaleMethod(); + + // Delete or create resources as necessary + CheckResources(upscaleMethod); + + // The game defaults this to a non-zero value + auto fDRClampOffset = RE::GetINISetting("fDRClampOffset:Display"); + fDRClampOffset->data.f = 0.0f; // Cache original TAA values for UI projectionPosScaleX = a_viewport->projectionPosScaleX; @@ -681,23 +735,26 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) auto screenHeight = static_cast(screenSize.y); if (upscaleMethod != UpscaleMethod::kNONE && upscaleMethod != UpscaleMethod::kTAA) { - float resolutionScaleBase = 1.0f; + float2 resolutionScaleBase = { 1.0f, 1.0f }; - if (globals::game::isVR) { - resolutionScaleBase = 1.0f; - } else if (upscaleMethod == UpscaleMethod::kXESS) { - resolutionScaleBase = xess.GetInputResolutionScale((uint32_t)screenSize.x, (uint32_t)screenSize.y, settings.qualityMode); - } else if (upscaleMethod == UpscaleMethod::kDLSS) { + if (upscaleMethod == UpscaleMethod::kDLSS) { resolutionScaleBase = streamline.GetInputResolutionScale((uint32_t)screenSize.x, (uint32_t)screenSize.y, settings.qualityMode); } else if (upscaleMethod == UpscaleMethod::kFSR) { resolutionScaleBase = fidelityFX.GetInputResolutionScale((uint32_t)screenSize.x, (uint32_t)screenSize.y, settings.qualityMode); } - auto renderWidth = static_cast(screenWidth * resolutionScaleBase); - auto renderHeight = static_cast(screenHeight * resolutionScaleBase); + auto renderWidth = static_cast(screenWidth * resolutionScaleBase.x); + auto renderHeight = static_cast(screenHeight * resolutionScaleBase.y); - resolutionScale.x = static_cast(renderWidth) / static_cast(screenWidth); - resolutionScale.y = static_cast(renderHeight) / static_cast(screenHeight); + // Use precise scale if the integer conversion doesn't change the dimensions + if (renderWidth == screenWidth && renderHeight == screenHeight) { + // For DLAA and other 1:1 modes, ensure exactly 1.0 + resolutionScale.x = 1.0f; + resolutionScale.y = 1.0f; + } else { + resolutionScale.x = static_cast(renderWidth) / static_cast(screenWidth); + resolutionScale.y = static_cast(renderHeight) / static_cast(screenHeight); + } auto phaseCount = GetJitterPhaseCount(renderWidth, screenWidth); @@ -730,9 +787,27 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) dynamicResolutionWidthRatio = resolutionScale.x; dynamicResolutionHeightRatio = resolutionScale.y; - // Disable dynamic resolution unless the game explictly enables it + // Disable dynamic resolution unless the game explicitly enables it if (!globals::game::isVR) runtimeData.dynamicResolutionLock = 1; + + // If running in VR and an external upscaler is active, force-disable + // the engine's depth-buffer culling immediately. This ensures that + // enabling upscaling at runtime (after game load) does not leave the + // VR depth-buffer culling enabled which can cause incorrect occlusion. + if (globals::game::isVR) { + auto& vr = globals::features::vr; + if (IsUpscalingActive()) { + if (vr.gDepthBufferCulling) { + if (*vr.gDepthBufferCulling) { + *vr.gDepthBufferCulling = false; + logger::info("[Upscaling] VR detected - forcing depth buffer culling OFF due to active downscaling upscaler (scale={})", resolutionScale.x); + } + } else { + logger::warn("[Upscaling] VR depth buffer culling pointer is null, cannot force disable"); + } + } + } } void Upscaling::SetupResources() @@ -756,18 +831,12 @@ void Upscaling::SetupResources() srvDesc.Format = texDesc.Format; uavDesc.Format = texDesc.Format; - // Initial resource allocation will be handled by CheckResources() during first upscaling call - // This avoids creating unnecessary resources at startup - - // Initialize all shared resources based on current settings - UpdateSharedResources(); - D3D11_DEPTH_STENCIL_DESC depthStencilDesc = {}; depthStencilDesc.DepthEnable = true; // Enable depth testing depthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL; // Write to all depth bits depthStencilDesc.DepthFunc = D3D11_COMPARISON_ALWAYS; // Always pass depth test (write all depths) - if (REL::Module::IsVR()) { + if (globals::game::isVR) { depthStencilDesc.StencilEnable = true; // Enable stencil testing depthStencilDesc.StencilReadMask = 0xFF; // Read all stencil bits depthStencilDesc.StencilWriteMask = 0xFF; // Write to all stencil bits @@ -795,21 +864,6 @@ void Upscaling::SetupResources() // Create upscaling data constant buffer for encode textures compute shader upscalingDataCB = new ConstantBuffer(ConstantBufferDesc()); - // Create NIS sharpener texture with swapchain format and UAV access - D3D11_TEXTURE2D_DESC nisTexDesc = texDesc; - nisTexDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - nisTexDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; - - D3D11_SHADER_RESOURCE_VIEW_DESC nisSrvDesc = srvDesc; - nisSrvDesc.Format = nisTexDesc.Format; - - D3D11_UNORDERED_ACCESS_VIEW_DESC nisUavDesc = uavDesc; - nisUavDesc.Format = nisTexDesc.Format; - - nisSharpenerTexture = new Texture2D(nisTexDesc); - nisSharpenerTexture->CreateSRV(nisSrvDesc); - nisSharpenerTexture->CreateUAV(nisUavDesc); - // Create blend state for depth upscaling D3D11_BLEND_DESC blendDesc = {}; blendDesc.AlphaToCoverageEnable = false; @@ -832,20 +886,14 @@ void Upscaling::SetupResources() rasterizerDesc.AntialiasedLineEnable = false; DX::ThrowIfFailed(globals::d3d::device->CreateRasterizerState(&rasterizerDesc, upscaleRasterizerState.put())); - // Create shared D3D11/D3D12 fences for synchronization - winrt::com_ptr d3d11Device5; - DX::ThrowIfFailed(globals::d3d::device->QueryInterface(IID_PPV_ARGS(d3d11Device5.put()))); + CheckResources(GetUpscaleMethod()); - HANDLE sharedFenceHandle; - DX::ThrowIfFailed(sharedD3D12Device->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_PPV_ARGS(sharedD3D12Fence.put()))); - DX::ThrowIfFailed(sharedD3D12Device->CreateSharedHandle(sharedD3D12Fence.get(), nullptr, GENERIC_ALL, nullptr, &sharedFenceHandle)); - DX::ThrowIfFailed(d3d11Device5->OpenSharedFence(sharedFenceHandle, IID_PPV_ARGS(sharedD3D11Fence.put()))); - CloseHandle(sharedFenceHandle); + rcas.Initialize(); - auto upscaleMethod = GetUpscaleMethod(); + if (d3d12SwapChainActive) + dx12SwapChain.CreateSharedResources(); - // Delete or create resources as necessary - CheckResources(upscaleMethod); + copyDepthToSharedBufferPS.attach((ID3D11PixelShader*)Util::CompileShader(L"Data\\Shaders\\Upscaling\\CopyDepthToSharedBufferPS.hlsl", { { "PSHADER", "" } }, "ps_5_0")); } void Upscaling::ClearShaderCache() @@ -859,249 +907,65 @@ void Upscaling::ClearShaderCache() upscaleVS = nullptr; // com_ptr automatically releases } -void Upscaling::CreateSharedD3D12Device(IDXGIAdapter* a_dxgiAdapter) -{ - // Create D3D12 device on same adapter - DX::ThrowIfFailed(D3D12CreateDevice(a_dxgiAdapter, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(sharedD3D12Device.put()))); - - // Create command queue - D3D12_COMMAND_QUEUE_DESC queueDesc = {}; - queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; - queueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; - queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; - queueDesc.NodeMask = 0; - - DX::ThrowIfFailed(sharedD3D12Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(sharedD3D12CommandQueue.put()))); - - // Create command allocator - DX::ThrowIfFailed(sharedD3D12Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(sharedD3D12CommandAllocator.put()))); - - // Create command list - DX::ThrowIfFailed(sharedD3D12Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, sharedD3D12CommandAllocator.get(), nullptr, IID_PPV_ARGS(sharedD3D12CommandList.put()))); - - // Create fence for synchronization - DX::ThrowIfFailed(sharedD3D12Device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(sharedD3D12Fence.put()))); - - sharedFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); - if (sharedFenceEvent == nullptr) { - throw std::runtime_error("Failed to create shared fence event"); - } - - // Close initial command list - sharedD3D12CommandList->Close(); - - logger::info("[Upscaling] Shared D3D12 device and interop resources created successfully"); -} - -void Upscaling::UpdateSharedResources() -{ - logger::debug("[Upscaling] Updating shared D3D12 resources"); - - auto currentMethod = GetUpscaleMethod(); - - // Determine current feature requirements - bool needsUpscalingResources = (currentMethod == UpscaleMethod::kFSR || currentMethod == UpscaleMethod::kXESS); - bool needsFSRSpecific = (currentMethod == UpscaleMethod::kFSR); - bool needsFrameGenResources = (settings.frameGenerationMode && d3d12Interop); - bool needsSharedBasics = needsUpscalingResources || needsFrameGenResources; - - if (!needsSharedBasics) { - // Clean up all resources when nothing is needed - if (inputColorBufferShared12) { - delete inputColorBufferShared12; - inputColorBufferShared12 = nullptr; - } - if (outputColorBufferShared12) { - delete outputColorBufferShared12; - outputColorBufferShared12 = nullptr; - } - if (reactiveMaskShared12) { - delete reactiveMaskShared12; - reactiveMaskShared12 = nullptr; - } - if (transparencyCompositionMaskShared12) { - delete transparencyCompositionMaskShared12; - transparencyCompositionMaskShared12 = nullptr; - } - if (!d3d12Interop) { - if (depthBufferShared12) { - delete depthBufferShared12; - depthBufferShared12 = nullptr; - } - if (motionVectorBufferShared12) { - delete motionVectorBufferShared12; - motionVectorBufferShared12 = nullptr; - } - } - copyDepthToSharedBufferPS = nullptr; - return; - } - - // Get required interfaces - winrt::com_ptr d3d11Device5; - DX::ThrowIfFailed(globals::d3d::device->QueryInterface(IID_PPV_ARGS(d3d11Device5.put()))); - - auto renderer = globals::game::renderer; - auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; - - D3D11_TEXTURE2D_DESC texDesc{}; - main.texture->GetDesc(&texDesc); - - // Upscaling-specific resources (FSR/XeSS) - if (needsUpscalingResources) { - if (!inputColorBufferShared12) { - inputColorBufferShared12 = new WrappedResource(texDesc, d3d11Device5.get(), sharedD3D12Device.get()); - } - if (!outputColorBufferShared12) { - outputColorBufferShared12 = new WrappedResource(texDesc, d3d11Device5.get(), sharedD3D12Device.get()); - } - - texDesc.Format = DXGI_FORMAT_R8_UNORM; - if (!reactiveMaskShared12) { - reactiveMaskShared12 = new WrappedResource(texDesc, d3d11Device5.get(), sharedD3D12Device.get()); - } - } else { - // Clean up upscaling-only resources - if (inputColorBufferShared12) { - delete inputColorBufferShared12; - inputColorBufferShared12 = nullptr; - } - if (outputColorBufferShared12) { - delete outputColorBufferShared12; - outputColorBufferShared12 = nullptr; - } - if (reactiveMaskShared12) { - delete reactiveMaskShared12; - reactiveMaskShared12 = nullptr; - } - } - - // FSR-specific resources - if (needsFSRSpecific) { - texDesc.Format = DXGI_FORMAT_R8_UNORM; - if (!transparencyCompositionMaskShared12) { - transparencyCompositionMaskShared12 = new WrappedResource(texDesc, d3d11Device5.get(), sharedD3D12Device.get()); - } - } else { - if (transparencyCompositionMaskShared12) { - delete transparencyCompositionMaskShared12; - transparencyCompositionMaskShared12 = nullptr; - } - } - - // Shared resources (depth/motion - needed by both upscaling and frame generation) - if (needsSharedBasics) { - texDesc.Format = DXGI_FORMAT_R32_FLOAT; - if (!depthBufferShared12) { - depthBufferShared12 = new WrappedResource(texDesc, d3d11Device5.get(), sharedD3D12Device.get()); - } - - auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; - motionVector.texture->GetDesc(&texDesc); - if (!motionVectorBufferShared12) { - motionVectorBufferShared12 = new WrappedResource(texDesc, d3d11Device5.get(), sharedD3D12Device.get()); - } - - if (!copyDepthToSharedBufferPS) { - copyDepthToSharedBufferPS.attach((ID3D11PixelShader*)Util::CompileShader(L"Data\\Shaders\\Upscaling\\CopyDepthToSharedBufferPS.hlsl", { { "PSHADER", "" } }, "ps_5_0")); - } - } else if (!d3d12Interop) { - if (depthBufferShared12) { - delete depthBufferShared12; - depthBufferShared12 = nullptr; - } - if (motionVectorBufferShared12) { - delete motionVectorBufferShared12; - motionVectorBufferShared12 = nullptr; - } - copyDepthToSharedBufferPS = nullptr; - } - - logger::debug("[Upscaling] Shared resource update complete - Upscaling: {}, FSR: {}, FrameGen: {}", - needsUpscalingResources, needsFSRSpecific, needsFrameGenResources); -} - void Upscaling::CopySharedD3D12Resources() { - // Only copy once per frame for all upscaling systems (XeSS, Frame Generation, etc.) - if (!sharedResourcesFrameChecker.IsNewFrame()) - return; - - auto upscaleMethod = GetUpscaleMethod(); - globals::state->BeginPerfEvent("Copy Shared D3D12 Resources"); auto renderer = globals::game::renderer; auto context = globals::d3d::context; - // Not required by XeSS - if (upscaleMethod == UpscaleMethod::kFSR || (d3d12Interop && settings.frameGenerationMode && upscaleMethod != UpscaleMethod::kXESS)) { - auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; - - // Copy only the dynamic resolution area - auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); - D3D11_BOX srcBox = {}; - srcBox.left = 0; - srcBox.top = 0; - srcBox.front = 0; - srcBox.right = (UINT)renderSize.x; - srcBox.bottom = (UINT)renderSize.y; - srcBox.back = 1; - - context->CopySubresourceRegion(motionVectorBufferShared12->resource11, 0, 0, 0, 0, motionVector.texture, 0, &srcBox); - } + auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + context->CopyResource(dx12SwapChain.motionVectorBufferShared12->resource11, motionVector.texture); - if (upscaleMethod == UpscaleMethod::kFSR || upscaleMethod == UpscaleMethod::kXESS || d3d12Interop && settings.frameGenerationMode) { - auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - { - // Set up viewport for fullscreen rendering - auto screenSize = globals::state->screenSize; - - D3D11_VIEWPORT viewport = {}; - viewport.TopLeftX = 0.0f; - viewport.TopLeftY = 0.0f; - viewport.Width = screenSize.x; - viewport.Height = screenSize.y; - viewport.MinDepth = 0.0f; - viewport.MaxDepth = 1.0f; - context->RSSetViewports(1, &viewport); - - // Set up Input Assembler for fullscreen triangle - context->IASetInputLayout(nullptr); - context->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); - context->IASetIndexBuffer(nullptr, DXGI_FORMAT_UNKNOWN, 0); - context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + { + // Set up viewport for fullscreen rendering + auto screenSize = globals::state->screenSize; - // Set up vertex shader - context->VSSetShader(GetUpscaleVS(), nullptr, 0); + D3D11_VIEWPORT viewport = {}; + viewport.TopLeftX = 0.0f; + viewport.TopLeftY = 0.0f; + viewport.Width = screenSize.x; + viewport.Height = screenSize.y; + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &viewport); - // Set up rasterizer and blend states - context->RSSetState(upscaleRasterizerState.get()); - context->OMSetBlendState(upscaleBlendState.get(), nullptr, 0xffffffff); + // Set up Input Assembler for fullscreen triangle + context->IASetInputLayout(nullptr); + context->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); + context->IASetIndexBuffer(nullptr, DXGI_FORMAT_UNKNOWN, 0); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - // Set up pixel shader resources - ID3D11ShaderResourceView* views[1] = { depth.depthSRV }; - context->PSSetShaderResources(0, ARRAYSIZE(views), views); + // Set up vertex shader + context->VSSetShader(GetUpscaleVS(), nullptr, 0); - // Set render target view for pixel shader output - ID3D11RenderTargetView* rtvs[1] = { depthBufferShared12->rtv }; - context->OMSetRenderTargets(ARRAYSIZE(rtvs), rtvs, nullptr); + // Set up rasterizer and blend states + context->RSSetState(upscaleRasterizerState.get()); + context->OMSetBlendState(upscaleBlendState.get(), nullptr, 0xffffffff); - context->PSSetShader(copyDepthToSharedBufferPS.get(), nullptr, 0); + // Set up pixel shader resources + ID3D11ShaderResourceView* views[1] = { depth.depthSRV }; + context->PSSetShaderResources(0, ARRAYSIZE(views), views); - context->Draw(3, 0); - } + // Set render target view for pixel shader output + ID3D11RenderTargetView* rtvs[1] = { dx12SwapChain.depthBufferShared12->rtv }; + context->OMSetRenderTargets(ARRAYSIZE(rtvs), rtvs, nullptr); - // Clean up - ID3D11ShaderResourceView* views[1] = { nullptr }; - context->PSSetShaderResources(0, ARRAYSIZE(views), views); + context->PSSetShader(copyDepthToSharedBufferPS.get(), nullptr, 0); - context->OMSetRenderTargets(0, nullptr, nullptr); - context->PSSetShader(nullptr, nullptr, 0); - context->VSSetShader(nullptr, nullptr, 0); + context->Draw(3, 0); } + // Clean up + ID3D11ShaderResourceView* views[1] = { nullptr }; + context->PSSetShaderResources(0, ARRAYSIZE(views), views); + + context->OMSetRenderTargets(0, nullptr, nullptr); + context->PSSetShader(nullptr, nullptr, 0); + context->VSSetShader(nullptr, nullptr, 0); + globals::state->EndPerfEvent(); } @@ -1130,7 +994,7 @@ void Upscaling::PostDisplay() globals::game::renderer->UpdateViewPort(0, 0, 1); UpdateCameraData(); - if (d3d12Interop) + if (d3d12SwapChainActive) SetUIBuffer(); globals::state->UpdateSharedData(false, false); @@ -1146,7 +1010,7 @@ void Upscaling::TimerSleepQPC(int64_t targetQPC) void Upscaling::FrameLimiter() { - if (d3d12Interop) { + if (d3d12SwapChainActive) { // Use frame latency waitable object if available for better frame pacing HANDLE waitableObject = GetFrameLatencyWaitableObject(); @@ -1218,12 +1082,10 @@ double Upscaling::GetRefreshRate(HWND a_window) // there may be the possibility that display may be duplicated and windows may be one of them in such scenario // there may be two callback because source is same target will be different // as window is on both the display so either selecting either one is ok - if (wcscmp(info.szDevice, sourceName.viewGdiDeviceName) == 0) { - // get the refresh rate - UINT numerator = p.targetInfo.refreshRate.Numerator; - UINT denominator = p.targetInfo.refreshRate.Denominator; - return (double)numerator / (double)denominator; - } + // get the refresh rate + UINT numerator = p.targetInfo.refreshRate.Numerator; + UINT denominator = p.targetInfo.refreshRate.Denominator; + return (double)numerator / (double)denominator; } } } @@ -1235,7 +1097,23 @@ double Upscaling::GetRefreshRate(HWND a_window) bool Upscaling::IsFrameGenerationActive() const { - return d3d12Interop && settings.frameGenerationMode && fidelityFX.isFrameGenActive; + return d3d12SwapChainActive && settings.frameGenerationMode && fidelityFX.isFrameGenActive && !globals::game::isVR; +} + +bool Upscaling::IsUpscalingActive() +{ + auto method = GetUpscaleMethod(); + + // Only consider vendor upscalers (FSR/DLSS) as "active" when the + // selected method actually produces a downscale. If the renderer is + // currently running at 1:1 (no downscale) then depth-buffer culling and + // other VR-sensitive behavior can remain enabled. + if (!(method == UpscaleMethod::kFSR || method == UpscaleMethod::kDLSS)) { + return false; + } + + // resolutionScale.x represents renderWidth / displayWidth. + return resolutionScale.x < .99f; } /** @@ -1262,11 +1140,10 @@ float Upscaling::GetFrameGenerationFrameTime() const // Unified interface methods void Upscaling::LoadUpscalingSDKs() { - // Initialize all upscaling SDK components during plugin startup + // Initialize upscaling SDK components during plugin startup // This ensures all SDKs are available before any D3D device creation streamline.LoadInterposer(); - fidelityFX.LoadFFX(); - xess.LoadXeSS(); + fidelityFX.LoadFFX(); // Only for frame generation now } void Upscaling::CheckFrameConstants() @@ -1358,6 +1235,7 @@ void Upscaling::Upscale() context->OMSetRenderTargets(0, nullptr, nullptr); // Unbind all bound render targets auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; auto dispatchCount = Util::GetScreenDispatchCount(true); @@ -1366,7 +1244,6 @@ void Upscaling::Upscale() auto& temporalAAMask = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kTEMPORAL_AA_MASK]; auto& normals = renderer->GetRuntimeData().renderTargets[globals::deferred->forwardRenderTargets[2]]; - auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; { @@ -1382,24 +1259,7 @@ void Upscaling::Upscale() ID3D11ShaderResourceView* views[4] = { temporalAAMask.SRV, normals.SRV, motionVector.SRV, depth.depthSRV }; context->CSSetShaderResources(0, ARRAYSIZE(views), views); - // Use shared D3D12 textures for FSR/XeSS, regular D3D11 textures for DLSS - ID3D11UnorderedAccessView* reactiveMaskUAV = upscaleMethod == UpscaleMethod::kDLSS ? reactiveMaskTexture->uav.get() : reactiveMaskShared12->uav; - - ID3D11UnorderedAccessView* transparencyUAV = nullptr; - if (upscaleMethod == UpscaleMethod::kDLSS) { - transparencyUAV = transparencyCompositionMaskTexture->uav.get(); - } else if (upscaleMethod == UpscaleMethod::kFSR) { - transparencyUAV = transparencyCompositionMaskShared12->uav; - } - - ID3D11UnorderedAccessView* motionVectorUAV = nullptr; - if (upscaleMethod == UpscaleMethod::kDLSS) { - motionVectorUAV = motionVectorCopyTexture->uav.get(); - } else if (upscaleMethod == UpscaleMethod::kXESS) { - motionVectorUAV = motionVectorBufferShared12->uav; - } - - ID3D11UnorderedAccessView* uavs[3] = { reactiveMaskUAV, transparencyUAV, motionVectorUAV }; + ID3D11UnorderedAccessView* uavs[3] = { reactiveMaskTexture->uav.get(), transparencyCompositionMaskTexture->uav.get(), upscaleMethod == UpscaleMethod::kDLSS ? motionVectorCopyTexture->uav.get() : nullptr }; context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); context->CSSetShader(GetEncodeTexturesCS(), nullptr, 0); @@ -1425,70 +1285,10 @@ void Upscaling::Upscale() { state->BeginPerfEvent("Upscaling"); - if (upscaleMethod == UpscaleMethod::kDLSS) - streamline.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVectorCopyTexture->resource.get(), sl::DLSSPreset::ePresetK); - else { - // Copy input color texture to shared D3D12 resource (only dynamic resolution area) - auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); - D3D11_BOX srcBox = {}; - srcBox.left = 0; - srcBox.top = 0; - srcBox.front = 0; - srcBox.right = (UINT)renderSize.x; - srcBox.bottom = (UINT)renderSize.y; - srcBox.back = 1; - - context->CopySubresourceRegion(inputColorBufferShared12->resource11, 0, 0, 0, 0, main.texture, 0, &srcBox); - - // Wait for D3D11 to finish - winrt::com_ptr d3d11Context4; - DX::ThrowIfFailed(context->QueryInterface(IID_PPV_ARGS(d3d11Context4.put()))); - DX::ThrowIfFailed(d3d11Context4->Signal(sharedD3D11Fence.get(), sharedInteropFenceValue)); - DX::ThrowIfFailed(sharedD3D12CommandQueue->Wait(sharedD3D12Fence.get(), sharedInteropFenceValue)); - sharedInteropFenceValue++; - - // Reset command allocator and list - DX::ThrowIfFailed(sharedD3D12CommandAllocator->Reset()); - DX::ThrowIfFailed(sharedD3D12CommandList->Reset(sharedD3D12CommandAllocator.get(), nullptr)); - - if (upscaleMethod == UpscaleMethod::kFSR) { - fidelityFX.Upscale( - inputColorBufferShared12->resource.get(), - motionVectorBufferShared12->resource.get(), - depthBufferShared12->resource.get(), - reactiveMaskShared12->resource.get(), - transparencyCompositionMaskShared12->resource.get(), - outputColorBufferShared12->resource.get(), - sharedD3D12CommandList.get(), - (uint32_t)renderSize.x, - (uint32_t)renderSize.y, - jitter); - } else { - xess.Upscale( - inputColorBufferShared12->resource.get(), - motionVectorBufferShared12->resource.get(), - depthBufferShared12->resource.get(), - reactiveMaskShared12->resource.get(), - outputColorBufferShared12->resource.get(), - sharedD3D12CommandList.get(), - (uint32_t)renderSize.x, - (uint32_t)renderSize.y, - jitter); - } - - // Close and execute command list - DX::ThrowIfFailed(sharedD3D12CommandList->Close()); - - ID3D12CommandList* commandLists[] = { sharedD3D12CommandList.get() }; - sharedD3D12CommandQueue->ExecuteCommandLists(1, commandLists); - - // Wait for D3D12 to finish - DX::ThrowIfFailed(sharedD3D12CommandQueue->Signal(sharedD3D12Fence.get(), sharedInteropFenceValue)); - DX::ThrowIfFailed(d3d11Context4->Wait(sharedD3D11Fence.get(), sharedInteropFenceValue)); - sharedInteropFenceValue++; - - // Copy back to main buffer - context->CopyResource(main.texture, outputColorBufferShared12->resource11); + if (upscaleMethod == UpscaleMethod::kDLSS) { + streamline.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVectorCopyTexture->resource.get()); + } else if (upscaleMethod == UpscaleMethod::kFSR) { + fidelityFX.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVector.texture, settings.sharpnessFSR); } state->EndPerfEvent(); @@ -1620,8 +1420,40 @@ void Upscaling::UpscaleDepth() } } +void Upscaling::ApplySharpening() +{ + if (settings.sharpnessDLSS <= 0.0f) + return; + + if (!sharpenerTexture) + return; + + float currentSharpness = (-2.0f * settings.sharpnessDLSS) + 2.0f; + currentSharpness = exp2(-currentSharpness); + + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + + ID3D11Resource* mainResource = nullptr; + main.SRV->GetResource(&mainResource); + + if (!mainResource) + return; + + context->OMSetRenderTargets(0, nullptr, nullptr); + + rcas.ApplySharpen(main.SRV, sharpenerTexture->uav.get(), currentSharpness); + context->CopyResource(mainResource, sharpenerTexture->resource.get()); + + mainResource->Release(); + + globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); +} + void Upscaling::Main_UpdateJitter::thunk(RE::BSGraphics::State* a_state) { + globals::features::upscaling.ConfigureTAA(); func(a_state); globals::features::upscaling.ConfigureUpscaling(a_state); } @@ -1632,24 +1464,27 @@ void Upscaling::MenuManagerDrawInterfaceStartHook::thunk(int64_t a1) func(a1); } -void Upscaling::Main_PostProcessing::thunk(RE::ImageSpaceManager* a1, uint32_t a3, uint32_t er8_) +void Upscaling::Main_PostProcessing::thunk(RE::ImageSpaceManager* a_this, uint32_t a3, RE::RENDER_TARGET a_target, void* a_4, bool a_5) { auto& upscaling = globals::features::upscaling; auto upscaleMethod = upscaling.GetUpscaleMethod(); - upscaling.CopySharedD3D12Resources(); + if (upscaling.d3d12SwapChainActive && upscaling.settings.frameGenerationMode) + upscaling.CopySharedD3D12Resources(); if (upscaleMethod != UpscaleMethod::kNONE && upscaleMethod != UpscaleMethod::kTAA) upscaling.PerformUpscaling(); + if (upscaleMethod == UpscaleMethod::kDLSS) + upscaling.ApplySharpening(); + auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); GET_INSTANCE_MEMBER(BSImagespaceShaderISTemporalAA, imageSpaceManager); BSImagespaceShaderISTemporalAA->taaEnabled = upscaleMethod == UpscaleMethod::kTAA; - func(a1, a3, er8_); + func(a_this, a3, a_target, a_4, a_5); - // Disable TAA in some menus BSImagespaceShaderISTemporalAA->taaEnabled = false; } @@ -1683,4 +1518,4 @@ void Upscaling::BSFaceGenManager_UpdatePendingCustomizationTextures::thunk() runtimeData.dynamicResolutionLock = 1; func(); runtimeData.dynamicResolutionLock = 0; -} \ No newline at end of file +} diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index 2d79597736..933e78f2bb 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -1,15 +1,16 @@ #pragma once #include "Feature.h" +#include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" +#include "Upscaling/RCAS/RCAS.h" #include "Upscaling/Streamline.h" -#include "Upscaling/XeSS.h" #include #include #include /** - * @brief Provides upscaling functionality including DLSS, FSR, XeSS and TAA. + * @brief Provides upscaling functionality including DLSS, FSR and TAA. * * This feature handles various upscaling methods and frame generation technologies * to improve performance while maintaining visual quality. @@ -20,7 +21,7 @@ struct Upscaling : Feature // Feature interface virtual inline std::string GetName() override { return "Upscaling"; } virtual inline std::string GetShortName() override { return "Upscaling"; } - virtual inline bool SupportsVR() override { return false; } + virtual inline bool SupportsVR() override { return true; } virtual inline bool IsCore() const override { return false; } virtual inline std::string_view GetCategory() const override { return "Display"; } @@ -30,7 +31,6 @@ struct Upscaling : Feature "Advanced upscaling and frame generation technologies for improved performance", { "DLSS (Deep Learning Super Sampling) support", "FSR (FidelityFX Super Resolution) support", - "XeSS (Intel Xe Super Sampling) support", "TAA (Temporal Anti-Aliasing) support", "Frame generation for supported systems" } }; @@ -43,7 +43,6 @@ struct Upscaling : Feature kNONE, kTAA, kFSR, - kXESS, kDLSS }; @@ -56,6 +55,9 @@ struct Upscaling : Feature uint frameGenerationMode = 1; uint frameGenerationForceEnable = 0; uint streamlineLogLevel = 0; // 0=Off, 1=Default, 2=Verbose + float sharpnessFSR = 1.0f; + float sharpnessDLSS = 1.0f; + uint DLSSPreset = 2; // VR-specific DLSS preset: 0=F, 1=J, 2=K }; Settings settings; @@ -79,7 +81,7 @@ struct Upscaling : Feature bool isWindowed = false; bool lowRefreshRate = false; bool fidelityFXMissing = false; - bool d3d12Interop = false; + bool d3d12SwapChainActive = false; // Timing and scaling double refreshRate = 0.0f; @@ -89,12 +91,14 @@ struct Upscaling : Feature // FG FPS Measurement for Overlay bool IsFrameGenerationActive() const; float GetFrameGenerationFrameTime() const; + bool IsUpscalingActive(); // Feature interface overrides virtual void DrawSettings() override; virtual void SaveSettings(json& o_json) override; virtual void LoadSettings(json& o_json) override; virtual void RestoreDefaultSettings() override; + virtual void DataLoaded() override; /** * @brief Installs Direct3D-related hooks for device and factory creation. @@ -110,7 +114,6 @@ struct Upscaling : Feature void CheckResources(UpscaleMethod a_upscalemethod); void CreateUpscalingTextureResources(UpscaleMethod a_upscalemethod); void DestroyUpscalingTextureResources(UpscaleMethod a_upscalemethod); - void UpdateSharedResources(); winrt::com_ptr encodeTexturesCS[5]; // One for each UpscaleMethod ID3D11ComputeShader* GetEncodeTexturesCS(); @@ -128,47 +131,23 @@ struct Upscaling : Feature winrt::com_ptr upscaleBlendState; winrt::com_ptr upscaleRasterizerState; + void ConfigureTAA(); void ConfigureUpscaling(RE::BSGraphics::State* a_state); void Upscale(); - void ApplyNISSharpening(); // D3D11 textures Texture2D* reactiveMaskTexture = nullptr; Texture2D* transparencyCompositionMaskTexture = nullptr; Texture2D* motionVectorCopyTexture = nullptr; - Texture2D* nisSharpenerTexture = nullptr; + Texture2D* sharpenerTexture = nullptr; virtual void ClearShaderCache() override; - // Shared D3D12 device and interop resources - winrt::com_ptr sharedD3D12Device; - winrt::com_ptr sharedD3D12CommandQueue; - winrt::com_ptr sharedD3D12CommandAllocator; - winrt::com_ptr sharedD3D12CommandList; - winrt::com_ptr sharedD3D12Fence; - HANDLE sharedFenceEvent = nullptr; - UINT64 sharedFenceValue = 0; - - // D3D11/D3D12 shared fence for interop synchronization - winrt::com_ptr sharedD3D11Fence; - UINT64 sharedInteropFenceValue = 0; - - // Shared D3D12 resources for upscaling systems - WrappedResource* depthBufferShared12 = nullptr; - WrappedResource* motionVectorBufferShared12 = nullptr; - WrappedResource* reactiveMaskShared12 = nullptr; - WrappedResource* transparencyCompositionMaskShared12 = nullptr; - WrappedResource* inputColorBufferShared12 = nullptr; - WrappedResource* outputColorBufferShared12 = nullptr; - - // Frame tracking to ensure shared resources are only copied once per frame - Util::FrameChecker sharedResourcesFrameChecker; - // Static instances instead of singletons static inline Streamline streamline; - static inline XeSS xess; - static inline FidelityFX fidelityFX; - static inline class DX12SwapChain dx12SwapChain; + static inline FidelityFX fidelityFX; ///< Only for frame generation + static inline DX12SwapChain dx12SwapChain; + static inline RCAS rcas; ///< Standalone RCAS sharpening for DLSS winrt::com_ptr copyDepthToSharedBufferPS; @@ -178,12 +157,20 @@ struct Upscaling : Feature float dynamicResolutionWidthRatio = 1.0f; float dynamicResolutionHeightRatio = 1.0f; - void CreateSharedD3D12Device(IDXGIAdapter* a_dxgiAdapter); + bool previousUpscalingWasActive = false; + void CopySharedD3D12Resources(); void PostDisplay(); void PerformUpscaling(); void UpscaleDepth(); + /** + * @brief Applies RCAS sharpening to the main render target after DLSS upscaling. + * + * Runs in HDR space before tonemapping. Only called when DLSS is active and sharpness > 0. + */ + void ApplySharpening(); + static void TimerSleepQPC(int64_t targetQPC); void FrameLimiter(); @@ -229,7 +216,7 @@ struct Upscaling : Feature struct Main_PostProcessing { - static void thunk(RE::ImageSpaceManager* a1, uint32_t a3, uint32_t er8_); + static void thunk(RE::ImageSpaceManager* a_this, uint32_t a3, RE::RENDER_TARGET a_target, void* a_4, bool a_5); static inline REL::Relocation func; }; diff --git a/src/Features/Upscaling/DX12SwapChain.cpp b/src/Features/Upscaling/DX12SwapChain.cpp index 0d232531ab..a2d005809f 100644 --- a/src/Features/Upscaling/DX12SwapChain.cpp +++ b/src/Features/Upscaling/DX12SwapChain.cpp @@ -7,24 +7,28 @@ #include "FidelityFX.h" #include "Streamline.h" -void DX12SwapChain::InitializeD3D12Resources() +void DX12SwapChain::CreateD3D12Device(IDXGIAdapter* a_adapter) { - auto& upscaling = globals::features::upscaling; + DX::ThrowIfFailed(D3D12CreateDevice(a_adapter, D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&d3d12Device))); + + D3D12_COMMAND_QUEUE_DESC queueDesc = {}; + queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; + queueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; + queueDesc.NodeMask = 0; + + DX::ThrowIfFailed(d3d12Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue))); - // Create frame-specific command allocators and lists using shared device for (int i = 0; i < 2; i++) { - DX::ThrowIfFailed(upscaling.sharedD3D12Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocators[i]))); - DX::ThrowIfFailed(upscaling.sharedD3D12Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocators[i].get(), nullptr, IID_PPV_ARGS(&commandLists[i]))); + DX::ThrowIfFailed(d3d12Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocators[i]))); + DX::ThrowIfFailed(d3d12Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocators[i].get(), nullptr, IID_PPV_ARGS(&commandLists[i]))); commandLists[i]->Close(); } } void DX12SwapChain::CreateSwapChain(IDXGIAdapter* adapter, DXGI_SWAP_CHAIN_DESC a_swapChainDesc) { - auto& upscaling = globals::features::upscaling; - - // Initialize D3D12 resources first - InitializeD3D12Resources(); + CreateD3D12Device(adapter); IDXGIFactory4* dxgiFactory; DX::ThrowIfFailed(adapter->GetParent(IID_PPV_ARGS(&dxgiFactory))); @@ -44,7 +48,7 @@ void DX12SwapChain::CreateSwapChain(IDXGIAdapter* adapter, DXGI_SWAP_CHAIN_DESC ffxSwapChainDesc.desc = &swapChainDesc; ffxSwapChainDesc.dxgiFactory = dxgiFactory; ffxSwapChainDesc.fullscreenDesc = nullptr; - ffxSwapChainDesc.gameQueue = upscaling.sharedD3D12CommandQueue.get(); + ffxSwapChainDesc.gameQueue = commandQueue.get(); ffxSwapChainDesc.hwnd = a_swapChainDesc.OutputWindow; ffxSwapChainDesc.swapchain = &swapChain; @@ -64,11 +68,9 @@ void DX12SwapChain::CreateSwapChain(IDXGIAdapter* adapter, DXGI_SWAP_CHAIN_DESC void DX12SwapChain::CreateInterop() { - auto& upscaling = globals::features::upscaling; - HANDLE sharedFenceHandle; - DX::ThrowIfFailed(upscaling.sharedD3D12Device->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_PPV_ARGS(&d3d12Fence))); - DX::ThrowIfFailed(upscaling.sharedD3D12Device->CreateSharedHandle(d3d12Fence.get(), nullptr, GENERIC_ALL, nullptr, &sharedFenceHandle)); + DX::ThrowIfFailed(d3d12Device->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_PPV_ARGS(&d3d12Fence))); + DX::ThrowIfFailed(d3d12Device->CreateSharedHandle(d3d12Fence.get(), nullptr, GENERIC_ALL, nullptr, &sharedFenceHandle)); DX::ThrowIfFailed(d3d11Device->OpenSharedFence(sharedFenceHandle, IID_PPV_ARGS(&d3d11Fence))); CloseHandle(sharedFenceHandle); @@ -84,10 +86,10 @@ void DX12SwapChain::CreateInterop() texDesc11.SampleDesc.Quality = 0; texDesc11.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; - swapChainBufferWrapped = new WrappedResource(texDesc11, d3d11Device.get(), upscaling.sharedD3D12Device.get()); + swapChainBufferWrapped = new WrappedResource(texDesc11, d3d11Device.get(), d3d12Device.get()); texDesc11.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - uiBufferWrapped = new WrappedResource(texDesc11, d3d11Device.get(), upscaling.sharedD3D12Device.get()); + uiBufferWrapped = new WrappedResource(texDesc11, d3d11Device.get(), d3d12Device.get()); } DXGISwapChainProxy* DX12SwapChain::GetSwapChainProxy() @@ -117,7 +119,7 @@ HRESULT DX12SwapChain::Present(UINT SyncInterval, UINT Flags) // Wait for D3D11 to finish DX::ThrowIfFailed(d3d11Context->Signal(d3d11Fence.get(), fenceValue)); - DX::ThrowIfFailed(upscaling.sharedD3D12CommandQueue->Wait(d3d12Fence.get(), fenceValue)); + DX::ThrowIfFailed(commandQueue->Wait(d3d12Fence.get(), fenceValue)); fenceValue++; // New frame, reset @@ -150,13 +152,13 @@ HRESULT DX12SwapChain::Present(UINT SyncInterval, UINT Flags) DX::ThrowIfFailed(commandLists[frameIndex]->Close()); ID3D12CommandList* commandListsToExecute[] = { commandLists[frameIndex].get() }; - upscaling.sharedD3D12CommandQueue->ExecuteCommandLists(1, commandListsToExecute); + commandQueue->ExecuteCommandLists(1, commandListsToExecute); // Present the frame DX::ThrowIfFailed(swapChain->Present(SyncInterval, Flags)); // Wait for D3D12 to finish - DX::ThrowIfFailed(upscaling.sharedD3D12CommandQueue->Signal(d3d12Fence.get(), fenceValue)); + DX::ThrowIfFailed(commandQueue->Signal(d3d12Fence.get(), fenceValue)); DX::ThrowIfFailed(d3d11Context->Wait(d3d11Fence.get(), fenceValue)); fenceValue++; @@ -213,24 +215,19 @@ float DX12SwapChain::GetFrameTime() const WrappedResource::WrappedResource(D3D11_TEXTURE2D_DESC a_texDesc, ID3D11Device5* a_d3d11Device, ID3D12Device* a_d3d12Device) { - D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET | D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS; - if (a_texDesc.BindFlags & D3D11_BIND_DEPTH_STENCIL) - flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; - if (a_texDesc.BindFlags & D3D11_BIND_UNORDERED_ACCESS) - flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; - if (!(a_texDesc.BindFlags & D3D11_BIND_SHADER_RESOURCE)) - flags |= D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE; - if (!(a_texDesc.BindFlags & D3D11_BIND_RENDER_TARGET)) - flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; - D3D12_RESOURCE_DESC desc12{ D3D12_RESOURCE_DIMENSION_TEXTURE2D, 0, a_texDesc.Width, a_texDesc.Height, (UINT16)a_texDesc.ArraySize, (UINT16)a_texDesc.MipLevels, a_texDesc.Format, { a_texDesc.SampleDesc.Count, a_texDesc.SampleDesc.Quality }, D3D12_TEXTURE_LAYOUT_UNKNOWN, flags }; - D3D12_HEAP_PROPERTIES heapProp = { D3D12_HEAP_TYPE_DEFAULT, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 1, 1 }; - - DX::ThrowIfFailed(a_d3d12Device->CreateCommittedResource(&heapProp, D3D12_HEAP_FLAG_SHARED, &desc12, D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&resource))); + // Create D3D11 shared texture directly instead of wrapping D3D12 resource + a_texDesc.MiscFlags |= D3D11_RESOURCE_MISC_SHARED | D3D11_RESOURCE_MISC_SHARED_NTHANDLE; + DX::ThrowIfFailed(a_d3d11Device->CreateTexture2D(&a_texDesc, nullptr, &resource11)); + // Get shared handle from D3D11 texture to enable D3D12 access + winrt::com_ptr dxgiResource; + DX::ThrowIfFailed(resource11->QueryInterface(IID_PPV_ARGS(dxgiResource.put()))); HANDLE sharedHandle = nullptr; - DX::ThrowIfFailed(a_d3d12Device->CreateSharedHandle(resource.get(), nullptr, GENERIC_ALL, nullptr, &sharedHandle)); + DX::ThrowIfFailed(dxgiResource->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, nullptr, &sharedHandle)); - DX::ThrowIfFailed(a_d3d11Device->OpenSharedResource1(sharedHandle, IID_PPV_ARGS(&resource11))); + // Open the shared D3D11 texture as D3D12 resource + DX::ThrowIfFailed(a_d3d12Device->OpenSharedHandle(sharedHandle, IID_PPV_ARGS(resource.put()))); + CloseHandle(sharedHandle); if (a_texDesc.BindFlags & D3D11_BIND_SHADER_RESOURCE) { D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; @@ -400,4 +397,21 @@ void DX12SwapChain::SetUIBuffer() data.RTV = uiBufferWrapped->rtv; d3d11Context->OMSetRenderTargets(1, &data.RTV, nullptr); } +} + +void DX12SwapChain::CreateSharedResources() +{ + auto renderer = globals::game::renderer; + + // Create depth buffer + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + D3D11_TEXTURE2D_DESC texDesc{}; + main.texture->GetDesc(&texDesc); + texDesc.Format = DXGI_FORMAT_R32_FLOAT; + depthBufferShared12 = new WrappedResource(texDesc, d3d11Device.get(), d3d12Device.get()); + + // Create motion vector buffer + auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + motionVector.texture->GetDesc(&texDesc); + motionVectorBufferShared12 = new WrappedResource(texDesc, d3d11Device.get(), d3d12Device.get()); } \ No newline at end of file diff --git a/src/Features/Upscaling/DX12SwapChain.h b/src/Features/Upscaling/DX12SwapChain.h index 087179b4b8..19d51e276a 100644 --- a/src/Features/Upscaling/DX12SwapChain.h +++ b/src/Features/Upscaling/DX12SwapChain.h @@ -61,7 +61,8 @@ struct DXGISwapChainProxy : IDXGISwapChain class DX12SwapChain { public: - // D3D12 resources for swap chain (uses shared device from Upscaling) + winrt::com_ptr d3d12Device; + winrt::com_ptr commandQueue; winrt::com_ptr commandAllocators[2]; winrt::com_ptr commandLists[2]; @@ -72,6 +73,10 @@ class DX12SwapChain WrappedResource* swapChainBufferWrapped; WrappedResource* uiBufferWrapped; + // D3D12 interop resources for frame generation + WrappedResource* depthBufferShared12 = nullptr; + WrappedResource* motionVectorBufferShared12 = nullptr; + winrt::com_ptr d3d11Device; winrt::com_ptr d3d11Context; @@ -92,7 +97,7 @@ class DX12SwapChain // Returns the current frame time (in seconds) for accurate FPS calculation when frame generation is active float GetFrameTime() const; - void InitializeD3D12Resources(); + void CreateD3D12Device(IDXGIAdapter* a_adapter); void CreateSwapChain(IDXGIAdapter* adapter, DXGI_SWAP_CHAIN_DESC swapChainDesc); void CreateInterop(); @@ -107,4 +112,7 @@ class DX12SwapChain HANDLE GetFrameLatencyWaitableObject(); void SetUIBuffer(); + + // D3D12 interop resource management + void CreateSharedResources(); }; diff --git a/src/Features/Upscaling/FidelityFX.cpp b/src/Features/Upscaling/FidelityFX.cpp index a3e062f010..a44c992343 100644 --- a/src/Features/Upscaling/FidelityFX.cpp +++ b/src/Features/Upscaling/FidelityFX.cpp @@ -13,14 +13,9 @@ std::vector> FidelityFX::dllVersions = {}; void FidelityFX::LoadFFX() { - // Load upscaler and frame generation DLLs and their function pointers - std::wstring upscalerDllName = L"amd_fidelityfx_upscaler_dx12.dll"; + // Load uframe generation DLL and its function pointers std::wstring framegenDllName = L"amd_fidelityfx_framegeneration_dx12.dll"; - - std::wstring upscalerPath = std::wstring(FidelityFX::PluginDir) + L"\\" + upscalerDllName; std::wstring framegenPath = std::wstring(FidelityFX::PluginDir) + L"\\" + framegenDllName; - - featureFSR3 = LoadLibrary(upscalerPath.c_str()); featureFSR3FG = LoadLibrary(framegenPath.c_str()); // Load loader DLL from plugin directory @@ -38,12 +33,6 @@ void FidelityFX::LoadFFX() ffxLoadFunctions(&ffxModule, module); - if (featureFSR3) { - logger::info("[FidelityFX] Upscaler DLL found and available"); - } else { - logger::warn("[FidelityFX] Upscaler DLL not found - FSR3 upscaling disabled"); - } - if (featureFSR3FG) { logger::info("[FidelityFX] Frame generation DLL found and available"); } else { @@ -58,16 +47,15 @@ void FidelityFX::LoadFFX() void FidelityFX::SetupFrameGeneration() { auto& swapChain = globals::features::upscaling.dx12SwapChain; - auto& upscaling = globals::features::upscaling; ffx::CreateContextDescFrameGeneration createFg{}; createFg.displaySize = { swapChain.swapChainDesc.Width, swapChain.swapChainDesc.Height }; createFg.maxRenderSize = createFg.displaySize; - createFg.flags = 0; + createFg.flags = FFX_FRAMEGENERATION_ENABLE_ASYNC_WORKLOAD_SUPPORT; createFg.backBufferFormat = ffxApiGetSurfaceFormatDX12(swapChain.swapChainDesc.Format); ffx::CreateBackendDX12Desc backendDesc{}; - backendDesc.device = upscaling.sharedD3D12Device.get(); + backendDesc.device = swapChain.d3d12Device.get(); if (ffx::CreateContext(frameGenContext, nullptr, createFg, backendDesc) != ffx::ReturnCode::Ok) logger::critical("[FidelityFX] Failed to create frame generation context!"); @@ -112,8 +100,8 @@ void FidelityFX::Present(bool a_useFrameGeneration) configParameters.frameID = frameID; configParameters.swapChain = swapChain.swapChain; configParameters.onlyPresentGenerated = false; - configParameters.allowAsyncWorkloads = false; configParameters.flags = 0; + configParameters.allowAsyncWorkloads = true; auto state = globals::state; @@ -137,14 +125,9 @@ void FidelityFX::Present(bool a_useFrameGeneration) } if (a_useFrameGeneration) { - auto commandList = swapChain.commandLists[swapChain.frameIndex].get(); - - auto depth = upscaling.depthBufferShared12->resource.get(); - auto motionVectors = upscaling.motionVectorBufferShared12->resource.get(); - ffx::DispatchDescFrameGenerationPrepare dispatchParameters{}; - dispatchParameters.commandList = commandList; + dispatchParameters.commandList = swapChain.commandLists[swapChain.frameIndex].get(); dispatchParameters.motionVectorScale.x = renderSize.x; dispatchParameters.motionVectorScale.y = renderSize.y; @@ -164,8 +147,8 @@ void FidelityFX::Present(bool a_useFrameGeneration) dispatchParameters.frameID = frameID; - dispatchParameters.depth = ffxApiGetResourceDX12(depth); - dispatchParameters.motionVectors = ffxApiGetResourceDX12(motionVectors); + dispatchParameters.depth = ffxApiGetResourceDX12(swapChain.depthBufferShared12->resource.get()); + dispatchParameters.motionVectors = ffxApiGetResourceDX12(swapChain.motionVectorBufferShared12->resource.get()); ffx::DispatchDescFrameGenerationPrepareCameraInfo cameraConfig{}; @@ -202,152 +185,146 @@ void FidelityFX::Present(bool a_useFrameGeneration) void FidelityFX::CreateFSRResources() { auto state = globals::state; - auto& upscaling = globals::features::upscaling; - ffx::CreateContextDescUpscale createUpscaling; - createUpscaling.maxRenderSize.width = (uint)state->screenSize.x; - createUpscaling.maxRenderSize.height = (uint)state->screenSize.y; - createUpscaling.maxUpscaleSize.width = (uint)state->screenSize.x; - createUpscaling.maxUpscaleSize.height = (uint)state->screenSize.y; - createUpscaling.flags = FFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE; - - createUpscaling.fpMessage = [](uint32_t type, const wchar_t* wideMessage) { - auto message = stl::utf16_to_utf8(wideMessage); - if (message.has_value()) { - if (type == FFX_API_MESSAGE_TYPE_ERROR) - logger::error("[FidelityFX] {}", message.value()); - else - logger::warn("[FidelityFX] {}", message.value()); - } - }; + // Prevent multiple allocations + if (fsrScratchBuffer) { + logger::warn("[FidelityFX] FSR resources already created, skipping allocation"); + return; + } - ffx::CreateBackendDX12Desc backendDesc{}; - backendDesc.device = upscaling.sharedD3D12Device.get(); + auto fsrDevice = ffxGetDeviceDX11(globals::d3d::device); - if (ffx::CreateContext(upscalingContext, nullptr, createUpscaling, backendDesc) != ffx::ReturnCode::Ok) - logger::critical("[FidelityFX] Failed to create FSR3 API context"); + size_t scratchBufferSize = ffxGetScratchMemorySizeDX11(FFX_FSR3UPSCALER_CONTEXT_COUNT); + fsrScratchBuffer = calloc(scratchBufferSize, 1); + if (!fsrScratchBuffer) { + logger::critical("[FidelityFX] Failed to allocate FSR3 scratch buffer memory!"); + return; + } + memset(fsrScratchBuffer, 0, scratchBufferSize); + + FfxInterface fsrInterface; + if (ffxGetInterfaceDX11(&fsrInterface, fsrDevice, fsrScratchBuffer, scratchBufferSize, FFX_FSR3UPSCALER_CONTEXT_COUNT) != FFX_OK) { + logger::critical("[FidelityFX] Failed to initialize FSR3 backend interface!"); + free(fsrScratchBuffer); + fsrScratchBuffer = nullptr; + return; + } - // Query version information after context creation - QueryVersion(); + FfxFsr3ContextDescription contextDescription; + contextDescription.maxRenderSize.width = (uint)state->screenSize.x; + contextDescription.maxRenderSize.height = (uint)state->screenSize.y; + contextDescription.maxUpscaleSize.width = (uint)state->screenSize.x; + contextDescription.maxUpscaleSize.height = (uint)state->screenSize.y; + contextDescription.displaySize.width = (uint)state->screenSize.x; + contextDescription.displaySize.height = (uint)state->screenSize.y; + contextDescription.flags = FFX_FSR3_ENABLE_UPSCALING_ONLY | FFX_FSR3_ENABLE_AUTO_EXPOSURE | FFX_FSR3_ENABLE_HIGH_DYNAMIC_RANGE; + contextDescription.backendInterfaceUpscaling = fsrInterface; + + if (ffxFsr3ContextCreate(&fsrContext, &contextDescription) != FFX_OK) { + logger::critical("[FidelityFX] Failed to initialize FSR3 context!"); + free(fsrScratchBuffer); + fsrScratchBuffer = nullptr; + return; + } } void FidelityFX::DestroyFSRResources() { - if (ffx::DestroyContext(upscalingContext) != ffx::ReturnCode::Ok) - logger::critical("[FidelityFX] Failed to destroy FSR3 API context"); - upscalingContext = {}; + if (ffxFsr3ContextDestroy(&fsrContext) != FFX_OK) + logger::critical("[FidelityFX] Failed to destroy FSR3 context!"); + + // Free the scratch buffer to prevent memory leak + if (fsrScratchBuffer) { + free(fsrScratchBuffer); + fsrScratchBuffer = nullptr; + } + + // Reset crash logging flag when resources are destroyed + fsrDispatchCrashLogged = false; } -float FidelityFX::GetInputResolutionScale([[maybe_unused]] uint32_t outputWidth, [[maybe_unused]] uint32_t outputHeight, uint32_t qualityMode) +float2 FidelityFX::GetInputResolutionScale([[maybe_unused]] uint32_t outputWidth, [[maybe_unused]] uint32_t outputHeight, uint32_t qualityMode) { - float upscaleRatio = 1.0f; - - ffx::QueryDescUpscaleGetUpscaleRatioFromQualityMode query{}; - query.qualityMode = qualityMode; - query.pOutUpscaleRatio = &upscaleRatio; + float scale = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)qualityMode); + return { scale, scale }; +} - if (ffx::Query(upscalingContext, query) != ffx::ReturnCode::Ok) { - logger::critical("[FidelityFX] Failed to query upscale ratio for quality preset {}", qualityMode); - return 1.0f; +FfxResource ffxGetResource(ID3D11Resource* dx11Resource, + [[maybe_unused]] wchar_t const* ffxResName, + FfxResourceStates state = FFX_RESOURCE_STATE_PIXEL_COMPUTE_READ) +{ + FfxResource resource = {}; + resource.resource = reinterpret_cast(const_cast(dx11Resource)); + resource.state = state; + resource.description = GetFfxResourceDescriptionDX11(dx11Resource); + +#ifdef _DEBUG + if (ffxResName) { + wcscpy_s(resource.name, ffxResName); } +#endif - // Convert upscale ratio to resolution scale (input resolution / output resolution) - float resolutionScale = 1.0f / upscaleRatio; - - return resolutionScale; + return resource; } -void FidelityFX::Upscale( - ID3D12Resource* a_inputColorTexture, - ID3D12Resource* a_motionVectorTexture, - ID3D12Resource* a_depthTexture, - ID3D12Resource* a_reactiveMask, - ID3D12Resource* a_transparencyCompositionMask, - ID3D12Resource* a_outputTexture, - ID3D12GraphicsCommandList* a_commandList, - uint32_t a_renderWidth, - uint32_t a_renderHeight, - float2 a_jitter) +void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors, float a_sharpness) { + auto renderer = globals::game::renderer; + auto context = globals::d3d::context; auto state = globals::state; + auto& depthTexture = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - ffx::DispatchDescUpscale dispatchUpscale{}; - - dispatchUpscale.commandList = a_commandList; - dispatchUpscale.color = ffxApiGetResourceDX12(a_inputColorTexture, FFX_API_RESOURCE_STATE_PIXEL_COMPUTE_READ); - dispatchUpscale.depth = ffxApiGetResourceDX12(a_depthTexture, FFX_API_RESOURCE_STATE_PIXEL_COMPUTE_READ); - dispatchUpscale.motionVectors = ffxApiGetResourceDX12(a_motionVectorTexture, FFX_API_RESOURCE_STATE_PIXEL_COMPUTE_READ); - dispatchUpscale.output = ffxApiGetResourceDX12(a_outputTexture, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS); - dispatchUpscale.exposure = ffxApiGetResourceDX12(nullptr, FFX_API_RESOURCE_STATE_PIXEL_COMPUTE_READ); - dispatchUpscale.reactive = ffxApiGetResourceDX12(a_reactiveMask, FFX_API_RESOURCE_STATE_PIXEL_COMPUTE_READ); - dispatchUpscale.transparencyAndComposition = ffxApiGetResourceDX12(a_transparencyCompositionMask, FFX_API_RESOURCE_STATE_PIXEL_COMPUTE_READ); - - dispatchUpscale.jitterOffset.x = -a_jitter.x; - dispatchUpscale.jitterOffset.y = -a_jitter.y; - dispatchUpscale.motionVectorScale.x = (globals::game::isVR ? 0.5f : 1.0f) * (float)a_renderWidth; - dispatchUpscale.motionVectorScale.y = (float)a_renderHeight; - dispatchUpscale.reset = false; - dispatchUpscale.enableSharpening = true; - dispatchUpscale.sharpness = 0.0f; + auto screenSize = state->screenSize; + auto renderSize = Util::ConvertToDynamic(screenSize); - dispatchUpscale.frameTimeDelta = static_cast(RE::GetSecondsSinceLastFrame() * 1000.f); + { + FfxFsr3DispatchUpscaleDescription dispatchParameters{}; - dispatchUpscale.preExposure = 1.0f; - dispatchUpscale.renderSize.width = a_renderWidth; - dispatchUpscale.renderSize.height = a_renderHeight; - dispatchUpscale.upscaleSize.width = (uint32_t)state->screenSize.x; - dispatchUpscale.upscaleSize.height = (uint32_t)state->screenSize.y; + dispatchParameters.commandList = ffxGetCommandListDX11(context); + dispatchParameters.color = ffxGetResource(a_upscalingTexture, L"FSR3_Input_OutputColor"); + dispatchParameters.depth = ffxGetResource(depthTexture.texture, L"FSR3_InputDepth"); + dispatchParameters.motionVectors = ffxGetResource(a_motionVectors, L"FSR3_InputMotionVectors"); + dispatchParameters.exposure = ffxGetResource(nullptr, L"FSR3_InputExposure"); + dispatchParameters.upscaleOutput = dispatchParameters.color; + dispatchParameters.reactive = ffxGetResource(a_reactiveMask, L"FSR3_InputReactiveMap"); + dispatchParameters.transparencyAndComposition = ffxGetResource(a_transparencyCompositionMask, L"FSR3_TransparencyAndCompositionMap"); - dispatchUpscale.cameraFovAngleVertical = Util::GetVerticalFOVRad(); + dispatchParameters.motionVectorScale.x = globals::game::isVR ? renderSize.x * 0.5f : renderSize.x; + dispatchParameters.motionVectorScale.y = renderSize.y; + dispatchParameters.renderSize.width = (uint)renderSize.x; + dispatchParameters.renderSize.height = (uint)renderSize.y; - dispatchUpscale.cameraFar = *globals::game::cameraFar; - dispatchUpscale.cameraNear = *globals::game::cameraNear; + auto& upscaling = globals::features::upscaling; + auto jitter = upscaling.jitter; - dispatchUpscale.viewSpaceToMetersFactor = 0.01428222656f; + dispatchParameters.jitterOffset.x = -jitter.x; + dispatchParameters.jitterOffset.y = -jitter.y; - dispatchUpscale.flags = FFX_UPSCALE_FLAG_NON_LINEAR_COLOR_SRGB; + dispatchParameters.frameTimeDelta = *globals::game::deltaTime * 1000.f; - if (ffx::Dispatch(upscalingContext, dispatchUpscale) != ffx::ReturnCode::Ok) - logger::critical("[FidelityFX] Failed to upscale"); -} + dispatchParameters.cameraFar = *globals::game::cameraFar; + dispatchParameters.cameraNear = *globals::game::cameraNear; -void FidelityFX::QueryVersion() -{ - auto& upscaling = globals::features::upscaling; + dispatchParameters.enableSharpening = true; + dispatchParameters.sharpness = a_sharpness; - // Clear existing version info - versionInfo.clear(); - - // Query upscaler versions if available - if (featureFSR3) { - ffxQueryDescGetVersions upscalerQuery{}; - upscalerQuery.header.type = FFX_API_QUERY_DESC_TYPE_GET_VERSIONS; - upscalerQuery.header.pNext = nullptr; - - ffx::CreateContextDescUpscale dummyUpscaler{}; - upscalerQuery.createDescType = dummyUpscaler.header.type; - upscalerQuery.device = upscaling.sharedD3D12Device.get(); - - uint64_t upscalerCount = 0; - upscalerQuery.outputCount = &upscalerCount; - upscalerQuery.versionIds = nullptr; - upscalerQuery.versionNames = nullptr; - - if (ffxModule.Query(nullptr, &upscalerQuery.header) == (ffxReturnCode_t)ffx::ReturnCode::Ok && upscalerCount > 0) { - // Allocate arrays for version info - std::vector upscalerIds(upscalerCount); - std::vector upscalerNames(upscalerCount); - - upscalerQuery.versionIds = upscalerIds.data(); - upscalerQuery.versionNames = upscalerNames.data(); - - // Second query to get actual data - if (ffxModule.Query(nullptr, &upscalerQuery.header) == (ffxReturnCode_t)ffx::ReturnCode::Ok) { - if (upscalerCount > 0 && upscalerNames[0]) { - versionInfo = upscalerNames[0]; - logger::info("[FidelityFX] Upscaler version: {}", versionInfo); - } + dispatchParameters.cameraFovAngleVertical = Util::GetVerticalFOVRad(); + dispatchParameters.viewSpaceToMetersFactor = 0.01428222656f; + dispatchParameters.reset = false; + dispatchParameters.preExposure = 1.0f; + + dispatchParameters.flags = 0; + + // Wrap FSR dispatch in SEH to catch crashes when RenderDoc is active + __try { + if (ffxFsr3ContextDispatchUpscale(&fsrContext, &dispatchParameters) != FFX_OK) + logger::critical("[FidelityFX] Failed to dispatch upscaling!"); + } __except (EXCEPTION_EXECUTE_HANDLER) { + if (!fsrDispatchCrashLogged) { + logger::critical("[FidelityFX] FSR3 dispatch crashed - this may be caused by RenderDoc capture interfering with FSR operations. Try disabling RenderDoc capture."); + fsrDispatchCrashLogged = true; } + // Continue execution instead of crashing } } } \ No newline at end of file diff --git a/src/Features/Upscaling/FidelityFX.h b/src/Features/Upscaling/FidelityFX.h index 6b5c0aa6f7..71d35c0bb9 100644 --- a/src/Features/Upscaling/FidelityFX.h +++ b/src/Features/Upscaling/FidelityFX.h @@ -3,13 +3,16 @@ #include #include +#include +#include +#include + #include #include #include #include #include -#include #include "../../Buffer.h" #include "../../State.h" @@ -23,10 +26,9 @@ class FidelityFX ffx::Context swapChainContext{}; ffx::Context frameGenContext; - ffx::Context upscalingContext; + FfxFsr3Context fsrContext; bool featureFSR3FG = false; - bool featureFSR3 = false; // Track if FidelityFX is currently being used for frame generation bool isFrameGenActive = false; @@ -34,25 +36,22 @@ class FidelityFX // Cached DLL version info for FidelityFX plugin directory static std::vector> dllVersions; - std::string versionInfo; - void LoadFFX(); - void QueryVersion(); void SetupFrameGeneration(); void Present(bool a_useFrameGeneration); void CreateFSRResources(); + void DestroyFSRResources(); - float GetInputResolutionScale(uint32_t outputWidth, uint32_t outputHeight, uint32_t qualityPreset); - void Upscale( - ID3D12Resource* a_inputColorTexture, - ID3D12Resource* a_motionVectorTexture, - ID3D12Resource* a_depthTexture, - ID3D12Resource* a_reactiveMask, - ID3D12Resource* a_transparencyCompositionMask, - ID3D12Resource* a_outputTexture, - ID3D12GraphicsCommandList* a_commandList, - uint32_t a_renderWidth, - uint32_t a_renderHeight, - float2 a_jitter); + + float2 GetInputResolutionScale(uint32_t outputWidth, uint32_t outputHeight, uint32_t qualityMode); + + void Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors, float a_sharpness); + +private: + // FSR scratch buffer - needs to be freed in DestroyFSRResources + void* fsrScratchBuffer = nullptr; + + // Flag to prevent spamming the log with FSR3 dispatch crash messages + bool fsrDispatchCrashLogged = false; }; diff --git a/src/Features/Upscaling/RCAS/RCAS.cpp b/src/Features/Upscaling/RCAS/RCAS.cpp new file mode 100644 index 0000000000..f7af7ce2b3 --- /dev/null +++ b/src/Features/Upscaling/RCAS/RCAS.cpp @@ -0,0 +1,78 @@ +#include "RCAS.h" + +#include "../../../Deferred.h" +#include "../../../State.h" +#include "../../../Util.h" + +struct RCASConfig +{ + float sharpness; + float3 pad; +}; + +RCAS::~RCAS() +{ + delete rcasConfigCB; + rcasConfigCB = nullptr; +} + +void RCAS::Initialize() +{ + if (rcasConfigCB) + return; + + logger::info("[RCAS] Creating resources"); + CreateComputeShader(); + rcasConfigCB = new ConstantBuffer(ConstantBufferDesc()); +} + +void RCAS::CreateComputeShader() +{ + std::vector> defines; + rcasComputeShader.attach((ID3D11ComputeShader*)Util::CompileShader(L"Data\\Shaders\\Upscaling\\RCAS\\RCAS.hlsl", defines, "cs_5_0")); +} + +void RCAS::ApplySharpen(ID3D11ShaderResourceView* inputSRV, ID3D11UnorderedAccessView* outputUAV, float sharpness) +{ + auto state = globals::state; + auto context = globals::d3d::context; + + if (!rcasComputeShader) { + logger::warn("[RCAS] Compute shader not compiled"); + return; + } + + state->BeginPerfEvent("RCAS Sharpening"); + + uint32_t screenWidth = (uint32_t)state->screenSize.x; + uint32_t screenHeight = (uint32_t)state->screenSize.y; + + RCASConfig config{}; + config.sharpness = sharpness; + + rcasConfigCB->Update(config); + auto bufferArray = rcasConfigCB->CB(); + + context->CSSetShader(rcasComputeShader.get(), nullptr, 0); + context->CSSetConstantBuffers(0, 1, &bufferArray); + + ID3D11ShaderResourceView* srvs[] = { inputSRV }; + context->CSSetShaderResources(0, 1, srvs); + + ID3D11UnorderedAccessView* uavs[] = { outputUAV }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + + uint32_t dispatchX = (screenWidth + 7) / 8; + uint32_t dispatchY = (screenHeight + 7) / 8; + context->Dispatch(dispatchX, dispatchY, 1); + + ID3D11ShaderResourceView* nullSRVs[] = { nullptr }; + context->CSSetShaderResources(0, 1, nullSRVs); + + ID3D11UnorderedAccessView* nullUAVs[] = { nullptr }; + context->CSSetUnorderedAccessViews(0, 1, nullUAVs, nullptr); + + context->CSSetShader(nullptr, nullptr, 0); + + state->EndPerfEvent(); +} diff --git a/src/Features/Upscaling/RCAS/RCAS.h b/src/Features/Upscaling/RCAS/RCAS.h new file mode 100644 index 0000000000..c5b42ae251 --- /dev/null +++ b/src/Features/Upscaling/RCAS/RCAS.h @@ -0,0 +1,42 @@ +#pragma once + +#include "../../../Buffer.h" +#include "../../../State.h" + +#include +#include + +/** + * @brief Robust Contrast Adaptive Sharpening (RCAS) implementation. + * + * Standalone sharpening pass based on AMD FidelityFX FSR1 RCAS algorithm. + * Used to apply sharpening to DLSS output in HDR space before tonemapping. + */ +class RCAS +{ +public: + RCAS() = default; + ~RCAS(); + + /** + * @brief Initializes RCAS resources including compute shader and constant buffer. + * + * Safe to call multiple times - will early-out if already initialized. + */ + void Initialize(); + + /** + * @brief Applies RCAS sharpening to the input texture. + * + * @param inputTexture SRV of the texture to sharpen (typically kMAIN render target). + * @param outputUAV UAV to write sharpened result to. + * @param sharpness Sharpening strength (0.0 = no sharpening, higher = more sharp). + */ + void ApplySharpen(ID3D11ShaderResourceView* inputTexture, ID3D11UnorderedAccessView* outputUAV, float sharpness); + +private: + void CreateComputeShader(); + + winrt::com_ptr rcasComputeShader; + ConstantBuffer* rcasConfigCB = nullptr; +}; diff --git a/src/Features/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index c65f6859b2..88eb7ccf75 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -251,15 +251,8 @@ void Streamline::CheckFrameConstants() } } -void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors, sl::DLSSPreset a_preset) +void Streamline::SetDLSSOptions() { - CheckFrameConstants(); - - auto state = globals::state; - - auto renderer = globals::game::renderer; - auto& depthTexture = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - sl::DLSSOptions dlssOptions{}; // Map quality mode to DLSS mode @@ -282,6 +275,8 @@ void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r break; } + auto state = globals::state; + dlssOptions.outputWidth = (uint)state->screenSize.x; dlssOptions.outputHeight = (uint)state->screenSize.y; dlssOptions.colorBuffersHDR = sl::Boolean::eTrue; @@ -290,15 +285,41 @@ void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r dlssOptions.preExposure = 1.0f; dlssOptions.sharpness = 0.0f; - dlssOptions.dlaaPreset = a_preset; - dlssOptions.qualityPreset = a_preset; - dlssOptions.balancedPreset = a_preset; - dlssOptions.performancePreset = a_preset; - dlssOptions.ultraPerformancePreset = a_preset; + // Set DLSS preset based on VR mode + sl::DLSSPreset preset = sl::DLSSPreset::ePresetK; // Default + switch (globals::features::upscaling.settings.DLSSPreset) { + case 0: + preset = sl::DLSSPreset::ePresetF; + break; + case 1: + preset = sl::DLSSPreset::ePresetJ; + break; + case 2: + default: + preset = sl::DLSSPreset::ePresetK; + break; + } + + dlssOptions.dlaaPreset = preset; + dlssOptions.qualityPreset = preset; + dlssOptions.balancedPreset = preset; + dlssOptions.performancePreset = preset; + dlssOptions.ultraPerformancePreset = preset; if (SL_FAILED(result, slDLSSSetOptions(viewport, dlssOptions))) { logger::critical("[Streamline] Could not enable DLSS"); } +} + +void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors) +{ + CheckFrameConstants(); + SetDLSSOptions(); + + auto state = globals::state; + + auto renderer = globals::game::renderer; + auto& depthTexture = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; { auto screenSize = state->screenSize; @@ -333,7 +354,7 @@ void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r slEvaluateFeature(sl::kFeatureDLSS, *frameToken, inputs, _countof(inputs), globals::d3d::context); } -float Streamline::GetInputResolutionScale(uint32_t outputWidth, uint32_t outputHeight, uint32_t qualityMode) +float2 Streamline::GetInputResolutionScale(uint32_t outputWidth, uint32_t outputHeight, uint32_t qualityMode) { sl::DLSSMode dlssMode; switch (qualityMode) { @@ -363,7 +384,7 @@ float Streamline::GetInputResolutionScale(uint32_t outputWidth, uint32_t outputH sl::Result result = slDLSSGetOptimalSettings(dlssOptions, optimalSettings); if (result != sl::Result::eOk) { logger::critical("[Streamline] Failed to get DLSS optimal settings, error code: {}", (int)result); - return 1.0f; + return { 1.0f, 1.0f }; } float scaleX; @@ -379,8 +400,8 @@ float Streamline::GetInputResolutionScale(uint32_t outputWidth, uint32_t outputH scaleY = (float)optimalSettings.optimalRenderHeight / (float)outputHeight; } - // Use the average scale (both should be the same for uniform scaling) - return (scaleX + scaleY) * 0.5f; + // Return separate X and Y scales for more precision + return { scaleX, scaleY }; } /** @@ -394,4 +415,4 @@ void Streamline::DestroyDLSSResources() dlssOptions.mode = sl::DLSSMode::eOff; slDLSSSetOptions(viewport, dlssOptions); slFreeResources(sl::kFeatureDLSS, viewport); -} \ No newline at end of file +} diff --git a/src/Features/Upscaling/Streamline.h b/src/Features/Upscaling/Streamline.h index b40803e64e..1397e36505 100644 --- a/src/Features/Upscaling/Streamline.h +++ b/src/Features/Upscaling/Streamline.h @@ -74,9 +74,11 @@ class Streamline void CheckFrameConstants(); - void Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors, sl::DLSSPreset a_preset); + void SetDLSSOptions(); - float GetInputResolutionScale(uint32_t outputWidth, uint32_t outputHeight, uint32_t qualityPreset); + void Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors); + + float2 GetInputResolutionScale(uint32_t outputWidth, uint32_t outputHeight, uint32_t qualityPreset); void DestroyDLSSResources(); }; diff --git a/src/Features/Upscaling/XeSS.cpp b/src/Features/Upscaling/XeSS.cpp deleted file mode 100644 index 10b1feda7c..0000000000 --- a/src/Features/Upscaling/XeSS.cpp +++ /dev/null @@ -1,212 +0,0 @@ -#include "XeSS.h" -#include "../../Utils/FileSystem.h" - -#include "../../State.h" -#include "../Upscaling.h" - -#include -#include -#include - -// Define the static member -std::vector> XeSS::dllVersions = {}; - -void XeSS::LoadXeSS() -{ - std::wstring dllPath = std::wstring(XeSS::PluginDir) + L"\\libxess.dll"; - module = LoadLibrary(dllPath.c_str()); - - // Cache all DLL versions in the XeSS directory - std::filesystem::path pluginDir = std::filesystem::path(XeSS::PluginDir); - XeSS::dllVersions = Util::EnumerateDllVersions(pluginDir); - - if (module) { - xessGetVersion = (xessGetVersionPtr)GetProcAddress(module, "xessGetVersion"); - xessGetIntelXeFXVersion = (xessGetIntelXeFXVersionPtr)GetProcAddress(module, "xessGetIntelXeFXVersion"); - xessD3D12CreateContext = (xessD3D12CreateContextPtr)GetProcAddress(module, "xessD3D12CreateContext"); - xessD3D12Init = (xessD3D12InitPtr)GetProcAddress(module, "xessD3D12Init"); - xessD3D12Execute = (xessD3D12ExecutePtr)GetProcAddress(module, "xessD3D12Execute"); - xessDestroyContext = (xessDestroyContextPtr)GetProcAddress(module, "xessDestroyContext"); - xessSetJitterScale = (xessSetJitterScalePtr)GetProcAddress(module, "xessSetJitterScale"); - xessSetVelocityScale = (xessSetVelocityScalePtr)GetProcAddress(module, "xessSetVelocityScale"); - xessGetInputResolution = (xessGetInputResolutionPtr)GetProcAddress(module, "xessGetInputResolution"); - featureXeSS = true; - logger::info("[XeSS] Successfully loaded XeSS SDK"); - } else { - featureXeSS = false; - logger::error("[XeSS] Failed to load libxess.dll"); - } -} - -void XeSS::QueryVersion() -{ - // Clear existing version info - versionInfo.clear(); - - xess_version_t version; - xess_version_t versionXeFX; - - if (xessGetVersion(&version) == XESS_RESULT_SUCCESS && xessGetIntelXeFXVersion(xessContext, &versionXeFX) == XESS_RESULT_SUCCESS) { - bool xeFX = versionXeFX.major != 0 && versionXeFX.minor != 0 && versionXeFX.patch != 0; - versionInfo = std::format("{}.{}.{} {}", version.major, version.minor, version.patch, xeFX ? "XMX" : "DP4a"); - logger::info("[XeSS] Upscaler version: {}", versionInfo); - } -} - -void XeSS::CreateXeSSResources() -{ - auto& upscaling = globals::features::upscaling; - if (!featureXeSS || !upscaling.sharedD3D12Device) { - logger::error("[XeSS] XeSS not available or shared D3D12 device not available, cannot create resources"); - return; - } - - auto state = globals::state; - - xess_result_t createResult = xessD3D12CreateContext(upscaling.sharedD3D12Device.get(), &xessContext); - if (createResult != XESS_RESULT_SUCCESS) { - logger::critical("[XeSS] Failed to create XeSS context, error: {} ({})", magic_enum::enum_name(createResult), (int)createResult); - return; - } - - xess_d3d12_init_params_t initParams{}; - initParams.outputResolution.x = (uint32_t)state->screenSize.x; - initParams.outputResolution.y = (uint32_t)state->screenSize.y; - initParams.qualitySetting = XESS_QUALITY_SETTING_AA; - initParams.initFlags = XESS_INIT_FLAG_ENABLE_AUTOEXPOSURE | XESS_INIT_FLAG_USE_NDC_VELOCITY | XESS_INIT_FLAG_RESPONSIVE_PIXEL_MASK; - - initParams.creationNodeMask = 1; - initParams.visibleNodeMask = 1; - initParams.pTempBufferHeap = nullptr; - initParams.bufferHeapOffset = 0; - initParams.pTempTextureHeap = nullptr; - initParams.textureHeapOffset = 0; - initParams.pPipelineLibrary = nullptr; - - xess_result_t initResult = xessD3D12Init(xessContext, &initParams); - if (initResult != XESS_RESULT_SUCCESS) { - logger::critical("[XeSS] Failed to initialize XeSS context, error: {} ({})", magic_enum::enum_name(initResult), (int)initResult); - return; - } - - QueryVersion(); -} - -void XeSS::DestroyXeSSResources() -{ - if (xessContext && xessDestroyContext) { - xess_result_t result = xessDestroyContext(xessContext); - if (result != XESS_RESULT_SUCCESS) { - logger::error("[XeSS] Failed to destroy XeSS context, error: {} ({})", magic_enum::enum_name(result), (int)result); - } - xessContext = nullptr; - } -} - -float XeSS::GetInputResolutionScale(uint32_t outputWidth, uint32_t outputHeight, uint32_t qualityMode) -{ - // Check if XeSS context is valid - if (!xessContext) { - logger::error("[XeSS] GetInputResolutionScale called with null context"); - return 1.0f; - } - - // Check if function pointer is valid - if (!xessGetInputResolution) { - logger::error("[XeSS] GetInputResolutionScale called with null function pointer"); - return 1.0f; - } - - // Validate input parameters - if (outputWidth == 0 || outputHeight == 0) { - logger::error("[XeSS] GetInputResolutionScale called with invalid resolution: {}x{}", outputWidth, outputHeight); - return 1.0f; - } - - xess_quality_settings_t xessQuality; - switch (qualityMode) { - case 1: - xessQuality = XESS_QUALITY_SETTING_QUALITY; - break; - case 2: - xessQuality = XESS_QUALITY_SETTING_BALANCED; - break; - case 3: - xessQuality = XESS_QUALITY_SETTING_PERFORMANCE; - break; - case 4: - xessQuality = XESS_QUALITY_SETTING_ULTRA_PERFORMANCE; - break; - default: - xessQuality = XESS_QUALITY_SETTING_AA; - break; - } - - xess_2d_t outputResolution = { outputWidth, outputHeight }; - xess_2d_t inputResolution = { 0, 0 }; - - xess_result_t result = xessGetInputResolution(xessContext, &outputResolution, xessQuality, &inputResolution); - if (result != XESS_RESULT_SUCCESS) { - logger::critical("[XeSS] Failed to get input resolution, error: {} ({})", magic_enum::enum_name(result), (int)result); - return 1.0f; - } - - // Calculate scale as ratio of input to output resolution - float scaleX = (float)inputResolution.x / (float)outputResolution.x; - float scaleY = (float)inputResolution.y / (float)outputResolution.y; - - // Use the average scale (both should be the same for uniform scaling) - return (scaleX + scaleY) * 0.5f; -} - -void XeSS::Upscale( - ID3D12Resource* a_inputColorTexture, - ID3D12Resource* a_motionVectorTexture, - ID3D12Resource* a_depthTexture, - ID3D12Resource* a_reactiveMask, - ID3D12Resource* a_outputTexture, - ID3D12GraphicsCommandList* a_commandList, - uint32_t a_renderWidth, - uint32_t a_renderHeight, - float2 a_jitter) -{ - // Set velocity and jitter scales - xess_result_t velocityResult = xessSetVelocityScale(xessContext, globals::game::isVR ? 1.0f : 2.0f, -2.0f); - if (velocityResult != XESS_RESULT_SUCCESS) { - logger::warn("[XeSS] Failed to set velocity scale, error: {} ({})", magic_enum::enum_name(velocityResult), (int)velocityResult); - } - - xess_result_t jitterResult = xessSetJitterScale(xessContext, 1.0f, 1.0f); - if (jitterResult != XESS_RESULT_SUCCESS) { - logger::warn("[XeSS] Failed to set jitter scale, error: {} ({})", magic_enum::enum_name(jitterResult), (int)jitterResult); - } - - // XeSS execution parameters - xess_d3d12_execute_params_t execParams{}; - execParams.pColorTexture = a_inputColorTexture; - execParams.pVelocityTexture = a_motionVectorTexture; - execParams.pDepthTexture = a_depthTexture; - execParams.pExposureScaleTexture = nullptr; - execParams.pResponsivePixelMaskTexture = a_reactiveMask; - execParams.pOutputTexture = a_outputTexture; - execParams.jitterOffsetX = -a_jitter.x; - execParams.jitterOffsetY = -a_jitter.y; - execParams.exposureScale = 1.0f; - execParams.resetHistory = 0; - execParams.inputWidth = a_renderWidth; - execParams.inputHeight = a_renderHeight; - execParams.inputColorBase = { 0, 0 }; - execParams.inputMotionVectorBase = { 0, 0 }; - execParams.inputDepthBase = { 0, 0 }; - execParams.inputResponsiveMaskBase = { 0, 0 }; - execParams.reserved0 = { 0, 0 }; - execParams.outputColorBase = { 0, 0 }; - execParams.pDescriptorHeap = nullptr; - execParams.descriptorHeapOffset = 0; - - xess_result_t result = xessD3D12Execute(xessContext, a_commandList, &execParams); - if (result != XESS_RESULT_SUCCESS) { - logger::error("[XeSS] Failed to execute XeSS upscaling, error: {} ({})", magic_enum::enum_name(result), (int)result); - return; - } -} \ No newline at end of file diff --git a/src/Features/Upscaling/XeSS.h b/src/Features/Upscaling/XeSS.h deleted file mode 100644 index 6beb0f0790..0000000000 --- a/src/Features/Upscaling/XeSS.h +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include "../../Buffer.h" -#include "../../State.h" -#include "DX12SwapChain.h" -#include -#include -#include - -// Include XeSS headers -#include -#include - -// XeSS function pointers - matching exact signatures from xess.h and xess_d3d12.h -typedef xess_result_t (*xessGetVersionPtr)(xess_version_t* pVersion); -typedef xess_result_t (*xessGetIntelXeFXVersionPtr)(xess_context_handle_t hContext, xess_version_t* pVersion); -typedef xess_result_t (*xessD3D12CreateContextPtr)(ID3D12Device* pDevice, xess_context_handle_t* phContext); -typedef xess_result_t (*xessD3D12InitPtr)(xess_context_handle_t hContext, const xess_d3d12_init_params_t* pInitParams); -typedef xess_result_t (*xessD3D12ExecutePtr)(xess_context_handle_t hContext, ID3D12GraphicsCommandList* pCommandList, const xess_d3d12_execute_params_t* pExecParams); -typedef xess_result_t (*xessDestroyContextPtr)(xess_context_handle_t hContext); -typedef xess_result_t (*xessSetJitterScalePtr)(xess_context_handle_t hContext, float x, float y); -typedef xess_result_t (*xessSetVelocityScalePtr)(xess_context_handle_t hContext, float x, float y); -typedef xess_result_t (*xessGetInputResolutionPtr)(xess_context_handle_t hContext, const xess_2d_t* pOutputResolution, xess_quality_settings_t qualitySettings, xess_2d_t* pInputResolution); - -class XeSS -{ -public: - static constexpr const wchar_t* PluginDir = L"Data\\Shaders\\Upscaling\\XeSS"; - - XeSS() = default; - - HMODULE module = nullptr; - - // XeSS function pointers - xessGetVersionPtr xessGetVersion = nullptr; - xessGetIntelXeFXVersionPtr xessGetIntelXeFXVersion = nullptr; - xessD3D12CreateContextPtr xessD3D12CreateContext = nullptr; - xessD3D12InitPtr xessD3D12Init = nullptr; - xessD3D12ExecutePtr xessD3D12Execute = nullptr; - xessDestroyContextPtr xessDestroyContext = nullptr; - xessSetJitterScalePtr xessSetJitterScale = nullptr; - xessSetVelocityScalePtr xessSetVelocityScale = nullptr; - xessGetInputResolutionPtr xessGetInputResolution = nullptr; - - xess_context_handle_t xessContext = nullptr; - - bool featureXeSS = false; // whether enabled - - // Cached DLL version info for XeSS plugin directory - static std::vector> dllVersions; - - std::string versionInfo; - - void LoadXeSS(); - void QueryVersion(); - void CreateXeSSResources(); - void DestroyXeSSResources(); - float GetInputResolutionScale(uint32_t outputWidth, uint32_t outputHeight, uint32_t qualityPreset); - void Upscale( - ID3D12Resource* a_inputColorTexture, - ID3D12Resource* a_motionVectorTexture, - ID3D12Resource* a_depthTexture, - ID3D12Resource* a_reactiveMask, - ID3D12Resource* a_outputTexture, - ID3D12GraphicsCommandList* a_commandList, - uint32_t a_renderWidth, - uint32_t a_renderHeight, - float2 a_jitter); -}; \ No newline at end of file diff --git a/src/Features/VR.cpp b/src/Features/VR.cpp index 3294c72779..7ebec819f8 100644 --- a/src/Features/VR.cpp +++ b/src/Features/VR.cpp @@ -1,8 +1,10 @@ #include "VR.h" #include "Menu.h" +#include "Menu/Fonts.h" #include "RE/B/BSOpenVR.h" #include "RE/N/NiPoint3.h" #include "RE/P/PlayerCharacter.h" +#include "Upscaling.h" #include #include "State.h" @@ -15,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -23,6 +24,14 @@ using AttachMode = VR::Settings::OverlayAttachMode; +namespace +{ + bool BeginTabItemWithFont(const char* label, Menu::FontRole role, ImGuiTabItemFlags flags = ImGuiTabItemFlags_None) + { + return MenuFonts::BeginTabItemWithFont(label, role, flags); + } +} + constexpr int kOverlayWidth = 1920; constexpr int kOverlayHeight = 1080; @@ -100,7 +109,18 @@ void VR::SetupResources() void VR::PostPostLoad() { gDepthBufferCulling = reinterpret_cast(REL::Offset(0x1EC6B88).address()); + if (!gDepthBufferCulling) { + static bool s_defaultDepthBufferCulling = false; // safe fallback + gDepthBufferCulling = &s_defaultDepthBufferCulling; + logger::warn("VR: gDepthBufferCulling address not found - using fallback default (false)"); + } + gMinOccludeeBoxExtent = reinterpret_cast(REL::Offset(0x1ED64E8).address()); + if (!gMinOccludeeBoxExtent) { + static float s_defaultMinOccludeeBoxExtent = 10.0f; + gMinOccludeeBoxExtent = &s_defaultMinOccludeeBoxExtent; + logger::warn("VR: gMinOccludeeBoxExtent address not found - using fallback default (10.0)"); + } // Patches BSGeometry::CopyTransformAndBounds to copy the model-bound translation across correctly instead of overwriting it with the bounding sphere centre REL::safe_write(REL::RelocationID(0, 0, 69528).address() + REL::Relocate(0, 0, 0xD9) + 0x2, 0x148); @@ -110,13 +130,24 @@ void VR::PostPostLoad() void VR::DataLoaded() { - *gDepthBufferCulling = settings.EnableDepthBufferCullingExterior; - *gMinOccludeeBoxExtent = settings.MinOccludeeBoxExtent; + // Initialize occlusion culling based on settings, but force-disable if an external + // upscaler is active (FSR/DLSS) since upscalers may modify the depth buffer. + bool desired = settings.EnableDepthBufferCullingExterior; + UpdateDepthBufferCulling(desired); + + if (gMinOccludeeBoxExtent) { + *gMinOccludeeBoxExtent = settings.MinOccludeeBoxExtent; + } else { + logger::warn("VR::DataLoaded: gMinOccludeeBoxExtent is null, skipping assignment"); + } } void VR::EarlyPrepass() { - *gDepthBufferCulling = globals::game::tes->interiorCell ? settings.EnableDepthBufferCullingInterior : settings.EnableDepthBufferCullingExterior; + // Respect user settings unless an external upscaler is active; if so, force-disable + // depth-buffer culling to avoid incorrect occlusion tests in VR. + bool desired = RE::TES::GetSingleton()->interiorCell ? settings.EnableDepthBufferCullingInterior : settings.EnableDepthBufferCullingExterior; + UpdateDepthBufferCulling(desired); } //============================================================================= @@ -196,7 +227,7 @@ void VR::DrawSettings() return; if (ImGui::BeginTabBar("##VRTabs", ImGuiTabBarFlags_None)) { // General Settings Tab - if (ImGui::BeginTabItem("General")) { + if (BeginTabItemWithFont("General", Menu::FontRole::Subheading)) { if (ImGui::BeginChild("##VRGeneralFrame", { 0, 0 }, true)) { DrawGeneralVRSettings(); DrawControllerInputInstructions(); @@ -210,7 +241,7 @@ void VR::DrawSettings() // Key Bindings Tab if (openVRInfo.isCompatible) { - if (ImGui::BeginTabItem("Bindings")) { + if (BeginTabItemWithFont("Bindings", Menu::FontRole::Subheading)) { if (ImGui::BeginChild("##VRBindingsFrame", { 0, 0 }, true)) { DrawKeyBindings(); } @@ -219,7 +250,7 @@ void VR::DrawSettings() } } // Debug Tab (existing debug functionality) - if (ImGui::BeginTabItem("Debug")) { + if (BeginTabItemWithFont("Debug", Menu::FontRole::Subheading)) { if (ImGui::BeginChild("##VRDebugFrame", { 0, 0 }, true)) { DrawDebugSection(); } @@ -556,13 +587,44 @@ namespace auto& vr = globals::features::vr; VR::Settings& settings = vr.settings; if (ImGui::CollapsingHeader("General Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + // If an upscaler is active that rewrites or repurposes the depth buffer, + // depth-buffer-culling must be disabled to avoid incorrect occlusion tests + // (which are especially problematic in VR). Query the Upscaling feature + // to see whether we're running FSR or DLSS. + // Determine if an external upscaler is active by reading the numeric + // setting value directly. Avoid referencing Upscaling types here to + // prevent header/type collisions in this translation unit. + // Query the Upscaling feature for an authoritative state flag. + bool upscalingActive = globals::features::upscaling.IsUpscalingActive(); + + // Exteriors + if (upscalingActive) + ImGui::BeginDisabled(); ImGui::Checkbox("Enable Depth Buffer Culling in Exteriors", &settings.EnableDepthBufferCullingExterior); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Improves performance in exteriors, recommended ON."); + if (upscalingActive) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Disabled while an external upscaler is active (FSR/DLSS) because upscalers may modify depth.\nThis prevents incorrect occlusion in VR."); + } + ImGui::EndDisabled(); + } else { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Improves performance in exteriors, recommended ON."); + } } + + // Interiors + if (upscalingActive) + ImGui::BeginDisabled(); ImGui::Checkbox("Enable Depth Buffer Culling in Interiors", &settings.EnableDepthBufferCullingInterior); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Improves performance in interiors, recommended OFF due to occasional visual glitches."); + if (upscalingActive) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Disabled while an external upscaler is active (FSR/DLSS) because upscalers may modify depth.\nThis prevents incorrect occlusion in VR."); + } + ImGui::EndDisabled(); + } else { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Improves performance in interiors, recommended OFF due to occasional visual glitches."); + } } if (ImGui::SliderFloat("Min Occludee Box Extent", &settings.MinOccludeeBoxExtent, 0.0f, 1000.0f, "%.1f")) *vr.gMinOccludeeBoxExtent = settings.MinOccludeeBoxExtent; @@ -1531,6 +1593,22 @@ void VR::SubmitOverlayFrame() } } +// Helper to centralize VR depth buffer culling logic, reducing duplication between DataLoaded and EarlyPrepass. +void VR::UpdateDepthBufferCulling(bool desired) +{ + if (globals::features::upscaling.IsUpscalingActive()) { + if (gDepthBufferCulling && *gDepthBufferCulling) { + logger::info("Upscaling detected, disabling incompatible depth buffer culling."); + *gDepthBufferCulling = false; + } + } else { + if (gDepthBufferCulling && *gDepthBufferCulling != desired) { + *gDepthBufferCulling = desired; + logger::info("VR depth buffer culling restored to {}", desired); + } + } +} + // Handles overlay/menu open/close logic based on controller input state void VR::UpdateOverlayMenuStateFromInput() { @@ -1554,18 +1632,27 @@ void VR::UpdateOverlayMenuStateFromInput() return; } - // Check if we're in a valid menu state for input - bool inValidMenuState = globals::game::ui && - (globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) || globals::game::ui->IsMenuOpen(RE::TweenMenu::MENU_NAME)); + // Compute whether the game's UI menus we care about are open. Do this early so + // downstream logic can reuse the result and we only check the UI once. + bool uiMenusOpen = globals::game::ui && + (globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) || globals::game::ui->IsMenuOpen(RE::TweenMenu::MENU_NAME)); + + // Valid menu state means either one of those UI menus is open, or our menu is + // enabled (but only if the game's UI system is present). + bool inValidMenuState = uiMenusOpen || (globals::game::ui && isEnabled); if (!inValidMenuState) return; - // Define menu state mappings + // Define menu state mappings. The `allowWhenUIMenusClosed` flag controls whether + // a mapping is allowed to run when our menu is enabled but the game's UI menus + // are not reported open. This prevents 'open' controls from firing in that state + // while still allowing 'close' actions. struct MenuStateMapping { std::function condition; std::function action; + bool allowWhenUIMenusClosed = false; }; // Generic combo checking function - makes the system truly extensible @@ -1616,7 +1703,8 @@ void VR::UpdateOverlayMenuStateFromInput() { [&]() { return CheckCombo(settings.VRMenuCloseKeys) && isEnabled; }, - [&]() { isEnabled = false; } }, + [&]() { isEnabled = false; }, + true }, // Open VR overlay when closed { [&]() { @@ -1631,8 +1719,15 @@ void VR::UpdateOverlayMenuStateFromInput() [&]() { overlayEnabled = false; } } }; - // Process mappings in order + // Process mappings in order. If our menu is enabled but the game's UI menus + // are not open, only allow mappings explicitly marked with + // allowWhenUIMenusClosed (close actions). + bool onlyAllowClose = isEnabled && !uiMenusOpen; + for (const auto& mapping : mappings) { + if (onlyAllowClose && !mapping.allowWhenUIMenusClosed) + continue; + if (mapping.condition()) { mapping.action(); break; // Only execute one action per frame diff --git a/src/Features/VR.h b/src/Features/VR.h index 5c0a777b5c..03ae0d0f1f 100644 --- a/src/Features/VR.h +++ b/src/Features/VR.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include #include @@ -282,6 +282,8 @@ struct VR : OverlayFeature virtual void DataLoaded() override; virtual void EarlyPrepass() override; + void UpdateDepthBufferCulling(bool desired); + virtual void LoadSettings(json& o_json) override; virtual void SaveSettings(json& o_json) override; virtual void RestoreDefaultSettings() override; diff --git a/src/Features/VolumetricLighting.cpp b/src/Features/VolumetricLighting.cpp index 3d0d8f5d8b..e2de451067 100644 --- a/src/Features/VolumetricLighting.cpp +++ b/src/Features/VolumetricLighting.cpp @@ -205,7 +205,7 @@ void VolumetricLighting::EarlyPrepass() vlData.screenYMin1 = height - 1; vlDataCB->Update(vlData); - const auto interiorCell = globals::game::tes->interiorCell; + const auto interiorCell = RE::TES::GetSingleton()->interiorCell; const bool currentlyInInterior = interiorCell != nullptr; if (initialised && currentlyInInterior == inInterior) diff --git a/src/Features/WetnessEffects.h b/src/Features/WetnessEffects.h index 3a4bda6a94..b55897fdc7 100644 --- a/src/Features/WetnessEffects.h +++ b/src/Features/WetnessEffects.h @@ -1,5 +1,7 @@ #pragma once +#include "Buffer.h" + struct WetnessEffects : Feature { private: @@ -69,6 +71,7 @@ struct WetnessEffects : Feature Settings settings; uint pad0; }; + STATIC_ASSERT_ALIGNAS_16(PerFrame); struct DebugSettings { diff --git a/src/FrameAnnotations.cpp b/src/FrameAnnotations.cpp index 613caa9718..cd7bf06168 100644 --- a/src/FrameAnnotations.cpp +++ b/src/FrameAnnotations.cpp @@ -6,14 +6,37 @@ namespace FrameAnnotations { + namespace + { + static std::string BuildEventName(RE::ImageSpaceManager::ImageSpaceEffectEnum EffectType) + { + auto enumName = RE::ImageSpaceManager::GetImageSpaceEffectName(EffectType); + + if (globals::state && globals::state->IsDeveloperMode()) { + uint16_t packed = static_cast(EffectType); + uint16_t se = RE::ImageSpaceManager::GetSEIndex(EffectType); + uint16_t vr = RE::ImageSpaceManager::GetVRIndex(EffectType); + std::string packedString = std::format(" (packed: 0x{:X}, SE: {}, VR: {})", packed, se, vr); + return enumName + packedString; + } else { + return enumName; + } + } + } + template struct BSShader_SetupGeometry { static void thunk(RE::BSShader* shader, RE::BSRenderPass* pass, uint32_t renderFlags) { if (globals::state->frameAnnotations) { - const std::string passName = std::format("[{}:{:X}] <{}> {}", magic_enum::enum_name(ShaderType), pass->passEnum, - pass->accumulationHint, pass->geometry->name.c_str()); + uint32_t descriptor = 0; + if (globals::game::currentPixelShader && *globals::game::currentPixelShader) { + descriptor = (*globals::game::currentPixelShader)->id; + } + std::string diskPath = std::format("Data/ShaderCache/{}/{:X}.pso", shader->fxpFilename, descriptor); + const std::string passName = std::format("[{}:{:X}] ({:X}) <{}> {} -> {}", magic_enum::enum_name(ShaderType), descriptor, pass->passEnum, + pass->accumulationHint, pass->geometry->name.c_str(), diskPath); globals::state->BeginPerfEvent(passName); } @@ -43,7 +66,8 @@ namespace FrameAnnotations { static void thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) { - globals::state->BeginPerfEvent(std::format("{} Draw", magic_enum::enum_name(EffectType))); + std::string eventName = BuildEventName(EffectType) + " Draw"; + globals::state->BeginPerfEvent(eventName); func(imageSpaceShader, shape, param); @@ -58,7 +82,8 @@ namespace FrameAnnotations { static void thunk(void* imageSpaceShader, uint32_t a1, uint32_t a2, uint32_t a3) { - globals::state->BeginPerfEvent(std::format("{} Dispatch", magic_enum::enum_name(EffectType))); + std::string eventName = BuildEventName(EffectType) + " Dispatch"; + globals::state->BeginPerfEvent(eventName); func(imageSpaceShader, a1, a2, a3); @@ -896,6 +921,66 @@ namespace FrameAnnotations RE::VTABLE_BSImagespaceShaderISUnderwaterMask[0]); stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( RE::VTABLE_BSImagespaceShaderWaterFlow[0]); + // VR-only shaders + if (globals::game::isVR) { + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderCopyDepthBuffer[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderCopyDepthBuffer_DR[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderISDownsampleHierarchicalDepthBufferCS[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderISDiffScaleDownsampleDepthBufferCS[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderISFullScreenVR[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderTransformLvl7PreTest[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderLvl6PreTest[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderLvl5PreTest[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderLvl4PreTest[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderLvl3PreTest[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderLvl2PreTest[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderLvl1PreTest[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderLvl0PreTest[3]); + stl::write_vfunc<0x1, BSImagespaceShader_Render>( + RE::VTABLE_BSImagespaceShaderSetupPreTest[3]); + + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderCopyDepthBuffer[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderCopyDepthBuffer_DR[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderISDownsampleHierarchicalDepthBufferCS[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderISDiffScaleDownsampleDepthBufferCS[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderISFullScreenVR[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderTransformLvl7PreTest[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderLvl6PreTest[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderLvl5PreTest[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderLvl4PreTest[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderLvl3PreTest[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderLvl2PreTest[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderLvl1PreTest[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderLvl0PreTest[0]); + stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( + RE::VTABLE_BSImagespaceShaderSetupPreTest[0]); + } stl::write_vfunc<0x2A, BSShaderAccumulator_FinishAccumulatingDispatch>( RE::VTABLE_BSShaderAccumulator[0]); @@ -919,7 +1004,6 @@ namespace FrameAnnotations stl::detour_thunk(REL::RelocationID(99963, 106609)); stl::detour_thunk(REL::RelocationID(100840, 107630)); stl::detour_thunk(REL::RelocationID(99940, 106585)); - stl::detour_thunk(REL::RelocationID(100306, 107023)); } void OnDataLoaded() @@ -930,7 +1014,7 @@ namespace FrameAnnotations auto renderer = globals::game::renderer; for (size_t renderTargetIndex = 0; - renderTargetIndex < (!REL::Module::IsVR() ? RE::RENDER_TARGETS::kTOTAL : RE::RENDER_TARGETS::kVRTOTAL); ++renderTargetIndex) { + renderTargetIndex < Util::GetRenderTargetCount(); ++renderTargetIndex) { const auto renderTargetName = magic_enum::enum_name( static_cast(renderTargetIndex)); if (auto texture = renderer->GetRuntimeData().renderTargets[renderTargetIndex].texture) { @@ -951,7 +1035,7 @@ namespace FrameAnnotations } for (size_t renderTargetIndex = 0; - renderTargetIndex < (!REL::Module::IsVR() ? RE::RENDER_TARGETS_DEPTHSTENCIL::kTOTAL : RE::RENDER_TARGETS_DEPTHSTENCIL::kVRTOTAL); + renderTargetIndex < Util::GetDepthStencilCount(); ++renderTargetIndex) { const auto renderTargetName = magic_enum::enum_name( static_cast( diff --git a/src/Globals.cpp b/src/Globals.cpp index 0bd953339f..bea9fe6682 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -15,6 +15,7 @@ #include "Features/LightLimitFix.h" #include "Features/PerformanceOverlay.h" #include "Features/PhysicalSky.h" +#include "Features/RenderDoc.h" #include "Features/ScreenSpaceGI.h" #include "Features/ScreenSpaceShadows.h" #include "Features/SkySync.h" @@ -33,11 +34,8 @@ #include "Menu.h" #include "ShaderCache.h" #include "State.h" -#include "Utils/Game.h" - -#include "Features/LightLimitFix/ParticleLights.h" - #include "TruePBR.h" +#include "Utils/Game.h" namespace globals { @@ -79,10 +77,10 @@ namespace globals WetnessEffects wetnessEffects{}; ExtendedTranslucency extendedTranslucency{}; Upscaling upscaling{}; + RenderDoc renderDoc{}; namespace llf { - ParticleLights particleLights{}; } } @@ -92,9 +90,7 @@ namespace globals RE::BSGraphics::State* graphicsState = nullptr; RE::BSGraphics::Renderer* renderer = nullptr; RE::BSShaderManager::State* smState = nullptr; - RE::TES* tes = nullptr; bool isVR = false; - RE::MemoryManager* memoryManager = nullptr; RE::INISettingCollection* iniSettingCollection = nullptr; RE::INIPrefSettingCollection* iniPrefSettingCollection = nullptr; RE::GameSettingCollection* gameSettingCollection = nullptr; @@ -155,7 +151,6 @@ namespace globals renderer = RE::BSGraphics::Renderer::GetSingleton(); smState = &RE::BSShaderManager::State::GetSingleton(); isVR = REL::Module::IsVR(); - memoryManager = RE::MemoryManager::GetSingleton(); iniSettingCollection = RE::INISettingCollection::GetSingleton(); iniPrefSettingCollection = RE::INIPrefSettingCollection::GetSingleton(); gameSettingCollection = RE::GameSettingCollection::GetSingleton(); @@ -191,7 +186,6 @@ namespace globals void OnDataLoaded() { using namespace game; - tes = RE::TES::GetSingleton(); sky = RE::Sky::GetSingleton(); utilityShader = RE::BSUtilityShader::GetSingleton(); diff --git a/src/Globals.h b/src/Globals.h index 0cdf70b193..801efcf5f7 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -30,11 +30,10 @@ struct WetnessEffects; struct ExtendedTranslucency; struct Upscaling; -class ParticleLights; - class State; class Deferred; struct TruePBR; +class RenderDoc; class Menu; namespace SIE @@ -82,10 +81,10 @@ namespace globals extern WetnessEffects wetnessEffects; extern ExtendedTranslucency extendedTranslucency; extern Upscaling upscaling; + extern RenderDoc renderDoc; namespace llf { - extern ParticleLights particleLights; } } @@ -202,9 +201,7 @@ namespace globals extern RE::BSGraphics::State* graphicsState; extern RE::BSGraphics::Renderer* renderer; extern RE::BSShaderManager::State* smState; - extern RE::TES* tes; extern bool isVR; - extern RE::MemoryManager* memoryManager; extern RE::INISettingCollection* iniSettingCollection; extern RE::INIPrefSettingCollection* iniPrefSettingCollection; extern RE::GameSettingCollection* gameSettingCollection; diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 73c18efea0..d29a3f04ad 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -343,6 +343,18 @@ struct ID3D11Device_CreatePixelShader static inline REL::Relocation func; }; +struct ID3D11Device_CreateSamplerState +{ + static HRESULT STDMETHODCALLTYPE thunk(ID3D11Device* This, D3D11_SAMPLER_DESC* pSamplerDesc, ID3D11SamplerState** ppSamplerState) + { + // Limit Anisotropy to 8x for performance + D3D11_SAMPLER_DESC descCopy = *pSamplerDesc; // make a copy, pSamplerDesc is supposed to be immutable + descCopy.MaxAnisotropy = std::min(descCopy.MaxAnisotropy, 8u); + return func(This, &descCopy, ppSamplerState); + } + static inline REL::Relocation func; +}; + struct BSShaderRenderTargets_Create { /** @@ -436,6 +448,8 @@ namespace Hooks stl::detour_vfunc<15, ID3D11Device_CreatePixelShader>(globals::d3d::device); } + stl::detour_vfunc<23, ID3D11Device_CreateSamplerState>(globals::d3d::device); + globals::InstallD3DHooks(globals::d3d::context); globals::menu->Init(); @@ -498,16 +512,6 @@ namespace Hooks static inline REL::Relocation func; }; - struct CreateRenderTarget_Snow - { - static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) - { - globals::state->ModifyRenderTarget(a_target, a_properties); - func(This, a_target, a_properties); - } - static inline REL::Relocation func; - }; - struct CreateRenderTarget_MotionVectors { static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) @@ -702,41 +706,6 @@ namespace Hooks static inline REL::Relocation func; }; - void BSBatchRenderer_RenderPassImmediately1::thunk(RE::BSRenderPass* a_pass, uint32_t a_technique, bool a_alphaTest, uint32_t a_renderFlags) - { - if (globals::features::lightLimitFix.loaded && !globals::features::lightLimitFix.CheckParticleLights(a_pass, a_technique)) - return; - - func(a_pass, a_technique, a_alphaTest, a_renderFlags); - } - - struct BSBatchRenderer_RenderPassImmediately2 - { - static void thunk(RE::BSRenderPass* a_pass, uint32_t a_technique, bool a_alphaTest, uint32_t a_renderFlags) - { - if (globals::features::lightLimitFix.loaded && !globals::features::lightLimitFix.CheckParticleLights(a_pass, a_technique)) - return; - - if (globals::features::interiorSun.loaded) - globals::features::interiorSun.UpdateRasterStateCullMode(a_pass, a_technique); - - func(a_pass, a_technique, a_alphaTest, a_renderFlags); - } - static inline REL::Relocation func; - }; - - struct BSBatchRenderer_RenderPassImmediately3 - { - static void thunk(RE::BSRenderPass* a_pass, uint32_t a_technique, bool a_alphaTest, uint32_t a_renderFlags) - { - if (globals::features::lightLimitFix.loaded && !globals::features::lightLimitFix.CheckParticleLights(a_pass, a_technique)) - return; - - func(a_pass, a_technique, a_alphaTest, a_renderFlags); - } - static inline REL::Relocation func; - }; - #ifdef TRACY_ENABLE struct Main_Update { @@ -877,13 +846,6 @@ namespace Hooks */ void Install() { - if (!globals::features::upscaling.loaded) { - logger::info("Hooking D3D11CreateDeviceAndSwapChain"); - *(uintptr_t*)&ptrD3D11CreateDeviceAndSwapChain = SKSE::PatchIAT(hk_D3D11CreateDeviceAndSwapChain, "d3d11.dll", "D3D11CreateDeviceAndSwapChain"); - } - - *(uintptr_t*)&ptrCreateDXGIFactory = SKSE::PatchIAT(hk_CreateDXGIFactory, "dxgi.dll", !REL::Module::IsVR() ? "CreateDXGIFactory" : "CreateDXGIFactory1"); - if (!REL::Module::IsVR()) { logger::info("Hooking BSImageSpace::Init::IBLF"); stl::detour_thunk(REL::RelocationID(100480, 107198)); @@ -917,7 +879,6 @@ namespace Hooks stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x3F0, 0x3F3, 0x548)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x458, 0x45B, 0x5B0)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x46B, 0x46E, 0x5C3)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x406, 0x409, 0x55e)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x4F0, 0x4EF, 0x64E)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x503, 0x502, 0x661)); @@ -955,11 +916,6 @@ namespace Hooks logger::info("Hooking BSLightingShader"); stl::write_vfunc<0x4, BSLightingShader_SetupMaterial>(RE::VTABLE_BSLightingShader[0]); - logger::info("Hooking BSBatchRenderer::RenderPassImmediately"); - stl::write_thunk_call(REL::RelocationID(100877, 107673).address() + REL::Relocate(0x1E5, 0x1EE)); - stl::write_thunk_call(REL::RelocationID(100852, 107642).address() + REL::Relocate(0x29E, 0x28F)); - stl::write_thunk_call(REL::RelocationID(100871, 107667).address() + REL::Relocate(0xEE, 0xED)); - // Patch render space in BSLightingShader::SetupGeometry to always use world space // The variable updateEyePosition is set to 1 when not skinned. By patching to be 0 it will always use world space // We offset from the base address of the containing function to the start of the patch @@ -987,4 +943,15 @@ namespace Hooks stl::write_thunk_call(REL::RelocationID(100565, 107300).address() + REL::Relocate(0x523, 0xB0E, 0x5FE)); } + + void InstallEarlyHooks() + { + if (!globals::features::upscaling.loaded) { + logger::info("Hooking D3D11CreateDeviceAndSwapChain"); + *(uintptr_t*)&ptrD3D11CreateDeviceAndSwapChain = SKSE::PatchIAT(hk_D3D11CreateDeviceAndSwapChain, "d3d11.dll", "D3D11CreateDeviceAndSwapChain"); + } + + logger::info("Hooking CreateDXGIFactory"); + *(uintptr_t*)&ptrCreateDXGIFactory = SKSE::PatchIAT(hk_CreateDXGIFactory, "dxgi.dll", !REL::Module::IsVR() ? "CreateDXGIFactory" : "CreateDXGIFactory1"); + } } \ No newline at end of file diff --git a/src/Hooks.h b/src/Hooks.h index f22fd880b6..335a7df7d8 100644 --- a/src/Hooks.h +++ b/src/Hooks.h @@ -20,4 +20,5 @@ namespace Hooks }; void Install(); + void InstallEarlyHooks(); } diff --git a/src/Menu.cpp b/src/Menu.cpp index b21014668a..7612fb244a 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -3,12 +3,20 @@ #ifndef DIRECTINPUT_VERSION # define DIRECTINPUT_VERSION 0x0800 #endif +#include +#include #include +#include #include +#include #include #include #include #include +#include +#include +#include +#include #include "Deferred.h" #include "Feature.h" @@ -16,8 +24,11 @@ #include "FeatureVersions.h" #include "Features/Upscaling.h" #include "Menu/AdvancedSettingsRenderer.h" +#include "Menu/BackgroundBlur.h" #include "Menu/FeatureListRenderer.h" +#include "Menu/Fonts.h" #include "Menu/HomePageRenderer.h" +#include "Menu/IconLoader.h" #include "Menu/MenuHeaderRenderer.h" #include "Menu/OverlayRenderer.h" #include "Menu/SettingsTabRenderer.h" @@ -28,7 +39,6 @@ #include "Util.h" #include "Utils/UI.h" -#include "Features/LightLimitFix/ParticleLights.h" #include "Features/PerformanceOverlay.h" #include "Features/PerformanceOverlay/ABTesting/ABTestAggregator.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" @@ -39,7 +49,10 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::PaletteColors, Background, Text, - Border) + WindowBorder, + FrameBorder, + Separator, + ResizeGrip) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::StatusPaletteColors, @@ -57,6 +70,20 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ColorHovered, MinimizedFactor) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Menu::ThemeSettings::ScrollbarOpacitySettings, + Background, + Thumb, + ThumbHovered, + ThumbActive) + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Menu::ThemeSettings::FontRoleSettings, + Family, + Style, + File, + SizeScale) + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ImGuiStyle, WindowPadding, @@ -97,10 +124,18 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings, FontSize, + FontName, GlobalScale, + FontRoles, UseSimplePalette, ShowActionIcons, + UseMonochromeIcons, + UseMonochromeLogo, + ShowFooter, + CenterHeader, TooltipHoverDelay, + BackgroundBlurEnabled, + ScrollbarOpacity, Palette, StatusPalette, FeatureHeading, @@ -113,17 +148,43 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( SkipCompilationKey, EffectToggleKey, OverlayToggleKey, - Theme) + ShaderBlockPrevKey, + ShaderBlockNextKey, + EnableShaderBlocking, + FirstTimeSetupCompleted, + Theme, + SelectedThemePreset) bool IsEnabled = false; std::unordered_map Menu::categoryCounts; +std::optional Menu::ResolveFontRole(std::string_view key) +{ + for (size_t i = 0; i < FontRoleDescriptors.size(); ++i) { + if (FontRoleDescriptors[i].key == key) { + return static_cast(i); + } + } + return std::nullopt; +} + +std::string Menu::BuildFontSignature(float baseFontSize) const +{ + return MenuFonts::BuildFontSignature(settings.Theme, baseFontSize); +} + +const Menu::ThemeSettings::FontRoleSettings& Menu::GetDefaultFontRole(FontRole role) +{ + return MenuFonts::GetDefaultRole(role); +} + Menu::~Menu() { // Release icon textures if loaded uiIcons.saveSettings.Release(); uiIcons.loadSettings.Release(); uiIcons.clearCache.Release(); uiIcons.logo.Release(); + uiIcons.featureSettingRevert.Release(); uiIcons.discord.Release(); uiIcons.characters.Release(); uiIcons.display.Release(); @@ -136,6 +197,9 @@ Menu::~Menu() uiIcons.materials.Release(); uiIcons.postProcessing.Release(); + // Clean up blur resources + BackgroundBlur::Cleanup(); + ImGui_ImplDX11_Shutdown(); ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); @@ -147,18 +211,288 @@ Menu::~Menu() void Menu::Load(json& o_json) { settings = o_json; + bool hasThemeObject = o_json.contains("Theme") && o_json["Theme"].is_object(); + bool hasFontRoles = hasThemeObject && o_json["Theme"].contains("FontRoles"); + MenuFonts::NormalizeFontRoles(settings.Theme, hasFontRoles); + auto& bodyRole = settings.Theme.FontRoles[static_cast(FontRole::Body)]; + if (!Util::ValidateFont(bodyRole.File)) { + const auto& defaults = Menu::GetDefaultFontRole(FontRole::Body); + logger::warn("Font '{}' not found while loading settings, falling back to default font '{}'", + bodyRole.File, defaults.File); + settings.Theme.FontRoles[static_cast(FontRole::Body)] = defaults; + settings.Theme.FontName = defaults.File; + } + + // Apply Default Dark theme on first launch if no theme is selected + if (!settings.FirstTimeSetupCompleted && settings.SelectedThemePreset.empty()) { + // Ensure default themes are created/available + CreateDefaultThemes(); + + // Load the Default Dark theme and mark it as selected to prevent override + if (LoadThemePreset("Default")) { + settings.SelectedThemePreset = "Default"; // Mark as selected to prevent State::LoadTheme override + logger::info("Applied Default Dark theme on first launch"); + } else { + logger::warn("Failed to load Default Dark theme on first launch"); + } + } } void Menu::Save(json& o_json) { + settings.Theme.FontName = settings.Theme.FontRoles[static_cast(FontRole::Body)].File; o_json = settings; } +void Menu::LoadTheme(json& o_json) +{ + if (o_json["Theme"].is_object()) { + bool hasFontRoles = o_json["Theme"].contains("FontRoles"); + settings.Theme = o_json["Theme"]; + MenuFonts::NormalizeFontRoles(settings.Theme, hasFontRoles); + + auto& bodyRole = settings.Theme.FontRoles[static_cast(FontRole::Body)]; + if (!Util::ValidateFont(bodyRole.File)) { + const auto& defaults = Menu::GetDefaultFontRole(FontRole::Body); + logger::warn("Font '{}' not found, falling back to default font '{}'", + bodyRole.File, defaults.File); + settings.Theme.FontRoles[static_cast(FontRole::Body)] = defaults; + settings.Theme.FontName = defaults.File; + } + + // Apply background blur enabled state from theme + BackgroundBlur::SetEnabled(settings.Theme.BackgroundBlurEnabled); + } +} +void Menu::SaveTheme(json& o_json) +{ + settings.Theme.FontName = settings.Theme.FontRoles[static_cast(FontRole::Body)].File; + + if (!Util::ValidateFont(settings.Theme.FontName)) { + const auto& defaults = Menu::GetDefaultFontRole(FontRole::Body); + logger::warn("Font '{}' not found during save, falling back to default font '{}'", + settings.Theme.FontName, defaults.File); + settings.Theme.FontRoles[static_cast(FontRole::Body)] = defaults; + settings.Theme.FontName = defaults.File; + } + + o_json["Theme"] = settings.Theme; +} + +std::vector Menu::DiscoverThemes() +{ + auto themeManager = ThemeManager::GetSingleton(); + if (themeManager) { + themeManager->DiscoverThemes(); + return themeManager->GetThemeNames(); + } + return {}; +} + +bool Menu::LoadThemePreset(const std::string& themeName) +{ + if (themeName.empty()) { + // Empty theme name means custom/user theme + settings.SelectedThemePreset = ""; + return true; + } + + auto themeManager = ThemeManager::GetSingleton(); + json themeSettings; + + if (themeManager->LoadTheme(themeName, themeSettings)) { + try { + // Create a backup of current theme in case loading fails + ThemeSettings backupTheme = settings.Theme; + ThemeSettings defaultTheme; // For fallback values + + bool hasFontRoles = themeSettings.contains("FontRoles"); + + // Attempt to load theme with protection against malformed data + try { + settings.Theme = themeSettings; + } catch (const json::out_of_range& e) { + // Most likely FullPalette array size mismatch + logger::warn("Theme '{}' has incomplete data ({}). Loading with defaults for missing fields.", themeName, e.what()); + + // Manually load fields that exist, use defaults for missing ones + if (themeSettings.contains("FontSize")) { + try { + settings.Theme.FontSize = themeSettings["FontSize"]; + } catch (...) {} + } + if (themeSettings.contains("FontName")) { + try { + settings.Theme.FontName = themeSettings["FontName"]; + } catch (...) {} + } + if (themeSettings.contains("GlobalScale")) { + try { + settings.Theme.GlobalScale = themeSettings["GlobalScale"]; + } catch (...) {} + } + if (themeSettings.contains("FontRoles")) { + try { + settings.Theme.FontRoles = themeSettings["FontRoles"]; + } catch (...) {} + } + if (themeSettings.contains("ShowActionIcons")) { + try { + settings.Theme.ShowActionIcons = themeSettings["ShowActionIcons"]; + } catch (...) {} + } + if (themeSettings.contains("UseMonochromeIcons")) { + try { + settings.Theme.UseMonochromeIcons = themeSettings["UseMonochromeIcons"]; + } catch (...) {} + } + if (themeSettings.contains("UseMonochromeLogo")) { + try { + settings.Theme.UseMonochromeLogo = themeSettings["UseMonochromeLogo"]; + } catch (...) {} + } + if (themeSettings.contains("TooltipHoverDelay")) { + try { + settings.Theme.TooltipHoverDelay = themeSettings["TooltipHoverDelay"]; + } catch (...) {} + } + if (themeSettings.contains("BackgroundBlurEnabled")) { + try { + settings.Theme.BackgroundBlurEnabled = themeSettings["BackgroundBlurEnabled"]; + } catch (...) {} + } + if (themeSettings.contains("ScrollbarOpacity")) { + try { + settings.Theme.ScrollbarOpacity = themeSettings["ScrollbarOpacity"]; + } catch (...) {} + } + if (themeSettings.contains("Palette")) { + try { + settings.Theme.Palette = themeSettings["Palette"]; + } catch (...) {} + } + if (themeSettings.contains("StatusPalette")) { + try { + settings.Theme.StatusPalette = themeSettings["StatusPalette"]; + } catch (...) {} + } + if (themeSettings.contains("FeatureHeading")) { + try { + settings.Theme.FeatureHeading = themeSettings["FeatureHeading"]; + } catch (...) {} + } + if (themeSettings.contains("Style")) { + try { + settings.Theme.Style = themeSettings["Style"]; + } catch (...) {} + } + + // Handle FullPalette with extra care + if (themeSettings.contains("FullPalette") && themeSettings["FullPalette"].is_array()) { + const auto& paletteJson = themeSettings["FullPalette"]; + size_t jsonSize = paletteJson.size(); + size_t requiredSize = settings.Theme.FullPalette.size(); // Should be ImGuiCol_COUNT (55) + + if (jsonSize < requiredSize) { + logger::warn("Theme '{}' FullPalette has {} elements, expected {}. Using defaults for missing colors.", + themeName, jsonSize, requiredSize); + } + + // Load colors that exist, use defaults for the rest + for (size_t i = 0; i < requiredSize; ++i) { + if (i < jsonSize) { + try { + if (paletteJson[i].is_array() && paletteJson[i].size() >= 4) { + settings.Theme.FullPalette[i] = ImVec4( + paletteJson[i][0].get(), + paletteJson[i][1].get(), + paletteJson[i][2].get(), + paletteJson[i][3].get()); + } else { + settings.Theme.FullPalette[i] = defaultTheme.FullPalette[i]; + } + } catch (...) { + settings.Theme.FullPalette[i] = defaultTheme.FullPalette[i]; + } + } else { + settings.Theme.FullPalette[i] = defaultTheme.FullPalette[i]; + } + } + } else { + // FullPalette missing, use all defaults + logger::warn("Theme '{}' missing FullPalette array, using defaults", themeName); + settings.Theme.FullPalette = defaultTheme.FullPalette; + } + } catch (const std::exception& e) { + logger::error("Error loading theme '{}': {}. Using previous theme.", themeName, e.what()); + settings.Theme = backupTheme; + return false; + } + + MenuFonts::NormalizeFontRoles(settings.Theme, hasFontRoles); + auto& bodyRole = settings.Theme.FontRoles[static_cast(FontRole::Body)]; + if (!Util::ValidateFont(bodyRole.File)) { + const auto& defaults = Menu::GetDefaultFontRole(FontRole::Body); + logger::warn("Font '{}' from theme '{}' not found, falling back to default font '{}'", + bodyRole.File, themeName, defaults.File); + settings.Theme.FontRoles[static_cast(FontRole::Body)] = defaults; + settings.Theme.FontName = defaults.File; + } + + settings.SelectedThemePreset = themeName; + + // Schedule deferred font reload if font has changed + if (settings.Theme.FontName != cachedFontName) { + pendingFontReload = true; + } + + // Schedule deferred icon reload to apply theme-specific icon overrides + pendingIconReload = true; + + // Apply background blur enabled state from theme + BackgroundBlur::SetEnabled(settings.Theme.BackgroundBlurEnabled); + + logger::info("Loaded theme preset: {}", themeName); + return true; + } catch (const std::exception& e) { + logger::error("Fatal error loading theme '{}': {}.", themeName, e.what()); + return false; + } + } else { + logger::warn("Failed to load theme preset: {}", themeName); + return false; + } +} + +void Menu::CreateDefaultThemes() +{ + // Use ThemeManager to create default theme files + auto themeManager = ThemeManager::GetSingleton(); + themeManager->CreateDefaultThemeFiles(); +} + void Menu::Init() { // Setup Dear ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); + + // IMPORTANT: Immediately override ImGui's default styles with our Default.json theme + // This prevents hardcoded ImGui defaults from ever showing through + auto* themeManager = ThemeManager::GetSingleton(); + json defaultThemeSettings; + if (themeManager->LoadTheme("Default", defaultThemeSettings)) { + // Temporarily create a minimal theme structure to apply defaults + json tempSettings; + tempSettings["Theme"] = defaultThemeSettings; + LoadTheme(tempSettings); + logger::info("Applied Default.json theme immediately after ImGui context creation"); + } else { + logger::warn("Could not load Default.json theme - trying direct force application"); + // Last resort: Apply Default.json colors directly to ImGui + ThemeManager::ForceApplyDefaultTheme(); + } + auto& imgui_io = ImGui::GetIO(); imgui_io.ConfigFlags = ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad | ImGuiConfigFlags_DockingEnable; imgui_io.BackendFlags = ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_HasGamepad; @@ -166,41 +500,15 @@ void Menu::Init() cachedIniPath = Util::PathHelpers::GetImGuiIniPath().string(); imgui_io.IniFilename = cachedIniPath.c_str(); - // Enhanced font configuration for sharper text rendering - ImFontConfig font_config; - font_config.OversampleH = ThemeManager::Constants::FCONF_OVERSAMPLE_H; - font_config.OversampleV = ThemeManager::Constants::FCONF_OVERSAMPLE_V; - font_config.PixelSnapH = ThemeManager::Constants::FCONF_PIXELSNAP_H; - font_config.RasterizerMultiply = ThemeManager::Constants::FCONF_RASTERIZER_MULTIPLY; - DXGI_SWAP_CHAIN_DESC desc{}; globals::d3d::swapChain->GetDesc(&desc); - float fontSize = settings.Theme.FontSize; - - if (std::round(fontSize) != std::round(ThemeManager::Constants::DEFAULT_FONT_SIZE)) { - if (globals::state->screenSize.y > 0) { - fontSize = globals::state->screenSize.y * ThemeManager::Constants::DEFAULT_FONT_RATIO; - } else { - logger::warn("Menu::Init() - Failed to get game resolution from globals::state->screenSize."); - } - } - - fontSize = std::clamp(fontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE); - - auto fontPath = Util::PathHelpers::GetFontsPath() / "Jost-Regular.ttf"; - if (!imgui_io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), - std::round(fontSize), &font_config)) { - logger::warn("Menu::Init() - Failed to load custom font. Using default font."); - imgui_io.Fonts->AddFontDefault(); - } - - imgui_io.FontGlobalScale = exp2(settings.Theme.GlobalScale); - // Setup Platform/Renderer backends ImGui_ImplWin32_Init(desc.OutputWindow); ImGui_ImplDX11_Init(globals::d3d::device, globals::d3d::context); + ThemeManager::ReloadFont(*this, cachedFontSize); + { winrt::com_ptr dxgiDevice; if (!FAILED(globals::d3d::device->QueryInterface(dxgiDevice.put()))) { @@ -215,6 +523,11 @@ void Menu::Init() logger::warn("Menu::Init() - Failed to load UI icons. Will fallback to text buttons"); } + // Initialize background blur system + if (!BackgroundBlur::Initialize()) { + logger::warn("Menu::Init() - Failed to initialize background blur system"); + } + BuildCategoryCounts(); if (globals::features::vr.IsOpenVRCompatible()) { @@ -243,6 +556,10 @@ void Menu::DrawSettings() OnFocusChanged(); focusChanged = false; } + + // Apply theme styling with universal contrast enhancement + ThemeManager::SetupImGuiStyle(*this); + ImGui::DockSpaceOverViewport(NULL, ImGuiDockNodeFlags_PassthruCentralNode); ImGui::SetNextWindowPos(Util::GetNativeViewportSizeScaled(0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); @@ -266,7 +583,14 @@ void Menu::DrawSettings() bool isDocked = ImGui::IsWindowDocked(); wasDocked = isDocked; - const float uiScale = exp2(settings.Theme.GlobalScale); // Get current UI scale + float globalScale = settings.Theme.GlobalScale; + + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default + if (std::abs(globalScale - ThemeManager::Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { + globalScale = ThemeManager::Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 + } + + const float uiScale = exp2(globalScale); // Get current UI scale // Check if we can show icons - require setting enabled and at least some icons loaded (for undocked) // For docked mode, always show icons if textures are available bool canShowIcons = settings.Theme.ShowActionIcons && @@ -280,7 +604,9 @@ void Menu::DrawSettings() // Main content starts here - no additional separator needed as it's already handled in the conditions above - float footer_height = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y * 3 + 3.0f; // text + separator + float footer_height = settings.Theme.ShowFooter ? + (ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y * 3 + 3.0f) : + 0.0f; // Static storage for menu state - must persist across frames static size_t selectedMenu = 0; @@ -296,11 +622,12 @@ void Menu::DrawSettings() [&]() { DrawGeneralSettings(); }, [&]() { DrawAdvancedSettings(); }); - ImGui::Spacing(); - ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, ThemeManager::Constants::SEPARATOR_THICKNESS); - ImGui::Spacing(); - - DrawFooter(); + if (settings.Theme.ShowFooter) { + ImGui::Spacing(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, ThemeManager::Constants::SEPARATOR_THICKNESS); + ImGui::Spacing(); + DrawFooter(); + } } ImGui::End(); } @@ -319,7 +646,9 @@ void Menu::DrawGeneralSettings() .settingToggleKey = settingToggleKey, .settingsEffectsToggle = settingsEffectsToggle, .settingSkipCompilationKey = settingSkipCompilationKey, - .settingOverlayToggleKey = settingOverlayToggleKey + .settingOverlayToggleKey = settingOverlayToggleKey, + .settingShaderBlockPrevKey = settingShaderBlockPrevKey, + .settingShaderBlockNextKey = settingShaderBlockNextKey }; // Render settings using extracted component @@ -339,7 +668,7 @@ void Menu::DrawAdvancedSettings() { // Render advanced settings using extracted component AdvancedSettingsRenderer::RenderAdvancedSettings( - []() { globals::truePBR->DrawSettings(); }, + [this]() { globals::truePBR->DrawSettings(); }, [this]() { DrawDisableAtBootSettings(); }); } @@ -348,49 +677,49 @@ void Menu::DrawDisableAtBootSettings() auto state = globals::state; auto& disabledFeatures = state->GetDisabledFeatures(); - if (ImGui::CollapsingHeader("Disable at Boot", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - ImGui::Text( - "Select features to disable at boot. " - "This is the same as deleting a feature.ini file. " - "Restart will be required to reenable."); - - if (ImGui::CollapsingHeader("Special Features")) { - // Prepare a sorted list of special feature names - std::vector specialFeatureNames; - for (const auto& [featureName, _] : state->specialFeatures) { - specialFeatureNames.push_back(featureName); - } - std::sort(specialFeatureNames.begin(), specialFeatureNames.end()); + ImGui::Text( + "Select features to disable at boot. " + "This is the same as deleting a feature.ini file. " + "Restart will be required to reenable."); - // Display sorted special features - for (const auto& featureName : specialFeatureNames) { - // Check if the feature is currently disabled - bool isDisabled = disabledFeatures.contains(featureName) && disabledFeatures[featureName]; + ImGui::Spacing(); - // Create a checkbox for each feature - if (ImGui::Checkbox(featureName.c_str(), &isDisabled)) { - // Update the disabledFeatures map based on user interaction - disabledFeatures[featureName] = isDisabled; - } + if (ImGui::CollapsingHeader("Special Features", ImGuiTreeNodeFlags_DefaultOpen)) { + // Prepare a sorted list of special feature names + std::vector specialFeatureNames; + for (const auto& [featureName, _] : state->specialFeatures) { + specialFeatureNames.push_back(featureName); + } + std::sort(specialFeatureNames.begin(), specialFeatureNames.end()); + + // Display sorted special features + for (const auto& featureName : specialFeatureNames) { + // Check if the feature is currently disabled + bool isDisabled = disabledFeatures.contains(featureName) && disabledFeatures[featureName]; + + // Create a checkbox for each feature + if (ImGui::Checkbox(featureName.c_str(), &isDisabled)) { + // Update the disabledFeatures map based on user interaction + disabledFeatures[featureName] = isDisabled; } } + } - if (ImGui::CollapsingHeader("Features")) { - // Prepare a sorted list of feature pointers - auto featureList = Feature::GetFeatureList(); - std::sort(featureList.begin(), featureList.end(), [](Feature* a, Feature* b) { - return a->GetShortName() < b->GetShortName(); - }); - - // Display sorted features - for (auto* feature : featureList) { - const std::string featureName = feature->GetShortName(); - bool isDisabled = disabledFeatures.contains(featureName) && disabledFeatures[featureName]; - - if (ImGui::Checkbox(featureName.c_str(), &isDisabled)) { - // Update the disabledFeatures map based on user interaction - disabledFeatures[featureName] = isDisabled; - } + if (ImGui::CollapsingHeader("Features", ImGuiTreeNodeFlags_DefaultOpen)) { + // Prepare a sorted list of feature pointers + auto featureList = Feature::GetFeatureList(); + std::sort(featureList.begin(), featureList.end(), [](Feature* a, Feature* b) { + return a->GetShortName() < b->GetShortName(); + }); + + // Display sorted features + for (auto* feature : featureList) { + const std::string featureName = feature->GetShortName(); + bool isDisabled = disabledFeatures.contains(featureName) && disabledFeatures[featureName]; + + if (ImGui::Checkbox(featureName.c_str(), &isDisabled)) { + // Update the disabledFeatures map based on user interaction + disabledFeatures[featureName] = isDisabled; } } } @@ -400,9 +729,9 @@ void Menu::DrawFooter() { ImGui::BulletText(std::format("Game Version: {} {}", magic_enum::enum_name(REL::Module::GetRuntime()), Util::GetFormattedVersion(REL::Module::get().version()).c_str()).c_str()); ImGui::SameLine(); - ImGui::BulletText(std::format("D3D12 Interop: {}", globals::features::upscaling.d3d12Interop ? "Active" : "Inactive").c_str()); + ImGui::BulletText(std::format("D3D12 Swap Chain: {}", globals::features::upscaling.d3d12SwapChainActive ? "Active" : "Inactive").c_str()); ImGui::SameLine(); - ImGui::Text(std::format("GPU: {}", globals::state->adapterDescription.c_str()).c_str()); + ImGui::BulletText(std::format("GPU: {}", globals::state->adapterDescription.c_str()).c_str()); } /** @@ -417,13 +746,39 @@ void Menu::DrawFooter() */ void Menu::DrawOverlay() { + // Only process reloads when ImGui is NOT in an active frame + ImGuiContext* ctx = ImGui::GetCurrentContext(); + bool canReload = ctx && !ctx->WithinFrameScope && !ctx->WithinEndChild; + + // Process deferred font reload BEFORE any ImGui operations + // This is the safest place to do font atlas modifications + if (pendingFontReload && canReload) { + // Call ReloadFont first - only clear flag if it succeeds + if (ThemeManager::ReloadFont(*this, cachedFontSize)) { + // Reload completed successfully + pendingFontReload = false; + } else { + // Reload failed - keep flag true to retry next frame + logger::warn("Menu::DrawOverlay() - Font reload failed, will retry next frame"); + } + } + + // Process deferred icon reload BEFORE rendering + if (pendingIconReload && canReload) { + if (Util::IconLoader::InitializeMenuIcons(this)) { + pendingIconReload = false; + } else { + logger::warn("Menu::DrawOverlay() - Icon reload failed, will retry next frame"); + } + } + OverlayRenderer::RenderOverlay( *this, [this]() { ProcessInputEventQueue(); }, [this]() { DrawSettings(); }, [](uint32_t key) { return Util::Input::KeyIdToString(key); }, cachedFontSize, - settings.Theme.FontSize); + ThemeManager::ResolveFontSize(*this)); } /** @@ -492,16 +847,24 @@ void Menu::ProcessInputEventQueue() std::function action; }; auto shaderCache = globals::shaderCache; - auto devMode = globals::state->IsDeveloperMode(); HotkeyAction hotkeyActions[] = { { &settings.ToggleKey, &settingToggleKey, [this](uint32_t key) { settings.ToggleKey = key; settingToggleKey = false; } }, { &settings.SkipCompilationKey, &settingSkipCompilationKey, [this](uint32_t key) { settings.SkipCompilationKey = key; settingSkipCompilationKey = false; } }, { &settings.EffectToggleKey, &settingsEffectsToggle, [this](uint32_t key) { settings.EffectToggleKey = key; settingsEffectsToggle = false; } }, { &settings.OverlayToggleKey, &settingOverlayToggleKey, [this](uint32_t key) { settings.OverlayToggleKey = key; settingOverlayToggleKey = false; } }, + { &settings.ShaderBlockPrevKey, &settingShaderBlockPrevKey, [this](uint32_t key) { settings.ShaderBlockPrevKey = key; settingShaderBlockPrevKey = false; } }, + { &settings.ShaderBlockNextKey, &settingShaderBlockNextKey, [this](uint32_t key) { settings.ShaderBlockNextKey = key; settingShaderBlockNextKey = false; } }, }; bool handled = false; for (auto& h : hotkeyActions) { if (*(h.settingFlag)) { + // During first-time setup, don't capture Enter or Escape as hotkeys + // These keys are reserved for closing the dialog + if (HomePageRenderer::ShouldShowFirstTimeSetup() && (key == VK_RETURN || key == VK_ESCAPE)) { + *(h.settingFlag) = false; // Cancel hotkey capture mode + handled = true; + break; + } h.action(key); handled = true; break; @@ -517,8 +880,8 @@ void Menu::ProcessInputEventQueue() { settings.ToggleKey, [this]() { IsEnabled = !IsEnabled; } }, { settings.SkipCompilationKey, [shaderCache]() { shaderCache->backgroundCompilation = true; } }, { settings.EffectToggleKey, [shaderCache]() { shaderCache->SetEnabled(!shaderCache->IsEnabled()); } }, - { priorShaderKey, [shaderCache, devMode]() { if (devMode) shaderCache->IterateShaderBlock(); } }, - { nextShaderKey, [shaderCache, devMode]() { if (devMode) shaderCache->IterateShaderBlock(false); } }, + { settings.ShaderBlockPrevKey, [this, shaderCache]() { if (settings.EnableShaderBlocking) shaderCache->IterateShaderBlock(); } }, + { settings.ShaderBlockNextKey, [this, shaderCache]() { if (settings.EnableShaderBlocking) shaderCache->IterateShaderBlock(false); } }, { settings.OverlayToggleKey, []() { Menu::GetSingleton()->overlayVisible = !Menu::GetSingleton()->overlayVisible; } }, diff --git a/src/Menu.h b/src/Menu.h index 2715a5383d..b559160dfa 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -3,17 +3,95 @@ #include "Feature.h" #include "Menu/ThemeManager.h" #include "Utils/Serialize.h" +#include +#include +#include #include #include +#include #include +#include +#include +#include #include #include using json = nlohmann::json; +struct ImFont; + class Menu { public: + /** + * @brief Semantic font roles for hierarchical UI typography + * + * FONT ROLE SYSTEM: + * ================= + * Replaces legacy single-font approach with semantic typography system. + * Each role can use different font family, style, and size scaling. + * + * Roles: + * - Body (0): Default UI text, setting labels, general content + * - Heading (1): Feature section headers + * - Subheading (2): Subsection headers within features + * - Subtitle (3): Secondary descriptive text, tooltips + * + * Theme JSON Configuration: + * "FontRoles": [ + * { "Family": "Jost", "Style": "Regular", "File": "Jost/Jost-Regular.ttf", "SizeScale": 1.0 }, + * { "Family": "Jost", "Style": "Regular", "File": "Jost/Jost-Regular.ttf", "SizeScale": 1.0 }, + * { "Family": "Jost", "Style": "Regular", "File": "Jost/Jost-Regular.ttf", "SizeScale": 1.0 }, + * { "Family": "Jost", "Style": "Regular", "File": "Jost/Jost-Regular.ttf", "SizeScale": 1.0 } + * ] + * + * SizeScale multiplies the base FontSize for each role. + * Example: FontSize=27, Heading SizeScale=1.05 → 28.35px rendered size + * + * Migration from Legacy: + * Old "FontName" field auto-populates Body role on theme load. + * Themes without FontRoles get defaults (Jost family). + */ + enum class FontRole : std::uint8_t + { + Body = 0, // Default UI text + Title, // Large title text (e.g., "Community Shaders" header) + Heading, // Section headers (tabs, category labels) + Subheading, // Subsection headers (feature names, separators) + Count // Total number of roles + }; + + struct FontRoleDescriptor + { + std::string_view key; + std::string_view displayName; + float defaultScale; + }; + + static inline constexpr std::array(FontRole::Count)> FontRoleDescriptors = { + FontRoleDescriptor{ "Body", "Body Text", 1.0f }, + FontRoleDescriptor{ "Title", "Title", 1.0f }, + FontRoleDescriptor{ "Heading", "Headings", 1.0f }, + FontRoleDescriptor{ "Subheading", "Subheadings", 1.0f } + }; + + static constexpr std::string_view GetFontRoleKey(FontRole role) + { + return FontRoleDescriptors[static_cast(role)].key; + } + + static constexpr std::string_view GetFontRoleDisplayName(FontRole role) + { + return FontRoleDescriptors[static_cast(role)].displayName; + } + + static constexpr float GetFontRoleDefaultScale(FontRole role) + { + return FontRoleDescriptors[static_cast(role)].defaultScale; + } + + static std::optional ResolveFontRole(std::string_view key); + ~Menu(); Menu(const Menu&) = delete; Menu& operator=(const Menu&) = delete; @@ -30,6 +108,14 @@ class Menu void Load(json& o_json); void Save(json& o_json); + void LoadTheme(json& o_json); + void SaveTheme(json& o_json); + + // Multi-theme support + std::vector DiscoverThemes(); + bool LoadThemePreset(const std::string& themeName); + void CreateDefaultThemes(); + void Init(); void DrawSettings(); @@ -40,6 +126,7 @@ class Menu void ProcessInputEvents(RE::InputEvent* const* a_events); bool ShouldSwallowInput(); + std::string BuildFontSignature(float baseFontSize) const; public: // Input handling flags (made public for InputEventHandler access) @@ -47,8 +134,30 @@ class Menu bool settingSkipCompilationKey = false; bool settingsEffectsToggle = false; bool settingOverlayToggleKey = false; - uint32_t priorShaderKey = VK_PRIOR; // used for blocking shaders in debugging - uint32_t nextShaderKey = VK_NEXT; // used for blocking shaders in debugging + bool settingShaderBlockPrevKey = false; // Debug: capture shader block prev key + bool settingShaderBlockNextKey = false; // Debug: capture shader block next key + + // Font caching (made public for ThemeManager and OverlayRenderer access) + // Marked mutable because they're cache fields that may be updated from const methods + float cachedFontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; // Tracks whether font has been modified and may require reloading + mutable std::string cachedFontName = "Jost/Jost-Regular.ttf"; // Tracks whether font file has changed and may require reloading + std::array(FontRole::Count)> cachedFontFilesByRole = []() { + std::array(FontRole::Count)> files{}; + auto setFile = [&files](FontRole role, std::string value) { + files[static_cast(role)] = std::move(value); + }; + setFile(FontRole::Body, "Jost/Jost-Regular.ttf"); + setFile(FontRole::Heading, "Jost/Jost-Regular.ttf"); + setFile(FontRole::Subheading, "Jost/Jost-Regular.ttf"); + return files; + }(); + mutable std::array(FontRole::Count)> cachedFontPixelSizesByRole = {}; + std::string cachedFontSignature; + mutable std::array(FontRole::Count)> loadedFontRoles = {}; + + // Deferred reload systems (public for SettingsTabRenderer access) + bool pendingFontReload = false; + bool pendingIconReload = false; // Used for resetting input keys to solve alt-tab stuck issue std::atomic focusChanged = false; @@ -78,8 +187,9 @@ class Menu UIIcon saveSettings; UIIcon loadSettings; UIIcon clearCache; - UIIcon logo; // New logo icon - UIIcon search; // Search icon for search bars + UIIcon logo; // New logo icon + UIIcon search; // Search icon for search bars + UIIcon featureSettingRevert; // Feature revert settings icon // Social media/external link icons UIIcon discord; @@ -99,46 +209,95 @@ class Menu struct ThemeSettings { + struct FontRoleSettings + { + std::string Family; + std::string Style; + std::string File; + float SizeScale = 1.0f; + }; + float FontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; + std::string FontName = "Jost/Jost-Regular.ttf"; // Default font file name (legacy) float GlobalScale = REL::Module::IsVR() ? -0.5f : 0.f; // exponential + std::array(FontRole::Count)> FontRoles = []() { + std::array(FontRole::Count)> roles{}; + auto setRole = [&roles](FontRole role, std::string family, std::string style, std::string file, float sizeScale) { + auto index = static_cast(role); + roles[index].Family = std::move(family); + roles[index].Style = std::move(style); + roles[index].File = std::move(file); + roles[index].SizeScale = sizeScale; + }; + + setRole(FontRole::Body, "Jost", "Regular", "Jost/Jost-Regular.ttf", 1.0f); + setRole(FontRole::Title, "Jost", "Regular", "Jost/Jost-Regular.ttf", 1.0f); + setRole(FontRole::Heading, "Jost", "Regular", "Jost/Jost-Regular.ttf", 1.0f); + setRole(FontRole::Subheading, "Jost", "Regular", "Jost/Jost-Regular.ttf", 1.0f); + + return roles; + }(); - bool UseSimplePalette = true; // simple palette or full customization - bool ShowActionIcons = true; // whether to show action buttons as icons - float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds + bool UseSimplePalette = false; // DEPRECATED: No longer affects behavior. UI now shows both Simple and Advanced controls. + bool ShowActionIcons = true; // whether to show action buttons as icons + bool UseMonochromeIcons = false; // whether to use monochrome (white) action icons with text color tinting + bool UseMonochromeLogo = false; // whether to use monochrome CS logo + bool ShowFooter = true; // whether to show the footer with game version/GPU info + bool CenterHeader = false; // whether to center the header title and logo + float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds + bool BackgroundBlurEnabled = false; // enable background blur effect + // Scrollbar opacity settings + struct ScrollbarOpacitySettings + { + float Background = 0.0f; // Background of the scrollbar area + float Thumb = 0.5f; // The draggable thumb/grip + float ThumbHovered = 0.75f; // Thumb when hovered + float ThumbActive = 0.9f; // Thumb when being dragged + } ScrollbarOpacity; struct PaletteColors { - ImVec4 Background{ 0.f, 0.f, 0.f, 0.5882353186607361f }; - ImVec4 Text{ 1.f, 1.f, 1.f, 1.f }; - ImVec4 Border{ 0.5882353186607361f, 0.5882353186607361f, 0.5882353186607361f, 0.5882353186607361f }; + ImVec4 Background{ 0.10f, 0.10f, 0.10f, 0.80f }; + ImVec4 Text{ 1.0f, 1.0f, 1.0f, 1.0f }; + // Separated border controls for better theming granularity + ImVec4 WindowBorder{ 0.5f, 0.5f, 0.5f, 0.8f }; // Outer window borders + ImVec4 FrameBorder{ 0.4f, 0.4f, 0.4f, 0.7f }; // Button, slider, input field borders + ImVec4 Separator{ 0.5f, 0.5f, 0.5f, 0.6f }; // Internal separators and dividers + ImVec4 ResizeGrip{ 0.6f, 0.6f, 0.6f, 0.8f }; // Window resize grips } Palette; struct StatusPaletteColors { - ImVec4 Disable{ 0.5f, 0.5f, 0.5f, 1.f }; - ImVec4 Error{ 1.f, 0.5f, 0.5f, 1.f }; + ImVec4 Disable{ 0.5f, 0.5f, 0.5f, 1.0f }; + ImVec4 Error{ 1.0f, 0.4f, 0.4f, 1.0f }; ImVec4 Warning{ 1.0f, 0.6f, 0.2f, 1.0f }; - ImVec4 RestartNeeded{ 0.5f, 1.f, 0.5f, 1.f }; - ImVec4 CurrentHotkey{ 1.f, 1.f, 0.f, 1.f }; + ImVec4 RestartNeeded{ 0.4f, 1.0f, 0.4f, 1.0f }; + ImVec4 CurrentHotkey{ 1.0f, 1.0f, 0.0f, 1.0f }; ImVec4 SuccessColor{ 0.0f, 1.0f, 0.0f, 1.0f }; - ImVec4 InfoColor{ 0.0f, 0.5f, 1.0f, 1.0f }; + ImVec4 InfoColor{ 0.2f, 0.6f, 1.0f, 1.0f }; } StatusPalette; struct FeatureHeadingColors { - ImVec4 ColorDefault{ 0.47f, 0.47f, 0.47f, 1.00f }; // ~120, 120, 120 - ImVec4 ColorHovered{ 0.39f, 0.39f, 0.39f, 1.00f }; // ~100, 100, 100 - float MinimizedFactor = 0.7f; // 70% of original alpha for when the header is minimized + ImVec4 ColorDefault{ 0.8f, 0.8f, 0.8f, 1.0f }; + ImVec4 ColorHovered{ 0.6f, 0.6f, 0.6f, 1.0f }; + float MinimizedFactor = 0.7f; // 70% of original alpha for when the header is minimized } FeatureHeading; ImGuiStyle Style = []() { ImGuiStyle style = {}; - style.WindowBorderSize = 3.f; - style.ChildBorderSize = 0.f; - style.FrameBorderSize = 1.5f; - style.WindowPadding = { 16.f, 16.f }; - style.WindowRounding = 0.f; - style.IndentSpacing = 8.f; - style.FramePadding = { 4.0f, 4.0f }; - style.CellPadding = { 16.f, 2.f }; - style.ItemSpacing = { 8.f, 12.f }; + style.WindowBorderSize = 2.0f; + style.ChildBorderSize = 0.0f; + style.FrameBorderSize = 1.0f; + style.WindowPadding = { 8.0f, 8.0f }; + style.WindowRounding = 12.0f; + style.IndentSpacing = 8.0f; + style.FramePadding = { 8.0f, 4.0f }; + style.CellPadding = { 8.0f, 2.0f }; + style.ItemSpacing = { 4.0f, 8.0f }; + style.FrameRounding = 4.0f; + style.TabRounding = 4.0f; + style.ScrollbarRounding = 9.0f; + style.ScrollbarSize = 12.0f; + style.GrabRounding = 3.0f; + style.GrabMinSize = 12.0f; return std::move(style); }(); // Theme by @Maksasj, edited by FiveLimbedCat @@ -202,17 +361,27 @@ class Menu }; }; + static const ThemeSettings::FontRoleSettings& GetDefaultFontRole(FontRole role); + struct Settings { uint32_t ToggleKey = VK_END; uint32_t SkipCompilationKey = VK_ESCAPE; uint32_t EffectToggleKey = VK_MULTIPLY; // toggle all effects uint32_t OverlayToggleKey = VK_F10; // Global overlay toggle key for all overlays + uint32_t ShaderBlockPrevKey = VK_PRIOR; // Debug: cycle backward through shaders (PageUp) + uint32_t ShaderBlockNextKey = VK_NEXT; // Debug: cycle forward through shaders (PageDown) + bool EnableShaderBlocking = false; // Enable shader blocking hotkeys for debugging + bool FirstTimeSetupCompleted = false; // Track if first-time setup has been completed ThemeSettings Theme; + std::string SelectedThemePreset = ""; // Currently selected theme preset (empty = custom/user theme) }; const ThemeSettings& GetTheme() const { return settings.Theme; } // Provide read-only access to the Theme. Settings& GetSettings() { return settings; } // Provide access to settings for other components winrt::com_ptr GetDXGIAdapter3() const { return dxgiAdapter3; } // Provide access to dxgiAdapter3 + ThemeSettings::FontRoleSettings& GetFontRoleSettings(FontRole role) { return settings.Theme.FontRoles[static_cast(role)]; } + const ThemeSettings::FontRoleSettings& GetFontRoleSettings(FontRole role) const { return settings.Theme.FontRoles[static_cast(role)]; } + ImFont* GetFont(FontRole role) const { return loadedFontRoles[static_cast(role)]; } void SelectFeatureMenu(const std::string& featureName); static std::unordered_map categoryCounts; // Number of features in each feature category @@ -275,8 +444,6 @@ class Menu private: Settings settings; - float cachedFontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; // Tracks whether font has been modified and may require reloading - std::string cachedIniPath; // io.IniFilename must point to a string that lives for the duration of the runtime // Menu navigation diff --git a/src/Menu/AdvancedSettingsRenderer.cpp b/src/Menu/AdvancedSettingsRenderer.cpp index d82785dcd9..5236697c84 100644 --- a/src/Menu/AdvancedSettingsRenderer.cpp +++ b/src/Menu/AdvancedSettingsRenderer.cpp @@ -1,5 +1,6 @@ #include "AdvancedSettingsRenderer.h" +#include #include #include #include @@ -7,202 +8,560 @@ #include "FeatureIssues.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" +#include "Fonts.h" #include "Globals.h" #include "Menu.h" #include "ShaderCache.h" #include "State.h" #include "TruePBR.h" #include "Util.h" +#include "Utils/Format.h" #include "Utils/UI.h" void AdvancedSettingsRenderer::RenderAdvancedSettings( const std::function& drawTruePBRSettings, const std::function& drawDisableAtBootSettings) { - RenderAdvancedSection(); - RenderShaderReplacementSection(); + // Use TabBar system - tabs sorted alphabetically + if (ImGui::BeginTabBar("##AdvancedSettingsTabs", ImGuiTabBarFlags_None)) { + // Developer Tab + if (MenuFonts::BeginTabItemWithFont("Developer", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##DeveloperContent", ImVec2(0, 0), false)) { + RenderDeveloperSection(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } - // TruePBR settings - drawTruePBRSettings(); + // Disable at Boot Tab + if (MenuFonts::BeginTabItemWithFont("Disable at Boot", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##DisableAtBootContent", ImVec2(0, 0), false)) { + RenderDisableAtBootSection(drawDisableAtBootSettings); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } - // Disable at boot settings - drawDisableAtBootSettings(); + // Logging Tab + if (MenuFonts::BeginTabItemWithFont("Logging", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##LoggingContent", ImVec2(0, 0), false)) { + RenderLoggingSection(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + // PBR Settings Tab + if (MenuFonts::BeginTabItemWithFont("PBR Settings", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##PBRSettingsContent", ImVec2(0, 0), false)) { + RenderPBRSection(drawTruePBRSettings); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + // Shader Debug Tab + if (MenuFonts::BeginTabItemWithFont("Shader Debug", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##ShaderDebugContent", ImVec2(0, 0), false)) { + RenderShaderDebugSection(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } - RenderDeveloperSection(); + ImGui::EndTabBar(); + } } -void AdvancedSettingsRenderer::RenderAdvancedSection() +void AdvancedSettingsRenderer::RenderLoggingSection() { auto shaderCache = globals::shaderCache; - if (ImGui::CollapsingHeader("Advanced", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - // Dump Shaders option - bool useDump = shaderCache->IsDump(); - if (ImGui::Checkbox("Dump Shaders", &useDump)) { - shaderCache->SetDump(useDump); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Dump shaders at startup. This should be used only when reversing shaders. Normal users don't need this."); + // Log Level selection + spdlog::level::level_enum logLevel = globals::state->GetLogLevel(); + const char* items[] = { + "trace", + "debug", + "info", + "warn", + "err", + "critical", + "off" + }; + static int item_current = static_cast(logLevel); + if (ImGui::Combo("Log Level", &item_current, items, IM_ARRAYSIZE(items))) { + ImGui::SameLine(); + globals::state->SetLogLevel(static_cast(item_current)); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Log level. Trace is most verbose. Default is info."); + } + + // Shader Defines input + auto& shaderDefines = globals::state->shaderDefinesString; + if (ImGui::InputText("Shader Defines", &shaderDefines)) { + globals::state->SetDefines(shaderDefines); + } + if (ImGui::IsItemDeactivatedAfterEdit() || (ImGui::IsItemActive() && + (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)) || + ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_KeypadEnter))))) { + globals::state->SetDefines(shaderDefines); + shaderCache->Clear(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Defines for Shader Compiler. Semicolon \";\" separated. Clear with space. Rebuild shaders after making change. Compute Shaders require a restart to recompile."); + } + + ImGui::Spacing(); + + // Compiler Thread controls + ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Number of threads to use to compile shaders. " + "The more threads the faster compilation will finish but may make the system unresponsive. "); + } + ImGui::SliderInt("Background Compiler Threads", &shaderCache->backgroundCompilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Number of threads to use to compile shaders while playing game. " + "This is activated if the startup compilation is skipped. " + "The more threads the faster compilation will finish but may make the system unresponsive. "); + } + + // A/B Testing settings + auto* abTestingManager = ABTestingManager::GetSingleton(); + abTestingManager->DrawSettingsUI(); + + // Dump Ini Settings button + if (ImGui::Button("Dump Ini Settings", { -1, 0 })) { + Util::DumpSettingsOptions(); + } +} + +void AdvancedSettingsRenderer::RenderShaderDebugSection() +{ + auto shaderCache = globals::shaderCache; + auto state = globals::state; + + // Dump Shaders option + bool useDump = shaderCache->IsDump(); + if (ImGui::Checkbox("Dump Shaders", &useDump)) { + shaderCache->SetDump(useDump); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Dump shaders at startup. This should be used only when reversing shaders. Normal users don't need this."); + } + + // Clear Shader Cache button + if (ImGui::Button("Clear Shader Cache", { -1, 0 })) { + shaderCache->Clear(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Clear all compiled shaders from memory. Forces recompilation of all shaders on next use."); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Shader Replacement section + Util::DrawSectionHeader("Replace Original Shaders"); + + if (ImGui::BeginTable("##ReplaceToggles", 3, ImGuiTableFlags_SizingStretchSame)) { + globals::state->ForEachShaderTypeWithIndex([&](auto type, int classIndex) { + ImGui::TableNextColumn(); + + if (!(SIE::ShaderCache::IsSupportedShader(type) || state->IsDeveloperMode())) { + ImGui::BeginDisabled(); + ImGui::Checkbox(std::format("{}", magic_enum::enum_name(type)).c_str(), &state->enabledClasses[classIndex]); + ImGui::EndDisabled(); + } else + ImGui::Checkbox(std::format("{}", magic_enum::enum_name(type)).c_str(), &state->enabledClasses[classIndex]); + }); + if (state->IsDeveloperMode()) { + ImGui::Checkbox("Vertex", &state->enableVShaders); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Replace Vertex Shaders. " + "When false, will disable the custom Vertex Shaders for the types above. " + "For developers to test whether CS shaders match vanilla behavior. "); + } + + ImGui::Checkbox("Pixel", &state->enablePShaders); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Replace Pixel Shaders. " + "When false, will disable the custom Pixel Shaders for the types above. " + "For developers to test whether CS shaders match vanilla behavior. "); + } + + ImGui::Checkbox("Compute", &state->enableCShaders); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Replace Compute Shaders. " + "When false, will disable the custom Compute Shaders for the types above. " + "For developers to test whether CS shaders match vanilla behavior. "); + } } + ImGui::EndTable(); + } - // Log Level selection - spdlog::level::level_enum logLevel = globals::state->GetLogLevel(); - const char* items[] = { - "trace", - "debug", - "info", - "warn", - "err", - "critical", - "off" - }; - static int item_current = static_cast(logLevel); - if (ImGui::Combo("Log Level", &item_current, items, IM_ARRAYSIZE(items))) { + // Only show shader blocking section in developer mode + if (!globals::state->IsDeveloperMode()) { + return; + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Show blocked shader status as a regular section + if (!shaderCache->blockedKey.empty()) { + // Create a visually distinct box for the blocked shader info with rounded corners and border + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 8.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 2.0f); + ImVec4 blockedBgColor = Util::Colors::GetError(); + blockedBgColor.w = 0.15f; // Semi-transparent background + ImGui::PushStyleColor(ImGuiCol_ChildBg, blockedBgColor); + + float maxHeight = ImGui::GetContentRegionAvail().y * 0.3f; // Limit to 30% to keep Active Shaders visible + if (ImGui::BeginChild("##BlockedShaderInfo", ImVec2(0, maxHeight), true, ImGuiChildFlags_AutoResizeY)) { + ImGui::TextColored(Util::Colors::GetError(), "Shader Blocking Active"); ImGui::SameLine(); - globals::state->SetLogLevel(static_cast(item_current)); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Log level. Trace is most verbose. Default is info."); - } + if (ImGui::SmallButton("Stop Blocking##Section")) { + shaderCache->DisableShaderBlocking(); + } - // Shader Defines input - auto& shaderDefines = globals::state->shaderDefinesString; - if (ImGui::InputText("Shader Defines", &shaderDefines)) { - globals::state->SetDefines(shaderDefines); - } - if (ImGui::IsItemDeactivatedAfterEdit() || (ImGui::IsItemActive() && - (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)) || - ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_KeypadEnter))))) { - globals::state->SetDefines(shaderDefines); - shaderCache->Clear(); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Defines for Shader Compiler. Semicolon \";\" separated. Clear with space. Rebuild shaders after making change. Compute Shaders require a restart to recompile."); + ImGui::Text("Blocked: %s", shaderCache->blockedKey.c_str()); + + // Try to get more details from active shaders + auto activeShaders = shaderCache->GetActiveShaders(); + for (const auto& shader : activeShaders) { + if (shader.key == shaderCache->blockedKey) { + ImGui::Text("Type: %s", magic_enum::enum_name(shader.shaderType).data()); + ImGui::Text("Class: %s", magic_enum::enum_name(shader.shaderClass).data()); + ImGui::Text("Descriptor: 0x%X", shader.descriptor); + + // Add button to copy shader info to clipboard + ImGui::PushID(shader.key.c_str()); + if (ImGui::SmallButton("Copy Info##BlockedShader")) { + std::string diskPathStr; + diskPathStr.reserve(shader.diskPath.size()); + for (wchar_t wc : shader.diskPath) { + diskPathStr += static_cast(wc); + } + + std::string fullInfo = std::format("Type: {}\nClass: {}\nDescriptor: 0x{:X}\nKey: {}\nCache Path: {}", + magic_enum::enum_name(shader.shaderType).data(), + magic_enum::enum_name(shader.shaderClass).data(), + shader.descriptor, + shader.key, + diskPathStr); + ImGui::SetClipboardText(fullInfo.c_str()); + } + ImGui::PopID(); + if (ImGui::IsItemHovered()) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Copy complete shader information including cache path to clipboard"); + } + } + + break; + } + } } + ImGui::EndChild(); - ImGui::Spacing(); + ImGui::PopStyleVar(); // ChildRounding + ImGui::PopStyleVar(); // WindowBorderSize + ImGui::PopStyleColor(); // ChildBg + } - // Compiler Thread controls - ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Number of threads to use to compile shaders. " - "The more threads the faster compilation will finish but may make the system unresponsive. "); + // Shader Debug section + if (ImGui::CollapsingHeader("Shader Debug")) { + auto menu = globals::menu; + auto& menuSettings = menu->GetSettings(); + auto& themeSettings = menuSettings.Theme; + + if (ImGui::Checkbox("Enable Shader Blocking", &menuSettings.EnableShaderBlocking)) { + // Setting saved automatically on next save } - ImGui::SliderInt("Background Compiler Threads", &shaderCache->backgroundCompilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Number of threads to use to compile shaders while playing game. " - "This is activated if the startup compilation is skipped. " - "The more threads the faster compilation will finish but may make the system unresponsive. "); + ImGui::Text("Enables hotkeys to cycle through and block individual shaders for debugging purposes."); } - // A/B Testing settings - auto* abTestingManager = ABTestingManager::GetSingleton(); - abTestingManager->DrawSettingsUI(); + if (menuSettings.EnableShaderBlocking) { + ImGui::Indent(); - // File Watcher option - bool useFileWatcher = shaderCache->UseFileWatcher(); - if (ImGui::Checkbox("Enable File Watcher", &useFileWatcher)) { - shaderCache->SetFileWatcher(useFileWatcher); + // Shader Block Previous Key + if (menu->settingShaderBlockPrevKey) { + ImGui::Text("Press any key for Shader Block Previous..."); + } else { + ImGui::AlignTextToFramePadding(); + ImGui::Text("Block Previous:"); + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(themeSettings.StatusPalette.CurrentHotkey, "%s", Util::Input::KeyIdToString(menuSettings.ShaderBlockPrevKey)); + ImGui::SameLine(); + if (ImGui::Button("Change##ShaderBlockPrev")) { + menu->settingShaderBlockPrevKey = true; + } + } + + // Shader Block Next Key + if (menu->settingShaderBlockNextKey) { + ImGui::Text("Press any key for Shader Block Next..."); + } else { + ImGui::AlignTextToFramePadding(); + ImGui::Text("Block Next:"); + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(themeSettings.StatusPalette.CurrentHotkey, "%s", Util::Input::KeyIdToString(menuSettings.ShaderBlockNextKey)); + ImGui::SameLine(); + if (ImGui::Button("Change##ShaderBlockNext")) { + menu->settingShaderBlockNextKey = true; + } + } + + ImGui::Unindent(); } + } + + // Active shaders list + if (ImGui::CollapsingHeader("Active Shaders", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("Active Shaders (Used Recently)"); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( - "Automatically recompile shaders on file change. " - "Intended for developing."); + "List of shaders that have been used in recent frames. " + "Enable Shader Blocking above to use hotkeys to cycle through and block shaders for debugging. " + "Shaders not used for ~1 second are removed from this list."); } - // Dump Ini Settings button - if (ImGui::Button("Dump Ini Settings", { -1, 0 })) { - Util::DumpSettingsOptions(); + // Get fresh active shaders data for accurate count and table + auto activeShaders = shaderCache->GetActiveShaders(); + uint32_t totalDrawCalls = 0; + for (const auto& shader : activeShaders) { + totalDrawCalls += shader.drawCalls; } - // Blocking shader controls - if (!shaderCache->blockedKey.empty()) { - auto blockingButtonString = std::format("Stop Blocking {} Shaders", shaderCache->blockedIDs.size()); - if (ImGui::Button(blockingButtonString.c_str(), { -1, 0 })) { + // Static variables to maintain table filter state + static char filterText[256] = ""; + static int searchColumn = 0; // 0 = All Columns, 1 = Type, 2 = Class, 3 = Descriptor, 4 = Draw Calls, 5 = Key + static size_t sortColumn = 4; // Default sort by Frame % (draw calls) + static bool sortAscending = false; // Descending by default (highest usage first) // Create shader rows for the table utility (simplified - no filter data needed) + struct ShaderRow + { + SIE::ShaderCache::ActiveShaderInfo shader; + uint32_t totalDrawCalls; + }; + + std::vector shaderRows; + for (const auto& shader : activeShaders) { + shaderRows.push_back({ shader, totalDrawCalls }); + } + + // Build column configurations + std::vector> columns = { + { "Type", "Shader type", [](const ShaderRow& row) { + return std::string(magic_enum::enum_name(row.shader.shaderType)); + } }, + { "Class", "Shader class", [](const ShaderRow& row) { + return std::string(magic_enum::enum_name(row.shader.shaderClass)); + } }, + { "Descriptor", "Shader descriptor", [](const ShaderRow& row) { + return std::format("0x{:X}", row.shader.descriptor); + } }, + { "Frame %", "Percentage of draw calls this frame", [](const ShaderRow& row) { + float percentage = Util::CalculatePercentage(static_cast(row.shader.drawCalls), static_cast(row.totalDrawCalls)); + return Util::FormatPercent(percentage); + } }, + { "Key", "Shader key", [](const ShaderRow& row) { + return row.shader.key; + } } + }; + + // Row click callbacks + auto onRowLeftClick = [shaderCache](const ShaderRow& row) { + if (row.shader.key == shaderCache->blockedKey) { shaderCache->DisableShaderBlocking(); + } else { + // Block this shader - use IterateShaderBlock to find and block it + // Or set blockedKey directly (simpler for click-to-block) + shaderCache->blockedKey = row.shader.key; + logger::info("Blocking shader: {}", row.shader.key); } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Stop blocking Community Shaders shader. " - "Blocking is helpful when debugging shader errors in game to determine which shader has issues. " - "Blocking is enabled if in developer mode and pressing PAGEUP and PAGEDOWN. " - "Specific shader will be printed to logfile. "); + }; + + auto onRowRightClick = [shaderCache](const ShaderRow& row) { + std::string diskPathStr; + diskPathStr.reserve(row.shader.diskPath.size()); + for (wchar_t wc : row.shader.diskPath) { + diskPathStr += static_cast(wc); } - } - // Debug addresses section - if (ImGui::TreeNodeEx("Addresses")) { - auto Renderer = globals::game::renderer; - auto BSShaderAccumulator = *globals::game::currentAccumulator.get(); - auto RendererShadowState = globals::game::shadowState; - ADDRESS_NODE(Renderer) - ADDRESS_NODE(BSShaderAccumulator) - ADDRESS_NODE(RendererShadowState) - ImGui::TreePop(); - } + std::string fullInfo = std::format("Type: {}\nClass: {}\nDescriptor: 0x{:X}\nKey: {}\nCache Path: {}", + magic_enum::enum_name(row.shader.shaderType).data(), + magic_enum::enum_name(row.shader.shaderClass).data(), + row.shader.descriptor, + row.shader.key, + diskPathStr); + ImGui::SetClipboardText(fullInfo.c_str()); + }; + auto getRowTooltip = [shaderCache](const ShaderRow& row) { + std::string clickAction = (row.shader.key == shaderCache->blockedKey) ? "Left-click to unblock this shader" : "Left-click to block this shader"; - // Statistics section - if (ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text(std::format("Shader Compiler : {}", shaderCache->GetShaderStatsString()).c_str()); - ImGui::TreePop(); - } + return std::format("Type: {}\nClass: {}\nDescriptor: 0x{:X}\nKey: {}\n\n{}", + magic_enum::enum_name(row.shader.shaderType).data(), + magic_enum::enum_name(row.shader.shaderClass).data(), + row.shader.descriptor, + row.shader.key, + clickAction); + }; + + // Define function to extract filterable fields (for TableFilterState) + auto getFilterableFields = [](const ShaderRow& row) -> std::vector { + return { + std::string(magic_enum::enum_name(row.shader.shaderType)), // Type + std::string(magic_enum::enum_name(row.shader.shaderClass)), // Class + std::format("0x{:X}", row.shader.descriptor), // Descriptor + Util::FormatPercent(Util::CalculatePercentage(static_cast(row.shader.drawCalls), static_cast(row.totalDrawCalls))), // Frame % + row.shader.key // Key + }; + }; - // Frame annotations toggle - ImGui::Checkbox("Frame Annotations", &globals::state->frameAnnotations); + // Define sorting comparators (customSorts parameter) + std::vector> sorters = { + // Type - string sort + [](const ShaderRow& a, const ShaderRow& b, bool ascending) { + std::string aVal = std::string(magic_enum::enum_name(a.shader.shaderType)); + std::string bVal = std::string(magic_enum::enum_name(b.shader.shaderType)); + return ascending ? (aVal < bVal) : (aVal > bVal); + }, + // Class - string sort + [](const ShaderRow& a, const ShaderRow& b, bool ascending) { + std::string aVal = std::string(magic_enum::enum_name(a.shader.shaderClass)); + std::string bVal = std::string(magic_enum::enum_name(b.shader.shaderClass)); + return ascending ? (aVal < bVal) : (aVal > bVal); + }, + // Descriptor - numeric sort + [](const ShaderRow& a, const ShaderRow& b, bool ascending) { + return ascending ? (a.shader.descriptor < b.shader.descriptor) : (a.shader.descriptor > b.shader.descriptor); + }, + // Frame % - numeric sort + [](const ShaderRow& a, const ShaderRow& b, bool ascending) { + float aPercent = Util::CalculatePercentage(static_cast(a.shader.drawCalls), static_cast(a.totalDrawCalls)); + float bPercent = Util::CalculatePercentage(static_cast(b.shader.drawCalls), static_cast(b.totalDrawCalls)); + return ascending ? (aPercent < bPercent) : (aPercent > bPercent); + }, + // Key - string sort + [](const ShaderRow& a, const ShaderRow& b, bool ascending) { + return ascending ? (a.shader.key < b.shader.key) : (a.shader.key > b.shader.key); + } + }; + + // Create filter state + Util::TableFilterState filterState(getFilterableFields); + + // Initialize filter state from existing variables + filterState.filterText = std::string(filterText, filterText + strlen(filterText)); + filterState.searchColumn = searchColumn; + + // Define input events for row interactions + std::vector> inputEvents = { + // Left-click to block/unblock shader + { Util::TableInputEventType::MouseClick, onRowLeftClick, "", 0 }, + // Right-click context menu for copying info + { Util::TableInputEventType::ContextMenu, onRowRightClick, "Copy Info", 1 } + }; + + // Render the table with all configurations + Util::ShowInteractiveTable( + "##ActiveShadersTable", + columns, + shaderRows, + sortColumn, + sortAscending, + sorters, + filterState, + inputEvents, + getRowTooltip); + + // Update static variables with modified filter state + strncpy_s(filterText, filterState.filterText.c_str(), sizeof(filterText) - 1); + filterText[sizeof(filterText) - 1] = '\0'; + searchColumn = filterState.searchColumn; } } -void AdvancedSettingsRenderer::RenderShaderReplacementSection() +void AdvancedSettingsRenderer::RenderPBRSection(const std::function& drawTruePBRSettings) { - if (ImGui::CollapsingHeader("Replace Original Shaders", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - auto state = globals::state; - if (ImGui::BeginTable("##ReplaceToggles", 3, ImGuiTableFlags_SizingStretchSame)) { - globals::state->ForEachShaderTypeWithIndex([&](auto type, int classIndex) { - ImGui::TableNextColumn(); - - if (!(SIE::ShaderCache::IsSupportedShader(type) || state->IsDeveloperMode())) { - ImGui::BeginDisabled(); - ImGui::Checkbox(std::format("{}", magic_enum::enum_name(type)).c_str(), &state->enabledClasses[classIndex]); - ImGui::EndDisabled(); - } else - ImGui::Checkbox(std::format("{}", magic_enum::enum_name(type)).c_str(), &state->enabledClasses[classIndex]); - }); - if (state->IsDeveloperMode()) { - ImGui::Checkbox("Vertex", &state->enableVShaders); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Replace Vertex Shaders. " - "When false, will disable the custom Vertex Shaders for the types above. " - "For developers to test whether CS shaders match vanilla behavior. "); - } - - ImGui::Checkbox("Pixel", &state->enablePShaders); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Replace Pixel Shaders. " - "When false, will disable the custom Pixel Shaders for the types above. " - "For developers to test whether CS shaders match vanilla behavior. "); - } + drawTruePBRSettings(); +} - ImGui::Checkbox("Compute", &state->enableCShaders); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Replace Compute Shaders. " - "When false, will disable the custom Compute Shaders for the types above. " - "For developers to test whether CS shaders match vanilla behavior. "); - } - } - ImGui::EndTable(); - } - } +void AdvancedSettingsRenderer::RenderDisableAtBootSection(const std::function& drawDisableAtBootSettings) +{ + drawDisableAtBootSettings(); } void AdvancedSettingsRenderer::RenderDeveloperSection() { + auto shaderCache = globals::shaderCache; + + // File Watcher option (moved from Advanced/Logging) + bool useFileWatcher = shaderCache->UseFileWatcher(); + if (ImGui::Checkbox("Enable File Watcher", &useFileWatcher)) { + shaderCache->SetFileWatcher(useFileWatcher); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Automatically recompile shaders on file change. " + "Intended for developing."); + } + + // Debug addresses section (moved from Advanced/Logging) + if (ImGui::TreeNodeEx("Addresses")) { + auto Renderer = globals::game::renderer; + auto BSShaderAccumulator = *globals::game::currentAccumulator.get(); + auto RendererShadowState = globals::game::shadowState; + ADDRESS_NODE(Renderer) + ADDRESS_NODE(BSShaderAccumulator) + ADDRESS_NODE(RendererShadowState) + ImGui::TreePop(); + } + + // Statistics section (moved from Advanced/Logging) + if (ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text(std::format("Shader Compiler : {}", shaderCache->GetShaderStatsString()).c_str()); + ImGui::TreePop(); + } + + // Frame annotations toggle (moved from Advanced/Logging) + ImGui::Checkbox("Frame Annotations", &globals::state->frameAnnotations); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Enable detailed frame annotations for debugging render passes and draw calls."); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + // Developer Mode Testing Section if (globals::state->IsDeveloperMode()) { FeatureIssues::Test::DrawDeveloperModeTestingUI(); + + ImGui::Spacing(); + // Test Conditions button - runs a set of console commands to prepare the player for testing + if (ImGui::Button("Test Conditions", { -1, 0 })) { + if (auto ui = RE::UI::GetSingleton(); ui && !ui->menuStack.empty() && RE::PlayerCharacter::GetSingleton()) { + RE::Console::ExecuteCommand("player.setav speedmult 1000"); + RE::Console::ExecuteCommand("tgm"); + RE::Console::ExecuteCommand("tcl"); + RE::Console::ExecuteCommand("set timescale to 0"); + RE::Console::ExecuteCommand("set gamehour to 12"); + RE::Console::ExecuteCommand("coc whiterun"); + RE::Console::ExecuteCommand("fw 81a"); + } + } } -} \ No newline at end of file +} diff --git a/src/Menu/AdvancedSettingsRenderer.h b/src/Menu/AdvancedSettingsRenderer.h index dbabd23b8a..7a1cefca2c 100644 --- a/src/Menu/AdvancedSettingsRenderer.h +++ b/src/Menu/AdvancedSettingsRenderer.h @@ -1,6 +1,7 @@ #pragma once #include +#include // Forward declaration class Menu; @@ -13,7 +14,9 @@ class AdvancedSettingsRenderer const std::function& drawDisableAtBootSettings); private: - static void RenderAdvancedSection(); - static void RenderShaderReplacementSection(); + static void RenderLoggingSection(); + static void RenderShaderDebugSection(); + static void RenderPBRSection(const std::function& drawTruePBRSettings); + static void RenderDisableAtBootSection(const std::function& drawDisableAtBootSettings); static void RenderDeveloperSection(); }; \ No newline at end of file diff --git a/src/Menu/BackgroundBlur.cpp b/src/Menu/BackgroundBlur.cpp new file mode 100644 index 0000000000..6becee2aec --- /dev/null +++ b/src/Menu/BackgroundBlur.cpp @@ -0,0 +1,631 @@ +// Inspired by Unrimp rendering engine's separable blur implementation +// Credits: Christian Ofenberg and the Unrimp project (https://github.com/cofenberg/unrimp) +// License: MIT License + +#include "BackgroundBlur.h" +#include "../Globals.h" +#include "../Util.h" + +#include +#include +#include +#include + +#include "RE/Skyrim.h" + +using namespace std::literals; + +// Blur intensity hardcoded. Super downscaled blur is very sensitive, this value looks best. +constexpr float BLUR_INTENSITY = 0.03f; + +// Downsampling factor (8 = eighth resolution for performance) +constexpr UINT DOWNSAMPLE_FACTOR = 8; + +namespace BackgroundBlur +{ + // Module-local state + namespace + { + std::mutex resourceMutex; + bool enabled = false; + + // DirectX resources (RAII managed) + winrt::com_ptr vertexShader; + winrt::com_ptr horizontalPixelShader; + winrt::com_ptr verticalPixelShader; + winrt::com_ptr constantBuffer; + winrt::com_ptr samplerState; + winrt::com_ptr blendState; + winrt::com_ptr scissorRasterizerState; + + // Downsampled textures for blur (quarter-res for performance) + winrt::com_ptr downsampleTexture; + winrt::com_ptr downsampleRTV; + winrt::com_ptr downsampleSRV; + + // Intermediate blur textures (at downsampled resolution) + winrt::com_ptr blurTexture1; + winrt::com_ptr blurTexture2; + winrt::com_ptr blurRTV1; + winrt::com_ptr blurRTV2; + winrt::com_ptr blurSRV1; + winrt::com_ptr blurSRV2; + + UINT textureWidth = 0; + UINT textureHeight = 0; + UINT downsampledWidth = 0; + UINT downsampledHeight = 0; + + bool initialized = false; + bool initializationFailed = false; + + // Blur shader constants structure + struct BlurConstants + { + float texelSize[4]; // x = 1/width, y = 1/height, z = blur strength, w = unused + int blurParams[4]; // x = samples, y = unused, z = unused, w = unused + }; + + } // anonymous namespace + + bool Initialize() + { + std::lock_guard lock(resourceMutex); + + if (initialized || initializationFailed) { + return initialized; + } + + auto device = globals::d3d::device; + if (!device) { + initializationFailed = true; + return false; + } + + // Compile vertex shader from horizontal blur file (both share same vertex shader) + vertexShader.attach(static_cast(Util::CompileShader(L"Data\\Shaders\\Menu\\BackgroundBlurHorizontal.hlsl", {}, "vs_5_0", "VS_Main"))); + if (!vertexShader) { + logger::error("Failed to compile blur vertex shader"); + initializationFailed = true; + return false; + } + + // Compile horizontal pixel shader + horizontalPixelShader.attach(static_cast(Util::CompileShader(L"Data\\Shaders\\Menu\\BackgroundBlurHorizontal.hlsl", {}, "ps_5_0", "PS_Main"))); + if (!horizontalPixelShader) { + logger::error("Failed to compile horizontal blur pixel shader"); + initializationFailed = true; + return false; + } + + // Compile vertical pixel shader + verticalPixelShader.attach(static_cast(Util::CompileShader(L"Data\\Shaders\\Menu\\BackgroundBlurVertical.hlsl", {}, "ps_5_0", "PS_Main"))); + if (!verticalPixelShader) { + logger::error("Failed to compile vertical blur pixel shader"); + initializationFailed = true; + return false; + } + + // Create constant buffer + D3D11_BUFFER_DESC bufferDesc = {}; + bufferDesc.Usage = D3D11_USAGE_DEFAULT; + bufferDesc.ByteWidth = sizeof(BlurConstants); + bufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + + HRESULT hr = device->CreateBuffer(&bufferDesc, nullptr, constantBuffer.put()); + if (FAILED(hr)) { + logger::error("Failed to create blur constant buffer"); + initializationFailed = true; + return false; + } + + // Create sampler state + D3D11_SAMPLER_DESC samplerDesc = {}; + samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.MaxAnisotropy = 1; + samplerDesc.MinLOD = 0; + samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; + + hr = device->CreateSamplerState(&samplerDesc, samplerState.put()); + if (FAILED(hr)) { + logger::error("Failed to create blur sampler state"); + initializationFailed = true; + return false; + } + + // Create blend state + D3D11_BLEND_DESC blendDesc = {}; + blendDesc.RenderTarget[0].BlendEnable = TRUE; + blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; + blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + + hr = device->CreateBlendState(&blendDesc, blendState.put()); + if (FAILED(hr)) { + logger::error("Failed to create blur blend state"); + initializationFailed = true; + return false; + } + + // Create scissor-enabled rasterizer state + D3D11_RASTERIZER_DESC rsDesc = {}; + rsDesc.FillMode = D3D11_FILL_SOLID; + rsDesc.CullMode = D3D11_CULL_BACK; + rsDesc.FrontCounterClockwise = FALSE; + rsDesc.DepthClipEnable = TRUE; + rsDesc.ScissorEnable = TRUE; + + hr = device->CreateRasterizerState(&rsDesc, scissorRasterizerState.put()); + if (FAILED(hr)) { + logger::error("Failed to create scissor rasterizer state"); + initializationFailed = true; + return false; + } + + initialized = true; + return true; + } + + void CreateBlurTextures(UINT width, UINT height, DXGI_FORMAT format) + { + std::lock_guard lock(resourceMutex); + + if (width == textureWidth && height == textureHeight && blurTexture1 && blurTexture2) { + return; + } + + auto device = globals::d3d::device; + if (!device) { + return; + } + + // Calculate downsampled dimensions + UINT dsWidth = (std::max)(1u, width / DOWNSAMPLE_FACTOR); + UINT dsHeight = (std::max)(1u, height / DOWNSAMPLE_FACTOR); + + // Release old textures + downsampleTexture = nullptr; + downsampleRTV = nullptr; + downsampleSRV = nullptr; + blurTexture1 = nullptr; + blurTexture2 = nullptr; + blurRTV1 = nullptr; + blurRTV2 = nullptr; + blurSRV1 = nullptr; + blurSRV2 = nullptr; + + // Create downsampled texture description + D3D11_TEXTURE2D_DESC texDesc = {}; + texDesc.Width = dsWidth; + texDesc.Height = dsHeight; + texDesc.MipLevels = 1; + texDesc.ArraySize = 1; + texDesc.Format = format; + texDesc.SampleDesc.Count = 1; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + + // Create downsample texture + HRESULT hr = device->CreateTexture2D(&texDesc, nullptr, downsampleTexture.put()); + if (FAILED(hr)) { + logger::error("Failed to create downsample texture"); + return; + } + + hr = device->CreateRenderTargetView(downsampleTexture.get(), nullptr, downsampleRTV.put()); + if (FAILED(hr)) { + logger::error("Failed to create downsample RTV"); + downsampleTexture = nullptr; + return; + } + + hr = device->CreateShaderResourceView(downsampleTexture.get(), nullptr, downsampleSRV.put()); + if (FAILED(hr)) { + logger::error("Failed to create downsample SRV"); + downsampleTexture = nullptr; + downsampleRTV = nullptr; + return; + } + + // Create first blur texture (at downsampled resolution) + hr = device->CreateTexture2D(&texDesc, nullptr, blurTexture1.put()); + if (FAILED(hr)) { + logger::error("Failed to create blur texture 1"); + downsampleTexture = nullptr; + downsampleRTV = nullptr; + downsampleSRV = nullptr; + return; + } + + // Create second blur texture + hr = device->CreateTexture2D(&texDesc, nullptr, blurTexture2.put()); + if (FAILED(hr)) { + logger::error("Failed to create blur texture 2"); + blurTexture1 = nullptr; + downsampleTexture = nullptr; + downsampleRTV = nullptr; + downsampleSRV = nullptr; + return; + } + + // Create render target views + hr = device->CreateRenderTargetView(blurTexture1.get(), nullptr, blurRTV1.put()); + if (FAILED(hr)) { + logger::error("Failed to create blur RTV 1"); + blurTexture1 = nullptr; + blurTexture2 = nullptr; + downsampleTexture = nullptr; + downsampleRTV = nullptr; + downsampleSRV = nullptr; + return; + } + + hr = device->CreateRenderTargetView(blurTexture2.get(), nullptr, blurRTV2.put()); + if (FAILED(hr)) { + logger::error("Failed to create blur RTV 2"); + blurTexture1 = nullptr; + blurTexture2 = nullptr; + blurRTV1 = nullptr; + downsampleTexture = nullptr; + downsampleRTV = nullptr; + downsampleSRV = nullptr; + return; + } + + // Create shader resource views + hr = device->CreateShaderResourceView(blurTexture1.get(), nullptr, blurSRV1.put()); + if (FAILED(hr)) { + logger::error("Failed to create blur SRV 1"); + blurTexture1 = nullptr; + blurTexture2 = nullptr; + blurRTV1 = nullptr; + blurRTV2 = nullptr; + downsampleTexture = nullptr; + downsampleRTV = nullptr; + downsampleSRV = nullptr; + return; + } + + hr = device->CreateShaderResourceView(blurTexture2.get(), nullptr, blurSRV2.put()); + if (FAILED(hr)) { + logger::error("Failed to create blur SRV 2"); + blurTexture1 = nullptr; + blurTexture2 = nullptr; + blurRTV1 = nullptr; + blurRTV2 = nullptr; + blurSRV1 = nullptr; + downsampleTexture = nullptr; + downsampleRTV = nullptr; + downsampleSRV = nullptr; + return; + } + + textureWidth = width; + textureHeight = height; + downsampledWidth = dsWidth; + downsampledHeight = dsHeight; + } + + void PerformBlur(ID3D11Texture2D* sourceTexture, ID3D11RenderTargetView* targetRTV, ImVec2 menuMin, ImVec2 menuMax) + { + std::lock_guard lock(resourceMutex); + + auto context = globals::d3d::context; + if (!context || !sourceTexture || !targetRTV) { + return; + } + + if (!vertexShader || !horizontalPixelShader || !verticalPixelShader) { + return; + } + + if (!blurTexture1 || !blurTexture2) { + return; + } + + // Get source texture description + D3D11_TEXTURE2D_DESC sourceDesc; + sourceTexture->GetDesc(&sourceDesc); + + // Create SRV for source + ID3D11ShaderResourceView* sourceSRV = nullptr; + HRESULT hr = globals::d3d::device->CreateShaderResourceView(sourceTexture, nullptr, &sourceSRV); + if (FAILED(hr)) { + logger::error("Failed to create source SRV for blur"); + return; + } + + // Save current state + ID3D11RenderTargetView* originalRTV = nullptr; + ID3D11DepthStencilView* originalDSV = nullptr; + context->OMGetRenderTargets(1, &originalRTV, &originalDSV); + + D3D11_VIEWPORT originalViewport; + UINT numViewports = 1; + context->RSGetViewports(&numViewports, &originalViewport); + + ID3D11RasterizerState* originalRS = nullptr; + context->RSGetState(&originalRS); + + // Downsample source to quarter resolution with bilinear filtering + D3D11_VIEWPORT downsampleViewport = {}; + downsampleViewport.Width = static_cast(downsampledWidth); + downsampleViewport.Height = static_cast(downsampledHeight); + downsampleViewport.MinDepth = 0.0f; + downsampleViewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &downsampleViewport); + + auto downsampleRTVPtr = downsampleRTV.get(); + context->OMSetRenderTargets(1, &downsampleRTVPtr, nullptr); + + auto constantBufferPtr = constantBuffer.get(); + auto samplerStatePtr = samplerState.get(); + context->VSSetShader(vertexShader.get(), nullptr, 0); + context->PSSetSamplers(0, 1, &samplerStatePtr); + + // Simple copy to downsample (bilinear filtering does the work) + BlurConstants downsampleConstants = {}; + downsampleConstants.texelSize[0] = 1.0f / static_cast(sourceDesc.Width); + downsampleConstants.texelSize[1] = 1.0f / static_cast(sourceDesc.Height); + downsampleConstants.texelSize[2] = 0.0f; + downsampleConstants.texelSize[3] = 0.0f; + downsampleConstants.blurParams[0] = 1; // Single sample for downsample + context->UpdateSubresource(constantBuffer.get(), 0, nullptr, &downsampleConstants, 0, 0); + + context->PSSetConstantBuffers(0, 1, &constantBufferPtr); + context->PSSetShader(horizontalPixelShader.get(), nullptr, 0); + context->PSSetShaderResources(0, 1, &sourceSRV); + context->Draw(3, 0); + + ID3D11ShaderResourceView* nullSRV = nullptr; + context->PSSetShaderResources(0, 1, &nullSRV); + + // Calculate blur parameters at eighth resolution + float blurRadius = BLUR_INTENSITY * 10.0f; + int sampleCount = 9; + + BlurConstants constants = {}; + constants.texelSize[0] = blurRadius / static_cast(downsampledWidth); + constants.texelSize[1] = blurRadius / static_cast(downsampledHeight); + constants.texelSize[2] = BLUR_INTENSITY; + constants.texelSize[3] = 0.0f; + constants.blurParams[0] = sampleCount; + constants.blurParams[1] = 0; + constants.blurParams[2] = 0; + constants.blurParams[3] = 0; + + context->UpdateSubresource(constantBuffer.get(), 0, nullptr, &constants, 0, 0); + + // Set up viewport for blur (quarter resolution) + D3D11_VIEWPORT blurViewport = {}; + blurViewport.Width = static_cast(downsampledWidth); + blurViewport.Height = static_cast(downsampledHeight); + blurViewport.MinDepth = 0.0f; + blurViewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &blurViewport); + + context->PSSetConstantBuffers(0, 1, &constantBufferPtr); + + // First pass: Horizontal blur (on downsampled texture) + auto rtv1Ptr = blurRTV1.get(); + auto downsampleSRVPtr = downsampleSRV.get(); + context->OMSetRenderTargets(1, &rtv1Ptr, nullptr); + context->PSSetShader(horizontalPixelShader.get(), nullptr, 0); + context->PSSetShaderResources(0, 1, &downsampleSRVPtr); + context->Draw(3, 0); + + // Second pass: Vertical blur (on downsampled texture) + context->PSSetShaderResources(0, 1, &nullSRV); + auto rtv2Ptr = blurRTV2.get(); + auto srv1Ptr = blurSRV1.get(); + context->OMSetRenderTargets(1, &rtv2Ptr, nullptr); + context->PSSetShader(verticalPixelShader.get(), nullptr, 0); + context->PSSetShaderResources(0, 1, &srv1Ptr); + context->Draw(3, 0); + context->PSSetShaderResources(0, 1, &nullSRV); + + // Final composition: upscale from quarter-res with scissor test + // Bilinear sampler smooths the upscale automatically + context->RSSetViewports(1, &originalViewport); + + D3D11_RECT scissorRect; + scissorRect.left = static_cast((std::max)(0.0f, menuMin.x)); + scissorRect.top = static_cast((std::max)(0.0f, menuMin.y)); + scissorRect.right = static_cast((std::min)(static_cast(sourceDesc.Width), menuMax.x)); + scissorRect.bottom = static_cast((std::min)(static_cast(sourceDesc.Height), menuMax.y)); + + context->RSSetState(scissorRasterizerState.get()); + context->RSSetScissorRects(1, &scissorRect); + + context->OMSetRenderTargets(1, &targetRTV, nullptr); + float blendFactor[4] = { 1.0f, 1.0f, 1.0f, BLUR_INTENSITY * 0.8f }; + context->OMSetBlendState(blendState.get(), blendFactor, 0xFFFFFFFF); + + // Use blurred quarter-res texture, bilinear filtering upscales smoothly + auto srv2Ptr = blurSRV2.get(); + context->PSSetShaderResources(0, 1, &srv2Ptr); + context->Draw(3, 0); + context->PSSetShaderResources(0, 1, &nullSRV); + + // Restore state + context->OMSetRenderTargets(1, &originalRTV, originalDSV); + context->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); + context->PSSetShaderResources(0, 1, &nullSRV); + context->RSSetState(originalRS); + context->RSSetScissorRects(0, nullptr); + + // Cleanup + if (sourceSRV) + sourceSRV->Release(); + if (originalRTV) + originalRTV->Release(); + if (originalDSV) + originalDSV->Release(); + if (originalRS) + originalRS->Release(); + } + + void Cleanup() + { + std::lock_guard lock(resourceMutex); + + vertexShader = nullptr; + horizontalPixelShader = nullptr; + verticalPixelShader = nullptr; + constantBuffer = nullptr; + samplerState = nullptr; + blendState = nullptr; + scissorRasterizerState = nullptr; + + downsampleTexture = nullptr; + downsampleRTV = nullptr; + downsampleSRV = nullptr; + + blurTexture1 = nullptr; + blurTexture2 = nullptr; + blurRTV1 = nullptr; + blurRTV2 = nullptr; + blurSRV1 = nullptr; + blurSRV2 = nullptr; + + textureWidth = 0; + textureHeight = 0; + downsampledWidth = 0; + downsampledHeight = 0; + enabled = false; + initialized = false; + initializationFailed = false; + } + + void SetEnabled(bool enable) + { + enabled = enable; + } + + bool GetEnabled() + { + return enabled; + } + + bool IsEnabled() + { + return enabled && initialized; + } + + void GetTextureDimensions(UINT& outWidth, UINT& outHeight) + { + std::lock_guard lock(resourceMutex); + outWidth = textureWidth; + outHeight = textureHeight; + } + + void RenderBackgroundBlur() + { + if (!enabled) { + return; + } + + if (!initialized || initializationFailed) { + return; + } + + auto device = globals::d3d::device; + auto context = globals::d3d::context; + if (!device || !context) { + return; + } + + // Get current render target + ID3D11RenderTargetView* currentRTV = nullptr; + context->OMGetRenderTargets(1, ¤tRTV, nullptr); + + if (!currentRTV) { + return; + } + + // Get render target texture and its dimensions + ID3D11Resource* currentRT = nullptr; + currentRTV->GetResource(¤tRT); + + ID3D11Texture2D* currentTexture = nullptr; + HRESULT hr = currentRT->QueryInterface(__uuidof(ID3D11Texture2D), (void**)¤tTexture); + + if (FAILED(hr) || !currentTexture) { + if (currentRT) + currentRT->Release(); + if (currentRTV) + currentRTV->Release(); + return; + } + + D3D11_TEXTURE2D_DESC texDesc; + currentTexture->GetDesc(&texDesc); + + // Create blur textures if needed + UINT currentWidth, currentHeight; + GetTextureDimensions(currentWidth, currentHeight); + if (currentWidth != texDesc.Width || currentHeight != texDesc.Height) { + CreateBlurTextures(texDesc.Width, texDesc.Height, texDesc.Format); + } + + // Find ImGui windows that need blur + ImGuiContext* ctx = ImGui::GetCurrentContext(); + if (!ctx || ctx->Windows.Size == 0) { + currentTexture->Release(); + currentRT->Release(); + currentRTV->Release(); + return; + } + + // Apply blur behind each visible ImGui window + for (int i = 0; i < ctx->Windows.Size; i++) { + ImGuiWindow* window = ctx->Windows[i]; + if (!window || window->Hidden || !window->WasActive || window->SkipItems) { + continue; + } + + // Skip child windows - only blur root windows to cover headers and footers + if (window->ParentWindow != nullptr) { + continue; + } + + // Skip tooltip windows + if (window->Flags & ImGuiWindowFlags_Tooltip) { + continue; + } + + // Skip Performance Overlay window (no blur) + if (window->Name && std::string_view(window->Name) == "Performance Overlay") { + continue; + } + + // Skip if window has no background (fully transparent) + if (window->Flags & ImGuiWindowFlags_NoBackground) { + continue; + } + + // Get window outer bounds (includes title bar, borders, etc.) + // Use window's inner rect which includes all content drawn inside the window + // including custom headers and footers, not just OuterRectClipped + ImRect windowRect = window->Rect(); + ImVec2 windowMin = windowRect.Min; + ImVec2 windowMax = windowRect.Max; + + // Perform blur for this window area + PerformBlur(currentTexture, currentRTV, windowMin, windowMax); + } + + // Cleanup + currentTexture->Release(); + currentRT->Release(); + currentRTV->Release(); + } + +} // namespace BackgroundBlur diff --git a/src/Menu/BackgroundBlur.h b/src/Menu/BackgroundBlur.h new file mode 100644 index 0000000000..c4891a904c --- /dev/null +++ b/src/Menu/BackgroundBlur.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +struct ImVec2; + +namespace BackgroundBlur +{ + /** + * @brief Initializes blur shaders and GPU resources + * @return True if initialization succeeded + */ + bool Initialize(); + + /** + * @brief Renders background blur behind all visible ImGui windows + * This is the main entry point - call after ImGui::Render() but before ImGui_ImplDX11_RenderDrawData() + */ + void RenderBackgroundBlur(); + + /** + * @brief Creates or recreates blur textures with specified dimensions + * @param width Texture width in pixels + * @param height Texture height in pixels + * @param format Texture format + */ + void CreateBlurTextures(UINT width, UINT height, DXGI_FORMAT format); + + /** + * @brief Performs two-pass Gaussian blur on source texture + * @param sourceTexture Input texture to blur + * @param targetRTV Output render target + * @param menuMin Top-left corner of menu area (for scissor test) + * @param menuMax Bottom-right corner of menu area (for scissor test) + */ + void PerformBlur(ID3D11Texture2D* sourceTexture, ID3D11RenderTargetView* targetRTV, ImVec2 menuMin, ImVec2 menuMax); + + /** + * @brief Cleans up all blur resources + */ + void Cleanup(); + + void SetEnabled(bool enable); + bool GetEnabled(); + + /** + * @brief Checks if blur is enabled + * @return True if blur intensity > 0 + */ + bool IsEnabled(); + + /** + * @brief Gets current blur texture dimensions + * @param outWidth Output width + * @param outHeight Output height + */ + void GetTextureDimensions(UINT& outWidth, UINT& outHeight); + +} // namespace BackgroundBlur diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 7923d1b9ae..0c460dce2f 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -8,6 +8,7 @@ #include "Feature.h" #include "FeatureIssues.h" +#include "Fonts.h" #include "Globals.h" #include "Menu.h" #include "Menu/HomePageRenderer.h" @@ -25,6 +26,22 @@ namespace { return std::find(CORE_MENU_NAMES.begin(), CORE_MENU_NAMES.end(), menuName) != CORE_MENU_NAMES.end(); } + + void SeparatorTextWithFont(const char* text, Menu::FontRole role) + { + MenuFonts::FontRoleGuard guard(role); + ImGui::SeparatorText(text); + } + + void SeparatorTextWithFont(const std::string& text, Menu::FontRole role) + { + SeparatorTextWithFont(text.c_str(), role); + } + + bool BeginTabItemWithFont(const char* label, Menu::FontRole role, ImGuiTabItemFlags flags = ImGuiTabItemFlags_None) + { + return MenuFonts::BeginTabItemWithFont(label, role, flags); + } } void FeatureListRenderer::RenderFeatureList( @@ -246,18 +263,21 @@ void FeatureListRenderer::RenderRightColumn( void FeatureListRenderer::ListMenuVisitor::operator()(const BuiltInMenu& menu) { + MenuFonts::FontRoleGuard fontGuard(Menu::FontRole::Subheading); + // Use error color for Feature Issues menu item bool isFeatureIssues = (menu.name == "Feature Issues"); if (isFeatureIssues) { auto& themeSettings = globals::menu->GetSettings().Theme; ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.Error); - } - if (ImGui::Selectable(fmt::format(" {} ", menu.name).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) - selectedMenuRef = listId; + if (ImGui::Selectable(fmt::format(" {} ", menu.name).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) + selectedMenuRef = listId; - if (isFeatureIssues) { ImGui::PopStyleColor(); + } else { + if (ImGui::Selectable(fmt::format(" {} ", menu.name).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) + selectedMenuRef = listId; } } @@ -267,8 +287,8 @@ void FeatureListRenderer::ListMenuVisitor::operator()(const std::string& label) if (label == "Unloaded Features") { Util::DrawSectionHeader(label.c_str(), true); } else { - // Use default separator text for other labels - ImGui::SeparatorText(label.c_str()); + // Use default separator text for other labels - should be themed via ImGuiCol_Separator + SeparatorTextWithFont(label, Menu::FontRole::Subheading); } } @@ -278,8 +298,12 @@ void FeatureListRenderer::ListMenuVisitor::operator()(const CategoryHeader& head bool isExpanded = categoryExpansionStates[header.name]; // Draw category header with custom styling using util:UI function - int count = Menu::categoryCounts[std::string(header.name)]; - Util::DrawCategoryHeader(header.name.c_str(), isExpanded, count); + // Use Heading font for category headers + { + MenuFonts::FontRoleGuard fontGuard(Menu::FontRole::Heading); + int count = Menu::categoryCounts[std::string(header.name)]; + Util::DrawCategoryHeader(header.name.c_str(), isExpanded, count); + } // Update expansion state categoryExpansionStates[header.name] = isExpanded; @@ -287,6 +311,8 @@ void FeatureListRenderer::ListMenuVisitor::operator()(const CategoryHeader& head void FeatureListRenderer::ListMenuVisitor::operator()(Feature* feat) { + MenuFonts::FontRoleGuard fontGuard(Menu::FontRole::Subheading); + const auto featureName = feat->GetShortName(); bool isDisabled = globals::state->IsFeatureDisabled(featureName); bool isLoaded = feat->loaded; @@ -313,15 +339,11 @@ void FeatureListRenderer::ListMenuVisitor::operator()(Feature* feat) } } - // Set text color + // Create selectable item with semantic color ImGui::PushStyleColor(ImGuiCol_Text, textColor); - - // Create selectable item if (ImGui::Selectable(fmt::format(" {} ", feat->GetName()).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) { selectedMenuRef = listId; } - - // Restore original text color ImGui::PopStyleColor(); // Display version if loaded @@ -363,6 +385,7 @@ void FeatureListRenderer::DrawMenuVisitor::operator()(Feature* feat) float buttonPadding = ThemeManager::Constants::BUTTON_PADDING; float buttonSpacing = ThemeManager::Constants::BUTTON_SPACING; + MenuFonts::TabBarPaddingGuard tabPaddingGuard(Menu::FontRole::Subheading); if (ImGui::BeginTabBar("##FeatureTabs", ImGuiTabBarFlags_Reorderable)) { // Render Settings and About tabs RenderFeatureSettingsTab(feat, isDisabled, isLoaded, hasFailedMessage); @@ -381,142 +404,164 @@ bool FeatureListRenderer::DrawMenuVisitor::IsFeatureInstalled(const std::string& void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettingsTab(Feature* feat, bool isDisabled, bool isLoaded, bool hasFailedMessage) { - if (ImGui::BeginTabItem("Settings")) { - if (ImGui::BeginChild("##FeatureSettingsFrame", { 0, 0 }, true)) { - auto& themeSettings = globals::menu->GetSettings().Theme; - - // Feature-specific settings section - ImGui::SeparatorText("Feature Settings"); - if (isDisabled) { - // Show disabled message - ImGui::TextColored(themeSettings.StatusPalette.Disable, "Feature settings are hidden because this feature is disabled at boot."); - ImGui::Spacing(); - ImGui::Text("Enable the feature above to access its configuration options."); + if (!BeginTabItemWithFont("Settings", Menu::FontRole::Subheading)) { + return; + } + + if (ImGui::BeginChild("##FeatureSettingsFrame", { 0, 0 }, true)) { + auto& themeSettings = globals::menu->GetSettings().Theme; + + SeparatorTextWithFont("Feature Settings", Menu::FontRole::Subheading); + if (isDisabled) { + ImGui::TextColored(themeSettings.StatusPalette.Disable, "Feature settings are hidden because this feature is disabled at boot."); + ImGui::Spacing(); + ImGui::Text("Enable the feature above to access its configuration options."); + } else { + if (isLoaded) { + ImVec2 cursorPosBefore = ImGui::GetCursorPos(); + feat->DrawSettings(); + ImVec2 cursorPosAfter = ImGui::GetCursorPos(); + + const float epsilon = 0.1f; + bool cursorMoved = (std::abs(cursorPosAfter.x - cursorPosBefore.x) > epsilon || + std::abs(cursorPosAfter.y - cursorPosBefore.y) > epsilon); + if (!cursorMoved) { + ImGui::TextColored(themeSettings.StatusPalette.Disable, "There are no settings available for this feature."); + } } else { - if (isLoaded) { - // Check if the feature has any settings by monitoring cursor position - ImVec2 cursorPosBefore = ImGui::GetCursorPos(); - feat->DrawSettings(); - ImVec2 cursorPosAfter = ImGui::GetCursorPos(); - - // If cursor position hasn't changed significantly, no visible settings were drawn - const float epsilon = 0.1f; - bool cursorMoved = (std::abs(cursorPosAfter.x - cursorPosBefore.x) > epsilon || - std::abs(cursorPosAfter.y - cursorPosBefore.y) > epsilon); - if (!cursorMoved) { - ImGui::TextColored(themeSettings.StatusPalette.Disable, "There are no settings available for this feature."); - } + if (FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { + feat->DrawUnloadedUI(); + } else if (IsFeatureInstalled(feat->GetShortName())) { + ImGui::Text("This feature will be available after restart."); } else { - // Check if feature is obsolete first - always show error for obsolete features - if (FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { - // Obsolete feature - show detailed unloaded UI with error info - feat->DrawUnloadedUI(); - } else if (IsFeatureInstalled(feat->GetShortName())) { - // INI file exists - show simple pending restart message - ImGui::Text("This feature will be available after restart."); - } else { - // INI file missing - show detailed unloaded UI with installation info - feat->DrawUnloadedUI(); - // Add download link if available - if (!feat->GetFeatureModLink().empty()) { - ImGui::Spacing(); - const auto downloadText = fmt::format("Click here to download this feature ({})", feat->GetFeatureModLink()); - if (ImGui::Selectable(downloadText.c_str())) { - ShellExecuteA(NULL, "open", feat->GetFeatureModLink().c_str(), NULL, NULL, SW_SHOWNORMAL); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Download the feature from the mod page."); - } + feat->DrawUnloadedUI(); + if (!feat->GetFeatureModLink().empty()) { + ImGui::Spacing(); + const auto downloadText = fmt::format("Click here to download this feature ({})", feat->GetFeatureModLink()); + if (ImGui::Selectable(downloadText.c_str())) { + ShellExecuteA(NULL, "open", feat->GetFeatureModLink().c_str(), NULL, NULL, SW_SHOWNORMAL); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Download the feature from the mod page."); } } } } + } - // Error Messages (Not for obsolete features as this is already covered by DrawUnloadedUI) - if (hasFailedMessage && feat->DrawFailLoadMessage() && !FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { - ImGui::Spacing(); - ImGui::SeparatorText("Error"); - ImGui::TextColored(themeSettings.StatusPalette.Error, feat->failedLoadedMessage.c_str()); + if (hasFailedMessage && feat->DrawFailLoadMessage() && !FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { + ImGui::Spacing(); + SeparatorTextWithFont("Error", Menu::FontRole::Subheading); + ImGui::TextColored(themeSettings.StatusPalette.Error, feat->failedLoadedMessage.c_str()); + } + + if (!isDisabled && isLoaded) { + // Position button in screen coordinates so it stays fixed in viewport when scrolling + ImVec2 windowPos = ImGui::GetWindowPos(); + ImVec2 windowSize = ImGui::GetWindowSize(); + float scrollbarWidth = ImGui::GetScrollMaxY() > 0 ? ImGui::GetStyle().ScrollbarSize : 0.0f; + + float iconDimension = ImGui::GetFrameHeight() * 1.2f; + ImVec2 iconSize = ImVec2(iconDimension, iconDimension); + + float padding = 10.0f; + ImVec2 buttonPos = ImVec2( + windowPos.x + windowSize.x - iconSize.x - padding - scrollbarWidth, + windowPos.y + windowSize.y - iconSize.y - padding); + ImGui::SetCursorScreenPos(buttonPos); + auto& theme = globals::menu->GetTheme().Palette; + ImVec4 iconColor = theme.Text; + iconColor.w *= 0.7f; + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 1.0f, 1.0f, 0.3f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 1.0f, 1.0f, 0.5f)); + + auto& menu = *globals::menu; + if (menu.uiIcons.featureSettingRevert.texture) { + if (ImGui::ImageButton("##RestoreDefaults", menu.uiIcons.featureSettingRevert.texture, iconSize)) { + feat->RestoreDefaultSettings(); + } + } else { + if (ImGui::Button("R##RestoreDefaults", iconSize)) { + feat->RestoreDefaultSettings(); + } + } + + ImGui::PopStyleColor(3); + + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Restore default settings for this feature"); } } - ImGui::EndChild(); - ImGui::EndTabItem(); } + ImGui::EndChild(); + ImGui::EndTabItem(); } void FeatureListRenderer::DrawMenuVisitor::RenderFeatureAboutTab(Feature* feat, bool isDisabled, bool isLoaded, bool hasFailedMessage) { - if (ImGui::BeginTabItem("About")) { - if (ImGui::BeginChild("##FeatureAboutFrame", { 0, 0 }, true)) { - auto& themeSettings = globals::menu->GetSettings().Theme; - - // Status Section - ImGui::SeparatorText("Status"); - - ImVec4 statusColor; - const char* statusText; - if (isDisabled) { - statusColor = themeSettings.StatusPalette.Disable; - statusText = "Disabled at boot."; - } else if (hasFailedMessage) { + if (!BeginTabItemWithFont("About", Menu::FontRole::Subheading)) { + return; + } + + if (ImGui::BeginChild("##FeatureAboutFrame", { 0, 0 }, true)) { + auto& themeSettings = globals::menu->GetSettings().Theme; + + SeparatorTextWithFont("Status", Menu::FontRole::Subheading); + + ImVec4 statusColor; + const char* statusText; + if (isDisabled) { + statusColor = themeSettings.StatusPalette.Disable; + statusText = "Disabled at boot."; + } else if (hasFailedMessage) { + statusColor = themeSettings.StatusPalette.Error; + statusText = "Failed to load."; + } else if (!isLoaded) { + if (!IsFeatureInstalled(feat->GetShortName())) { statusColor = themeSettings.StatusPalette.Error; - statusText = "Failed to load."; - } else if (!isLoaded) { - // Check if INI file exists to determine actual status - if (!IsFeatureInstalled(feat->GetShortName())) { - // INI file missing - feature not installed - statusColor = themeSettings.StatusPalette.Error; - statusText = "Not installed."; - } else { - // INI file exists but feature not loaded - truly pending restart - statusColor = themeSettings.StatusPalette.RestartNeeded; - statusText = "Pending restart."; - } + statusText = "Not installed."; } else { - statusColor = themeSettings.StatusPalette.SuccessColor; - statusText = "Active."; + statusColor = themeSettings.StatusPalette.RestartNeeded; + statusText = "Pending restart."; } + } else { + statusColor = themeSettings.StatusPalette.SuccessColor; + statusText = "Active."; + } - ImGui::TextColored(statusColor, "Current State: %s", statusText); + ImGui::TextColored(statusColor, "Current State: %s", statusText); - // Feature Info - Description and key features - if (isLoaded) { - auto [description, keyFeatures] = feat->GetFeatureSummary(); - if (!description.empty()) { - ImGui::Spacing(); - ImGui::SeparatorText("Description"); - ImGui::TextWrapped("%s", description.c_str()); + if (isLoaded) { + auto [description, keyFeatures] = feat->GetFeatureSummary(); + if (!description.empty()) { + ImGui::Spacing(); + SeparatorTextWithFont("Description", Menu::FontRole::Subheading); + ImGui::TextWrapped("%s", description.c_str()); - if (!keyFeatures.empty()) { - ImGui::Spacing(); - ImGui::SeparatorText("Key Features"); - for (const auto& feature : keyFeatures) { - ImGui::BulletText("%s", feature.c_str()); - } + if (!keyFeatures.empty()) { + ImGui::Spacing(); + SeparatorTextWithFont("Key Features", Menu::FontRole::Subheading); + for (const auto& feature : keyFeatures) { + ImGui::BulletText("%s", feature.c_str()); } } + } + } else { + ImGui::Spacing(); + SeparatorTextWithFont("Information", Menu::FontRole::Subheading); + if (hasFailedMessage) { + ImGui::TextColored(themeSettings.StatusPalette.Error, "%s", feat->failedLoadedMessage.c_str()); + } else if (!IsFeatureInstalled(feat->GetShortName())) { + ImGui::Text("Feature installation details are available in the Settings tab."); } else { - // For unloaded features, show basic info if available - ImGui::Spacing(); - ImGui::SeparatorText("Information"); - if (hasFailedMessage) { - ImGui::TextColored(themeSettings.StatusPalette.Error, "%s", feat->failedLoadedMessage.c_str()); - } else { - // For features that are pending restart or not installed, - // the detailed information is shown in the Settings tab. - // Here we just show a simple message directing users there. - if (!IsFeatureInstalled(feat->GetShortName())) { - ImGui::Text("Feature installation details are available in the Settings tab."); - } else { - // INI file exists but feature not loaded - truly pending restart - ImGui::Text("This feature is pending restart."); - } - } + ImGui::Text("This feature is pending restart."); } } - ImGui::EndChild(); - ImGui::EndTabItem(); } + ImGui::EndChild(); + ImGui::EndTabItem(); } void FeatureListRenderer::DrawMenuVisitor::RenderFeatureActionButtons(Feature* feat, bool isDisabled, bool isLoaded, float buttonPadding, float buttonSpacing) @@ -525,24 +570,19 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureActionButtons(Feature* f const auto featureName = feat->GetShortName(); // Calculate button widths based on text content - const char* bootButtonText = isDisabled ? "Enable at Boot" : "Disable at Boot"; - const char* defaultsButtonText = "Restore Defaults"; const char* overrideButtonText = "Apply Override"; - float bootButtonWidth = ImGui::CalcTextSize(bootButtonText).x + buttonPadding; - float defaultsButtonWidth = ImGui::CalcTextSize(defaultsButtonText).x + buttonPadding; + // Toggle is more compact without label - just the toggle width + float bootToggleWidth = ImGui::GetFrameHeight() * 1.6f; float overrideButtonWidth = ImGui::CalcTextSize(overrideButtonText).x + buttonPadding; // Check if override is available for this feature auto overrideManager = SettingsOverrideManager::GetSingleton(); bool hasOverrides = overrideManager && overrideManager->HasFeatureOverrides(featureName); - float totalButtonWidth = bootButtonWidth; - if (!isDisabled && isLoaded) { - totalButtonWidth += defaultsButtonWidth + buttonSpacing; - if (hasOverrides) { - totalButtonWidth += overrideButtonWidth + buttonSpacing; - } + float totalButtonWidth = bootToggleWidth; + if (!isDisabled && isLoaded && hasOverrides) { + totalButtonWidth += overrideButtonWidth + buttonSpacing; } // Position buttons on the right side of the tab bar @@ -553,64 +593,48 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureActionButtons(Feature* f ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rightOffset); } - // Disable/Enable at boot button - ImVec4 textColor; - if (isDisabled) { - textColor = themeSettings.StatusPalette.Disable; - } else if (!feat->failedLoadedMessage.empty()) { - textColor = themeSettings.StatusPalette.Error; - } else { - textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); + // Enable/Disable at boot toggle + bool bootEnabled = !isDisabled; + + // Apply disabled styling if feature has failed to load + if (!feat->failedLoadedMessage.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.Error); } - ImGui::PushStyleColor(ImGuiCol_Text, textColor); - if (ImGui::Button(bootButtonText, { bootButtonWidth, 0 })) { + if (Util::FeatureToggle("##BootToggle", &bootEnabled)) { bool newState = feat->ToggleAtBootSetting(); logger::info("{}: {} at boot.", featureName, newState ? "Enabled" : "Disabled"); } - ImGui::PopStyleColor(); + + if (!feat->failedLoadedMessage.empty()) { + ImGui::PopStyleColor(); + } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( - "Current State: %s\n" - "%s the feature settings at boot. " - "Restart will be required to reenable. " - "This is the same as deleting the ini file. " - "This should remove any performance impact for the feature.", - isDisabled ? "Disabled" : "Enabled", - isDisabled ? "Enable" : "Disable"); + "Toggle feature loading at boot.\n" + "Current state: %s\n" + "Restart required for changes to take effect.\n" + "Disabling removes performance impact.", + bootEnabled ? "Enabled" : "Disabled"); } - // Restore Defaults button (when feature is not disabled and is loaded) - if (!isDisabled && isLoaded) { + // Apply Override button (when feature has available overrides) + if (!isDisabled && isLoaded && hasOverrides) { ImGui::SameLine(); - if (ImGui::Button(defaultsButtonText, { defaultsButtonWidth, 0 })) { - feat->RestoreDefaultSettings(); + if (ImGui::Button(overrideButtonText, { overrideButtonWidth, 0 })) { + if (feat->ReapplyOverrideSettings()) { + logger::info("Successfully reapplied override settings for {}", featureName); + } else { + logger::warn("Failed to reapply override settings for {}", featureName); + } } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( - "Restores the feature's settings back to their default values. " + "Reapplies override settings from mod override JSON files. " + "This will overwrite current settings with override values. " "You will still need to Save Settings to make these changes permanent."); } - - // Apply Override button (when feature has available overrides) - if (hasOverrides) { - ImGui::SameLine(); - if (ImGui::Button(overrideButtonText, { overrideButtonWidth, 0 })) { - if (feat->ReapplyOverrideSettings()) { - logger::info("Successfully reapplied override settings for {}", featureName); - } else { - logger::warn("Failed to reapply override settings for {}", featureName); - } - } - - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Reapplies override settings from mod override JSON files. " - "This will overwrite current settings with override values. " - "You will still need to Save Settings to make these changes permanent."); - } - } } } \ No newline at end of file diff --git a/src/Menu/Fonts.cpp b/src/Menu/Fonts.cpp new file mode 100644 index 0000000000..32ce72047d --- /dev/null +++ b/src/Menu/Fonts.cpp @@ -0,0 +1,661 @@ +#include "Fonts.h" + +#include "../Globals.h" +#include "../Utils/FileSystem.h" +#include "ThemeManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MenuFonts +{ + namespace + { + constexpr size_t RoleIndex(Menu::FontRole role) + { + return static_cast(role); + } + + const Menu::ThemeSettings::FontRoleSettings& GetDefaultRoleInternal(Menu::FontRole role) + { + static const Menu::ThemeSettings defaults{}; + return defaults.FontRoles[RoleIndex(role)]; + } + + std::string NormalizeFontFilePath(const std::string& path) + { + if (path.empty()) { + return {}; + } + std::filesystem::path asPath(path); + auto normalized = asPath.generic_string(); + while (!normalized.empty() && (normalized.front() == '/' || normalized.front() == '\\')) { + normalized.erase(normalized.begin()); + } + return normalized; + } + + void DeriveFamilyAndStyle(FontRoleSettings& role) + { + if (role.File.empty()) { + return; + } + std::filesystem::path relative(role.File); + if (role.Family.empty()) { + auto parent = relative.parent_path(); + if (!parent.empty()) { + role.Family = parent.begin()->string(); + } else { + auto stem = relative.stem().string(); + auto split = stem.find_first_of("-_ "); + role.Family = split != std::string::npos ? stem.substr(0, split) : stem; + } + } + if (role.Style.empty()) { + auto stem = relative.stem().string(); + auto split = stem.find_first_of("-_ "); + if (split != std::string::npos && split + 1 < stem.size()) { + role.Style = stem.substr(split + 1); + } else { + role.Style = "Regular"; + } + } + } + + void ApplyRoleDefaults(FontRoleSettings& target, Menu::FontRole role) + { + const auto& defaults = GetDefaultRoleInternal(role); + if (target.File.empty()) { + target.File = defaults.File; + } + if (target.SizeScale <= 0.f) { + target.SizeScale = defaults.SizeScale; + } + if (target.Family.empty()) { + target.Family = defaults.Family; + } + if (target.Style.empty()) { + target.Style = defaults.Style; + } + } + } + + void NormalizeFontRoles(Menu::ThemeSettings& theme, bool themeProvidedFontRoles) + { + if (!themeProvidedFontRoles && !theme.FontName.empty()) { + theme.FontRoles[RoleIndex(Menu::FontRole::Body)].File = NormalizeFontFilePath(theme.FontName); + } + + for (size_t i = 0; i < static_cast(Menu::FontRole::Count); ++i) { + Menu::FontRole role = static_cast(i); + auto& settings = theme.FontRoles[i]; + settings.File = NormalizeFontFilePath(settings.File); + ApplyRoleDefaults(settings, role); + DeriveFamilyAndStyle(settings); + settings.SizeScale = std::clamp(settings.SizeScale, 0.1f, 4.0f); + } + + if (theme.FontRoles[RoleIndex(Menu::FontRole::Body)].File.empty()) { + ApplyRoleDefaults(theme.FontRoles[RoleIndex(Menu::FontRole::Body)], Menu::FontRole::Body); + } + + if (!theme.FontName.empty()) { + theme.FontName = NormalizeFontFilePath(theme.FontName); + } + + if (theme.FontName.empty()) { + theme.FontName = theme.FontRoles[RoleIndex(Menu::FontRole::Body)].File; + } + } + + const FontRoleSettings& GetDefaultRole(Menu::FontRole role) + { + return GetDefaultRoleInternal(role); + } + + FontRoleGuard::FontRoleGuard(FontRole role) + { + Menu* menuInstance = globals::menu; + if (!menuInstance) { + menuInstance = Menu::GetSingleton(); + } + if (menuInstance) { + font_ = menuInstance->GetFont(role); + if (font_) { + ImGui::PushFont(font_); + } + } + } + + FontRoleGuard::~FontRoleGuard() + { + if (font_) { + ImGui::PopFont(); + } + } + + TabBarPaddingGuard::TabBarPaddingGuard(FontRole tabFontRole) + { + // Get the font that will be used for tabs + ImFont* tabFont = globals::menu->GetFont(tabFontRole); + ImFont* bodyFont = globals::menu->GetFont(FontRole::Body); + + if (tabFont && bodyFont) { + float fontScale = tabFont->FontSize / bodyFont->FontSize; + + // Only scale if the tab font is noticeably larger + if (fontScale > 1.05f) { + ImGuiStyle& style = ImGui::GetStyle(); + originalPadding_ = style.FramePadding; + + // Scale padding proportionally to font size + style.FramePadding.x *= fontScale; + style.FramePadding.y *= fontScale; + + scaled_ = true; + } + } + } + + TabBarPaddingGuard::~TabBarPaddingGuard() + { + if (scaled_) { + ImGuiStyle& style = ImGui::GetStyle(); + style.FramePadding = originalPadding_; + } + } + + bool BeginTabItemWithFont(const char* label, FontRole role, ImGuiTabItemFlags flags) + { + // Push the font for this role + FontRoleGuard guard(role); + + // Simply begin the tab item - padding adjustments should be handled + // by the tab bar wrapper, not individual tab items + return ImGui::BeginTabItem(label, nullptr, flags); + } + + std::string BuildFontSignature(const Menu::ThemeSettings& theme, float baseFontSize) + { + std::string signature; + signature.reserve(256); + for (size_t i = 0; i < static_cast(Menu::FontRole::Count); ++i) { + Menu::FontRole role = static_cast(i); + const auto& roleSettings = theme.FontRoles[i]; + float scaledSize = baseFontSize * roleSettings.SizeScale; + scaledSize = std::clamp(scaledSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE); + float roundedSize = std::round(scaledSize); + signature += std::format("{}|{}|{:.2f};", Menu::GetFontRoleKey(role), roleSettings.File, roundedSize); + } + signature += std::format("base|{:.2f};", std::round(baseFontSize)); + return signature; + } +} // namespace MenuFonts + +namespace Util +{ + // Security: Validate that a path stays within an allowed directory + bool IsPathWithinDirectory(const std::filesystem::path& basePath, const std::filesystem::path& testPath) + { + try { + // Canonicalize both paths to resolve all symlinks and .. sequences + auto canonicalBase = std::filesystem::canonical(basePath); + auto canonicalTest = std::filesystem::weakly_canonical(testPath); + + // Check if test path is a subpath of base path + auto [baseIt, testIt] = std::mismatch( + canonicalBase.begin(), canonicalBase.end(), + canonicalTest.begin(), canonicalTest.end()); + + return baseIt == canonicalBase.end(); + } catch (const std::filesystem::filesystem_error&) { + // If canonicalization fails, reject the path + return false; + } + } + + namespace + { + // Security: Sanitize user input to prevent path traversal + std::string SanitizeFontPath(const std::string& input) + { + std::string sanitized = input; + + // Remove any leading path separators or traversal sequences + while (!sanitized.empty() && + (sanitized.front() == '/' || sanitized.front() == '\\' || + sanitized.starts_with("../") || sanitized.starts_with("..\\") || + sanitized.starts_with("./") || sanitized.starts_with(".\\"))) { + if (sanitized.starts_with("../") || sanitized.starts_with("..\\")) { + sanitized = sanitized.substr(3); + } else if (sanitized.starts_with("./") || sanitized.starts_with(".\\")) { + sanitized = sanitized.substr(2); + } else { + sanitized = sanitized.substr(1); + } + } + + // Replace all instances of .. in the path + size_t pos = 0; + while ((pos = sanitized.find("..", pos)) != std::string::npos) { + sanitized.replace(pos, 2, ""); + } + + return sanitized; + } + + std::string NormalizeRelativeFontPath(const std::filesystem::path& root, const std::filesystem::path& absolute) + { + auto relative = absolute.lexically_relative(root); + auto normalized = relative.generic_string(); + while (!normalized.empty() && (normalized.front() == '/' || normalized.front() == '\\')) { + normalized.erase(normalized.begin()); + } + return normalized; + } + + std::string ToLowerCopy(std::string value) + { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return value; + } + + // Font width variants that should be part of the style, not the family name + bool IsWidthVariant(const std::string& token) + { + static const std::vector widthVariants = { + "condensed", "narrow", "compressed", "compact", + "extended", "expanded", "wide", + "ultracompressed", "ultracondensed", "ultraexpanded" + }; + std::string lower = ToLowerCopy(token); + return std::find(widthVariants.begin(), widthVariants.end(), lower) != widthVariants.end(); + } + + std::string ExtractFamilyName(const std::filesystem::path& relativePath) + { + if (relativePath.has_parent_path()) { + auto it = relativePath.begin(); + if (it != relativePath.end()) { + return it->string(); + } + } + auto stem = relativePath.stem().string(); + + // Split by delimiters and take tokens before width variants + std::vector tokens; + std::string token; + for (char c : stem) { + if (c == '-' || c == '_' || c == ' ') { + if (!token.empty()) { + tokens.push_back(token); + token.clear(); + } + } else { + token += c; + } + } + if (!token.empty()) { + tokens.push_back(token); + } + + // Find first width variant and take everything before it + std::string family; + for (const auto& t : tokens) { + if (IsWidthVariant(t)) { + break; + } + if (!family.empty()) { + family += " "; + } + family += t; + } + + // Fallback to simple split + if (family.empty()) { + auto pos = stem.find_first_of("-_ "); + family = (pos != std::string::npos && pos > 0) ? stem.substr(0, pos) : stem; + } + + return family; + } + + std::string ExtractStyleName(const std::filesystem::path& relativePath, const std::string& family) + { + std::string stem = relativePath.stem().string(); + std::string lowerStem = ToLowerCopy(stem); + std::string lowerFamily = ToLowerCopy(family); + + // Remove family prefix if present + if (!lowerFamily.empty()) { + std::string hyphen = lowerFamily + "-"; + std::string underscore = lowerFamily + "_"; + std::string space = lowerFamily + " "; + if (lowerStem.rfind(hyphen, 0) == 0) { + stem = stem.substr(family.size() + 1); + } else if (lowerStem.rfind(underscore, 0) == 0) { + stem = stem.substr(family.size() + 1); + } else if (lowerStem.rfind(space, 0) == 0) { + stem = stem.substr(family.size() + 1); + } + } + + // Parse remaining tokens and build style + std::vector tokens; + std::string token; + for (char c : stem) { + if (c == '-' || c == '_' || c == ' ') { + if (!token.empty()) { + tokens.push_back(token); + token.clear(); + } + } else { + token += c; + } + } + if (!token.empty()) { + tokens.push_back(token); + } + + // Build style from all tokens + std::string style; + for (const auto& t : tokens) { + if (!style.empty()) { + style += " "; + } + style += t; + } + + if (style.empty() || ToLowerCopy(style) == lowerFamily) { + style = "Regular"; + } + return style; + } + + std::string ToDisplayLabel(const std::string& token) + { + if (token.empty()) { + return "Regular"; + } + std::string display; + display.reserve(token.size() + 4); + char prev = '\0'; + for (char c : token) { + if (c == '-' || c == '_') { + if (!display.empty() && display.back() != ' ') { + display.push_back(' '); + } + prev = ' '; + continue; + } + if (!display.empty() && std::islower(static_cast(prev)) && std::isupper(static_cast(c))) { + display.push_back(' '); + } + display.push_back(c); + prev = c; + } + if (!display.empty()) { + display[0] = static_cast(std::toupper(static_cast(display[0]))); + } + return display; + } + + // Helper function to format any font filename into a user-friendly display name + std::string FormatFontDisplayName(const std::string& filename) + { + if (filename.empty()) { + return "Unknown"; + } + + std::filesystem::path filePath(filename); + std::string stem = filePath.stem().string(); + + if (stem.empty()) { + return "Unknown"; + } + + // Remove common font file prefixes if present + std::vector prefixes = { "Font-", "Font_", "TTF-", "TTF_" }; + for (const auto& prefix : prefixes) { + if (stem.size() > prefix.size() && + ToLowerCopy(stem.substr(0, prefix.size())) == ToLowerCopy(prefix)) { + stem = stem.substr(prefix.size()); + break; + } + } + + return ToDisplayLabel(stem); + } + + int StyleRank(const std::string& style) + { + std::string lower = ToLowerCopy(style); + struct WeightRank + { + const char* token; + int rank; + }; + static constexpr WeightRank weights[] = { + { "thin", 0 }, + { "hairline", 0 }, + { "extra light", 1 }, + { "extralight", 1 }, + { "light", 2 }, + { "regular", 3 }, + { "book", 3 }, + { "normal", 3 }, + { "medium", 4 }, + { "semibold", 5 }, + { "demibold", 5 }, + { "bold", 6 }, + { "extrabold", 7 }, + { "heavy", 7 }, + { "black", 8 } + }; + int rank = 9; + for (const auto& weight : weights) { + if (lower.find(weight.token) != std::string::npos) { + rank = weight.rank; + break; + } + } + if (lower.find("italic") != std::string::npos || lower.find("oblique") != std::string::npos) { + rank += 1; + } + return rank; + } + } // namespace + + namespace Fonts + { + // Performance: Cache font catalog to avoid repeated filesystem scans + static std::optional cachedCatalog; + static std::mutex catalogMutex; + + Catalog DiscoverFontCatalog(bool forceRefresh) + { + std::lock_guard lock(catalogMutex); + + // Return cached catalog if available and not forcing refresh + if (!forceRefresh && cachedCatalog.has_value()) { + logger::debug("DiscoverFontCatalog: Using cached catalog ({} families)", + cachedCatalog->families.size()); + return *cachedCatalog; + } + + Catalog catalog; + try { + auto fontsPath = Util::PathHelpers::GetFontsPath(); + logger::debug("DiscoverFontCatalog: Scanning fonts directory: {}", fontsPath.string()); + + if (!std::filesystem::exists(fontsPath)) { + logger::warn("DiscoverFontCatalog: Fonts directory does not exist: {}", fontsPath.string()); + cachedCatalog = catalog; // Cache empty result + return catalog; + } + + std::unordered_map familyIndex; + + for (const auto& entry : std::filesystem::recursive_directory_iterator(fontsPath)) { + if (!entry.is_regular_file()) { + continue; + } + + auto extension = entry.path().extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + if (extension != ".ttf" && extension != ".otf") { + continue; + } + + std::string relativePath; + try { + relativePath = NormalizeRelativeFontPath(fontsPath, entry.path()); + } catch (const std::exception& e) { + logger::warn("DiscoverFontCatalog: Unable to relativize '{}': {}", entry.path().string(), e.what()); + continue; + } + + std::filesystem::path relPath(relativePath); + std::string family = ExtractFamilyName(relPath); + if (family.empty()) { + family = relPath.stem().string(); + } + std::string style = ExtractStyleName(relPath, family); + std::string familyKey = ToLowerCopy(family); + size_t familyIdx; + auto found = familyIndex.find(familyKey); + if (found == familyIndex.end()) { + FamilyInfo info; + info.name = family; + info.displayName = ToDisplayLabel(family); + familyIdx = catalog.families.size(); + catalog.families.push_back(std::move(info)); + familyIndex.emplace(familyKey, familyIdx); + } else { + familyIdx = found->second; + } + + auto& familyInfo = catalog.families[familyIdx]; + auto styleIt = std::find_if(familyInfo.styles.begin(), familyInfo.styles.end(), [&](const StyleInfo& existing) { + return _stricmp(existing.style.c_str(), style.c_str()) == 0; + }); + if (styleIt == familyInfo.styles.end()) { + StyleInfo styleInfo; + styleInfo.style = style.empty() ? "Regular" : style; + styleInfo.displayName = ToDisplayLabel(styleInfo.style); + styleInfo.file = relativePath; + styleInfo.family = familyInfo.name; + familyInfo.styles.push_back(std::move(styleInfo)); + } else { + logger::debug("DiscoverFontCatalog: Duplicate style '{}' for family '{}', keeping first entry.", style, familyInfo.name); + } + } + + std::sort(catalog.families.begin(), catalog.families.end(), [](const FamilyInfo& a, const FamilyInfo& b) { + return _stricmp(a.displayName.c_str(), b.displayName.c_str()) < 0; + }); + + for (auto& family : catalog.families) { + std::sort(family.styles.begin(), family.styles.end(), [](const StyleInfo& a, const StyleInfo& b) { + int rankA = StyleRank(a.style); + int rankB = StyleRank(b.style); + if (rankA != rankB) { + return rankA < rankB; + } + return _stricmp(a.displayName.c_str(), b.displayName.c_str()) < 0; + }); + } + + logger::info("DiscoverFontCatalog: Found {} font families", catalog.families.size()); + cachedCatalog = catalog; // Cache the result + } catch (const std::exception& e) { + logger::error("DiscoverFontCatalog: Exception occurred while scanning fonts: {}", e.what()); + cachedCatalog = catalog; // Cache even on error to avoid repeated failures + } + + return catalog; + } + + // Convenience overload that uses cached catalog by default + Catalog DiscoverFontCatalog() + { + return DiscoverFontCatalog(false); + } + } // namespace Fonts + + std::vector DiscoverFonts() + { + std::vector fonts; + auto catalog = Fonts::DiscoverFontCatalog(); + for (const auto& family : catalog.families) { + for (const auto& style : family.styles) { + fonts.push_back(style.file); + } + } + return fonts; + } + + bool ValidateFont(const std::string& fontName) + { + if (fontName.empty()) { + return false; + } + + try { + auto fontsPath = Util::PathHelpers::GetFontsPath(); + + // Security: Sanitize input to prevent path traversal + std::string sanitizedName = SanitizeFontPath(fontName); + if (sanitizedName.empty()) { + logger::warn("ValidateFont: Rejected potentially malicious path: {}", fontName); + return false; + } + + std::filesystem::path relative(sanitizedName); + std::filesystem::path directPath = fontsPath / relative; + + // Security: Verify the resolved path stays within fonts directory + if (!IsPathWithinDirectory(fontsPath, directPath)) { + logger::warn("ValidateFont: Path traversal attempt detected: {}", fontName); + return false; + } + + auto isValidExtension = [](const std::filesystem::path& candidate) { + auto extension = candidate.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + return extension == ".ttf" || extension == ".otf"; + }; + + if (std::filesystem::exists(directPath) && std::filesystem::is_regular_file(directPath) && isValidExtension(directPath)) { + return true; + } + + // Performance: Use cached catalog for case-insensitive search instead of scanning filesystem + auto catalog = Fonts::DiscoverFontCatalog(); // Uses cache + std::string targetNormalized = ToLowerCopy(relative.generic_string()); + std::string targetFilename = ToLowerCopy(relative.filename().string()); + + for (const auto& family : catalog.families) { + for (const auto& style : family.styles) { + std::string fileLower = ToLowerCopy(style.file); + std::string filenameLower = ToLowerCopy(std::filesystem::path(style.file).filename().string()); + + if (fileLower == targetNormalized || filenameLower == targetFilename) { + return true; + } + } + } + } catch (const std::exception& e) { + logger::error("ValidateFont: Exception occurred while validating font '{}': {}", fontName, e.what()); + } + + return false; + } +} // namespace Util diff --git a/src/Menu/Fonts.h b/src/Menu/Fonts.h new file mode 100644 index 0000000000..96c39592db --- /dev/null +++ b/src/Menu/Fonts.h @@ -0,0 +1,148 @@ +#pragma once + +#include "../Menu.h" +#include +#include +#include +#include +#include +#include +#include + +namespace MenuFonts +{ + using FontRole = Menu::FontRole; + using FontRoleSettings = Menu::ThemeSettings::FontRoleSettings; + + void NormalizeFontRoles(Menu::ThemeSettings& theme, bool themeProvidedFontRoles); + const FontRoleSettings& GetDefaultRole(FontRole role); + std::string BuildFontSignature(const Menu::ThemeSettings& theme, float baseFontSize); + + /** + * @brief RAII guard for pushing/popping ImGui fonts based on font roles + * + * Automatically pushes the specified font role on construction and pops it on destruction. + * This ensures proper font stack management even if exceptions occur. + * + * Usage: + * { + * MenuFonts::FontRoleGuard guard(Menu::FontRole::Heading); + * ImGui::Text("This text uses the Heading font"); + * } // Font automatically popped here + */ + class FontRoleGuard + { + public: + explicit FontRoleGuard(FontRole role); + ~FontRoleGuard(); + + FontRoleGuard(const FontRoleGuard&) = delete; + FontRoleGuard& operator=(const FontRoleGuard&) = delete; + + [[nodiscard]] ImFont* Get() const { return font_; } + + private: + ImFont* font_ = nullptr; + }; + + /** + * @brief RAII guard for tab bars that automatically scales padding for larger tab fonts + * + * Scales FramePadding when tab fonts are larger than body text to ensure proper + * tab bar height and separator positioning. Automatically restores original padding on destruction. + * + * Usage: + * { + * MenuFonts::TabBarPaddingGuard tabGuard(Menu::FontRole::Subheading); + * if (ImGui::BeginTabBar("##MyTabs")) { + * // Tab items... + * ImGui::EndTabBar(); + * } + * } // Padding automatically restored here + */ + class TabBarPaddingGuard + { + public: + explicit TabBarPaddingGuard(FontRole tabFontRole); + ~TabBarPaddingGuard(); + + TabBarPaddingGuard(const TabBarPaddingGuard&) = delete; + TabBarPaddingGuard& operator=(const TabBarPaddingGuard&) = delete; + + private: + ImVec2 originalPadding_; + bool scaled_ = false; + }; + + /** + * @brief Begins an ImGui tab item with the specified font role + * + * Convenience wrapper that combines FontRoleGuard with ImGui::BeginTabItem. + * The font is automatically managed and will be popped when the tab ends. + * + * @param label Tab label text + * @param role Font role to use for the tab + * @param flags ImGui tab item flags + * @return true if the tab is selected and visible, false otherwise + */ + bool BeginTabItemWithFont(const char* label, FontRole role, ImGuiTabItemFlags flags = ImGuiTabItemFlags_None); +} + +namespace Util +{ + namespace Fonts + { + struct StyleInfo + { + std::string style; + std::string displayName; + std::string file; + std::string family; + }; + + struct FamilyInfo + { + std::string name; + std::string displayName; + std::vector styles; + }; + + struct Catalog + { + std::vector families; + + const FamilyInfo* FindFamily(const std::string& name) const; + const StyleInfo* FindStyle(const std::string& family, const std::string& style) const; + }; + + Catalog DiscoverFontCatalog(); + Catalog DiscoverFontCatalog(bool forceRefresh); // Explicit refresh control + std::string FormatFontDisplayName(const std::string& filename); + } + + std::vector DiscoverFonts(); + bool ValidateFont(const std::string& fontName); + + // Security: Path validation helpers + bool IsPathWithinDirectory(const std::filesystem::path& basePath, const std::filesystem::path& testPath); +} + +inline const Util::Fonts::FamilyInfo* Util::Fonts::Catalog::FindFamily(const std::string& name) const +{ + auto it = std::find_if(families.begin(), families.end(), [&](const FamilyInfo& info) { + return _stricmp(info.name.c_str(), name.c_str()) == 0; + }); + return it != families.end() ? &(*it) : nullptr; +} + +inline const Util::Fonts::StyleInfo* Util::Fonts::Catalog::FindStyle(const std::string& family, const std::string& style) const +{ + const FamilyInfo* familyInfo = FindFamily(family); + if (!familyInfo) { + return nullptr; + } + auto it = std::find_if(familyInfo->styles.begin(), familyInfo->styles.end(), [&](const StyleInfo& info) { + return _stricmp(info.style.c_str(), style.c_str()) == 0; + }); + return it != familyInfo->styles.end() ? &(*it) : nullptr; +} diff --git a/src/Menu/HomePageRenderer.cpp b/src/Menu/HomePageRenderer.cpp index 8e1a4628b1..b7b8b9d74f 100644 --- a/src/Menu/HomePageRenderer.cpp +++ b/src/Menu/HomePageRenderer.cpp @@ -1,25 +1,14 @@ #include "HomePageRenderer.h" #include "PCH.h" -#include -#include -#include #include -#include -#include -#include -#include "Feature.h" #include "Globals.h" #include "Menu.h" -#include "Menu/ThemeManager.h" #include "Plugin.h" -#include "SettingsOverrideManager.h" #include "State.h" #include "Util.h" -using json = nlohmann::json; - // Static member definitions bool HomePageRenderer::isFirstTimeSetupShown = false; @@ -97,7 +86,18 @@ void HomePageRenderer::RenderWelcomeSection() } if (discordIconAvailable) { - ImVec2 iconSize = ImVec2(menu->uiIcons.discord.size.x, menu->uiIcons.discord.size.y); + // Calculate scaled icon size based on window width, with min/max constraints + ImVec2 originalSize = ImVec2(menu->uiIcons.discord.size.x, menu->uiIcons.discord.size.y); + + // Compute width based on window size with constraints and padding (handles very small windows) + float ratioWidth = windowSize.x * DISCORD_BANNER_TARGET_WIDTH_RATIO; + float aspectRatio = originalSize.y / originalSize.x; + float maxAllowed = std::max(1.0f, windowSize.x - DISCORD_BANNER_PADDING_MARGIN); + float upperBound = std::min(DISCORD_BANNER_MAX_WIDTH, maxAllowed); + float lowerBound = std::min(DISCORD_BANNER_MIN_WIDTH, upperBound); + float targetWidth = std::clamp(ratioWidth, lowerBound, upperBound); + + ImVec2 iconSize = ImVec2(targetWidth, targetWidth * aspectRatio); ImGui::SetCursorPosX((windowSize.x - iconSize.x) * 0.5f); // Push style to remove border @@ -242,7 +242,6 @@ void HomePageRenderer::RenderFAQSection() "Yes! Community Shaders is completely open source and available on GitHub. You can view " "the source code, report issues, suggest features, and contribute to the project. " "The project is licensed under GPL, ensuring it remains free and open for everyone." - "The project is licensed under GPL, ensuring it remains free and open for everyone." " Branding materials and assets (icons, nexus branding, typography, etc) are not covered by the GPL Licence." " Any included assets may not be used without explicit permission."); } @@ -259,7 +258,8 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() // Center the window properly with rounded corners and thin border ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_Always); + // Set a minimum width for better layout, but allow auto-sizing for height + ImGui::SetNextWindowSizeConstraints(ImVec2(500, 0), ImVec2(600, FLT_MAX)); // Style for rounded window with thin border ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); @@ -267,7 +267,7 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoTitleBar; // Prevent scrolling and remove title + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize; if (!ImGui::Begin("##FirstTimeSetup", nullptr, flags)) { ImGui::PopStyleVar(2); @@ -275,6 +275,12 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() return; } + // Set absolute font size for better readability in this welcome dialog + float targetFontSize = 27.0f; + float currentFontSize = io.FontDefault ? io.FontDefault->FontSize : io.FontGlobalScale * 13.0f; + float fontScale = targetFontSize / currentFontSize; + ImGui::SetWindowFontScale(fontScale); + auto menu = Menu::GetSingleton(); // Render CS logo as background watermark with proper aspect ratio @@ -294,8 +300,17 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() windowPos.y + (windowSize.y - logoHeight) * 0.5f); ImVec2 logoMax(logoMin.x + logoWidth, logoMin.y + logoHeight); + // Determine watermark color based on monochrome logo setting + ImU32 watermarkColor; + if (menu->GetSettings().Theme.UseMonochromeLogo) { + ImVec4 textColor = menu->GetSettings().Theme.Palette.Text; + textColor.w = 0.24f; // Low alpha for watermark effect + watermarkColor = ImGui::GetColorU32(textColor); + } else { + watermarkColor = IM_COL32(255, 255, 255, 60); + } + // Render as subtle watermark background - ImU32 watermarkColor = IM_COL32(255, 255, 255, 60); ImGui::GetWindowDrawList()->AddImage(menu->uiIcons.logo.texture, logoMin, logoMax, ImVec2(0, 0), ImVec2(1, 1), watermarkColor); } @@ -344,6 +359,9 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() auto& themeSettings = menu->GetTheme(); const char* currentKeyName = Util::Input::KeyIdToString(menu->GetSettings().ToggleKey); + // Increase font size for hotkey text + ImGui::SetWindowFontScale(fontScale * HOTKEY_TEXT_SCALE_MULTIPLIER); + // Calculate text dimensions for centering and button area float hotkeyWidth = ImGui::CalcTextSize(currentKeyName).x; float centerX = (windowWidth - hotkeyWidth) * 0.5f; @@ -375,6 +393,9 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() ImGui::TextColored(hotkeyColor, "%s", currentKeyName); + // Reset font scale + ImGui::SetWindowFontScale(fontScale); + // Handle click to start hotkey capture if (clicked) { menu->settingToggleKey = true; @@ -419,26 +440,22 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() ImGui::Spacing(); - // Center the continue button - float continueButtonWidth = 140.0f; - ImGui::SetCursorPosX((windowWidth - continueButtonWidth) * 0.5f); + // Check for Enter or Escape key to close, but only if not capturing a hotkey + bool shouldClose = (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_Escape)) && !menu->settingToggleKey; - // Check for Enter or Escape key first - bool shouldClose = ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_Escape); - - if (ImGui::Button("Continue", ImVec2(continueButtonWidth, 30)) || shouldClose) { - // No need to apply any hotkey - user has already set it or it defaults to VK_END + if (shouldClose) { MarkFirstTimeSetupComplete(); + // Note: Settings are automatically saved to ensure welcome screen won't show again } // Center the help text - const char* helpText = "(Press Enter or Escape to continue)"; + const char* helpText = "Press Escape or Enter to continue"; float helpWidth = ImGui::CalcTextSize(helpText).x; ImGui::SetCursorPosX((windowWidth - helpWidth) * 0.5f); ImGui::TextDisabled("%s", helpText); - ImGui::PopStyleVar(2); // Pop WindowRounding and WindowBorderSize ImGui::End(); + ImGui::PopStyleVar(2); } bool HomePageRenderer::ShouldShowFirstTimeSetup() @@ -453,70 +470,20 @@ bool HomePageRenderer::ShouldShowFirstTimeSetup() return false; } - // Check if first-time setup has been completed by looking at UserSettings.json - std::filesystem::path userSettingsPath = Util::PathHelpers::GetUserSettingsPath(); - - // If UserSettings.json doesn't exist at all, this is definitely a first-time launch - if (!std::filesystem::exists(userSettingsPath)) { - return true; - } - - // If UserSettings.json exists, check if FirstTimeSetupCompleted flag is set - try { - std::ifstream file(userSettingsPath); - if (!file.is_open()) { - return true; // If we can't read the file, assume first time - } - - nlohmann::json settings; - file >> settings; - file.close(); - - // Check if FirstTimeSetupCompleted exists and is true - if (settings.contains("FirstTimeSetupCompleted") && - settings["FirstTimeSetupCompleted"].is_boolean() && - settings["FirstTimeSetupCompleted"] == true) { - return false; // Setup already completed - } - - return true; // Field doesn't exist or is false, show setup - - } catch (const std::exception&) { - // If there's any error reading the file, assume first time - return true; - } + // Check if first-time setup has been completed using the Menu settings + auto menu = Menu::GetSingleton(); + return !menu->GetSettings().FirstTimeSetupCompleted; } void HomePageRenderer::MarkFirstTimeSetupComplete() { - std::filesystem::path userSettingsPath = Util::PathHelpers::GetUserSettingsPath(); - - try { - nlohmann::json settings; - - // Read existing settings if file exists - if (std::filesystem::exists(userSettingsPath)) { - std::ifstream file(userSettingsPath); - if (file.is_open()) { - file >> settings; - file.close(); - } - } - - // Set the FirstTimeSetupCompleted flag - settings["FirstTimeSetupCompleted"] = true; - - // Write back to file - std::filesystem::create_directories(userSettingsPath.parent_path()); - std::ofstream outFile(userSettingsPath); - if (outFile.is_open()) { - outFile << settings.dump(2); - outFile.close(); - } + // Set the flag in the Menu settings + auto menu = Menu::GetSingleton(); + menu->GetSettings().FirstTimeSetupCompleted = true; - } catch (const std::exception&) { - // If we can't write the file, just mark as shown this session to avoid repeated popups - } + // Immediately save settings to ensure the flag is persisted + // This prevents the welcome screen from showing again even if user doesn't manually save + globals::state->Save(); isFirstTimeSetupShown = true; // Mark as shown this session } diff --git a/src/Menu/HomePageRenderer.h b/src/Menu/HomePageRenderer.h index aec34b4696..e95c0484e4 100644 --- a/src/Menu/HomePageRenderer.h +++ b/src/Menu/HomePageRenderer.h @@ -1,6 +1,5 @@ #pragma once -#include #include class HomePageRenderer @@ -9,8 +8,15 @@ class HomePageRenderer // Constants static constexpr const char* DISCORD_URL = "https://discord.com/invite/nkrQybAsyy"; static constexpr float TITLE_FONT_SCALE = 2.0f; + static constexpr float HOTKEY_TEXT_SCALE_MULTIPLIER = 1.25f; static constexpr float QUICK_LINKS_BUTTON_WIDTH = 180.0f; - static constexpr float LOGO_WATERMARK_HEIGHT = 260.0f; + static constexpr float LOGO_WATERMARK_HEIGHT = 200.0f; + + // Discord banner scaling constants + static constexpr float DISCORD_BANNER_TARGET_WIDTH_RATIO = 0.85f; // 25% of window width + static constexpr float DISCORD_BANNER_MIN_WIDTH = 150.0f; + static constexpr float DISCORD_BANNER_MAX_WIDTH = 1200.0f; + static constexpr float DISCORD_BANNER_PADDING_MARGIN = 40.0f; static void RenderHomePage(); diff --git a/src/Menu/IconLoader.cpp b/src/Menu/IconLoader.cpp new file mode 100644 index 0000000000..884c2f9fa6 --- /dev/null +++ b/src/Menu/IconLoader.cpp @@ -0,0 +1,247 @@ +#include "PCH.h" + +#include "IconLoader.h" + +#include "Globals.h" +#include "Menu.h" +#include "Utils/FileSystem.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Util::IconLoader +{ + struct IconDefinition + { + std::string filename; + ID3D11ShaderResourceView** texture; + ImVec2* size; + }; + + bool LoadTextureFromFile(ID3D11Device* device, const char* filename, ID3D11ShaderResourceView** out_srv, ImVec2& out_size) + { + int image_width = 0; + int image_height = 0; + unsigned char* image_data = stbi_load(filename, &image_width, &image_height, nullptr, 4); + if (image_data == nullptr) { + return false; + } + + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = image_width; + desc.Height = image_height; + desc.MipLevels = 0; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; + desc.CPUAccessFlags = 0; + desc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS; + + ID3D11Texture2D* pTexture = nullptr; + device->CreateTexture2D(&desc, nullptr, &pTexture); + if (!pTexture) { + stbi_image_free(image_data); + return false; + } + + ID3D11DeviceContext* context = nullptr; + device->GetImmediateContext(&context); + if (context) { + context->UpdateSubresource(pTexture, 0, nullptr, image_data, desc.Width * 4, 0); + } + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = static_cast(-1); + srvDesc.Texture2D.MostDetailedMip = 0; + + HRESULT hr = device->CreateShaderResourceView(pTexture, &srvDesc, out_srv); + if (FAILED(hr)) { + pTexture->Release(); + stbi_image_free(image_data); + if (context) + context->Release(); + return false; + } + + if (context) { + context->GenerateMips(*out_srv); + context->Release(); + } + + pTexture->Release(); + stbi_image_free(image_data); + + out_size = ImVec2((float)image_width, (float)image_height); + return true; + } + + std::vector GetIconDefinitions(Menu* menu) + { + const bool useMonochrome = menu->GetSettings().Theme.UseMonochromeIcons; + const bool useMonochromeLogo = menu->GetSettings().Theme.UseMonochromeLogo; + const char* iconFolder = useMonochrome ? "Action Icons\\Monochrome" : "Action Icons"; + const char* logoPath = useMonochromeLogo ? "Community Shaders Logo\\Monochrome\\cs-logo.png" : "Community Shaders Logo\\cs-logo.png"; + + return { + { std::string(iconFolder) + "\\save-settings.png", &menu->uiIcons.saveSettings.texture, &menu->uiIcons.saveSettings.size }, + { std::string(iconFolder) + "\\load-settings.png", &menu->uiIcons.loadSettings.texture, &menu->uiIcons.loadSettings.size }, + { std::string(iconFolder) + "\\clear-cache.png", &menu->uiIcons.clearCache.texture, &menu->uiIcons.clearCache.size }, + { logoPath, &menu->uiIcons.logo.texture, &menu->uiIcons.logo.size }, + { std::string(iconFolder) + "\\restore-settings.png", &menu->uiIcons.featureSettingRevert.texture, &menu->uiIcons.featureSettingRevert.size }, + { std::string(iconFolder) + "\\discord.png", &menu->uiIcons.discord.texture, &menu->uiIcons.discord.size }, + { "Categories\\characters.png", &menu->uiIcons.characters.texture, &menu->uiIcons.characters.size }, + { "Categories\\display.png", &menu->uiIcons.display.texture, &menu->uiIcons.display.size }, + { "Categories\\grass.png", &menu->uiIcons.grass.texture, &menu->uiIcons.grass.size }, + { "Categories\\lighting.png", &menu->uiIcons.lighting.texture, &menu->uiIcons.lighting.size }, + { "Categories\\sky.png", &menu->uiIcons.sky.texture, &menu->uiIcons.sky.size }, + { "Categories\\landscape.png", &menu->uiIcons.landscape.texture, &menu->uiIcons.landscape.size }, + { "Categories\\water.png", &menu->uiIcons.water.texture, &menu->uiIcons.water.size }, + { "Categories\\debug.png", &menu->uiIcons.debug.texture, &menu->uiIcons.debug.size }, + { "Categories\\materials.png", &menu->uiIcons.materials.texture, &menu->uiIcons.materials.size }, + { "Categories\\post-processing.png", &menu->uiIcons.postProcessing.texture, &menu->uiIcons.postProcessing.size } + }; + } + + void LoadThemeSpecificIcons(Menu* menu, ID3D11Device* device, const std::vector& iconDefs) + { + const auto& selectedTheme = menu->GetSettings().SelectedThemePreset; + if (selectedTheme.empty()) { + return; + } + + std::filesystem::path themeIconsPath = Util::PathHelpers::GetThemesPath() / selectedTheme; + if (!std::filesystem::exists(themeIconsPath) || !std::filesystem::is_directory(themeIconsPath)) { + logger::debug("LoadThemeSpecificIcons: Theme folder does not exist: {}", themeIconsPath.string()); + return; + } + + logger::info("LoadThemeSpecificIcons: Checking for custom icons in theme '{}' at path: {}", selectedTheme, themeIconsPath.string()); + + ID3D11DeviceContext* context = globals::d3d::context; + if (context) + context->Flush(); + + int iconsOverridden = 0; + + for (const auto& iconDef : iconDefs) { + std::filesystem::path iconPath = themeIconsPath / std::filesystem::path(iconDef.filename).filename(); + + logger::trace("LoadThemeSpecificIcons: Checking for icon: {}", iconPath.string()); + + if (std::filesystem::exists(iconPath)) { + if (*iconDef.texture) { + (*iconDef.texture)->Release(); + *iconDef.texture = nullptr; + } + + if (LoadTextureFromFile(device, iconPath.string().c_str(), iconDef.texture, *iconDef.size)) { + logger::debug("LoadThemeSpecificIcons: Loaded custom icon: {}", iconPath.filename().string()); + iconsOverridden++; + } + } + } + + if (iconsOverridden > 0) { + logger::info("LoadThemeSpecificIcons: Loaded {} custom icon(s) from theme '{}'", iconsOverridden, selectedTheme); + } + } + + bool InitializeMenuIcons(Menu* menu) + { + if (!menu) { + logger::warn("InitializeMenuIcons: Menu pointer is null"); + return false; + } + + ID3D11Device* device = globals::d3d::device; + ID3D11DeviceContext* context = globals::d3d::context; + if (!device || !context) { + logger::warn("InitializeMenuIcons: D3D device or context is null"); + return false; + } + + // Flush and wait for GPU idle before releasing textures + context->Flush(); + winrt::com_ptr eventQuery; + D3D11_QUERY_DESC queryDesc = { D3D11_QUERY_EVENT, 0 }; + if (SUCCEEDED(device->CreateQuery(&queryDesc, eventQuery.put()))) { + context->End(eventQuery.get()); + BOOL queryData = FALSE; + for (int i = 0; i < 1000 && context->GetData(eventQuery.get(), &queryData, sizeof(BOOL), 0) != S_OK; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + std::string basePath = Util::PathHelpers::GetIconsPath().string() + "\\"; + logger::info("InitializeMenuIcons: Loading icons from base path: {}", basePath); + + auto iconDefs = GetIconDefinitions(menu); + + for (auto* texturePtr : { &menu->uiIcons.saveSettings.texture, &menu->uiIcons.loadSettings.texture, + &menu->uiIcons.clearCache.texture, &menu->uiIcons.logo.texture, + &menu->uiIcons.featureSettingRevert.texture, &menu->uiIcons.discord.texture, + &menu->uiIcons.characters.texture, &menu->uiIcons.display.texture, + &menu->uiIcons.grass.texture, &menu->uiIcons.lighting.texture, + &menu->uiIcons.sky.texture, &menu->uiIcons.landscape.texture, + &menu->uiIcons.water.texture, &menu->uiIcons.debug.texture, + &menu->uiIcons.materials.texture, &menu->uiIcons.postProcessing.texture }) { + if (*texturePtr) { + (*texturePtr)->Release(); + *texturePtr = nullptr; + } + } + + bool anyIconLoaded = false; + int iconsLoaded = 0; + + for (const auto& iconDef : iconDefs) { + std::string fullPath = basePath + iconDef.filename; + if (LoadTextureFromFile(device, fullPath.c_str(), iconDef.texture, *iconDef.size)) { + iconsLoaded++; + anyIconLoaded = true; + } else { + // If monochrome icon failed to load, try fallback to colored version + if (basePath.find("Monochrome") != std::string::npos) { + std::string fallbackPath = basePath; + size_t pos = fallbackPath.find("\\Monochrome"); + if (pos != std::string::npos) { + fallbackPath.erase(pos, 11); // Remove "\Monochrome" + } + fallbackPath += iconDef.filename; + // Try to extract just the filename from iconDef.filename if it has path + size_t lastSlash = iconDef.filename.find_last_of("\\/"); + if (lastSlash != std::string::npos) { + std::string justFilename = iconDef.filename.substr(lastSlash + 1); + fallbackPath = fallbackPath.substr(0, fallbackPath.find_last_of("\\/") + 1) + justFilename; + } + if (LoadTextureFromFile(device, fallbackPath.c_str(), iconDef.texture, *iconDef.size)) { + iconsLoaded++; + anyIconLoaded = true; + } else { + logger::warn("InitializeMenuIcons: Failed to load icon from: {} (and fallback)", fullPath); + } + } else { + logger::warn("InitializeMenuIcons: Failed to load icon from: {}", fullPath); + } + } + } + + logger::info("InitializeMenuIcons: Loaded {}/{} icons successfully", iconsLoaded, iconDefs.size()); + + LoadThemeSpecificIcons(menu, device, iconDefs); + + return anyIconLoaded; + } +} diff --git a/src/Menu/IconLoader.h b/src/Menu/IconLoader.h new file mode 100644 index 0000000000..19702238f7 --- /dev/null +++ b/src/Menu/IconLoader.h @@ -0,0 +1,12 @@ +#pragma once + +struct ID3D11Device; +class Menu; + +namespace Util +{ + namespace IconLoader + { + bool InitializeMenuIcons(Menu* menu); + } +} diff --git a/src/Menu/MenuHeaderRenderer.cpp b/src/Menu/MenuHeaderRenderer.cpp index ec4ea74ffa..dff99024ff 100644 --- a/src/Menu/MenuHeaderRenderer.cpp +++ b/src/Menu/MenuHeaderRenderer.cpp @@ -3,7 +3,6 @@ #include #include -#include "Features/LightLimitFix/ParticleLights.h" #include "Globals.h" #include "Plugin.h" #include "ShaderCache.h" @@ -11,8 +10,53 @@ #include "ThemeManager.h" #include "Util.h" +namespace +{ + class RoleFontGuard + { + public: + explicit RoleFontGuard(Menu::FontRole role) + { + Menu* menuInstance = globals::menu; + if (!menuInstance) { + logger::error("RoleFontGuard: globals::menu is null, cannot retrieve font for role"); + return; + } + + font_ = menuInstance->GetFont(role); + if (font_) { + ImGui::PushFont(font_); + fontPushed_ = true; + } else { + logger::warn("RoleFontGuard: Failed to retrieve font for role {}", static_cast(role)); + } + } + + ~RoleFontGuard() + { + if (fontPushed_) { + ImGui::PopFont(); + } + } + + RoleFontGuard(const RoleFontGuard&) = delete; + RoleFontGuard& operator=(const RoleFontGuard&) = delete; + + [[nodiscard]] ImFont* Get() const { return font_; } + + private: + ImFont* font_ = nullptr; + bool fontPushed_ = false; + }; +} + void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShowIcons, float uiScale, const Menu::UIIcons& uiIcons) { + if (!globals::menu) { + logger::error("MenuHeaderRenderer::RenderHeader: globals::menu is null, cannot render header"); + return; + } + auto title = std::format("Community Shaders {}", Util::GetFormattedVersion(Plugin::VERSION)); auto actionIcons = BuildActionIcons(canShowIcons, uiIcons); @@ -26,6 +70,8 @@ void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShow RenderDockedIcons(actionIcons, uiScale); } else { // When not docked, show the custom header + bool centerHeader = globals::menu->GetTheme().CenterHeader; + if ((showLogo || canShowIcons) && ImGui::BeginTable("##HeaderLayout", 2, ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Title", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Buttons", ImGuiTableColumnFlags_WidthFixed); @@ -40,25 +86,61 @@ void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShow const float textScaleFactor = baseTextScale * uiScale; const float logoSize = baseIconSize * uiScale; // Match action icon size + if (centerHeader) { + // Calculate the width of the content + float contentWidth = 0.0f; + + if (showLogo) { + float logoAspectRatio = uiIcons.logo.size.x / uiIcons.logo.size.y; + contentWidth = (logoSize * logoAspectRatio) + 8.0f; // Logo width + spacing + } + + // Calculate text width + { + RoleFontGuard titleFont(Menu::FontRole::Title); + ImGui::SetWindowFontScale(textScaleFactor); + contentWidth += ImGui::CalcTextSize(title.c_str()).x; + ImGui::SetWindowFontScale(1.0f); + } + + float offset = Util::GetCenterOffsetForContent(contentWidth); + if (offset > 0.0f) { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offset); + } + } else { + // Add padding for left-aligned layout + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ThemeManager::Constants::CURSOR_POSITION_PADDING); + } + // Always display logo if texture is available if (showLogo) { float logoAspectRatio = uiIcons.logo.size.x / uiIcons.logo.size.y; ImVec2 logoSizeVec(logoSize * logoAspectRatio, logoSize); - // Add a bit of padding before the logo and text - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ThemeManager::Constants::CURSOR_POSITION_PADDING); + // Determine tint color for logo + ImU32 logoTint = IM_COL32_WHITE; + if (globals::menu->GetSettings().Theme.UseMonochromeLogo) { + ImVec4 textColor = globals::menu->GetSettings().Theme.Palette.Text; + logoTint = ImGui::GetColorU32(textColor); + } // Use our helper to render aligned logo and text with perfect vertical alignment - Util::DrawAlignedTextWithLogo( - uiIcons.logo.texture, - logoSizeVec, - title.c_str(), - textScaleFactor); + { + RoleFontGuard titleFont(Menu::FontRole::Title); + Util::DrawAlignedTextWithLogo( + uiIcons.logo.texture, + logoSizeVec, + title.c_str(), + textScaleFactor, + logoTint); + } } else { // No logo, just render the text with proper alignment ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ThemeManager::Constants::CURSOR_POSITION_PADDING); - Util::DrawSharpText(title.c_str(), true, textScaleFactor); + { + RoleFontGuard titleFont(Menu::FontRole::Title); + Util::DrawSharpText(title.c_str(), true, textScaleFactor); + } ImGui::PopStyleVar(); } @@ -72,8 +154,28 @@ void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShow const float baseTextScale = ThemeManager::Constants::HEADER_FALLBACK_TEXT_SCALE; const float textScaleFactor = baseTextScale * uiScale; // Apply UI scale + if (centerHeader) { + // Calculate text width for centering + float textWidth = 0.0f; + { + RoleFontGuard titleFont(Menu::FontRole::Title); + ImGui::SetWindowFontScale(textScaleFactor); + textWidth = ImGui::CalcTextSize(title.c_str()).x; + ImGui::SetWindowFontScale(1.0f); + } + + // Use helper to get centering offset + float offset = Util::GetCenterOffsetForContent(textWidth); + if (offset > 0.0f) { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offset); + } + } + ImGui::SetWindowFontScale(textScaleFactor); - ImGui::TextUnformatted(title.c_str()); + { + RoleFontGuard titleFont(Menu::FontRole::Title); + ImGui::TextUnformatted(title.c_str()); + } ImGui::SetWindowFontScale(1.0f); } } @@ -91,15 +193,15 @@ void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShow if (ImGui::BeginTable("##ActionButtons", 4, ImGuiTableFlags_SizingStretchSame)) { // Save Settings Button ImGui::TableNextColumn(); - if (ImGui::Button("Save Settings", { -1, 0 })) { + if (Util::ButtonWithFlash("Save Settings", { -1, 0 })) { globals::state->Save(); + globals::state->SaveTheme(); } // Restore Saved Settings Button ImGui::TableNextColumn(); if (ImGui::Button("Restore Saved Settings", { -1, 0 })) { globals::state->Load(); - globals::features::llf::particleLights.GetConfigs(); } // Clear Shader Cache Button @@ -176,14 +278,16 @@ std::vector MenuHeaderRenderer::BuildActionIcons if (uiIcons.saveSettings.texture) { actionIcons.push_back({ uiIcons.saveSettings.texture, "Save Settings", - []() { globals::state->Save(); } }); + []() { + globals::state->Save(); + globals::state->SaveTheme(); + } }); } if (uiIcons.loadSettings.texture) { actionIcons.push_back({ uiIcons.loadSettings.texture, "Restore Saved Settings", []() { globals::state->Load(); - globals::features::llf::particleLights.GetConfigs(); } }); } if (uiIcons.clearCache.texture) { @@ -256,9 +360,23 @@ void MenuHeaderRenderer::RenderDockedIcons(const std::vector& action bool isHovered = mousePos.x >= interactionMin.x && mousePos.x <= interactionMax.x && mousePos.y >= interactionMin.y && mousePos.y <= interactionMax.y; - // Draw icon with hover effect, using reduced area to minimize padding - ImU32 tintColor = isHovered ? IM_COL32(255, 255, 255, 255) : IM_COL32(220, 220, 220, 220); - fgDrawList->AddImage(it->texture, iconMin, iconMax, ImVec2(0, 0), ImVec2(1, 1), tintColor); + // Only render if texture is valid + if (it->texture) { + // Draw icon with hover effect, using reduced area to minimize padding + ImU32 tintColor; + if (globals::menu->GetSettings().Theme.UseMonochromeIcons) { + // Use theme text color for monochrome icons + ImVec4 textColor = globals::menu->GetSettings().Theme.Palette.Text; + if (!isHovered) { + textColor.w *= 0.85f; // Slightly reduce alpha when not hovered + } + tintColor = ImGui::GetColorU32(textColor); + } else { + // Use white/gray tint for colored icons + tintColor = isHovered ? IM_COL32(255, 255, 255, 255) : IM_COL32(220, 220, 220, 220); + } + fgDrawList->AddImage(it->texture, iconMin, iconMax, ImVec2(0, 0), ImVec2(1, 1), tintColor); + } // Handle interaction if (isHovered) { @@ -294,13 +412,25 @@ void MenuHeaderRenderer::RenderUndockedIcons(const std::vector& acti ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); // Transparent button background ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); // Slightly more visible hover effect + // Get tint color for monochrome icons + ImVec4 tintColor = ImVec4(1, 1, 1, 1); + if (globals::menu->GetSettings().Theme.UseMonochromeIcons) { + tintColor = globals::menu->GetSettings().Theme.Palette.Text; + } + // Draw action icons as ImageButtons for (size_t i = 0; i < actionIcons.size(); ++i) { const auto& icon = actionIcons[i]; + + // Skip if texture is null + if (!icon.texture) { + continue; + } + std::string buttonId = std::format("##ActionBtn{}", i); // Use ImageButton with reduced image size to minimize padding - if (ImGui::ImageButton(buttonId.c_str(), icon.texture, imageSize)) { + if (ImGui::ImageButton(buttonId.c_str(), icon.texture, imageSize, ImVec2(0, 0), ImVec2(1, 1), ImVec4(0, 0, 0, 0), tintColor)) { icon.callback(); } if (auto _tt = Util::HoverTooltipWrapper()) { @@ -344,7 +474,15 @@ void MenuHeaderRenderer::RenderWatermarkLogo(const Menu::UIIcons& uiIcons) ImVec2 logoMin(logoX, logoY); ImVec2 logoMax(logoX + watermarkWidth, logoY + watermarkHeight); - // Use very low alpha for subtle watermark effect - ImU32 watermarkColor = IM_COL32(255, 255, 255, 45); + // Determine watermark color based on monochrome logo setting + ImU32 watermarkColor; + if (globals::menu->GetSettings().Theme.UseMonochromeLogo) { + ImVec4 textColor = globals::menu->GetSettings().Theme.Palette.Text; + textColor.w = 0.18f; // Very low alpha for subtle watermark effect + watermarkColor = ImGui::GetColorU32(textColor); + } else { + watermarkColor = IM_COL32(255, 255, 255, 45); + } + drawList->AddImage(uiIcons.logo.texture, logoMin, logoMax, ImVec2(0, 0), ImVec2(1, 1), watermarkColor); } \ No newline at end of file diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index ac0c73dda8..4fbb43a7f5 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -1,16 +1,21 @@ #include "OverlayRenderer.h" +#include "BackgroundBlur.h" #include "HomePageRenderer.h" #include "ThemeManager.h" #include #include #include +#include #include "Feature.h" #include "FeatureIssues.h" +#include "Features/RenderDoc.h" +#include "Globals.h" #include "Menu.h" #include "ShaderCache.h" #include "State.h" +#include "Util.h" #include "Features/PerformanceOverlay.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" @@ -21,7 +26,7 @@ void OverlayRenderer::RenderOverlay( const std::function& processInputEventQueue, const std::function& drawSettings, const std::function& keyIdToString, - float cachedFontSize, + float& cachedFontSize, float currentFontSize) { HandleVRSetup(); @@ -42,6 +47,7 @@ void OverlayRenderer::RenderOverlay( InitializeImGuiFrame(menu); RenderShaderCompilationStatus(keyIdToString); + RenderShaderBlockingStatus(); RenderFirstTimeSetupOverlay(); if (menu.IsEnabled || HomePageRenderer::ShouldShowFirstTimeSetup()) { @@ -71,19 +77,26 @@ bool OverlayRenderer::ShouldSkipRendering() auto failed = shaderCache->GetFailedTasks(); auto hide = shaderCache->IsHideErrors(); auto* abTestingManager = ABTestingManager::GetSingleton(); + auto* renderDoc = RenderDoc::GetSingleton(); return !(shaderCache->IsCompiling() || Menu::GetSingleton()->IsEnabled || abTestingManager->IsEnabled() || (failed && !hide) || - globals::features::performanceOverlay.settings.ShowInOverlay); + globals::features::performanceOverlay.settings.ShowInOverlay || + renderDoc->IsAvailable()); } void OverlayRenderer::HandleFontReload(Menu& menu, float& cachedFontSize, float currentFontSize) { - // Reload font if user changed something - if (std::abs(cachedFontSize - currentFontSize) > ThemeManager::Constants::FONT_CACHE_EPSILON) { - ThemeManager::ReloadFont(menu, cachedFontSize); + bool fontSizeChanged = std::abs(cachedFontSize - currentFontSize) > ThemeManager::Constants::FONT_CACHE_EPSILON; + std::string desiredSignature = menu.BuildFontSignature(currentFontSize); + bool signatureChanged = desiredSignature != menu.cachedFontSignature; + + if (fontSizeChanged || signatureChanged) { + if (!ThemeManager::ReloadFont(menu, cachedFontSize)) { + logger::warn("OverlayRenderer::HandleFontReload() - Font reload failed"); + } } } @@ -108,6 +121,9 @@ void OverlayRenderer::RenderShaderCompilationStatus(const std::functionGetTheme(); + auto* renderDoc = RenderDoc::GetSingleton(); + bool renderDocAvailable = renderDoc->IsAvailable(); + const auto renderDocInformation = renderDoc->GetOverlayWarningMessage(); auto progressTitle = fmt::format("{}Compiling Shaders: {}", shaderCache->backgroundCompilation ? "Background " : "", @@ -131,6 +147,9 @@ void OverlayRenderer::RenderShaderCompilationStatus(const std::functionIsDeveloperMode() || shaderCache->blockedKey.empty()) { + return; + } + + ImGui::SetNextWindowPos(ImVec2(ThemeManager::Constants::OVERLAY_WINDOW_POSITION, ThemeManager::Constants::OVERLAY_WINDOW_POSITION + 100)); + if (!ImGui::Begin("ShaderBlockingInfo", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings)) { + ImGui::End(); + return; + } + + ImGui::TextColored(Util::Colors::GetError(), "Shader Blocking Active"); + ImGui::Text("Blocked: %s", shaderCache->blockedKey.c_str()); + + // Try to get more details from active shaders + auto activeShaders = shaderCache->GetActiveShaders(); + + // Find the index of the blocked shader in the active list (or show N/A if not found) + size_t blockedIndex = 0; + bool foundBlocked = false; + for (size_t i = 0; i < activeShaders.size(); ++i) { + if (activeShaders[i].key == shaderCache->blockedKey) { + blockedIndex = i + 1; // 1-based indexing for display + foundBlocked = true; + break; + } + } + + if (foundBlocked) { + ImGui::Text("Index: %zu/%zu", blockedIndex, activeShaders.size()); + } else { + ImGui::Text("Index: N/A (%zu active)", activeShaders.size()); + } + + for (const auto& shader : activeShaders) { + if (shader.key == shaderCache->blockedKey) { + ImGui::Text("Type: %s | Class: %s | Descriptor: 0x%X", + magic_enum::enum_name(shader.shaderType).data(), + magic_enum::enum_name(shader.shaderClass).data(), + shader.descriptor); + break; + } + } + + ImGui::End(); } \ No newline at end of file diff --git a/src/Menu/OverlayRenderer.h b/src/Menu/OverlayRenderer.h index e5010395af..3f80eca384 100644 --- a/src/Menu/OverlayRenderer.h +++ b/src/Menu/OverlayRenderer.h @@ -40,7 +40,7 @@ class OverlayRenderer const std::function& processInputEventQueue, const std::function& drawSettings, const std::function& keyIdToString, - float cachedFontSize, + float& cachedFontSize, float currentFontSize); private: @@ -49,6 +49,7 @@ class OverlayRenderer static void HandleFontReload(Menu& menu, float& cachedFontSize, float currentFontSize); static void InitializeImGuiFrame(Menu& menu); static void RenderShaderCompilationStatus(const std::function& keyIdToString); + static void RenderShaderBlockingStatus(); static void RenderFirstTimeSetupOverlay(); static void RenderFeatureOverlays(); static void HandleABTesting(); diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 9dc95ff51e..831f56fc62 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -1,17 +1,187 @@ #include "SettingsTabRenderer.h" +#include +#include +#include +#include +#include #include #include +#include +#include +#include "BackgroundBlur.h" +#include "Fonts.h" #include "Globals.h" +#include "IconLoader.h" #include "Menu.h" #include "ShaderCache.h" +#include "ThemeManager.h" #include "Util.h" +using json = nlohmann::json; + +namespace +{ + using FontRoleGuard = MenuFonts::FontRoleGuard; // Convenience alias + + // Portable case-insensitive string comparison + bool iequals(const std::string& a, const std::string& b) + { + return std::equal(a.begin(), a.end(), b.begin(), b.end(), + [](char ca, char cb) { + return std::tolower(static_cast(ca)) == + std::tolower(static_cast(cb)); + }); + } + + // Convert ImGui internal color names to user-friendly display names + const char* GetFriendlyColorName(int colorIndex) + { + switch (colorIndex) { + case ImGuiCol_Text: + return "Text"; + case ImGuiCol_TextDisabled: + return "Text (Disabled)"; + case ImGuiCol_WindowBg: + return "Window Background"; + case ImGuiCol_ChildBg: + return "Child Window Background"; + case ImGuiCol_PopupBg: + return "Popup Background"; + case ImGuiCol_Border: + return "Border"; + case ImGuiCol_BorderShadow: + return "Border Shadow"; + case ImGuiCol_FrameBg: + return "Frame Background"; + case ImGuiCol_FrameBgHovered: + return "Frame Background (Hovered)"; + case ImGuiCol_FrameBgActive: + return "Frame Background (Active)"; + case ImGuiCol_TitleBg: + return "Title Bar Background"; + case ImGuiCol_TitleBgActive: + return "Title Bar Background (Active)"; + case ImGuiCol_TitleBgCollapsed: + return "Title Bar Background (Collapsed)"; + case ImGuiCol_MenuBarBg: + return "Menu Bar Background"; + case ImGuiCol_ScrollbarBg: + return "Scrollbar Background"; + case ImGuiCol_ScrollbarGrab: + return "Scrollbar Grab"; + case ImGuiCol_ScrollbarGrabHovered: + return "Scrollbar Grab (Hovered)"; + case ImGuiCol_ScrollbarGrabActive: + return "Scrollbar Grab (Active)"; + case ImGuiCol_CheckMark: + return "Checkbox Checkmark"; + case ImGuiCol_SliderGrab: + return "Slider Grab"; + case ImGuiCol_SliderGrabActive: + return "Slider Grab (Active)"; + case ImGuiCol_Button: + return "Button"; + case ImGuiCol_ButtonHovered: + return "Button (Hovered)"; + case ImGuiCol_ButtonActive: + return "Button (Active)"; + case ImGuiCol_Header: + return "Header"; + case ImGuiCol_HeaderHovered: + return "Header (Hovered)"; + case ImGuiCol_HeaderActive: + return "Header (Active)"; + case ImGuiCol_Separator: + return "Separator"; + case ImGuiCol_SeparatorHovered: + return "Separator (Hovered)"; + case ImGuiCol_SeparatorActive: + return "Separator (Active)"; + case ImGuiCol_ResizeGrip: + return "Resize Grip"; + case ImGuiCol_ResizeGripHovered: + return "Resize Grip (Hovered)"; + case ImGuiCol_ResizeGripActive: + return "Resize Grip (Active)"; + case ImGuiCol_Tab: + return "Tab"; + case ImGuiCol_TabHovered: + return "Tab (Hovered)"; + case ImGuiCol_TabActive: + return "Tab (Active)"; + case ImGuiCol_TabUnfocused: + return "Tab (Unfocused)"; + case ImGuiCol_TabUnfocusedActive: + return "Tab (Unfocused Active)"; + case ImGuiCol_DockingPreview: + return "Docking Preview"; + case ImGuiCol_DockingEmptyBg: + return "Docking Empty Background"; + case ImGuiCol_PlotLines: + return "Plot Lines"; + case ImGuiCol_PlotLinesHovered: + return "Plot Lines (Hovered)"; + case ImGuiCol_PlotHistogram: + return "Plot Histogram"; + case ImGuiCol_PlotHistogramHovered: + return "Plot Histogram (Hovered)"; + case ImGuiCol_TableHeaderBg: + return "Table Header Background"; + case ImGuiCol_TableBorderStrong: + return "Table Border (Strong)"; + case ImGuiCol_TableBorderLight: + return "Table Border (Light)"; + case ImGuiCol_TableRowBg: + return "Table Row Background"; + case ImGuiCol_TableRowBgAlt: + return "Table Row Background (Alternate)"; + case ImGuiCol_TextSelectedBg: + return "Text Selection Background"; + case ImGuiCol_DragDropTarget: + return "Drag & Drop Target"; + case ImGuiCol_NavHighlight: + return "Navigation Highlight"; + case ImGuiCol_NavWindowingHighlight: + return "Window Navigation Highlight"; + case ImGuiCol_NavWindowingDimBg: + return "Window Navigation Dim Background"; + case ImGuiCol_ModalWindowDimBg: + return "Modal Window Dim Background"; + default: + return ImGui::GetStyleColorName(colorIndex); + } + } + + void SeparatorTextWithFont(const char* text, Menu::FontRole role) + { + MenuFonts::FontRoleGuard guard(role); + ImGui::SeparatorText(text); + } + + void SeparatorTextWithFont(const std::string& text, Menu::FontRole role) + { + SeparatorTextWithFont(text.c_str(), role); + } + + bool BeginTabItemWithFont(const char* label, Menu::FontRole role, ImGuiTabItemFlags flags = ImGuiTabItemFlags_None) + { + return MenuFonts::BeginTabItemWithFont(label, role, flags); + } + + bool ComboWithFont(const char* label, int* currentItem, const char* const items[], int itemCount, Menu::FontRole role) + { + FontRoleGuard guard(role); + return ImGui::Combo(label, currentItem, items, itemCount); + } +} + void SettingsTabRenderer::RenderGeneralSettings( SettingsState& state, const std::function& keyIdToString) { + MenuFonts::TabBarPaddingGuard tabPaddingGuard(Menu::FontRole::Heading); if (ImGui::BeginTabBar("##GeneralTabBar", ImGuiTabBarFlags_None)) { RenderShadersTab(); RenderKeybindingsTab(state, keyIdToString); @@ -22,7 +192,7 @@ void SettingsTabRenderer::RenderGeneralSettings( void SettingsTabRenderer::RenderShadersTab() { - if (ImGui::BeginTabItem("Shaders")) { + if (BeginTabItemWithFont("Shaders", Menu::FontRole::Heading)) { auto shaderCache = globals::shaderCache; bool useCustomShaders = shaderCache->IsEnabled(); @@ -62,7 +232,7 @@ void SettingsTabRenderer::RenderKeybindingsTab( SettingsState& state, const std::function& keyIdToString) { - if (ImGui::BeginTabItem("Keybindings")) { + if (BeginTabItemWithFont("Keybindings", Menu::FontRole::Heading)) { auto& settings = globals::menu->GetSettings(); auto& themeSettings = globals::menu->GetSettings().Theme; @@ -140,10 +310,12 @@ void SettingsTabRenderer::RenderKeybindingsTab( void SettingsTabRenderer::RenderInterfaceTab() { - if (ImGui::BeginTabItem("Interface")) { + if (BeginTabItemWithFont("Interface", Menu::FontRole::Heading)) { + MenuFonts::TabBarPaddingGuard tabPaddingGuard(Menu::FontRole::Subheading); if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_None)) { - RenderUIOptionsTab(); - RenderSizesTab(); + RenderThemesTab(); + RenderFontsTab(); + RenderStylingTab(); RenderColorsTab(); ImGui::EndTabBar(); } @@ -151,42 +323,475 @@ void SettingsTabRenderer::RenderInterfaceTab() } } -void SettingsTabRenderer::RenderUIOptionsTab() +void SettingsTabRenderer::RenderThemesTab() { - if (ImGui::BeginTabItem("UI Options")) { + if (BeginTabItemWithFont("Themes", Menu::FontRole::Heading)) { auto& themeSettings = globals::menu->GetSettings().Theme; - ImGui::SeparatorText("UI Elements"); - ImGui::Checkbox("Use Icon Buttons in Header", &themeSettings.ShowActionIcons); + // Static variables for popup state and new theme creation + static bool showCreateThemePopup = false; + static bool isCreatingNewTheme = false; + static char newThemeName[128] = ""; + static char newThemeDisplayName[128] = ""; + static char newThemeDescription[256] = ""; + + // Theme Preset Selection + SeparatorTextWithFont("Theme Preset", Menu::FontRole::Subheading); + + // Get theme manager + auto themeManager = ThemeManager::GetSingleton(); + + // Get available themes (force discovery if not done) + if (!themeManager->IsDiscovered()) { + themeManager->DiscoverThemes(); + } + + const auto& themes = themeManager->GetThemes(); + + // Create dropdown items - using static storage to avoid dangling pointers + static std::vector displayNames; + static std::vector items; + + // Clear and rebuild the lists + displayNames.clear(); + items.clear(); + + // Reserve capacity to prevent reallocations that would invalidate pointers + displayNames.reserve(themes.size() + 1); + items.reserve(themes.size() + 1); + + // Add "+ Create New" option at the top + displayNames.push_back("+ Create New"); + items.push_back(displayNames.back().c_str()); + + for (const auto& theme : themes) { + displayNames.push_back(theme.displayName); + items.push_back(displayNames.back().c_str()); + } + + // Find current selection index - default to "Default" if no theme selected + // Note: Add 1 to account for "+ Create New" option at index 0 + int currentItem = 1; // Default to first actual theme (Default Dark) + std::string currentThemePreset = globals::menu->GetSettings().SelectedThemePreset; + + // If no theme is selected, default to "Default" + if (currentThemePreset.empty()) { + currentThemePreset = "Default"; + globals::menu->GetSettings().SelectedThemePreset = "Default"; + } + + // If we're in create new mode, show that as selected + if (isCreatingNewTheme) { + currentItem = 0; // "+ Create New" + } else { + // Find the theme in the list (skip index 0 which is "+ Create New") + for (size_t i = 0; i < themes.size(); ++i) { + if (themes[i].name == currentThemePreset) { + currentItem = static_cast(i + 1); // +1 for "+ Create New" offset + break; + } + } + } + + // Theme preset dropdown + if (ComboWithFont("##ThemePreset", ¤tItem, items.data(), static_cast(items.size()), Menu::FontRole::Body)) { + if (currentItem == 0) { + // "+ Create New" selected + isCreatingNewTheme = true; + // Keep current theme settings as starting point + } else if (currentItem >= 1 && currentItem <= static_cast(themes.size())) { + // Actual theme selected (subtract 1 for "+ Create New" offset) + isCreatingNewTheme = false; + std::string selectedTheme = themes[currentItem - 1].name; + if (globals::menu->LoadThemePreset(selectedTheme)) { + // Theme loaded successfully, update UI + themeSettings = globals::menu->GetSettings().Theme; + } + } + } + + // Show theme description as tooltip (only for actual themes, not "+ Create New") + if (currentItem >= 1 && currentItem <= static_cast(themes.size())) { + const auto& selectedTheme = themes[currentItem - 1]; // -1 for "+ Create New" offset + if (!selectedTheme.description.empty()) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", selectedTheme.description.c_str()); + } + } + } + + // Theme action buttons (moved below dropdown to prevent clipping) + if (ImGui::Button("Refresh Themes")) { + themeManager->RefreshThemes(); + // Ensure a valid theme is still selected + const auto* themeInfo = themeManager->GetThemeInfo(globals::menu->GetSettings().SelectedThemePreset); + if (!themeInfo) { + globals::menu->GetSettings().SelectedThemePreset = "Default"; + } + } + + ImGui::SameLine(); + if (ImGui::Button("Open Themes Folder")) { + std::filesystem::path themesPath = Util::PathHelpers::GetThemesRealPath(); + ShellExecuteA(NULL, "open", themesPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); + } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "When enabled: Shows action buttons (Save, Load, Clear Cache) as icons in the header\n" - "When disabled: Shows as text buttons below the header"); + ImGui::Text("Opens the Themes folder where you can add custom theme files."); } - ImGui::SliderFloat("Tooltip Hover Delay", &themeSettings.TooltipHoverDelay, 0.0f, 2.0f, "%.2f s", ImGuiSliderFlags_AlwaysClamp); + // Save/Update Theme Button (show based on context) + if (isCreatingNewTheme || (!currentThemePreset.empty() && currentThemePreset != "Default")) { + ImGui::SameLine(); + + const char* buttonText = isCreatingNewTheme ? "Save Theme" : "Update Theme"; + if (Util::ButtonWithFlash(buttonText)) { + if (isCreatingNewTheme) { + // Show popup for new theme creation + showCreateThemePopup = true; + // Clear the input fields + memset(newThemeName, 0, sizeof(newThemeName)); + memset(newThemeDisplayName, 0, sizeof(newThemeDisplayName)); + memset(newThemeDescription, 0, sizeof(newThemeDescription)); + } else { + // Update existing theme + const auto* currentThemeInfo = themeManager->GetThemeInfo(currentThemePreset); + if (currentThemeInfo) { + // Use the existing SaveTheme method to serialize the theme settings + json currentThemeJson; + globals::menu->SaveTheme(currentThemeJson); + + logger::info("Attempting to update theme: '{}'", currentThemePreset); + + // Overwrite the current theme with updated settings + if (themeManager->SaveTheme(currentThemePreset, currentThemeJson["Theme"], + currentThemeInfo->displayName, currentThemeInfo->description)) { + logger::info("Theme '{}' updated successfully", currentThemePreset); + } else { + logger::error("Failed to update theme: '{}'", currentThemePreset); + } + } else { + logger::warn("Cannot update theme '{}' - theme info not found", currentThemePreset); + } + } + } + if (auto _tt = Util::HoverTooltipWrapper()) { + if (isCreatingNewTheme) { + ImGui::Text("Create a new theme with current settings"); + } else { + ImGui::Text("Updates the currently selected theme (%s) with your current settings", currentThemePreset.c_str()); + } + } + } + + // Create Theme Popup + if (showCreateThemePopup) { + ImGui::OpenPopup("Create New Theme"); + } + + // Popup modal for creating new theme + if (ImGui::BeginPopupModal("Create New Theme", &showCreateThemePopup, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Create a new theme with your current settings:"); + ImGui::Separator(); + + ImGui::InputText("Theme Name", newThemeName, sizeof(newThemeName)); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("File name for the theme (without .json extension)"); + } + + ImGui::InputText("Display Name", newThemeDisplayName, sizeof(newThemeDisplayName)); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Human-readable name shown in the dropdown"); + } + + ImGui::InputTextMultiline("Description", newThemeDescription, sizeof(newThemeDescription), ImVec2(400, 80)); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Optional description for the theme"); + } + + ImGui::Separator(); + + // Buttons + if (Util::ButtonWithFlash("Create Theme") && strlen(newThemeName) > 0) { + // Use the existing SaveTheme method to serialize the theme settings + json currentThemeJson; + globals::menu->SaveTheme(currentThemeJson); + + std::string displayName = strlen(newThemeDisplayName) > 0 ? std::string(newThemeDisplayName) : std::string(newThemeName); + std::string description = strlen(newThemeDescription) > 0 ? std::string(newThemeDescription) : ""; + + logger::info("Attempting to save new theme: '{}' with display name: '{}'", newThemeName, displayName); + + if (themeManager->SaveTheme(std::string(newThemeName), currentThemeJson["Theme"], displayName, description)) { + logger::info("Theme saved successfully. Loading theme preset: '{}'", newThemeName); + // Theme created successfully, load it and exit create mode + globals::menu->LoadThemePreset(std::string(newThemeName)); + isCreatingNewTheme = false; + showCreateThemePopup = false; + logger::info("Theme creation complete. Total themes: {}", themeManager->GetThemes().size()); + } else { + logger::error("Failed to save theme: '{}'", newThemeName); + } + } + + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + showCreateThemePopup = false; + } + + ImGui::EndPopup(); + } + + ImGui::EndTabItem(); + } +} + +void SettingsTabRenderer::RenderFontsTab() +{ + if (BeginTabItemWithFont("Fonts", Menu::FontRole::Heading)) { + auto* menuInstance = globals::menu; + auto& themeSettings = menuInstance->GetSettings().Theme; + + SeparatorTextWithFont("Font", Menu::FontRole::Subheading); + + bool useAutoFont = (themeSettings.FontSize <= 0.0f); + if (ImGui::Checkbox("Use resolution-based font size", &useAutoFont)) { + if (useAutoFont) { + themeSettings.FontSize = 0.0f; + } else { + float effective = ThemeManager::ResolveFontSize(*menuInstance); + themeSettings.FontSize = std::clamp(effective, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE); + } + menuInstance->pendingFontReload = true; + } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Time in seconds to wait before a tooltip appears when hovering over an item."); + ImGui::TextUnformatted("When enabled, the UI font size scales with your screen resolution. Disable to set a fixed size."); + } + + ImGui::BeginDisabled(useAutoFont); + if (ImGui::SliderFloat("Base Font Size", &themeSettings.FontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE, "%.0f")) { + menuInstance->pendingFontReload = true; + } + ImGui::EndDisabled(); + + float effectiveNow = ThemeManager::ResolveFontSize(*menuInstance); + ImGui::Text("Effective size: %.0f px", std::round(effectiveNow)); + + static Util::Fonts::Catalog fontCatalog; + static bool catalogInitialized = false; + auto refreshFontCatalog = [&]() { + fontCatalog = Util::Fonts::DiscoverFontCatalog(); + }; + + if (!catalogInitialized) { + refreshFontCatalog(); + catalogInitialized = true; + } + + ImGui::Spacing(); + SeparatorTextWithFont("Font Roles", Menu::FontRole::Subheading); + + if (fontCatalog.families.empty()) { + ImGui::TextColored(ImVec4(0.9f, 0.6f, 0.2f, 1.0f), "No fonts found. Place .ttf files in Interface/CommunityShaders/Fonts/"); + } + + for (size_t roleIndex = 0; roleIndex < Menu::FontRoleDescriptors.size(); ++roleIndex) { + auto role = static_cast(roleIndex); + auto descriptor = Menu::FontRoleDescriptors[roleIndex]; + auto& roleSettings = themeSettings.FontRoles[roleIndex]; + + ImGui::PushID(static_cast(roleIndex)); + { + FontRoleGuard headingFont(Menu::FontRole::Subheading); + ImGui::TextUnformatted(descriptor.displayName.data()); + } + + int familyIndex = 0; + if (!fontCatalog.families.empty()) { + for (size_t i = 0; i < fontCatalog.families.size(); ++i) { + if (iequals(fontCatalog.families[i].name, roleSettings.Family)) { + familyIndex = static_cast(i); + break; + } + } + if (familyIndex >= static_cast(fontCatalog.families.size())) { + familyIndex = 0; + } + } + + const char* familyPreview = fontCatalog.families.empty() ? "No families" : fontCatalog.families[familyIndex].displayName.c_str(); + std::string familyLabel = std::format("{} Family##{}", descriptor.displayName, roleIndex); + { + FontRoleGuard familyComboFont(Menu::FontRole::Body); + if (ImGui::BeginCombo(familyLabel.c_str(), familyPreview)) { + if (fontCatalog.families.empty()) { + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No font families available"); + } else { + for (int i = 0; i < static_cast(fontCatalog.families.size()); ++i) { + bool isSelected = (i == familyIndex); + if (ImGui::Selectable(fontCatalog.families[i].displayName.c_str(), isSelected)) { + familyIndex = i; + if (!isSelected) { + const auto& newFamily = fontCatalog.families[i]; + roleSettings.Family = newFamily.name; + if (!newFamily.styles.empty()) { + const auto& firstStyle = newFamily.styles.front(); + roleSettings.Style = firstStyle.style; + roleSettings.File = firstStyle.file; + } else { + roleSettings.Style.clear(); + roleSettings.File.clear(); + } + if (role == Menu::FontRole::Body) { + themeSettings.FontName = roleSettings.File; + } + menuInstance->pendingFontReload = true; + } + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + } + ImGui::EndCombo(); + } + } + + const Util::Fonts::FamilyInfo* selectedFamily = (fontCatalog.families.empty()) ? nullptr : &fontCatalog.families[familyIndex]; + if (selectedFamily && selectedFamily->styles.empty()) { + ImGui::TextColored(ImVec4(0.9f, 0.6f, 0.2f, 1.0f), "No style variants found for this family."); + } else if (selectedFamily) { + int styleIndex = 0; + for (size_t s = 0; s < selectedFamily->styles.size(); ++s) { + if (iequals(selectedFamily->styles[s].style, roleSettings.Style)) { + styleIndex = static_cast(s); + break; + } + } + if (styleIndex >= static_cast(selectedFamily->styles.size())) { + styleIndex = 0; + } + const char* stylePreview = selectedFamily->styles.empty() ? "No styles" : selectedFamily->styles[styleIndex].displayName.c_str(); + std::string styleLabel = std::format("{} Style##{}", descriptor.displayName, roleIndex); + { + FontRoleGuard styleComboFont(Menu::FontRole::Body); + if (ImGui::BeginCombo(styleLabel.c_str(), stylePreview)) { + for (int s = 0; s < static_cast(selectedFamily->styles.size()); ++s) { + bool isSelected = (s == styleIndex); + if (ImGui::Selectable(selectedFamily->styles[s].displayName.c_str(), isSelected)) { + if (!isSelected) { + const auto& chosen = selectedFamily->styles[s]; + roleSettings.Style = chosen.style; + roleSettings.File = chosen.file; + roleSettings.Family = selectedFamily->name; + if (role == Menu::FontRole::Body) { + themeSettings.FontName = roleSettings.File; + } + menuInstance->pendingFontReload = true; + } + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + } + } + + ImGui::TextDisabled("File: %s", roleSettings.File.c_str()); + + std::string scaleLabel = std::format("{} Scale##{}", descriptor.displayName, roleIndex); + if (ImGui::SliderFloat(scaleLabel.c_str(), &roleSettings.SizeScale, 0.5f, 2.5f, "%.2fx", ImGuiSliderFlags_AlwaysClamp)) { + menuInstance->pendingFontReload = true; + } + ImGui::SameLine(); + std::string resetLabel = std::format("Reset##Scale{}", roleIndex); + if (ImGui::Button(resetLabel.c_str())) { + roleSettings.SizeScale = Menu::GetFontRoleDefaultScale(role); + menuInstance->pendingFontReload = true; + } + + ImGui::Separator(); + ImGui::PopID(); + } + + if (ImGui::Button("Refresh Font Families")) { + refreshFontCatalog(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::TextUnformatted("Rescan the Fonts directory after adding or removing font files."); } ImGui::EndTabItem(); } } -void SettingsTabRenderer::RenderSizesTab() +void SettingsTabRenderer::RenderStylingTab() { - if (ImGui::BeginTabItem("Sizes")) { + if (BeginTabItemWithFont("Styling", Menu::FontRole::Heading)) { auto& themeSettings = globals::menu->GetSettings().Theme; auto& style = themeSettings.Style; - ImGui::SeparatorText("Main"); + SeparatorTextWithFont("Styling Options", Menu::FontRole::Subheading); + + ImGui::Checkbox("Show Icon Buttons in Header", &themeSettings.ShowActionIcons); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "When enabled: Shows action buttons (Save, Load, Clear Cache) as icons in the header\n" + "When disabled: Shows as text buttons below the header"); + } + + if (themeSettings.ShowActionIcons) { + ImGui::Indent(); + if (ImGui::Checkbox("Use Monochrome Icons", &themeSettings.UseMonochromeIcons)) { + // Defer icon reload to next frame to avoid rendering with released textures + globals::menu->pendingIconReload = true; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Uses white monochrome icons that adapt to your theme's text color"); + } + ImGui::SameLine(); + if (ImGui::Checkbox("Use Monochrome CS Logo", &themeSettings.UseMonochromeLogo)) { + globals::menu->pendingIconReload = true; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Uses monochrome version of the Community Shaders logo"); + } + ImGui::Unindent(); + } + + ImGui::Checkbox("Show Footer", &themeSettings.ShowFooter); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Shows the footer with game version, swap chain, and GPU information at the bottom of the window"); + } + + ImGui::Checkbox("Center Header Title", &themeSettings.CenterHeader); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Centers the Community Shaders title and logo in the header title bar"); + } + + ImGui::SliderFloat("Tooltip Hover Delay", &themeSettings.TooltipHoverDelay, 0.0f, 2.0f, "%.2f s", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::TextUnformatted("Time in seconds to wait before a tooltip appears when hovering over an item."); + } + + if (ImGui::Checkbox("Background Blur", &themeSettings.BackgroundBlurEnabled)) { + BackgroundBlur::SetEnabled(themeSettings.BackgroundBlurEnabled); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Applies a blur effect to the background behind the menu window."); + } + + SeparatorTextWithFont("Main", Menu::FontRole::Subheading); if (ImGui::SliderFloat("Global Scale", &themeSettings.GlobalScale, -1.f, 1.f, "%.2f")) { float trueScale = exp2(themeSettings.GlobalScale); auto& io = ImGui::GetIO(); io.FontGlobalScale = trueScale; } - ImGui::SliderFloat("Font Size", &themeSettings.FontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE, "%.0f"); + + SeparatorTextWithFont("Layout", Menu::FontRole::Subheading); + ImGui::SliderFloat2("Window Padding", (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("Frame Padding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("Item Spacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); @@ -195,7 +800,21 @@ void SettingsTabRenderer::RenderSizesTab() ImGui::SliderFloat("Scrollbar Size", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); ImGui::SliderFloat("Grab Min Size", &style.GrabMinSize, 1.0f, 20.0f, "%.0f"); - ImGui::SeparatorText("Borders"); + SeparatorTextWithFont("Scrollbar Opacity", Menu::FontRole::Subheading); + ImGui::SliderFloat("Track Opacity", &themeSettings.ScrollbarOpacity.Background, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Controls the opacity of the scrollbar track/channel (the background area behind the scrollbar)."); + ImGui::SliderFloat("Thumb Opacity", &themeSettings.ScrollbarOpacity.Thumb, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Controls the opacity of the scrollbar thumb (the draggable part)."); + ImGui::SliderFloat("Thumb Hovered Opacity", &themeSettings.ScrollbarOpacity.ThumbHovered, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Controls the opacity of the scrollbar thumb when hovered."); + ImGui::SliderFloat("Thumb Active Opacity", &themeSettings.ScrollbarOpacity.ThumbActive, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Controls the opacity of the scrollbar thumb when being dragged."); + + SeparatorTextWithFont("Borders", Menu::FontRole::Subheading); ImGui::SliderFloat("Window Border Size", &style.WindowBorderSize, 0.0f, 5.0f, "%.0f"); ImGui::SliderFloat("Child Border Size", &style.ChildBorderSize, 0.0f, 5.0f, "%.0f"); ImGui::SliderFloat("Popup Border Size", &style.PopupBorderSize, 0.0f, 5.0f, "%.0f"); @@ -203,7 +822,7 @@ void SettingsTabRenderer::RenderSizesTab() ImGui::SliderFloat("Tab Border Size", &style.TabBorderSize, 0.0f, 5.0f, "%.0f"); ImGui::SliderFloat("Tab Bar Border Size", &style.TabBarBorderSize, 0.0f, 5.0f, "%.0f"); - ImGui::SeparatorText("Rounding"); + SeparatorTextWithFont("Rounding", Menu::FontRole::Subheading); ImGui::SliderFloat("Window Rounding", &style.WindowRounding, 0.0f, 12.0f, "%.0f"); ImGui::SliderFloat("Child Rounding", &style.ChildRounding, 0.0f, 12.0f, "%.0f"); ImGui::SliderFloat("Frame Rounding", &style.FrameRounding, 0.0f, 12.0f, "%.0f"); @@ -212,12 +831,15 @@ void SettingsTabRenderer::RenderSizesTab() ImGui::SliderFloat("Grab Rounding", &style.GrabRounding, 0.0f, 12.0f, "%.0f"); ImGui::SliderFloat("Tab Rounding", &style.TabRounding, 0.0f, 12.0f, "%.0f"); - ImGui::SeparatorText("Tables"); + SeparatorTextWithFont("Tables", Menu::FontRole::Subheading); ImGui::SliderFloat2("Cell Padding", (float*)&style.CellPadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderAngle("Table Angled Headers Angle", &style.TableAngledHeadersAngle, -50.0f, +50.0f); - ImGui::SeparatorText("Widgets"); - ImGui::Combo("ColorButtonPosition", (int*)&style.ColorButtonPosition, "Left\0Right\0"); + SeparatorTextWithFont("Widgets", Menu::FontRole::Subheading); + { + FontRoleGuard comboFont(Menu::FontRole::Body); + ImGui::Combo("ColorButtonPosition", (int*)&style.ColorButtonPosition, "Left\0Right\0"); + } ImGui::SliderFloat2("Button Text Align", (float*)&style.ButtonTextAlign, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text("Alignment applies when a button is larger than its text content."); @@ -229,7 +851,7 @@ void SettingsTabRenderer::RenderSizesTab() ImGui::SliderFloat2("Separator Text Padding", (float*)&style.SeparatorTextPadding, 0.0f, 40.0f, "%.0f"); ImGui::SliderFloat("Log Slider Deadzone", &style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f"); - ImGui::SeparatorText("Docking"); + SeparatorTextWithFont("Docking", Menu::FontRole::Subheading); ImGui::SliderFloat("Docking Splitter Size", &style.DockingSeparatorSize, 0.0f, 12.0f, "%.0f"); ImGui::EndTabItem(); @@ -238,48 +860,98 @@ void SettingsTabRenderer::RenderSizesTab() void SettingsTabRenderer::RenderColorsTab() { - if (ImGui::BeginTabItem("Colors")) { + if (BeginTabItemWithFont("Colors", Menu::FontRole::Heading)) { auto& themeSettings = globals::menu->GetSettings().Theme; auto& colors = themeSettings.FullPalette; - ImGui::SeparatorText("Status"); + // Color filter at the top with search icon + static ImGuiTextFilter colorFilter; - ImGui::ColorEdit4("Disabled Text", (float*)&themeSettings.StatusPalette.Disable); - ImGui::ColorEdit4("Error Text", (float*)&themeSettings.StatusPalette.Error); - ImGui::ColorEdit4("Warning Text", (float*)&themeSettings.StatusPalette.Warning); - ImGui::ColorEdit4("Restart Needed Text", (float*)&themeSettings.StatusPalette.RestartNeeded); - ImGui::ColorEdit4("Current Hotkey Text", (float*)&themeSettings.StatusPalette.CurrentHotkey); - ImGui::ColorEdit4("Success Text", (float*)&themeSettings.StatusPalette.SuccessColor); - ImGui::ColorEdit4("Info Text", (float*)&themeSettings.StatusPalette.InfoColor); + float iconSize = 20.0f; + float iconSpace = iconSize + 14.0f; + ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + float availableWidth = ImGui::GetFontSize() * 16; + float frameHeight = ImGui::GetFrameHeight(); - ImGui::SeparatorText("Feature Headings"); + // Custom style for filter with icon space + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f)); + colorFilter.Draw("Filter colors", availableWidth); + ImGui::PopStyleVar(); - ImGui::ColorEdit4("Regular", (float*)&themeSettings.FeatureHeading.ColorDefault); - ImGui::ColorEdit4("Hovered", (float*)&themeSettings.FeatureHeading.ColorHovered); - ImGui::SliderFloat("Minimized Alpha Factor", &themeSettings.FeatureHeading.MinimizedFactor, 0.0f, 1.0f, "%.2f"); + // Draw search icon + ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 center = ImVec2(iconPos.x + iconSize * 0.46f, iconPos.y + iconSize * 0.5f); + float radius = iconSize * 0.3f; - ImGui::SeparatorText("Palette"); + auto& palette = globals::menu->GetTheme().Palette; + ImVec4 iconColor = palette.Text; + iconColor.w *= 0.7f; + ImU32 iconColorU32 = ImGui::GetColorU32(iconColor); - if (ImGui::RadioButton("Simple Palette", themeSettings.UseSimplePalette)) - themeSettings.UseSimplePalette = true; - ImGui::SameLine(); - if (ImGui::RadioButton("Full Palette", !themeSettings.UseSimplePalette)) - themeSettings.UseSimplePalette = false; + drawList->AddCircle(center, radius, iconColorU32, 12, 2.2f); + ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); + ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); + drawList->AddLine(handleStart, handleEnd, iconColorU32, 2.1f); - if (themeSettings.UseSimplePalette) { + ImGui::Spacing(); + + // Background & Text + if (colorFilter.PassFilter("Background")) ImGui::ColorEdit4("Background", (float*)&themeSettings.Palette.Background); + if (colorFilter.PassFilter("Text")) ImGui::ColorEdit4("Text", (float*)&themeSettings.Palette.Text); - ImGui::ColorEdit4("Border", (float*)&themeSettings.Palette.Border); - } else { - static ImGuiTextFilter filter; - filter.Draw("Filter colors", ImGui::GetFontSize() * 16); + + if (ImGui::TreeNodeEx("Borders & Separators", ImGuiTreeNodeFlags_DefaultOpen)) { + if (colorFilter.PassFilter("Window Border")) + ImGui::ColorEdit4("Window Border", (float*)&themeSettings.Palette.WindowBorder); + if (colorFilter.PassFilter("Slider & Input Background")) + ImGui::ColorEdit4("Slider & Input Background", (float*)&themeSettings.Palette.FrameBorder); + if (colorFilter.PassFilter("Separator Line")) + ImGui::ColorEdit4("Separator Line", (float*)&themeSettings.Palette.Separator); + if (colorFilter.PassFilter("Resize Grip")) + ImGui::ColorEdit4("Resize Grip", (float*)&themeSettings.Palette.ResizeGrip); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Feature Headings", ImGuiTreeNodeFlags_DefaultOpen)) { + if (colorFilter.PassFilter("Default")) + ImGui::ColorEdit4("Default", (float*)&themeSettings.FeatureHeading.ColorDefault); + if (colorFilter.PassFilter("Hovered")) + ImGui::ColorEdit4("Hovered", (float*)&themeSettings.FeatureHeading.ColorHovered); + if (colorFilter.PassFilter("Minimized Transparency")) + ImGui::SliderFloat("Minimized Transparency", &themeSettings.FeatureHeading.MinimizedFactor, 0.0f, 1.0f, "%.2f"); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Status", ImGuiTreeNodeFlags_DefaultOpen)) { + if (colorFilter.PassFilter("Disabled")) + ImGui::ColorEdit4("Disabled", (float*)&themeSettings.StatusPalette.Disable); + if (colorFilter.PassFilter("Error")) + ImGui::ColorEdit4("Error", (float*)&themeSettings.StatusPalette.Error); + if (colorFilter.PassFilter("Warning")) + ImGui::ColorEdit4("Warning", (float*)&themeSettings.StatusPalette.Warning); + if (colorFilter.PassFilter("Restart Needed")) + ImGui::ColorEdit4("Restart Needed", (float*)&themeSettings.StatusPalette.RestartNeeded); + if (colorFilter.PassFilter("Current Hotkey")) + ImGui::ColorEdit4("Current Hotkey", (float*)&themeSettings.StatusPalette.CurrentHotkey); + if (colorFilter.PassFilter("Success")) + ImGui::ColorEdit4("Success", (float*)&themeSettings.StatusPalette.SuccessColor); + if (colorFilter.PassFilter("Info")) + ImGui::ColorEdit4("Info", (float*)&themeSettings.StatusPalette.InfoColor); + ImGui::TreePop(); + } + + if (ImGui::TreeNode("Full Palette")) { + ImGui::TextWrapped("Advanced color controls for detailed customization of all UI elements."); for (int i = 0; i < ImGuiCol_COUNT; i++) { - const char* name = ImGui::GetStyleColorName(i); - if (!filter.PassFilter(name)) + const char* friendlyName = GetFriendlyColorName(i); + if (!colorFilter.PassFilter(friendlyName)) continue; - ImGui::ColorEdit4(name, (float*)&colors[i], ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_AlphaPreviewHalf); + ImGui::ColorEdit4(friendlyName, (float*)&colors[i], ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_AlphaPreviewHalf); } + ImGui::TreePop(); } ImGui::EndTabItem(); diff --git a/src/Menu/SettingsTabRenderer.h b/src/Menu/SettingsTabRenderer.h index f4d6ce67da..2247a0c6cb 100644 --- a/src/Menu/SettingsTabRenderer.h +++ b/src/Menu/SettingsTabRenderer.h @@ -15,6 +15,8 @@ class SettingsTabRenderer bool& settingsEffectsToggle; bool& settingSkipCompilationKey; bool& settingOverlayToggleKey; + bool& settingShaderBlockPrevKey; // Debug: shader block previous key + bool& settingShaderBlockNextKey; // Debug: shader block next key }; static void RenderGeneralSettings( @@ -29,7 +31,8 @@ class SettingsTabRenderer static void RenderInterfaceTab(); // Interface sub-tabs - static void RenderUIOptionsTab(); - static void RenderSizesTab(); + static void RenderThemesTab(); + static void RenderFontsTab(); + static void RenderStylingTab(); static void RenderColorsTab(); }; \ No newline at end of file diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 787899ba3c..f15d229ed5 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -1,13 +1,68 @@ #include "ThemeManager.h" #include "../Menu.h" +#include "BackgroundBlur.h" +#include "Fonts.h" + #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include +#include #include "RE/Skyrim.h" -#include "Util.h" +#include "State.h" + +#include "../Globals.h" +#include "../Util.h" +#include "../Utils/FileSystem.h" +#include "../Utils/UI.h" +using namespace SKSE; + +namespace +{ + // Theme System Constants + // ====================== + + // Text Contrast and Opacity + // ------------------------- + // Disabled text alpha: Makes inactive UI elements visually distinct but still readable + // Value calibrated for accessibility - too low = invisible, too high = looks enabled + constexpr float DISABLED_TEXT_ALPHA = 0.3f; // 30% opacity for disabled elements + + // Resize grip hover alpha: Subtle hover effect to avoid visual clutter + // Low value maintains minimalist aesthetic while providing hover feedback + constexpr float RESIZE_GRIP_HOVER_ALPHA = 0.1f; // 10% opacity for hover state + + /** + * @brief Gets file modification time + */ + std::time_t GetFileModTime(const std::filesystem::path& filePath) + { + try { + auto fileTime = std::filesystem::last_write_time(filePath); + auto systemTime = std::chrono::time_point_cast( + fileTime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); + return std::chrono::system_clock::to_time_t(systemTime); + } catch (...) { + return 0; + } + } +} + +// Static UI helper methods void ThemeManager::SetupImGuiStyle(const Menu& menu) { auto& style = ImGui::GetStyle(); @@ -16,129 +71,734 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) // Theme based on https://github.com/powerof3/DialogueHistory auto& themeSettings = menu.GetTheme(); + // Safety check: If theme appears corrupted/empty, force reload Default.json + // This prevents fallback to ImGui's hardcoded defaults + bool isThemeCorrupted = (themeSettings.FullPalette.size() < ImGuiCol_COUNT / 2) || + (themeSettings.Palette.Background.w == 0.0f && themeSettings.Palette.Text.w == 0.0f); + + if (isThemeCorrupted) { + logger::warn("Theme appears corrupted, attempting emergency reload of Default.json"); + // Emergency recovery: const_cast is acceptable here to prevent total UI failure + if (const_cast(&menu)->LoadThemePreset("Default")) { + logger::info("Successfully recovered with Default.json theme"); + } else { + logger::error("Failed to reload Default.json - ImGui may revert to hardcoded defaults"); + } + } + // rescale here auto styleCopy = themeSettings.Style; - styleCopy.ScaleAllSizes(exp2(themeSettings.GlobalScale)); + + float globalScale = themeSettings.GlobalScale; + + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default + if (std::abs(globalScale - Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { + globalScale = Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 + } + + styleCopy.ScaleAllSizes(exp2(globalScale)); styleCopy.MouseCursorScale = 1.f; style = styleCopy; style.HoverDelayNormal = themeSettings.TooltipHoverDelay; - if (themeSettings.UseSimplePalette) { - float hoveredAlpha{ 0.1f }; + // Always use the unified FullPalette system instead of switching between simple/full + // This ensures consistent behavior regardless of UI presentation mode + for (size_t i = 0; i < std::min(themeSettings.FullPalette.size(), static_cast(ImGuiCol_COUNT)); ++i) { + colors[i] = themeSettings.FullPalette[i]; + } - ImVec4 resizeGripHovered = themeSettings.Palette.Border; - resizeGripHovered.w = hoveredAlpha; + // Apply simple palette overrides to the FullPalette for key colors + // This allows the simple palette controls to work by updating the FullPalette + colors[ImGuiCol_WindowBg] = themeSettings.Palette.Background; + colors[ImGuiCol_Text] = themeSettings.Palette.Text; + colors[ImGuiCol_Border] = themeSettings.Palette.WindowBorder; + colors[ImGuiCol_Separator] = themeSettings.Palette.Separator; + colors[ImGuiCol_ResizeGrip] = themeSettings.Palette.ResizeGrip; + + // Apply frame border to UI elements with frames/borders + colors[ImGuiCol_FrameBg] = themeSettings.Palette.FrameBorder; + colors[ImGuiCol_CheckMark] = themeSettings.Palette.Text; + colors[ImGuiCol_SliderGrab] = themeSettings.Palette.FrameBorder; + colors[ImGuiCol_SliderGrabActive] = themeSettings.Palette.FrameBorder; + + // Apply derived colors based on simple palette + ImVec4 textDisabled = themeSettings.Palette.Text; + textDisabled.w = DISABLED_TEXT_ALPHA; + colors[ImGuiCol_TextDisabled] = textDisabled; + + ImVec4 resizeGripHovered = themeSettings.Palette.ResizeGrip; + resizeGripHovered.w = RESIZE_GRIP_HOVER_ALPHA; + colors[ImGuiCol_ResizeGripHovered] = resizeGripHovered; + colors[ImGuiCol_ResizeGripActive] = resizeGripHovered; + + // Apply scrollbar opacity settings + colors[ImGuiCol_ScrollbarBg].w = themeSettings.ScrollbarOpacity.Background; + colors[ImGuiCol_ScrollbarGrab].w = themeSettings.ScrollbarOpacity.Thumb; + colors[ImGuiCol_ScrollbarGrabHovered].w = themeSettings.ScrollbarOpacity.ThumbHovered; + colors[ImGuiCol_ScrollbarGrabActive].w = themeSettings.ScrollbarOpacity.ThumbActive; +} - ImVec4 textDisabled = themeSettings.Palette.Text; - textDisabled.w = 0.3f; +void ThemeManager::ForceApplyDefaultTheme() +{ + // This function applies Default.json colors directly to ImGui, bypassing any hardcoded defaults + // It's used when the theme system fails or ImGui resets to defaults unexpectedly - ImVec4 header{ 1.0f, 1.0f, 1.0f, 0.15f }; - ImVec4 headerHovered = header; - headerHovered.w = hoveredAlpha; + auto* themeManager = GetSingleton(); + json defaultThemeSettings; - ImVec4 tabHovered{ 0.2f, 0.2f, 0.2f, 1.0f }; + if (!themeManager->LoadTheme("Default", defaultThemeSettings)) { + logger::warn("ForceApplyDefaultTheme: Could not load Default.json theme"); + return; + } - ImVec4 sliderGrab{ 1.0f, 1.0f, 1.0f, 0.245f }; - ImVec4 sliderGrabActive{ 1.0f, 1.0f, 1.0f, 0.531f }; + auto& style = ImGui::GetStyle(); + auto& colors = style.Colors; - ImVec4 scrollbarGrab{ 0.31f, 0.31f, 0.31f, 1.0f }; - ImVec4 scrollbarGrabHovered{ 0.41f, 0.41f, 0.41f, 1.0f }; - ImVec4 scrollbarGrabActive{ 0.51f, 0.51f, 0.51f, 1.0f }; + // Apply the Default.json theme's FullPalette directly to ImGui colors + if (defaultThemeSettings.contains("FullPalette") && defaultThemeSettings["FullPalette"].is_array()) { + auto& palette = defaultThemeSettings["FullPalette"]; + + for (size_t i = 0; i < std::min(palette.size(), static_cast(ImGuiCol_COUNT)); ++i) { + if (palette[i].is_array() && palette[i].size() >= 4) { + colors[i] = ImVec4( + palette[i][0].get(), + palette[i][1].get(), + palette[i][2].get(), + palette[i][3].get()); + } + } + logger::info("ForceApplyDefaultTheme: Applied Default.json colors directly to ImGui"); + } else { + logger::warn("ForceApplyDefaultTheme: Default.json missing FullPalette - applying basic dark theme"); + + // Fallback: Apply a basic dark theme that matches Default.json style + colors[ImGuiCol_WindowBg] = ImVec4(0.05f, 0.05f, 0.05f, 1.0f); // Dark background + colors[ImGuiCol_Text] = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White text + colors[ImGuiCol_Border] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Gray border + colors[ImGuiCol_ChildBg] = ImVec4(0.03f, 0.03f, 0.03f, 1.0f); // Slightly darker child background + colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 1.0f); // Popup background + colors[ImGuiCol_Header] = ImVec4(0.2f, 0.2f, 0.2f, 1.0f); // Header background + colors[ImGuiCol_HeaderHovered] = ImVec4(0.3f, 0.3f, 0.3f, 1.0f); // Header hover + colors[ImGuiCol_HeaderActive] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Header active + colors[ImGuiCol_Button] = ImVec4(0.2f, 0.2f, 0.2f, 1.0f); // Button background + colors[ImGuiCol_ButtonHovered] = ImVec4(0.3f, 0.3f, 0.3f, 1.0f); // Button hover + colors[ImGuiCol_ButtonActive] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Button active + } +} - colors[ImGuiCol_WindowBg] = themeSettings.Palette.Background; - colors[ImGuiCol_ChildBg] = ImVec4(); - colors[ImGuiCol_ScrollbarBg] = ImVec4(); - colors[ImGuiCol_TableHeaderBg] = ImVec4(); - colors[ImGuiCol_TableRowBg] = ImVec4(); - colors[ImGuiCol_TableRowBgAlt] = ImVec4(); +bool ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) +{ + // Thread-safe reentrancy guard using atomic flag + static std::atomic isReloading{ false }; + bool expected = false; + if (!isReloading.compare_exchange_strong(expected, true)) { + return false; + } - colors[ImGuiCol_Border] = themeSettings.Palette.Border; - colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; - colors[ImGuiCol_ResizeGrip] = colors[ImGuiCol_Border]; - colors[ImGuiCol_ResizeGripHovered] = resizeGripHovered; - colors[ImGuiCol_ResizeGripActive] = colors[ImGuiCol_ResizeGripHovered]; + // RAII scope guard to ensure isReloading is always reset on exit (exceptions, returns, etc.) + struct ReloadGuard + { + std::atomic& flag; + explicit ReloadGuard(std::atomic& f) : + flag(f) {} + ~ReloadGuard() { flag = false; } + } guard(isReloading); - colors[ImGuiCol_Text] = themeSettings.Palette.Text; - colors[ImGuiCol_TextDisabled] = textDisabled; + auto& themeSettings = menu.GetTheme(); - colors[ImGuiCol_FrameBg] = themeSettings.Palette.Background; - colors[ImGuiCol_FrameBgHovered] = headerHovered; - colors[ImGuiCol_FrameBgActive] = colors[ImGuiCol_FrameBg]; + ImGuiIO& io = ImGui::GetIO(); - colors[ImGuiCol_DockingEmptyBg] = themeSettings.Palette.Border; - colors[ImGuiCol_DockingPreview] = themeSettings.Palette.Border; + // Additional safety checks: ensure ImGui is in a valid state + ImGuiContext* ctx = ImGui::GetCurrentContext(); + if (!ctx) { + logger::error("ReloadFont: No valid ImGui context"); + return false; + } - colors[ImGuiCol_PlotHistogram] = themeSettings.Palette.Border; + // Ensure we're not in the middle of a frame + if (ctx->WithinFrameScope) { + logger::error("ReloadFont: Cannot reload font within frame scope"); + return false; + } - colors[ImGuiCol_SliderGrab] = sliderGrab; - colors[ImGuiCol_SliderGrabActive] = sliderGrabActive; + // Additional rendering state checks + if (ctx->CurrentWindow || ctx->CurrentTable) { + logger::error("ReloadFont: ImGui has active window/table state"); + return false; + } - colors[ImGuiCol_Header] = header; - colors[ImGuiCol_HeaderActive] = colors[ImGuiCol_Header]; - colors[ImGuiCol_HeaderHovered] = headerHovered; + // Additional check: make sure font atlas exists + if (!io.Fonts) { + logger::error("ReloadFont: No font atlas available"); + return false; + } - colors[ImGuiCol_Button] = ImVec4(); - colors[ImGuiCol_ButtonHovered] = headerHovered; - colors[ImGuiCol_ButtonActive] = ImVec4(); + // Verify D3D11 device is valid + auto device = globals::d3d::device; + auto context = globals::d3d::context; + if (!device || !context) { + logger::error("ReloadFont: D3D11 device or context is null"); + return false; + } - colors[ImGuiCol_ScrollbarGrab] = scrollbarGrab; - colors[ImGuiCol_ScrollbarGrabHovered] = scrollbarGrabHovered; - colors[ImGuiCol_ScrollbarGrabActive] = scrollbarGrabActive; + // Clear existing fonts from the atlas + io.Fonts->Clear(); + io.Fonts->TexGlyphPadding = 1; - colors[ImGuiCol_TitleBg] = themeSettings.Palette.Background; - colors[ImGuiCol_TitleBgActive] = colors[ImGuiCol_TitleBg]; - colors[ImGuiCol_TitleBgCollapsed] = colors[ImGuiCol_TitleBg]; + ImFontConfig font_config; - colors[ImGuiCol_MenuBarBg] = colors[ImGuiCol_TitleBg]; + font_config.OversampleH = Constants::FCONF_OVERSAMPLE_H; + font_config.OversampleV = Constants::FCONF_OVERSAMPLE_V; + font_config.PixelSnapH = Constants::FCONF_PIXELSNAP_H; + font_config.RasterizerMultiply = Constants::FCONF_RASTERIZER_MULTIPLY; + + float fontSize = ResolveFontSize(menu); + auto fontsRoot = Util::PathHelpers::GetFontsPath(); + menu.loadedFontRoles.fill(nullptr); + + std::unordered_map atlasCache; + std::vector rolesNeedingFallback; + + for (size_t i = 0; i < static_cast(Menu::FontRole::Count); ++i) { + Menu::FontRole role = static_cast(i); + auto& mutableRoleSettings = const_cast(menu).GetFontRoleSettings(role); + Menu::ThemeSettings::FontRoleSettings effective = themeSettings.FontRoles[i]; + + if (effective.SizeScale <= 0.f) { + effective.SizeScale = Menu::GetFontRoleDefaultScale(role); + } + + if (effective.File.empty()) { + effective = Menu::GetDefaultFontRole(role); + } + + float scaledSize = std::clamp(fontSize * effective.SizeScale, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); + float roundedSize = std::round(scaledSize); + menu.cachedFontPixelSizesByRole[i] = roundedSize; + + ImFont* loadedFont = nullptr; + if (!effective.File.empty()) { + auto fontPath = fontsRoot / effective.File; + + // Security: Validate font path stays within fonts directory + if (!Util::IsPathWithinDirectory(fontsRoot, fontPath)) { + logger::error("Security: Font path traversal attempt for role '{}': {}", + Menu::GetFontRoleKey(role), effective.File); + effective = Menu::GetDefaultFontRole(role); + fontPath = fontsRoot / effective.File; + } + + if (std::filesystem::exists(fontPath)) { + std::string cacheKey = std::format("{}|{}", effective.File, static_cast(roundedSize)); + auto cached = atlasCache.find(cacheKey); + if (cached != atlasCache.end()) { + loadedFont = cached->second; + } else { + ImFontConfig cfg = font_config; + auto* font = io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), roundedSize, &cfg); + if (font) { + atlasCache.emplace(cacheKey, font); + loadedFont = font; + } + } + } + } + + if (!loadedFont) { + rolesNeedingFallback.push_back(i); + } else { + menu.loadedFontRoles[i] = loadedFont; + mutableRoleSettings = effective; + const_cast(menu).cachedFontFilesByRole[i] = effective.File; + } + } + + const size_t bodyIndex = static_cast(Menu::FontRole::Body); + if (!menu.loadedFontRoles[bodyIndex]) { + const auto& defaults = Menu::GetDefaultFontRole(Menu::FontRole::Body); + float bodySize = std::clamp(fontSize * defaults.SizeScale, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); + float roundedBodySize = std::round(bodySize); + menu.cachedFontPixelSizesByRole[bodyIndex] = roundedBodySize; + + ImFont* bodyFont = nullptr; + auto defaultPath = fontsRoot / defaults.File; + if (std::filesystem::exists(defaultPath)) { + std::string cacheKey = std::format("{}|{}", defaults.File, static_cast(roundedBodySize)); + ImFontConfig cfg = font_config; + bodyFont = io.Fonts->AddFontFromFileTTF(defaultPath.string().c_str(), roundedBodySize, &cfg); + if (bodyFont) { + atlasCache.emplace(cacheKey, bodyFont); + } + } + if (!bodyFont) { + bodyFont = io.Fonts->AddFontDefault(); + } + + menu.loadedFontRoles[bodyIndex] = bodyFont; + const_cast(menu).GetFontRoleSettings(Menu::FontRole::Body) = defaults; + const_cast(menu).cachedFontFilesByRole[bodyIndex] = defaults.File; + menu.cachedFontName = defaults.File; + const_cast(menu).GetSettings().Theme.FontName = defaults.File; + } + + ImFont* bodyFont = menu.loadedFontRoles[bodyIndex]; + for (size_t idx : rolesNeedingFallback) { + if (idx == bodyIndex) { + continue; + } + Menu::FontRole role = static_cast(idx); + const auto& defaults = Menu::GetDefaultFontRole(role); + float fallbackSize = std::clamp(fontSize * defaults.SizeScale, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); + menu.cachedFontPixelSizesByRole[idx] = std::round(fallbackSize); + menu.loadedFontRoles[idx] = bodyFont; + const_cast(menu).GetFontRoleSettings(role) = defaults; + const_cast(menu).cachedFontFilesByRole[idx] = defaults.File; + } + + if (!bodyFont) { + bodyFont = io.Fonts->AddFontDefault(); + menu.loadedFontRoles[bodyIndex] = bodyFont; + } + + io.FontDefault = bodyFont ? bodyFont : io.Fonts->AddFontDefault(); + menu.cachedFontName = const_cast(menu).GetFontRoleSettings(Menu::FontRole::Body).File; + cachedFontSize = fontSize; + const_cast(menu).GetSettings().Theme.FontName = menu.cachedFontName; + const_cast(menu).cachedFontSignature = const_cast(menu).BuildFontSignature(fontSize); + + // Build the font atlas - this bakes all fonts into the texture + if (!io.Fonts->Build()) { + logger::error("ReloadFont: Failed to build font atlas"); + + // Emergency fallback: try to restore with default font before giving up + io.Fonts->Clear(); + ImFont* fallbackFont = io.Fonts->AddFontDefault(); + if (fallbackFont && io.Fonts->Build()) { + menu.loadedFontRoles.fill(fallbackFont); + io.FontDefault = fallbackFont; + } else { + logger::error("ReloadFont: Emergency fallback failed"); + return false; + } + } + + // Recreate device objects - this is where crashes can occur + // Must be done between frames with no active rendering state + + // Flush and wait for GPU idle before invalidating resources + context->Flush(); + + winrt::com_ptr eventQuery; + D3D11_QUERY_DESC queryDesc = { D3D11_QUERY_EVENT, 0 }; + if (SUCCEEDED(device->CreateQuery(&queryDesc, eventQuery.put()))) { + context->End(eventQuery.get()); + BOOL queryData = FALSE; + for (int i = 0; i < 1000 && context->GetData(eventQuery.get(), &queryData, sizeof(BOOL), 0) != S_OK; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } - colors[ImGuiCol_CheckMark] = themeSettings.Palette.Text; + ImGui_ImplDX11_InvalidateDeviceObjects(); - colors[ImGuiCol_Tab] = themeSettings.FullPalette[ImGuiCol_Tab]; - colors[ImGuiCol_TabActive] = themeSettings.FullPalette[ImGuiCol_TabActive]; - colors[ImGuiCol_TabHovered] = tabHovered; - colors[ImGuiCol_TabUnfocused] = themeSettings.FullPalette[ImGuiCol_TabUnfocused]; - colors[ImGuiCol_TabUnfocusedActive] = themeSettings.FullPalette[ImGuiCol_TabUnfocusedActive]; + if (!ImGui_ImplDX11_CreateDeviceObjects()) { + logger::error("ReloadFont: Failed to create device objects"); + + // Emergency fallback: restore with default font and retry device objects + io.Fonts->Clear(); + ImFont* fallbackFont = io.Fonts->AddFontDefault(); + + bool recoverySucceeded = false; + if (fallbackFont && io.Fonts->Build()) { + ImGui_ImplDX11_InvalidateDeviceObjects(); + if (ImGui_ImplDX11_CreateDeviceObjects()) { + menu.loadedFontRoles.fill(fallbackFont); + io.FontDefault = fallbackFont; + menu.cachedFontName = "ImGui Default"; + recoverySucceeded = true; + } + } + + if (!recoverySucceeded) { + logger::error("ReloadFont: Critical failure - unable to recover device objects"); + } + + return false; + } - colors[ImGuiCol_PopupBg] = themeSettings.Palette.Background; + // Verify font texture was created successfully + if (!io.Fonts->TexID) { + logger::error("ReloadFont: Font texture not created"); + return false; + } - colors[ImGuiCol_TableBorderStrong] = colors[ImGuiCol_Border]; - colors[ImGuiCol_TableBorderLight] = colors[ImGuiCol_Border]; + float globalScale = themeSettings.GlobalScale; + + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default + if (std::abs(globalScale - Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { + globalScale = Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 + } + + io.FontGlobalScale = exp2(globalScale); + + cachedFontSize = fontSize; + // Also update cached font name in the menu instance + menu.cachedFontName = themeSettings.FontName; + + return true; +} - colors[ImGuiCol_TextSelectedBg] = header; +// Theme management methods +size_t ThemeManager::DiscoverThemes() +{ + if (discovered) { + return themes.size(); + } + + themes.clear(); + + // Collect all theme directories to search + std::vector searchPaths; + + // Primary themes directory (always check this first) + auto themesDir = GetThemesDirectory(); + logger::info("Checking base themes directory: {}", themesDir.string()); + if (std::filesystem::exists(themesDir)) { + searchPaths.push_back(themesDir); + logger::info("Base themes directory exists, added to search paths"); } else { - std::copy(themeSettings.FullPalette.begin(), themeSettings.FullPalette.end(), std::span(colors).begin()); + logger::warn("Base themes directory does not exist: {}", themesDir.string()); } + + // Check for MO2 Overwrite directory + auto dataPath = Util::PathHelpers::GetDataPath(); + auto parentPath = dataPath.parent_path(); // Go up from Data to game root or MO2 instance + + logger::info("Data path: {}", dataPath.string()); + logger::info("Parent path: {}", parentPath.string()); + + // MO2 Overwrite path: /overwrite/SKSE/Plugins/CommunityShaders/Themes + auto mo2OverwritePath = parentPath / "overwrite" / "SKSE" / "Plugins" / "CommunityShaders" / "Themes"; + logger::info("Checking MO2 Overwrite path: {}", mo2OverwritePath.string()); + if (std::filesystem::exists(mo2OverwritePath)) { + searchPaths.push_back(mo2OverwritePath); + logger::info("Found MO2 Overwrite themes directory"); + } else { + logger::info("MO2 Overwrite themes directory does not exist"); + } + + if (searchPaths.empty()) { + logger::info("No theme directories found"); + discovered = true; + return 0; + } + + logger::info("Discovering themes in {} directories", searchPaths.size()); + + // Search all paths for theme files + for (const auto& searchPath : searchPaths) { + logger::info("Searching for themes in: {}", searchPath.string()); + + try { + for (const auto& entry : std::filesystem::directory_iterator(searchPath)) { + if (!entry.is_regular_file() || entry.path().extension() != ".json") { + continue; + } + + // Check file size + auto fileSize = entry.file_size(); + if (fileSize > MAX_FILE_SIZE) { + logger::warn("Theme file too large, skipping: {} ({}MB)", + entry.path().filename().string(), fileSize / (1024 * 1024)); + continue; + } + + if (themes.size() >= MAX_THEMES) { + logger::warn("Maximum number of themes ({}) reached, skipping remaining files", MAX_THEMES); + break; + } + + auto themeInfo = LoadThemeFile(entry.path()); + if (themeInfo && themeInfo->isValid) { + themes.push_back(std::move(*themeInfo)); + logger::info("Discovered theme: {} ({})", themes.back().name, themes.back().displayName); + } + } + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Error discovering themes in {}: {}", searchPath.string(), e.what()); + } + } + + // Sort themes alphabetically by display name + std::sort(themes.begin(), themes.end(), [](const ThemeInfo& a, const ThemeInfo& b) { + return a.displayName < b.displayName; + }); + + discovered = true; + logger::info("Theme discovery complete. Found {} themes", themes.size()); + return themes.size(); } -void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) +std::vector ThemeManager::GetThemeNames() const { - auto& themeSettings = menu.GetTheme(); + std::vector names; + names.reserve(themes.size()); - ImGuiIO& io = ImGui::GetIO(); - io.Fonts->Clear(); + for (const auto& theme : themes) { + names.push_back(theme.name); + } - ImFontConfig font_config; + return names; +} - font_config.OversampleH = Constants::FCONF_OVERSAMPLE_H; - font_config.OversampleV = Constants::FCONF_OVERSAMPLE_V; - font_config.PixelSnapH = Constants::FCONF_PIXELSNAP_H; - font_config.RasterizerMultiply = Constants::FCONF_RASTERIZER_MULTIPLY; +bool ThemeManager::LoadTheme(const std::string& themeName, json& themeSettings) +{ + if (!discovered) { + DiscoverThemes(); + } + + if (themeName.empty()) { + // Empty theme name means use current/custom theme + return true; + } - float fontSize = themeSettings.FontSize; - fontSize = std::clamp(fontSize, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); + auto it = std::find_if(themes.begin(), themes.end(), + [&themeName](const ThemeInfo& theme) { return theme.name == themeName; }); - auto fontPath = Util::PathHelpers::GetFontsPath() / "Jost-Regular.ttf"; - if (!io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), - std::round(fontSize), &font_config)) { - logger::warn("ThemeManager::ReloadFont() - Failed to load custom font. Using default font."); - io.Fonts->AddFontDefault(); + if (it == themes.end()) { + logger::warn("Theme not found: {}", themeName); + return false; } - io.Fonts->Build(); + if (!it->isValid) { + logger::warn("Theme is invalid: {}", themeName); + return false; + } - ImGui_ImplDX11_InvalidateDeviceObjects(); + try { + if (it->themeData.contains("Theme") && it->themeData["Theme"].is_object()) { + themeSettings = it->themeData["Theme"]; + logger::info("Loaded theme: {} ({})", it->name, it->displayName); + return true; + } else { + logger::warn("Theme file missing 'Theme' object: {}", themeName); + return false; + } + } catch (const std::exception& e) { + logger::warn("Error loading theme {}: {}", themeName, e.what()); + return false; + } +} + +bool ThemeManager::SaveTheme(const std::string& themeName, const json& themeSettings, + const std::string& displayName, const std::string& description) +{ + if (themeName.empty()) { + logger::warn("Cannot save theme with empty name"); + return false; + } + + // Create the full theme JSON structure + json fullTheme = { + { "DisplayName", displayName.empty() ? themeName : displayName }, + { "Description", description.empty() ? "Custom user theme" : description }, + { "Version", "1.0.0" }, + { "Author", "User" }, + { "Theme", themeSettings } + }; + + // Generate safe filename (remove invalid characters) + std::string safeFileName = themeName; + std::replace_if(safeFileName.begin(), safeFileName.end(), [](char c) { return c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|'; }, '_'); + + auto themesDir = GetThemesDirectory(); + auto filePath = themesDir / (safeFileName + ".json"); + + logger::info("SaveTheme: Saving theme '{}' to file: {}", themeName, filePath.string()); + logger::debug("SaveTheme: Theme has {} top-level keys", fullTheme.size()); + + try { + // Ensure themes directory exists + std::filesystem::create_directories(themesDir); + logger::debug("SaveTheme: Themes directory ensured: {}", themesDir.string()); + + // Write the theme file + std::ofstream file(filePath); + if (!file.is_open()) { + logger::warn("Failed to create theme file: {}", filePath.string()); + return false; + } + + file << fullTheme.dump(4); // Pretty print with 4-space indentation + file.close(); + + logger::info("Saved theme: {} to {}", themeName, filePath.string()); + + // Refresh themes to include the new one + RefreshThemes(); + + return true; + } catch (const std::exception& e) { + logger::warn("Error saving theme {}: {}", themeName, e.what()); + return false; + } +} + +const ThemeManager::ThemeInfo* ThemeManager::GetThemeInfo(const std::string& themeName) const +{ + auto it = std::find_if(themes.begin(), themes.end(), + [&themeName](const ThemeInfo& theme) { return theme.name == themeName; }); - io.FontGlobalScale = exp2(themeSettings.GlobalScale); + return (it != themes.end()) ? &(*it) : nullptr; +} + +void ThemeManager::RefreshThemes() +{ + discovered = false; + DiscoverThemes(); +} + +std::filesystem::path ThemeManager::GetThemesDirectory() const +{ + return Util::PathHelpers::GetThemesPath(); +} + +void ThemeManager::CreateDefaultThemeFiles() +{ + auto themesDir = GetThemesDirectory(); + + try { + std::filesystem::create_directories(themesDir); + logger::info("Ensured themes directory exists: {}", themesDir.string()); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Failed to create themes directory: {}", e.what()); + return; + } + + // Check if any theme files exist - if so, use those instead of creating defaults + bool hasThemes = false; + try { + for (const auto& entry : std::filesystem::directory_iterator(themesDir)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + hasThemes = true; + break; + } + } + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Failed to check for existing themes: {}", e.what()); + } + + if (hasThemes) { + logger::info("Theme files already exist, skipping default creation"); + return; + } + + // Only create a minimal default theme if no themes exist at all (rare fallback) + auto defaultThemeFile = themesDir / "Default.json"; + try { + std::ofstream file(defaultThemeFile); + if (!file.is_open()) { + logger::warn("Failed to create default theme file: {}", defaultThemeFile.string()); + return; + } + + file << R"({ + "DisplayName": "Default Theme", + "Description": "Default community shaders theme", + "Version": "1.0", + "Author": "Community Shaders", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.05, 0.05, 0.05, 1.0], + "Text": [1.0, 1.0, 1.0, 1.0], + "Border": [0.4, 0.4, 0.4, 1.0] + }, + "FontSize": 27.0, + "GlobalScale": 0.0, + "TooltipHoverDelay": 0.5 + } +})"; + + file.close(); + logger::info("Created default theme file: {}", defaultThemeFile.string()); + } catch (const std::exception& e) { + logger::warn("Failed to create default theme file: {}", e.what()); + } +} + +std::unique_ptr ThemeManager::LoadThemeFile(const std::filesystem::path& filePath) +{ + auto themeInfo = std::make_unique(); + themeInfo->name = filePath.stem().string(); + themeInfo->filePath = filePath.string(); + themeInfo->lastModified = GetFileModTime(filePath); + + try { + std::ifstream file(filePath); + if (!file.is_open()) { + logger::warn("Failed to open theme file: {}", filePath.string()); + return themeInfo; + } + + json data; + file >> data; + + if (!ValidateThemeData(data)) { + logger::warn("Invalid theme data in file: {}", filePath.string()); + return themeInfo; + } + + themeInfo->themeData = data; + + // Extract metadata + if (data.contains("DisplayName") && data["DisplayName"].is_string()) { + themeInfo->displayName = data["DisplayName"].get(); + } else { + themeInfo->displayName = themeInfo->name; + } + + if (data.contains("Description") && data["Description"].is_string()) { + themeInfo->description = data["Description"].get(); + } + + if (data.contains("Version") && data["Version"].is_string()) { + themeInfo->version = data["Version"].get(); + } + + if (data.contains("Author") && data["Author"].is_string()) { + themeInfo->author = data["Author"].get(); + } + + themeInfo->isValid = true; + + } catch (const std::exception& e) { + logger::warn("Error parsing theme file {}: {}", filePath.string(), e.what()); + } - cachedFontSize = themeSettings.FontSize; -} \ No newline at end of file + return themeInfo; +} + +bool ThemeManager::ValidateThemeData(const json& themeData) const +{ + return themeData.contains("Theme") && themeData["Theme"].is_object(); +} + +float ThemeManager::ResolveFontSize(const Menu& menu) +{ + const auto& themeSettings = menu.GetTheme(); + float configured = themeSettings.FontSize; + + // If user configured a positive size, use it (clamped) + if (std::round(configured) > 0) { + return std::clamp(configured, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); + } + + // Otherwise, compute dynamic default based on current screen resolution + float dynamicSize = Constants::DEFAULT_FONT_SIZE; + if (globals::state && globals::state->screenSize.y > 0) { + dynamicSize = globals::state->screenSize.y * Constants::DEFAULT_FONT_RATIO; + } else { + logger::warn("ThemeManager::ResolveFontSize() - Falling back to DEFAULT_FONT_SIZE due to missing screen height."); + } + return std::clamp(dynamicSize, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); +} diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index e002719473..7856df94c8 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -1,12 +1,146 @@ #pragma once +#include +#include #include +#include +#include +#include +#include +#include +using json = nlohmann::json; + +/** + * @brief Manages hot-swappable theme system for Community Shaders menu + * + * THEME JSON SCHEMA: + * ================== + * Theme files use JSON format with the following structure: + * + * { + * "DisplayName": "Human-readable theme name", + * "Description": "Theme description", + * "Version": "1.0.0", + * "Author": "Your name", + * "Theme": { + * "FontSize": 27.0, // Base font size (16-108px range) + * "FontName": "Jost/Jost-Regular.ttf", // Legacy font path (use FontRoles instead) + * "GlobalScale": 0.0, // UI scale exponent (-2.0 to 2.0, 0.0=100%) + * + * // Font Role System (4 roles: Body, Heading, Subheading, Subtitle) + * "FontRoles": [ + * { "Family": "Jost", "Style": "Regular", "File": "Jost/Jost-Regular.ttf", "SizeScale": 1.0 }, + * { "Family": "Jost", "Style": "Regular", "File": "Jost/Jost-Regular.ttf", "SizeScale": 1.0 }, + * { "Family": "Jost", "Style": "Regular", "File": "Jost/Jost-Regular.ttf", "SizeScale": 1.0 }, + * { "Family": "Jost", "Style": "Regular", "File": "Jost/Jost-Regular.ttf", "SizeScale": 1.0 } + * ], + * + * "TooltipHoverDelay": 0.5, // Seconds before tooltip appears + * "ShowActionIcons": true, // Show icons on action buttons + * + * // Simple color palette (6 key colors) + * "Palette": { + * "Background": [0.03, 0.03, 0.03, 0.39], // Window background RGBA + * "Text": [1.0, 1.0, 1.0, 1.0], // Primary text color + * "WindowBorder": [0.5, 0.5, 0.5, 0.8], // Outer window borders + * "FrameBorder": [0.4, 0.4, 0.4, 0.7], // Button/input borders + * "Separator": [0.5, 0.5, 0.5, 0.6], // Divider lines + * "ResizeGrip": [0.6, 0.6, 0.6, 0.8] // Window resize handle + * }, + * + * // Status indicator colors + * "StatusPalette": { + * "Disable": [0.5, 0.5, 0.5, 1.0], // Disabled elements + * "Error": [1.0, 0.4, 0.4, 1.0], // Error messages + * "Warning": [1.0, 0.6, 0.2, 1.0], // Warning messages + * "RestartNeeded": [0.4, 1.0, 0.4, 1.0], // Restart required indicator + * "CurrentHotkey": [1.0, 1.0, 0.0, 1.0], // Active hotkey highlight + * "SuccessColor": [0.0, 1.0, 0.0, 1.0], // Success messages + * "InfoColor": [0.2, 0.6, 1.0, 1.0] // Info messages + * }, + * + * // Feature header styling + * "FeatureHeading": { + * "ColorDefault": [0.8, 0.8, 0.8, 1.0], // Default header color + * "ColorHovered": [0.6, 0.6, 0.6, 1.0], // Hovered header color + * "MinimizedFactor": 0.7 // Alpha multiplier when minimized + * }, + * + * // Scrollbar transparency + * "ScrollbarOpacity": { + * "Background": 0.0, // Scrollbar track alpha + * "Thumb": 0.5, // Scroll handle alpha + * "ThumbHovered": 0.75, // Hovered handle alpha + * "ThumbActive": 0.9 // Dragged handle alpha + * }, + * + * // ImGui style settings (spacing, rounding, etc.) + * "Style": { + * "WindowBorderSize": 2.0, + * "WindowPadding": [8.0, 8.0], + * "WindowRounding": 12.0, + * "FrameRounding": 4.0, + * // ... see ImGuiStyle for all available fields + * }, + * + * // Full ImGui color palette (55 colors, overrides simple palette) + * "FullPalette": [ [r,g,b,a], [r,g,b,a], ... ] + * } + * } + * + * FONT ROLE SYSTEM: + * ================= + * Replaces legacy single-font system with semantic font roles: + * - Role 0 (Body): Default UI text, settings labels + * - Role 1 (Heading): Feature section headers + * - Role 2 (Subheading): Subsection headers + * - Role 3 (Subtitle): Secondary text, descriptions + * + * Each role can have different font family, style, and size scale. + * Fonts must exist in Data\SKSE\Plugins\CommunityShaders\Fonts\ + * + * BLUR SHADER SYSTEM: + * =================== + * Separable Gaussian blur (horizontal + vertical passes) rendered at eighth resolution. + * Hardcoded intensity (0.04) for consistent appearance. Toggle via BackgroundBlurEnabled. + * Based on Unrimp rendering engine: https://github.com/cofenberg/unrimp + * + * MIGRATION FROM OLD CONFIGS: + * =========================== + * Legacy "FontName" field still supported for backward compatibility. + * New themes should use "FontRoles" array instead. + * Old themes without FontRoles auto-populate with defaults on load. + * + * Theme files location: Data\SKSE\Plugins\CommunityShaders\Themes\ + * File naming: {ThemeName}.json (e.g., "Default.json", "DragonBlood.json") + */ class ThemeManager { public: + struct ThemeInfo + { + std::string name; // Filename without extension + std::string displayName; // Human-readable name from JSON + std::string description; // Theme description from JSON + std::string filePath; // Full path to theme file + json themeData; // Complete theme settings + bool isValid = false; // Whether theme loaded successfully + + // Metadata + std::string version; + std::string author; + std::time_t lastModified = 0; + }; + + // Returns the effective font size to use. If the user setting is <= 0, a dynamic + // default based on current screen resolution is returned; otherwise the user value. + static float ResolveFontSize(const class Menu& menu); + + // Static UI helper methods static void SetupImGuiStyle(const class Menu& menu); - static void ReloadFont(const class Menu& menu, float& cachedFontSize); + static bool ReloadFont(const class Menu& menu, float& cachedFontSize); + static void ForceApplyDefaultTheme(); // Force Default.json colors to ImGui (bypass hardcoded defaults) struct Constants { @@ -17,6 +151,9 @@ class ThemeManager static constexpr float MAX_FONT_SIZE = 108.0f; // 5.0% @ 2160px height static constexpr float DEFAULT_FONT_SIZE = 27.0f; + // Global scale constants + static constexpr float DEFAULT_GLOBAL_SCALE = 0.0f; // Default global scale for built-in themes + // Font configuration constants static constexpr int FCONF_OVERSAMPLE_H = 3; // ImGui default = 2 static constexpr int FCONF_OVERSAMPLE_V = 2; // ImGui default = 1 @@ -25,9 +162,9 @@ class ThemeManager // Header rendering constants static constexpr float HEADER_BASE_TEXT_SCALE = 1.7f; - static constexpr float HEADER_BASE_ICON_MULTIPLIER = 1.5f; + static constexpr float HEADER_BASE_ICON_MULTIPLIER = 1.85f; static constexpr float HEADER_FALLBACK_TEXT_SCALE = 1.5f; - static constexpr float DOCKED_ICON_SIZE_MULTIPLIER = 1.25f; + static constexpr float DOCKED_ICON_SIZE_MULTIPLIER = 1.5f; static constexpr float DOCKED_ICON_SPACING = 8.0f; static constexpr float DOCKED_RIGHT_MARGIN = 45.0f; static constexpr float WATERMARK_HEIGHT_PERCENT = 0.50f; @@ -39,8 +176,107 @@ class ThemeManager static constexpr float BUTTON_SPACING = 8.0f; static constexpr float OVERLAY_WINDOW_POSITION = 10.0f; static constexpr float FONT_CACHE_EPSILON = 0.01f; - static constexpr float CURSOR_POSITION_PADDING = 5.0f; + static constexpr float CURSOR_POSITION_PADDING = 14.0f; static constexpr float SEPARATOR_THICKNESS = 3.0f; static constexpr float UNDOCKED_ICON_ITEM_SPACING = 6.0f; }; + + static ThemeManager* GetSingleton() + { + static ThemeManager instance; + return &instance; + } + + // Static UI helper methods + + /** + * @brief Discovers all theme files in the themes directory + * @return Number of theme files discovered + */ + size_t DiscoverThemes(); + + /** + * @brief Gets list of all discovered themes + * @return Vector of theme information + */ + const std::vector& GetThemes() const { return themes; } + + /** + * @brief Gets theme names for dropdown display + * @return Vector of theme names + */ + std::vector GetThemeNames() const; + + /** + * @brief Loads a specific theme by name + * @param themeName Name of the theme to load + * @param themeSettings Output parameter for loaded theme settings + * @return True if theme was loaded successfully + */ + bool LoadTheme(const std::string& themeName, json& themeSettings); + + /** + * @brief Saves current theme settings to a new theme file + * @param themeName Name for the new theme file + * @param themeSettings Theme settings to save + * @param displayName Display name for the theme + * @param description Description for the theme + * @return True if theme was saved successfully + */ + bool SaveTheme(const std::string& themeName, const json& themeSettings, + const std::string& displayName, const std::string& description); + + /** + * @brief Gets theme info by name + * @param themeName Name of the theme + * @return Pointer to theme info or nullptr if not found + */ + const ThemeInfo* GetThemeInfo(const std::string& themeName) const; + + /** + * @brief Refreshes theme discovery (for runtime updates) + */ + void RefreshThemes(); + + /** + * @brief Checks if themes have been discovered + */ + bool IsDiscovered() const { return discovered; } + + /** + * @brief Gets the themes directory path + */ + std::filesystem::path GetThemesDirectory() const; + + /** + * @brief Creates default theme files if they don't exist + */ + void CreateDefaultThemeFiles(); + +private: + ThemeManager() = default; + ~ThemeManager() = default; + ThemeManager(const ThemeManager&) = delete; + ThemeManager& operator=(const ThemeManager&) = delete; + + /** + * @brief Loads a single theme file + * @param filePath Path to the theme file + * @return Theme info if successful, nullptr otherwise + */ + std::unique_ptr LoadThemeFile(const std::filesystem::path& filePath); + + /** + * @brief Validates theme data structure + * @param themeData JSON data to validate + * @return True if theme data is valid + */ + bool ValidateThemeData(const json& themeData) const; + + std::vector themes; + bool discovered = false; + + // Constants + static constexpr size_t MAX_THEMES = 100; // Prevent excessive theme loading + static constexpr size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB max theme file size }; \ No newline at end of file diff --git a/src/SettingsOverrideManager.cpp b/src/SettingsOverrideManager.cpp index f8e6a68932..1aab846fbc 100644 --- a/src/SettingsOverrideManager.cpp +++ b/src/SettingsOverrideManager.cpp @@ -1,6 +1,7 @@ #include "SettingsOverrideManager.h" #include "FeatureIssues.h" +#include "Util.h" #include #include @@ -279,7 +280,7 @@ void SettingsOverrideManager::RefreshOverrides() std::filesystem::path SettingsOverrideManager::GetOverridesDirectory() const { - return std::filesystem::path(OVERRIDES_DIR); + return Util::PathHelpers::GetOverridesPath(); } json SettingsOverrideManager::LoadAppliedOverridesTracking() const @@ -403,7 +404,7 @@ void SettingsOverrideManager::SaveAppliedOverridesTracking(const json& appliedOv std::filesystem::path SettingsOverrideManager::GetAppliedOverridesTrackingPath() const { - return std::filesystem::path(APPLIED_OVERRIDES_TRACKING_FILE); + return Util::PathHelpers::GetAppliedOverridesPath(); } size_t SettingsOverrideManager::ApplyNewOverrides(json& baseSettings, json& appliedOverrides) diff --git a/src/SettingsOverrideManager.h b/src/SettingsOverrideManager.h index c6b08431af..91ab11da6b 100644 --- a/src/SettingsOverrideManager.h +++ b/src/SettingsOverrideManager.h @@ -220,9 +220,7 @@ class SettingsOverrideManager bool enabled = true; bool discovered = false; - static constexpr const char* OVERRIDES_DIR = "Data\\SKSE\\Plugins\\CommunityShaders\\Overrides"; static constexpr const char* GLOBAL_SUFFIX = "_Global.json"; - static constexpr const char* APPLIED_OVERRIDES_TRACKING_FILE = "Data\\SKSE\\Plugins\\CommunityShaders\\AppliedOverrides.json"; // Security limits for JSON validation static constexpr size_t MAX_JSON_DEPTH = 10; diff --git a/src/ShaderCache.cpp b/src/ShaderCache.cpp index 086aff33f6..39ddc25a61 100644 --- a/src/ShaderCache.cpp +++ b/src/ShaderCache.cpp @@ -13,6 +13,26 @@ namespace SIE { static void GetShaderDefines(const RE::BSShader&, uint32_t, D3D_SHADER_MACRO*); static std::string GetShaderString(ShaderClass, const RE::BSShader&, uint32_t, bool = false); + /** + * @brief Resolve image-space shader descriptor when applicable. + * + * If @p shader is an image-space shader, attempts to map it to a + * runtime image-space descriptor via GetImagespaceShaderDescriptor and + * returns true on success. If the shader is not image-space the + * function returns true and leaves @p descriptor unchanged. Returns + * false only when the shader is image-space and no valid descriptor + * could be resolved. + * + * This helper is used by the shader loading and caching code paths to + * determine whether an image-space shader can be loaded or cached. If + * this function returns false the caller should skip loading/compiling + * and caching that shader. + * + * @param shader The shader to resolve (may be an image-space shader). + * @param[out] descriptor Resolved descriptor for image-space shaders. + * @return True if descriptor is valid or not applicable, false on failure. + */ + static bool ResolveImageSpaceDescriptor(const RE::BSShader& shader, uint32_t& descriptor); /** @brief Get the BSShader::Type from the ShaderString @param a_key The key generated from GetShaderString @@ -1254,6 +1274,10 @@ namespace SIE static ID3DBlob* CompileShader(ShaderClass shaderClass, const RE::BSShader& shader, uint32_t descriptor, bool useDiskCache) { + if (!SShaderCache::ResolveImageSpaceDescriptor(shader, descriptor)) { + return nullptr; + } + // check hashmap auto& cache = ShaderCache::Instance(); ID3DBlob* shaderBlob = cache.GetCompletedShader(shaderClass, shader, descriptor); @@ -1508,147 +1532,163 @@ namespace SIE using enum RE::ImageSpaceManager::ImageSpaceEffectEnum; static const ankerl::unordered_dense::map descriptors{ - // { "BSImagespaceShaderISBlur", static_cast(ISBlur) }, - // { "BSImagespaceShaderBlur3", static_cast(ISBlur3) }, - // { "BSImagespaceShaderBlur5", static_cast(ISBlur5) }, - // { "BSImagespaceShaderBlur7", static_cast(ISBlur7) }, - // { "BSImagespaceShaderBlur9", static_cast(ISBlur9) }, - // { "BSImagespaceShaderBlur11", static_cast(ISBlur11) }, - // { "BSImagespaceShaderBlur13", static_cast(ISBlur13) }, - // { "BSImagespaceShaderBlur15", static_cast(ISBlur15) }, - // { "BSImagespaceShaderBrightPassBlur3", static_cast(ISBrightPassBlur3) }, - // { "BSImagespaceShaderBrightPassBlur5", static_cast(ISBrightPassBlur5) }, - // { "BSImagespaceShaderBrightPassBlur7", static_cast(ISBrightPassBlur7) }, - // { "BSImagespaceShaderBrightPassBlur9", static_cast(ISBrightPassBlur9) }, - // { "BSImagespaceShaderBrightPassBlur11", static_cast(ISBrightPassBlur11) }, - // { "BSImagespaceShaderBrightPassBlur13", static_cast(ISBrightPassBlur13) }, - // { "BSImagespaceShaderBrightPassBlur15", static_cast(ISBrightPassBlur15) }, - // { "BSImagespaceShaderNonHDRBlur3", static_cast(ISNonHDRBlur3) }, - // { "BSImagespaceShaderNonHDRBlur5", static_cast(ISNonHDRBlur5) }, - // { "BSImagespaceShaderNonHDRBlur7", static_cast(ISNonHDRBlur7) }, - // { "BSImagespaceShaderNonHDRBlur9", static_cast(ISNonHDRBlur9) }, - // { "BSImagespaceShaderNonHDRBlur11", static_cast(ISNonHDRBlur11) }, - // { "BSImagespaceShaderNonHDRBlur13", static_cast(ISNonHDRBlur13) }, - // { "BSImagespaceShaderNonHDRBlur15", static_cast(ISNonHDRBlur15) }, - // { "BSImagespaceShaderISBasicCopy", static_cast(ISBasicCopy) }, - // { "BSImagespaceShaderISSimpleColor", static_cast(ISSimpleColor) }, - // { "BSImagespaceShaderApplyReflections", static_cast(ISApplyReflections) }, - // { "BSImagespaceShaderISExp", static_cast(ISExp) }, - // { "BSImagespaceShaderISDisplayDepth", static_cast(ISDisplayDepth) }, - // { "BSImagespaceShaderAlphaBlend", static_cast(ISAlphaBlend) }, - // { "BSImagespaceShaderWaterFlow", static_cast(ISWaterFlow) }, - // { "BSImagespaceShaderISWaterBlend", static_cast(ISWaterBlend) }, - // { "BSImagespaceShaderGreyScale", static_cast(ISCopyGrayScale) }, - // { "BSImagespaceShaderCopy", static_cast(ISCopy) }, - // { "BSImagespaceShaderCopyScaleBias", static_cast(ISCopyScaleBias) }, + // { "BSImagespaceShaderISBlur", RE::ImageSpaceManager::GetCurrentIndex(ISBlur) }, + // { "BSImagespaceShaderBlur3", RE::ImageSpaceManager::GetCurrentIndex(ISBlur3) }, + // { "BSImagespaceShaderBlur5", RE::ImageSpaceManager::GetCurrentIndex(ISBlur5) }, + // { "BSImagespaceShaderBlur7", RE::ImageSpaceManager::GetCurrentIndex(ISBlur7) }, + // { "BSImagespaceShaderBlur9", RE::ImageSpaceManager::GetCurrentIndex(ISBlur9) }, + // { "BSImagespaceShaderBlur11", RE::ImageSpaceManager::GetCurrentIndex(ISBlur11) }, + // { "BSImagespaceShaderBlur13", RE::ImageSpaceManager::GetCurrentIndex(ISBlur13) }, + // { "BSImagespaceShaderBlur15", RE::ImageSpaceManager::GetCurrentIndex(ISBlur15) }, + // { "BSImagespaceShaderBrightPassBlur3", RE::ImageSpaceManager::GetCurrentIndex(ISBrightPassBlur3) }, + // { "BSImagespaceShaderBrightPassBlur5", RE::ImageSpaceManager::GetCurrentIndex(ISBrightPassBlur5) }, + // { "BSImagespaceShaderBrightPassBlur7", RE::ImageSpaceManager::GetCurrentIndex(ISBrightPassBlur7) }, + // { "BSImagespaceShaderBrightPassBlur9", RE::ImageSpaceManager::GetCurrentIndex(ISBrightPassBlur9) }, + // { "BSImagespaceShaderBrightPassBlur11", RE::ImageSpaceManager::GetCurrentIndex(ISBrightPassBlur11) }, + // { "BSImagespaceShaderBrightPassBlur13", RE::ImageSpaceManager::GetCurrentIndex(ISBrightPassBlur13) }, + // { "BSImagespaceShaderBrightPassBlur15", RE::ImageSpaceManager::GetCurrentIndex(ISBrightPassBlur15) }, + // { "BSImagespaceShaderNonHDRBlur3", RE::ImageSpaceManager::GetCurrentIndex(ISNonHDRBlur3) }, + // { "BSImagespaceShaderNonHDRBlur5", RE::ImageSpaceManager::GetCurrentIndex(ISNonHDRBlur5) }, + // { "BSImagespaceShaderNonHDRBlur7", RE::ImageSpaceManager::GetCurrentIndex(ISNonHDRBlur7) }, + // { "BSImagespaceShaderNonHDRBlur9", RE::ImageSpaceManager::GetCurrentIndex(ISNonHDRBlur9) }, + // { "BSImagespaceShaderNonHDRBlur11", RE::ImageSpaceManager::GetCurrentIndex(ISNonHDRBlur11) }, + // { "BSImagespaceShaderNonHDRBlur13", RE::ImageSpaceManager::GetCurrentIndex(ISNonHDRBlur13) }, + // { "BSImagespaceShaderNonHDRBlur15", RE::ImageSpaceManager::GetCurrentIndex(ISNonHDRBlur15) }, + // { "BSImagespaceShaderISBasicCopy", RE::ImageSpaceManager::GetCurrentIndex(ISBasicCopy) }, + // { "BSImagespaceShaderISSimpleColor", RE::ImageSpaceManager::GetCurrentIndex(ISSimpleColor) }, + // { "BSImagespaceShaderApplyReflections", RE::ImageSpaceManager::GetCurrentIndex(ISApplyReflections) }, + // { "BSImagespaceShaderISExp", RE::ImageSpaceManager::GetCurrentIndex(ISExp) }, + // { "BSImagespaceShaderISDisplayDepth", RE::ImageSpaceManager::GetCurrentIndex(ISDisplayDepth) }, + // { "BSImagespaceShaderAlphaBlend", RE::ImageSpaceManager::GetCurrentIndex(ISAlphaBlend) }, + // { "BSImagespaceShaderWaterFlow", RE::ImageSpaceManager::GetCurrentIndex(ISWaterFlow) }, + { "BSImagespaceShaderISWaterBlend", RE::ImageSpaceManager::GetCurrentIndex(ISWaterBlend) }, + // { "BSImagespaceShaderGreyScale", RE::ImageSpaceManager::GetCurrentIndex(ISCopyGrayScale) }, + // { "BSImagespaceShaderCopy", RE::ImageSpaceManager::GetCurrentIndex(ISCopy) }, + // { "BSImagespaceShaderCopyScaleBias", RE::ImageSpaceManager::GetCurrentIndex(ISCopyScaleBias) }, // { "BSImagespaceShaderCopyCustomViewport", - // static_cast(ISCopyCustomViewport) }, - // { "BSImagespaceShaderCopyTextureMask", static_cast(ISCopyTextureMask) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISCopyCustomViewport) }, + // { "BSImagespaceShaderCopyTextureMask", RE::ImageSpaceManager::GetCurrentIndex(ISCopyTextureMask) }, // { "BSImagespaceShaderCopyDynamicFetchDisabled", - // static_cast(ISCopyDynamicFetchDisabled) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISCopyDynamicFetchDisabled) }, { "BSImagespaceShaderISCompositeVolumetricLighting", - static_cast(ISCompositeVolumetricLighting) }, + RE::ImageSpaceManager::GetCurrentIndex(ISCompositeVolumetricLighting) }, { "BSImagespaceShaderISCompositeLensFlare", - static_cast(ISCompositeLensFlare) }, + RE::ImageSpaceManager::GetCurrentIndex(ISCompositeLensFlare) }, { "BSImagespaceShaderISCompositeLensFlareVolumetricLighting", - static_cast(ISCompositeLensFlareVolumetricLighting) }, - // { "BSImagespaceShaderISDebugSnow", static_cast(ISDebugSnow) }, - { "BSImagespaceShaderDepthOfField", static_cast(ISDepthOfField) }, + RE::ImageSpaceManager::GetCurrentIndex(ISCompositeLensFlareVolumetricLighting) }, + // { "BSImagespaceShaderISDebugSnow", RE::ImageSpaceManager::GetCurrentIndex(ISDebugSnow) }, + { "BSImagespaceShaderDepthOfField", RE::ImageSpaceManager::GetCurrentIndex(ISDepthOfField) }, { "BSImagespaceShaderDepthOfFieldFogged", - static_cast(ISDepthOfFieldFogged) }, + RE::ImageSpaceManager::GetCurrentIndex(ISDepthOfFieldFogged) }, { "BSImagespaceShaderDepthOfFieldMaskedFogged", - static_cast(ISDepthOfFieldMaskedFogged) }, - // { "BSImagespaceShaderDistantBlur", static_cast(ISDistantBlur) }, + RE::ImageSpaceManager::GetCurrentIndex(ISDepthOfFieldMaskedFogged) }, + // { "BSImagespaceShaderDistantBlur", RE::ImageSpaceManager::GetCurrentIndex(ISDistantBlur) }, // { "BSImagespaceShaderDistantBlurFogged", - // static_cast(ISDistantBlurFogged) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISDistantBlurFogged) }, // { "BSImagespaceShaderDistantBlurMaskedFogged", - // static_cast(ISDistantBlurMaskedFogged) }, - // { "BSImagespaceShaderDoubleVision", static_cast(ISDoubleVision) }, - { "BSImagespaceShaderISDownsample", static_cast(ISDownsample) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISDistantBlurMaskedFogged) }, + // { "BSImagespaceShaderDoubleVision", RE::ImageSpaceManager::GetCurrentIndex(ISDoubleVision) }, + { "BSImagespaceShaderISDownsample", RE::ImageSpaceManager::GetCurrentIndex(ISDownsample) }, { "BSImagespaceShaderISDownsampleIgnoreBrightest", - static_cast(ISDownsampleIgnoreBrightest) }, + RE::ImageSpaceManager::GetCurrentIndex(ISDownsampleIgnoreBrightest) }, // { "BSImagespaceShaderISUpsampleDynamicResolution", - // static_cast(ISUpsampleDynamicResolution) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISUpsampleDynamicResolution) }, { "BSImageSpaceShaderVolumetricLighting", - static_cast(ISVolumetricLighting) }, - { "BSImagespaceShaderHDRDownSample4", static_cast(ISHDRDownSample4) }, + RE::ImageSpaceManager::GetCurrentIndex(ISVolumetricLighting) }, + { "BSImagespaceShaderHDRDownSample4", RE::ImageSpaceManager::GetCurrentIndex(ISHDRDownSample4) }, { "BSImagespaceShaderHDRDownSample4LightAdapt", - static_cast(ISHDRDownSample4LightAdapt) }, + RE::ImageSpaceManager::GetCurrentIndex(ISHDRDownSample4LightAdapt) }, { "BSImagespaceShaderHDRDownSample4LumClamp", - static_cast(ISHDRDownSample4LumClamp) }, + RE::ImageSpaceManager::GetCurrentIndex(ISHDRDownSample4LumClamp) }, { "BSImagespaceShaderHDRDownSample4RGB2Lum", - static_cast(ISHDRDownSample4RGB2Lum) }, - { "BSImagespaceShaderHDRDownSample16", static_cast(ISHDRDownSample16) }, + RE::ImageSpaceManager::GetCurrentIndex(ISHDRDownSample4RGB2Lum) }, + { "BSImagespaceShaderHDRDownSample16", RE::ImageSpaceManager::GetCurrentIndex(ISHDRDownSample16) }, { "BSImagespaceShaderHDRDownSample16LightAdapt", - static_cast(ISHDRDownSample16LightAdapt) }, + RE::ImageSpaceManager::GetCurrentIndex(ISHDRDownSample16LightAdapt) }, { "BSImagespaceShaderHDRDownSample16Lum", - static_cast(ISHDRDownSample16Lum) }, + RE::ImageSpaceManager::GetCurrentIndex(ISHDRDownSample16Lum) }, { "BSImagespaceShaderHDRDownSample16LumClamp", - static_cast(ISHDRDownSample16LumClamp) }, + RE::ImageSpaceManager::GetCurrentIndex(ISHDRDownSample16LumClamp) }, { "BSImagespaceShaderHDRTonemapBlendCinematic", - static_cast(ISHDRTonemapBlendCinematic) }, + RE::ImageSpaceManager::GetCurrentIndex(ISHDRTonemapBlendCinematic) }, { "BSImagespaceShaderHDRTonemapBlendCinematicFade", - static_cast(ISHDRTonemapBlendCinematicFade) }, - // { "BSImagespaceShaderISIBLensFlares", static_cast(ISIBLensFlares) }, + RE::ImageSpaceManager::GetCurrentIndex(ISHDRTonemapBlendCinematicFade) }, + // { "BSImagespaceShaderISIBLensFlares", RE::ImageSpaceManager::GetCurrentIndex(ISIBLensFlares) }, // Those cause issue because of typo in shader name in vanilla code but at the same time they are not used by vanilla game. - /*{ "BSImagespaceShaderISLightingComposite", - static_cast(ISLightingComposite) }, - { "BSImagespaceShaderISLightingCompositeMenu", - static_cast(ISLightingCompositeMenu) }, - { "BSImagespaceShaderISLightingCompositeNoDirectionalLight", - static_cast(ISLightingCompositeNoDirectionalLight) },*/ - - // { "BSImagespaceShaderLocalMap", static_cast(ISLocalMap) }, - // { "BSISWaterBlendHeightmaps", static_cast(ISWaterBlendHeightmaps) }, + // { "BSImagespaceShaderISLightingComposite", + // RE::ImageSpaceManager::GetCurrentIndex(ISLightingComposite) }, + // { "BSImagespaceShaderISLightingCompositeMenu", + // RE::ImageSpaceManager::GetCurrentIndex(ISLightingCompositeMenu) }, + // { "BSImagespaceShaderISLightingCompositeNoDirectionalLight", + // RE::ImageSpaceManager::GetCurrentIndex(ISLightingCompositeNoDirectionalLight) }, + + // { "BSImagespaceShaderLocalMap", RE::ImageSpaceManager::GetCurrentIndex(ISLocalMap) }, + // { "BSISWaterBlendHeightmaps", RE::ImageSpaceManager::GetCurrentIndex(ISWaterBlendHeightmaps) }, // { "BSISWaterDisplacementClearSimulation", - // static_cast(ISWaterDisplacementClearSimulation) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISWaterDisplacementClearSimulation) }, // { "BSISWaterDisplacementNormals", - // static_cast(ISWaterDisplacementNormals) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISWaterDisplacementNormals) }, // { "BSISWaterDisplacementRainRipple", - // static_cast(ISWaterDisplacementRainRipple) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISWaterDisplacementRainRipple) }, // { "BSISWaterDisplacementTexOffset", - // static_cast(ISWaterDisplacementTexOffset) }, - // { "BSISWaterWadingHeightmap", static_cast(ISWaterWadingHeightmap) }, - // { "BSISWaterRainHeightmap", static_cast(ISWaterRainHeightmap) }, - // { "BSISWaterSmoothHeightmap", static_cast(ISWaterSmoothHeightmap) }, - // { "BSISWaterWadingHeightmap", static_cast(ISWaterWadingHeightmap) }, - // { "BSImagespaceShaderMap", static_cast(ISMap) }, - // { "BSImagespaceShaderMap", static_cast(ISMap) }, - // { "BSImagespaceShaderWorldMap", static_cast(ISWorldMap) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISWaterDisplacementTexOffset) }, + // { "BSISWaterWadingHeightmap", RE::ImageSpaceManager::GetCurrentIndex(ISWaterWadingHeightmap) }, + // { "BSISWaterRainHeightmap", RE::ImageSpaceManager::GetCurrentIndex(ISWaterRainHeightmap) }, + // { "BSISWaterSmoothHeightmap", RE::ImageSpaceManager::GetCurrentIndex(ISWaterSmoothHeightmap) }, + // { "BSISWaterWadingHeightmap", RE::ImageSpaceManager::GetCurrentIndex(ISWaterWadingHeightmap) }, + // { "BSImagespaceShaderMap", RE::ImageSpaceManager::GetCurrentIndex(ISMap) }, + // { "BSImagespaceShaderMap", RE::ImageSpaceManager::GetCurrentIndex(ISMap) }, + // { "BSImagespaceShaderWorldMap", RE::ImageSpaceManager::GetCurrentIndex(ISWorldMap) }, // { "BSImagespaceShaderWorldMapNoSkyBlur", - // static_cast(ISWorldMapNoSkyBlur) }, - // { "BSImagespaceShaderISMinify", static_cast(ISMinify) }, - // { "BSImagespaceShaderISMinifyContrast", static_cast(ISMinifyContrast) }, - // { "BSImagespaceShaderNoiseNormalmap", static_cast(ISNoiseNormalmap) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISWorldMapNoSkyBlur) }, + // { "BSImagespaceShaderISMinify", RE::ImageSpaceManager::GetCurrentIndex(ISMinify) }, + // { "BSImagespaceShaderISMinifyContrast", RE::ImageSpaceManager::GetCurrentIndex(ISMinifyContrast) }, + // { "BSImagespaceShaderNoiseNormalmap", RE::ImageSpaceManager::GetCurrentIndex(ISNoiseNormalmap) }, // { "BSImagespaceShaderNoiseScrollAndBlend", - // static_cast(ISNoiseScrollAndBlend) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISNoiseScrollAndBlend) }, // { "BSImagespaceShaderRadialBlur", - // static_cast(ISRadialBlur) }, - // { "BSImagespaceShaderRadialBlurHigh", static_cast(ISRadialBlurHigh) }, - // { "BSImagespaceShaderRadialBlurMedium", static_cast(ISRadialBlurMedium) }, - { "BSImagespaceShaderRefraction", static_cast(ISRefraction) }, - { "BSImagespaceShaderISSAOCompositeSAO", static_cast(ISSAOCompositeSAO) }, - { "BSImagespaceShaderISSAOCompositeFog", static_cast(ISSAOCompositeFog) }, - { "BSImagespaceShaderISSAOCompositeSAOFog", static_cast(ISSAOCompositeSAOFog) }, - // { "BSImagespaceShaderISSAOCameraZ", static_cast(ISSAOCameraZ) }, - // { "BSImagespaceShaderISSILComposite", static_cast(ISSILComposite) }, - // { "BSImagespaceShaderISSnowSSS", static_cast(ISSnowSSS) }, - // { "BSImagespaceShaderISSAOBlurH", static_cast(ISSAOBlurH) }, - // { "BSImagespaceShaderISSAOBlurV", static_cast(ISSAOBlurV) }, - // { "BSImagespaceShaderISUnderwaterMask", static_cast(ISUnderwaterMask) }, - { "BSImagespaceShaderISApplyVolumetricLighting", static_cast(ISApplyVolumetricLighting) }, - { "BSImagespaceShaderReflectionsRayTracing", static_cast(ISReflectionsRayTracing) }, - //{ "BSImagespaceShaderReflectionsDebugSpecMask", static_cast(ISReflectionsDebugSpecMask) }, + // RE::ImageSpaceManager::GetCurrentIndex(ISRadialBlur) }, + // { "BSImagespaceShaderRadialBlurHigh", RE::ImageSpaceManager::GetCurrentIndex(ISRadialBlurHigh) }, + // { "BSImagespaceShaderRadialBlurMedium", RE::ImageSpaceManager::GetCurrentIndex(ISRadialBlurMedium) }, + { "BSImagespaceShaderRefraction", RE::ImageSpaceManager::GetCurrentIndex(ISRefraction) }, + { "BSImagespaceShaderISSAOCompositeSAO", RE::ImageSpaceManager::GetCurrentIndex(ISSAOCompositeSAO) }, + { "BSImagespaceShaderISSAOCompositeFog", RE::ImageSpaceManager::GetCurrentIndex(ISSAOCompositeFog) }, + { "BSImagespaceShaderISSAOCompositeSAOFog", RE::ImageSpaceManager::GetCurrentIndex(ISSAOCompositeSAOFog) }, + // { "BSImagespaceShaderISSAOCameraZ", RE::ImageSpaceManager::GetCurrentIndex(ISSAOCameraZ) }, + // { "BSImagespaceShaderISSILComposite", RE::ImageSpaceManager::GetCurrentIndex(ISSILComposite) }, + // { "BSImagespaceShaderISSnowSSS", RE::ImageSpaceManager::GetCurrentIndex(ISSnowSSS) }, + // { "BSImagespaceShaderISSAOBlurH", RE::ImageSpaceManager::GetCurrentIndex(ISSAOBlurH) }, + // { "BSImagespaceShaderISSAOBlurV", RE::ImageSpaceManager::GetCurrentIndex(ISSAOBlurV) }, + // { "BSImagespaceShaderISUnderwaterMask", RE::ImageSpaceManager::GetCurrentIndex(ISUnderwaterMask) }, + { "BSImagespaceShaderISApplyVolumetricLighting", RE::ImageSpaceManager::GetCurrentIndex(ISApplyVolumetricLighting) }, + { "BSImagespaceShaderReflectionsRayTracing", RE::ImageSpaceManager::GetCurrentIndex(ISReflectionsRayTracing) }, + //{ "BSImagespaceShaderReflectionsDebugSpecMask", RE::ImageSpaceManager::GetCurrentIndex(ISReflectionsDebugSpecMask) }, { "BSImagespaceShaderVolumetricLightingRaymarchCS", 256 }, { "BSImagespaceShaderVolumetricLightingGenerateCS", 257 }, - { "BSImagespaceShaderVolumetricLightingBlurHCS", static_cast(ISVolumetricLightingBlurHCS) }, - { "BSImagespaceShaderVolumetricLightingBlurVCS", static_cast(ISVolumetricLightingBlurVCS) }, - { "BSImagespaceShaderCopyDepthBuffer", 98 }, - { "BSImagespaceShaderCopyDepthBuffer", 99 }, - { "BSImagespaceShaderCopyDepthBuffer", 100 }, - { "BSImagespaceShaderISFullScreenVR", 129 }, + { "BSImagespaceShaderVolumetricLightingBlurHCS", RE::ImageSpaceManager::GetCurrentIndex(ISVolumetricLightingBlurHCS) }, + { "BSImagespaceShaderVolumetricLightingBlurVCS", RE::ImageSpaceManager::GetCurrentIndex(ISVolumetricLightingBlurVCS) }, + + // VR only shaders + // Disable BSImagespaceShaderCopyDepthBuffer since we don't have it REed and it causes issues with cache and upscaling + // https://github.com/doodlum/skyrim-community-shaders/issues/1552 + // { "BSImagespaceShaderCopyDepthBuffer", RE::ImageSpaceManager::GetCurrentIndex(ISCopyDepthBuffer) }, + // { "BSImagespaceShaderCopyDepthBuffer_DR", RE::ImageSpaceManager::GetCurrentIndex(ISCopyDepthBuffer_DR) }, + // { "BSImagespaceShaderCopyDepthBufferTargetSize", RE::ImageSpaceManager::GetCurrentIndex(ISCopyDepthBufferTargetSize) }, + { "BSImagespaceShaderGraphicsTextureFilterMode", RE::ImageSpaceManager::GetCurrentIndex(ISGraphicsTextureFilterMode) }, + { "BSImagespaceShaderISDownsampleHierarchicalDepthBufferCS", RE::ImageSpaceManager::GetCurrentIndex(ISDownsampleHierarchicalDepthBufferCS) }, + { "BSImagespaceShaderISDiffScaleDownsampleDepthBufferCS", RE::ImageSpaceManager::GetCurrentIndex(ISDiffScaleDownsampleDepthBufferCS) }, + { "BSImagespaceShaderISFullScreenVR", RE::ImageSpaceManager::GetCurrentIndex(ISFullScreenVR) }, + { "BSImagespaceShaderISTransformLvl7PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISTransformLvl7PreTest) }, + { "BSImagespaceShaderISLvl6PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl6PreTest) }, + { "BSImagespaceShaderISLvl5PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl5PreTest) }, + { "BSImagespaceShaderISLvl4PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl4PreTest) }, + { "BSImagespaceShaderISLvl3PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl3PreTest) }, + { "BSImagespaceShaderISLvl2PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl2PreTest) }, + { "BSImagespaceShaderISLvl1PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl1PreTest) }, + { "BSImagespaceShaderISLvl0PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl0PreTest) }, + { "BSImagespaceShaderISSetupPreTest", RE::ImageSpaceManager::GetCurrentIndex(ISSetupPreTest) }, }; auto it = descriptors.find(imagespaceShader.name); @@ -1658,16 +1698,22 @@ namespace SIE descriptor = it->second; return true; } + + static bool ResolveImageSpaceDescriptor(const RE::BSShader& shader, uint32_t& descriptor) + { + if (shader.shaderType == RE::BSShader::Type::ImageSpace) { + const auto& isShader = static_cast(shader); + return GetImagespaceShaderDescriptor(isShader, descriptor); + } + return true; + } } RE::BSGraphics::VertexShader* ShaderCache::GetVertexShader(const RE::BSShader& shader, uint32_t descriptor) { - if (shader.shaderType == RE::BSShader::Type::ImageSpace) { - const auto& isShader = static_cast(shader); - if (!SShaderCache::GetImagespaceShaderDescriptor(isShader, descriptor)) { - return nullptr; - } + if (!SShaderCache::ResolveImageSpaceDescriptor(shader, descriptor)) { + return nullptr; } auto state = globals::state; @@ -1680,6 +1726,9 @@ namespace SIE } if (state->IsDeveloperMode()) { + // Track this shader as active + TrackActiveShader(ShaderClass::Vertex, shader, descriptor); + auto key = SIE::SShaderCache::GetShaderString(ShaderClass::Vertex, shader, descriptor, true); if (blockedKeyIndex != -1 && !blockedKey.empty() && key == blockedKey) { if (std::find(blockedIDs.begin(), blockedIDs.end(), descriptor) == blockedIDs.end()) { @@ -1720,14 +1769,14 @@ namespace SIE return nullptr; } - if (shader.shaderType == RE::BSShader::Type::ImageSpace) { - const auto& isShader = static_cast(shader); - if (!SShaderCache::GetImagespaceShaderDescriptor(isShader, descriptor)) { - return nullptr; - } + if (!SShaderCache::ResolveImageSpaceDescriptor(shader, descriptor)) { + return nullptr; } if (state->IsDeveloperMode()) { + // Track this shader as active + TrackActiveShader(ShaderClass::Pixel, shader, descriptor); + auto key = SIE::SShaderCache::GetShaderString(ShaderClass::Pixel, shader, descriptor, true); if (blockedKeyIndex != -1 && !blockedKey.empty() && key == blockedKey) { if (std::find(blockedIDs.begin(), blockedIDs.end(), descriptor) == blockedIDs.end()) { @@ -1764,14 +1813,14 @@ namespace SIE return nullptr; } - if (shader.shaderType == RE::BSShader::Type::ImageSpace) { - const auto& isShader = static_cast(shader); - if (!SShaderCache::GetImagespaceShaderDescriptor(isShader, descriptor)) { - return nullptr; - } + if (!SShaderCache::ResolveImageSpaceDescriptor(shader, descriptor)) { + return nullptr; } if (state->IsDeveloperMode()) { + // Track this shader as active + TrackActiveShader(ShaderClass::Compute, shader, descriptor); + auto key = SIE::SShaderCache::GetShaderString(ShaderClass::Compute, shader, descriptor, true); if (blockedKeyIndex != -1 && !blockedKey.empty() && key == blockedKey) { if (std::find(blockedIDs.begin(), blockedIDs.end(), descriptor) == blockedIDs.end()) { @@ -2428,6 +2477,44 @@ namespace SIE void ShaderCache::IterateShaderBlock(bool a_forward) { + // Try to use active shaders list if available in developer mode + if (globals::state->IsDeveloperMode()) { + std::lock_guard lockActive(activeShadersMutex); + if (!activeShaders.empty()) { + // Build sorted list of active shader keys + std::vector keys; + keys.reserve(activeShaders.size()); + for (const auto& [key, _] : activeShaders) { + keys.push_back(key); + } + std::sort(keys.begin(), keys.end()); + + // Find current position or start + int currentIdx = -1; + if (!blockedKey.empty()) { + auto it = std::find(keys.begin(), keys.end(), blockedKey); + if (it != keys.end()) { + currentIdx = static_cast(std::distance(keys.begin(), it)); + } + } + + // Calculate next index + int targetIdx = 0; + if (currentIdx >= 0) { + targetIdx = a_forward ? (currentIdx + 1) % static_cast(keys.size()) : (currentIdx - 1 + static_cast(keys.size())) % static_cast(keys.size()); + } else { + targetIdx = a_forward ? 0 : static_cast(keys.size()) - 1; + } + + blockedKey = keys[targetIdx]; + blockedKeyIndex = -2; // Set to -2 for dev selections to distinguish from shaderMap indices + blockedIDs.clear(); + logger::debug("Blocking active shader ({}/{}) {}", targetIdx + 1, keys.size(), blockedKey); + return; + } + } + + // Fallback to original behavior with full shader map std::scoped_lock lockM{ mapMutex }; auto targetIndex = a_forward ? 0 : shaderMap.size() - 1; // default start or last element if (blockedKeyIndex >= 0 && shaderMap.size() > blockedKeyIndex) { // grab next element @@ -2437,7 +2524,7 @@ namespace SIE for (auto& [key, value] : shaderMap) { if (index++ == targetIndex) { blockedKey = key; - blockedKeyIndex = (uint)targetIndex; + blockedKeyIndex = -1; blockedIDs.clear(); logger::debug("Blocking shader ({}/{}) {}", blockedKeyIndex + 1, shaderMap.size(), blockedKey); return; @@ -2448,11 +2535,79 @@ namespace SIE void ShaderCache::DisableShaderBlocking() { blockedKey = ""; - blockedKeyIndex = (uint)-1; + blockedKeyIndex = -1; blockedIDs.clear(); logger::debug("Stopped blocking shaders"); } + void ShaderCache::TrackActiveShader(ShaderClass shaderClass, const RE::BSShader& shader, uint32_t descriptor) + { + if (!globals::state->IsDeveloperMode()) + return; + + auto key = SIE::SShaderCache::GetShaderString(shaderClass, shader, descriptor, true); + std::lock_guard lock(activeShadersMutex); + + auto& info = activeShaders[key]; + if (info.key.empty()) { + // First time seeing this shader + info.key = key; + info.shaderType = shader.shaderType.get(); + info.shaderClass = shaderClass; + info.descriptor = descriptor; + + // Construct disk path + info.diskPath = SIE::SShaderCache::GetDiskPath( + shader.shaderType == RE::BSShader::Type::ImageSpace ? + static_cast(shader).originalShaderName : + shader.fxpFilename, + descriptor, shaderClass); + } + + info.isActive = true; + info.drawCalls++; + info.lastUsed = std::chrono::steady_clock::now(); + } + + void ShaderCache::ResetFrameShaderTracking() + { + if (!globals::state->IsDeveloperMode()) + return; + + std::lock_guard lock(activeShadersMutex); + + // Mark all shaders as inactive for this frame + // Keep shaders that were used recently (within last 60 frames / ~1 second at 60fps) + auto now = std::chrono::steady_clock::now(); + auto timeout = std::chrono::seconds(1); + + for (auto it = activeShaders.begin(); it != activeShaders.end();) { + auto& info = it->second; + info.isActive = false; + info.drawCalls = 0; + + // Remove shaders that haven't been used recently + if (now - info.lastUsed > timeout) { + it = activeShaders.erase(it); + } else { + ++it; + } + } + } + + std::vector ShaderCache::GetActiveShaders() const + { + std::lock_guard lock(activeShadersMutex); + std::vector result; + result.reserve(activeShaders.size()); + + for (const auto& [key, info] : activeShaders) { + result.push_back(info); + } + + return result; + } + void ShaderCache::ManageCompilationSet(std::stop_token stoken) { managementThread = GetCurrentThread(); @@ -2558,20 +2713,46 @@ namespace SIE auto& cache = ShaderCache::Instance(); auto key = task.GetString(); auto shaderBlob = cache.GetCompletedShader(task); - if (shaderBlob) { - logger::debug("Compiling Task succeeded: {}", key); - completedTasks++; - } else { - logger::debug("Compiling Task failed: {}", key); - failedTasks++; + + bool shouldLogCompletion = false; + double completionTimeMs = 0.0; + + // Perform all completion operations under one mutex acquisition + { + std::scoped_lock lock(compilationMutex); + + // Update task counters + if (shaderBlob) { + logger::debug("Compiling Task succeeded: {}", key); + completedTasks++; + } else { + logger::debug("Compiling Task failed: {}", key); + failedTasks++; + } + + // Update timing + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + totalTime.QuadPart += now.QuadPart - lastCalculation.QuadPart; + lastCalculation = now; + + // Check if compilation is complete and set completion time if needed + if (completionTime.load(std::memory_order_relaxed) == 0 && completedTasks + failedTasks >= totalTasks) { + completionTime.store(now.QuadPart, std::memory_order_relaxed); + completionTimeMs = static_cast(now.QuadPart - lastReset.QuadPart) * 1000.0 / frequency.QuadPart; + shouldLogCompletion = true; + } + + // Update task tracking + processedTasks.insert(task); + tasksInProgress.erase(task); } - LARGE_INTEGER now; - QueryPerformanceCounter(&now); - totalTime.QuadPart += now.QuadPart - lastCalculation.QuadPart; - lastCalculation = now; - std::scoped_lock lock(compilationMutex); - processedTasks.insert(task); - tasksInProgress.erase(task); + + // Log completion outside the lock + if (shouldLogCompletion) { + logger::debug("Compilation completed in {} ms", GetHumanTime(completionTimeMs)); + } + conditionVariable.notify_one(); } @@ -2587,12 +2768,13 @@ namespace SIE cacheHitTasks = 0; QueryPerformanceCounter(&lastReset); QueryPerformanceCounter(&lastCalculation); + completionTime = { 0 }; // Reset completion time totalTime = { 0 }; } std::string CompilationSet::GetHumanTime(double a_totalMs) { - int milliseconds = (int)a_totalMs; + int milliseconds = static_cast(a_totalMs); int seconds = milliseconds / 1000; int minutes = seconds / 60; seconds %= 60; @@ -2604,6 +2786,8 @@ namespace SIE double CompilationSet::GetEta() { + // For ETA calculation, we still use the active compilation time (totalTime) + // because it reflects the actual work time, not wall-clock time double totalMs = static_cast(totalTime.QuadPart) * 1000.0 / frequency.QuadPart; if (totalMs == 0.0) { @@ -2616,7 +2800,13 @@ namespace SIE std::string CompilationSet::GetStatsString(bool a_timeOnly, bool a_elapsedOnly) { - double totalMs = static_cast(totalTime.QuadPart) * 1000.0 / frequency.QuadPart; + // Calculate elapsed time since compilation started + LARGE_INTEGER currentTime; + QueryPerformanceCounter(¤tTime); + + // Use completion time if compilation is finished, otherwise current time + int64_t endTime = (completionTime.load(std::memory_order_relaxed) != 0) ? completionTime.load(std::memory_order_relaxed) : currentTime.QuadPart; + double totalMs = static_cast(endTime - lastReset.QuadPart) * 1000.0 / frequency.QuadPart; if (a_timeOnly) { if (a_elapsedOnly) { diff --git a/src/ShaderCache.h b/src/ShaderCache.h index 6261526009..22568c2540 100644 --- a/src/ShaderCache.h +++ b/src/ShaderCache.h @@ -3,7 +3,7 @@ #include #include -static constexpr REL::Version SHADER_CACHE_VERSION = { 0, 0, 0, 37 }; +static constexpr REL::Version SHADER_CACHE_VERSION = { 0, 0, 0, 41 }; using namespace std::chrono; @@ -244,6 +244,7 @@ namespace SIE public: LARGE_INTEGER lastReset; LARGE_INTEGER lastCalculation; + std::atomic completionTime; // When compilation completed (QuadPart equivalent) LARGE_INTEGER frequency; LARGE_INTEGER totalTime = { 0 }; @@ -252,6 +253,7 @@ namespace SIE QueryPerformanceFrequency(&frequency); QueryPerformanceCounter(&lastReset); QueryPerformanceCounter(&lastCalculation); + completionTime.store(0, std::memory_order_relaxed); } std::optional WaitTake(std::stop_token stoken); @@ -631,9 +633,36 @@ namespace SIE OpaqueEffect = 1 << 29, }; - uint blockedKeyIndex = (uint)-1; // index in shaderMap; negative value indicates disabled + // Shader blocking data for developer mode + int blockedKeyIndex = -1; // index in shaderMap; negative value indicates disabled std::string blockedKey = ""; std::vector blockedIDs; // more than one descriptor could be blocked based on shader hash + + // Active shader tracking for developer mode + struct ActiveShaderInfo + { + std::string key; + RE::BSShader::Type shaderType; + ShaderClass shaderClass; + uint32_t descriptor; + std::wstring diskPath; + uint32_t drawCalls = 0; + bool isActive = false; // Used in current/recent frames + std::chrono::steady_clock::time_point lastUsed; + + bool operator<(const ActiveShaderInfo& other) const + { + return key < other.key; + } + }; + + ankerl::unordered_dense::map activeShaders; + mutable std::mutex activeShadersMutex; + + void TrackActiveShader(ShaderClass shaderClass, const RE::BSShader& shader, uint32_t descriptor); + void ResetFrameShaderTracking(); + std::vector GetActiveShaders() const; + HANDLE managementThread = nullptr; private: diff --git a/src/State.cpp b/src/State.cpp index b5c5bfd612..3846ab0046 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -15,6 +15,7 @@ #include "SettingsOverrideManager.h" #include "ShaderCache.h" #include "TruePBR.h" +#include "Utils/FileSystem.h" void State::Draw() { @@ -74,6 +75,9 @@ void State::Debug() for (auto& ft : frameTimePerType) ft = 0.0f; + // Reset active shader tracking for developer mode + globals::shaderCache->ResetFrameShaderTracking(); + // Start timing for this frame if (frameTimingFrequency.QuadPart == 0) { QueryPerformanceFrequency(&frameTimingFrequency); @@ -151,16 +155,18 @@ void State::Setup() globals::deferred->SetupResources(); } -static const std::string& GetConfigPath(State::ConfigMode a_configMode) +static std::string GetConfigPath(State::ConfigMode a_configMode) { switch (a_configMode) { case State::ConfigMode::USER: - return globals::state->userConfigPath; + return Util::PathHelpers::GetSettingsUserPath().string(); case State::ConfigMode::TEST: - return globals::state->testConfigPath; + return Util::PathHelpers::GetSettingsTestPath().string(); + case State::ConfigMode::THEME: + return Util::PathHelpers::GetSettingsThemePath().string(); case State::ConfigMode::DEFAULT: default: - return globals::state->defaultConfigPath; + return Util::PathHelpers::GetSettingsDefaultPath().string(); } } @@ -396,9 +402,9 @@ void State::Save(ConfigMode a_configMode) std::ofstream o{ configPath }; try { - std::filesystem::create_directories(folderPath); + std::filesystem::create_directories(Util::PathHelpers::GetCommunityShaderPath()); } catch (const std::filesystem::filesystem_error& e) { - logger::warn("Error creating directory during Save ({}) : {}\n", folderPath, e.what()); + logger::warn("Error creating directory during Save ({}) : {}\n", Util::PathHelpers::GetCommunityShaderPath().string(), e.what()); return; } @@ -610,11 +616,7 @@ void State::ModifyShaderLookup(const RE::BSShader& a_shader, uint& a_vertexDescr a_pixelDescriptor &= ~(uint32_t)SIE::ShaderCache::LightingShaderFlags::AdditionalAlphaMask; } - static auto enableImprovedSnow = RE::GetINISetting("bEnableImprovedSnow:Display"); - static bool vr = REL::Module::IsVR(); - - if (vr || !enableImprovedSnow->GetBool()) - a_pixelDescriptor &= ~((uint32_t)SIE::ShaderCache::LightingShaderFlags::Snow); + a_pixelDescriptor &= ~((uint32_t)SIE::ShaderCache::LightingShaderFlags::Snow); if (deferred->deferredPass || a_forceDeferred) a_pixelDescriptor |= (uint32_t)SIE::ShaderCache::LightingShaderFlags::Deferred; @@ -707,7 +709,7 @@ void State::SetAdapterDescription(const std::wstring& description) adapterDescription = converter.to_bytes(description); } -void State::UpdateSharedData(bool a_inWorld, bool a_prepass) +void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] bool a_prepass) { { SharedDataCB data{}; @@ -763,9 +765,12 @@ void State::UpdateSharedData(bool a_inWorld, bool a_prepass) auto& upscaling = globals::features::upscaling; if (upscaling.loaded) { - if (temporal && (a_inWorld || a_prepass) && upscaling.GetUpscaleMethod() != Upscaling::UpscaleMethod::kTAA) { - auto renderSize = Util::ConvertToDynamic(screenSize); - data.MipBias = std::log2f(renderSize.x / screenSize.x) - 1.0f; + auto upscaleMethod = upscaling.GetUpscaleMethod(); + if (temporal && upscaleMethod != Upscaling::UpscaleMethod::kTAA) { + auto renderSize = Util::ConvertToDynamic(screenSize, true); + data.MipBias = std::log2f(renderSize.x / screenSize.x); + if (upscaleMethod == Upscaling::UpscaleMethod::kDLSS) + data.MipBias -= 1.0f; } else { data.MipBias = 0; } @@ -827,3 +832,64 @@ float State::GetTotalSmoothedDrawCalls() const { return static_cast(smoothDrawCalls[magic_enum::enum_integer(RE::BSShader::Type::Total)]); } + +void State::LoadTheme() +{ + // Don't override if a theme preset is already selected (e.g., first-time Default Dark setup) + if (!globals::menu->GetSettings().SelectedThemePreset.empty()) { + logger::info("Theme preset '{}' already selected, skipping SettingsTheme.json load", globals::menu->GetSettings().SelectedThemePreset); + return; + } + + auto themeConfigPath = Util::PathHelpers::GetSettingsThemePath(); + + if (!std::filesystem::exists(themeConfigPath)) { + logger::info("No theme config file found at: {}", themeConfigPath.string()); + return; + } + + try { + std::ifstream themeFile(themeConfigPath); + if (!themeFile.is_open()) { + logger::warn("Unable to open theme config file: {}", themeConfigPath.string()); + return; + } + + json themeSettings; + themeFile >> themeSettings; + themeFile.close(); + + if (themeSettings["Menu"].is_object()) { + logger::info("Loading theme settings from: {}", themeConfigPath.string()); + globals::menu->LoadTheme(themeSettings["Menu"]); + } + } catch (const std::exception& e) { + logger::warn("Error loading theme config file: {}", e.what()); + } +} + +void State::SaveTheme() +{ + auto themeConfigPath = Util::PathHelpers::GetSettingsThemePath(); + + try { + std::filesystem::create_directories(themeConfigPath.parent_path()); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Error creating directory during SaveTheme: {}", e.what()); + return; + } + + std::ofstream themeFile(themeConfigPath); + if (!themeFile.is_open()) { + logger::warn("Failed to open theme config file for saving: {}", themeConfigPath.string()); + return; + } + + json themeSettings; + globals::menu->SaveTheme(themeSettings["Menu"]); + + themeFile << std::setw(4) << themeSettings << std::endl; + themeFile.close(); + + logger::info("Theme settings saved to: {}", themeConfigPath.string()); +} diff --git a/src/State.h b/src/State.h index ce49517510..d529e904ff 100644 --- a/src/State.h +++ b/src/State.h @@ -51,10 +51,6 @@ class State spdlog::level::level_enum logLevel = spdlog::level::info; std::string shaderDefinesString = ""; std::vector> shaderDefines{}; // data structure to parse string into; needed to avoid dangling pointers - const std::string folderPath = "Data\\SKSE\\Plugins\\CommunityShaders"; - const std::string testConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsTest.json"; - const std::string userConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsUser.json"; - const std::string defaultConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsDefault.json"; float timer = 0; double smoothDrawCalls[RE::BSShader::Type::Total + 1]; @@ -73,7 +69,8 @@ class State { DEFAULT, USER, - TEST + TEST, + THEME }; void Draw(); @@ -84,6 +81,9 @@ class State void Load(ConfigMode a_configMode = ConfigMode::USER, bool a_allowReload = true); void Save(ConfigMode a_configMode = ConfigMode::USER); + void LoadTheme(); + void SaveTheme(); + bool ValidateCache(CSimpleIniA& a_ini); void WriteDiskCacheInfo(CSimpleIniA& a_ini); @@ -162,6 +162,7 @@ class State THLand4HasDisplacement = 1 << 4, THLand5HasDisplacement = 1 << 5, ETMaterialModel = 0b111 << 6, + THLandHasDisplacement = 1 << 9 }; bool inWorld = false; @@ -183,6 +184,7 @@ class State ExtraFeatureDescriptor == other.ExtraFeatureDescriptor; } }; + STATIC_ASSERT_ALIGNAS_16(PermutationCB); ConstantBuffer* permutationCB = nullptr; @@ -203,6 +205,7 @@ class State float MipBias; float pad0; }; + STATIC_ASSERT_ALIGNAS_16(SharedDataCB); ConstantBuffer* sharedDataCB = nullptr; ConstantBuffer* featureDataCB = nullptr; diff --git a/src/TruePBR.cpp b/src/TruePBR.cpp index 4328b0062f..86afc11300 100644 --- a/src/TruePBR.cpp +++ b/src/TruePBR.cpp @@ -7,6 +7,7 @@ #include "Hooks.h" #include "ShaderCache.h" #include "State.h" +#include "Util.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( GlintParameters, @@ -107,14 +108,8 @@ void TruePBR::DrawSettings() { if (ImGui::CollapsingHeader("PBR", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { if (ImGui::TreeNodeEx("Texture Set Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::BeginCombo("Texture Set", selectedPbrTextureSetName.c_str())) { - for (auto& [textureSetName, textureSet] : pbrTextureSets) { - if (ImGui::Selectable(textureSetName.c_str(), textureSetName == selectedPbrTextureSetName)) { - selectedPbrTextureSetName = textureSetName; - selectedPbrTextureSet = &textureSet; - } - } - ImGui::EndCombo(); + if (Util::SearchableCombo("Texture Set", selectedPbrTextureSetName, pbrTextureSets)) { + selectedPbrTextureSet = &pbrTextureSets[selectedPbrTextureSetName]; } if (selectedPbrTextureSet != nullptr) { @@ -200,14 +195,8 @@ void TruePBR::DrawSettings() } if (ImGui::TreeNodeEx("Material Object Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::BeginCombo("Material Object", selectedPbrMaterialObjectName.c_str())) { - for (auto& [materialObjectName, materialObject] : pbrMaterialObjects) { - if (ImGui::Selectable(materialObjectName.c_str(), materialObjectName == selectedPbrMaterialObjectName)) { - selectedPbrMaterialObjectName = materialObjectName; - selectedPbrMaterialObject = &materialObject; - } - } - ImGui::EndCombo(); + if (Util::SearchableCombo("Material Object", selectedPbrMaterialObjectName, pbrMaterialObjects)) { + selectedPbrMaterialObject = &pbrMaterialObjects[selectedPbrMaterialObjectName]; } if (selectedPbrMaterialObject != nullptr) { @@ -1109,10 +1098,12 @@ bool TruePBR::TESObjectLAND_SetupMaterial(RE::TESObjectLAND* land) return false; } + auto memoryManager = RE::MemoryManager::GetSingleton(); + if (land->loadedData != nullptr && land->loadedData->mesh[0] != nullptr) { land->data.flags.set(static_cast(8)); for (uint32_t quadIndex = 0; quadIndex < 4; ++quadIndex) { - auto shaderProperty = static_cast(globals::game::memoryManager->Allocate(REL::Module::IsVR() ? 0x178 : sizeof(RE::BSLightingShaderProperty), 0, false)); + auto shaderProperty = static_cast(memoryManager->Allocate(REL::Module::IsVR() ? 0x178 : sizeof(RE::BSLightingShaderProperty), 0, false)); shaderProperty->Ctor(); { @@ -1148,7 +1139,7 @@ bool TruePBR::TESObjectLAND_SetupMaterial(RE::TESObjectLAND* land) } bool noLODLandBlend = false; - auto tes = globals::game::tes; + auto tes = RE::TES::GetSingleton(); auto worldSpace = tes->GetRuntimeData2().worldSpace; if (worldSpace != nullptr) { if (auto terrainManager = worldSpace->GetTerrainManager()) { @@ -1249,7 +1240,7 @@ struct BSTempEffectGeometryDecal_Initialize auto* singleton = globals::truePBR; if (decal->decal != nullptr && singleton->IsPBRTextureSet(decal->texSet)) { - auto shaderProperty = static_cast(globals::game::memoryManager->Allocate(sizeof(RE::BSLightingShaderProperty), 0, false)); + auto shaderProperty = static_cast(RE::MemoryManager::GetSingleton()->Allocate(sizeof(RE::BSLightingShaderProperty), 0, false)); shaderProperty->Ctor(); { diff --git a/src/TruePBR.h b/src/TruePBR.h index d6963fe5ce..2ead82c6e4 100644 --- a/src/TruePBR.h +++ b/src/TruePBR.h @@ -1,5 +1,7 @@ #pragma once +#include + struct GlintParameters { bool enabled = false; diff --git a/src/Utils/D3D.cpp b/src/Utils/D3D.cpp index bfbb9e18da..cb7e180e3f 100644 --- a/src/Utils/D3D.cpp +++ b/src/Utils/D3D.cpp @@ -12,7 +12,7 @@ namespace Util { if (a_rtv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < RE::RENDER_TARGETS::kTOTAL; i++) { + for (int i = 0; i < GetRenderTargetCount(); i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_rtv == rt.RTV) { return rt.SRV; @@ -27,7 +27,7 @@ namespace Util { if (a_srv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < RE::RENDER_TARGETS::kTOTAL; i++) { + for (int i = 0; i < GetRenderTargetCount(); i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_srv == rt.SRV || a_srv == rt.SRVCopy) { return rt.RTV; @@ -44,7 +44,7 @@ namespace Util if (a_srv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < RENDER_TARGET::kTOTAL; i++) { + for (int i = 0; i < GetRenderTargetCount(); i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_srv == rt.SRV || a_srv == rt.SRVCopy) { return std::string(magic_enum::enum_name(static_cast(i))); @@ -60,7 +60,7 @@ namespace Util using RENDER_TARGET = RE::RENDER_TARGETS::RENDER_TARGET; if (a_rtv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < RENDER_TARGET::kTOTAL; i++) { + for (int i = 0; i < GetRenderTargetCount(); i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_rtv == rt.RTV) { return std::string(magic_enum::enum_name(static_cast(i))); diff --git a/src/Utils/D3D.h b/src/Utils/D3D.h index fc0789e772..93893532e9 100644 --- a/src/Utils/D3D.h +++ b/src/Utils/D3D.h @@ -15,4 +15,15 @@ namespace Util // Texture manipulation utilities void ApplyHighlightTintToTexture(ID3D11Texture2D* texture, bool isHighlighted, const std::array& highlightColor = { 1.0f, 0.5f, 0.0f, 0.3f }); HRESULT CreateOverlayTextureAndRTV(ID3D11Device* device, int width, int height, ID3D11Texture2D** outTex, ID3D11RenderTargetView** outRTV); + + // VR-aware counts for render targets + inline int GetRenderTargetCount() + { + return REL::Module::IsVR() ? RE::RENDER_TARGETS::kVRTOTAL : RE::RENDER_TARGETS::kTOTAL; + } + + inline int GetDepthStencilCount() + { + return REL::Module::IsVR() ? RE::RENDER_TARGETS_DEPTHSTENCIL::kVRTOTAL : RE::RENDER_TARGETS_DEPTHSTENCIL::kTOTAL; + } } // namespace Util diff --git a/src/Utils/FileSystem.cpp b/src/Utils/FileSystem.cpp index cbaa08e899..1d41c7f17c 100644 --- a/src/Utils/FileSystem.cpp +++ b/src/Utils/FileSystem.cpp @@ -1,4 +1,6 @@ #include "FileSystem.h" +#include +#include #include #include @@ -20,7 +22,8 @@ namespace Util auto executablePath = std::filesystem::path(buffer); auto gamePath = executablePath.parent_path(); - return gamePath / "Data"; + auto dataPath = gamePath / "Data"; + return dataPath; } catch (const std::exception& e) { // Fallback to current_path if Windows API method fails logger::warn("Failed to get game path via Windows API, falling back to current_path: {}", e.what()); @@ -38,11 +41,6 @@ namespace Util return GetDataPath() / "SKSE" / "Plugins" / "CommunityShaders_ImGui.ini"; } - std::filesystem::path GetUserSettingsPath() - { - return GetCommunityShaderPath() / "UserSettings.json"; - } - std::filesystem::path GetInterfacePath() { return GetDataPath() / "Interface" / "CommunityShaders"; @@ -73,6 +71,16 @@ namespace Util return GetCommunityShaderPath() / "SettingsDefault.json"; } + std::filesystem::path GetSettingsThemePath() + { + return GetCommunityShaderPath() / "SettingsTheme.json"; + } + + std::filesystem::path GetThemesPath() + { + return GetCommunityShaderPath() / "Themes"; + } + std::filesystem::path GetOverridesPath() { return GetCommunityShaderPath() / "Overrides"; @@ -93,16 +101,6 @@ namespace Util return GetShadersPath() / "Features"; } - std::filesystem::path GetFeatureIniPath(const std::string& featureName) - { - return GetFeaturesPath() / (featureName + ".ini"); - } - - std::filesystem::path GetFeatureShaderPath(const std::string& featureName) - { - return GetShadersPath() / featureName; - } - std::filesystem::path GetCurrentModuleRealPath() { try { @@ -141,10 +139,25 @@ namespace Util return GetRootRealPath() / "Shaders"; } + std::filesystem::path GetThemesRealPath() + { + return GetRootRealPath() / "SKSE" / "Plugins" / "CommunityShaders" / "Themes"; + } + std::filesystem::path GetFeaturesRealPath() { return GetShadersRealPath() / "Features"; } + + std::filesystem::path GetFeatureIniPath(const std::string& featureName) + { + return GetFeaturesPath() / (featureName + ".ini"); + } + + std::filesystem::path GetFeatureShaderPath(const std::string& featureName) + { + return GetFeaturesPath() / featureName; + } } // File system utilities implementation @@ -176,6 +189,15 @@ namespace Util return result; } + + void EnsureDirectoryExists(const std::filesystem::path& path) + { + std::error_code ec; + std::filesystem::create_directories(path, ec); + if (ec) { + logger::warn("Failed to create directory '{}': {}", path.string(), ec.message()); + } + } } } diff --git a/src/Utils/FileSystem.h b/src/Utils/FileSystem.h index 3085a3a23c..8e3e431347 100644 --- a/src/Utils/FileSystem.h +++ b/src/Utils/FileSystem.h @@ -1,8 +1,7 @@ #pragma once -#include "FileSystem.h" #include "Format.h" -#include "Winapi.h" +#include "WinApi.h" #include #include #include @@ -44,12 +43,6 @@ namespace Util */ std::filesystem::path GetImGuiIniPath(); - /** - * Gets the UserSettings.json file path - * @return CommunityShaderPath / "UserSettings.json" - */ - std::filesystem::path GetUserSettingsPath(); - /** * Gets the CommunityShaders Interface directory path * @return Data / "Interface" / "CommunityShaders" @@ -86,6 +79,18 @@ namespace Util */ std::filesystem::path GetSettingsDefaultPath(); + /** + * Gets the SettingsTheme.json file path + * @return CommunityShaderPath / "SettingsTheme.json" + */ + std::filesystem::path GetSettingsThemePath(); + + /** + * Gets the Themes directory path + * @return CommunityShaderPath / "Themes" + */ + std::filesystem::path GetThemesPath(); + /** * Gets the Overrides directory path * @return CommunityShaderPath / "Overrides" @@ -148,6 +153,12 @@ namespace Util */ std::filesystem::path GetShadersRealPath(); + /** + * Returns the real path to the Themes directory containing theme JSON files. + * @return / "SKSE" / "Plugins" / "CommunityShaders" / "Themes" + */ + std::filesystem::path GetThemesRealPath(); + /** * Returns the real path to the Features directory containing feature INI files. * @return / "Shaders" / "Features" @@ -178,6 +189,12 @@ namespace Util * @return DeletionResult with success status and details */ DeletionResult SafeDelete(const std::string& path, const std::string& description); + + /** + * Ensures a directory exists, creating it if necessary with proper error handling + * @param path The directory path to ensure exists + */ + void EnsureDirectoryExists(const std::filesystem::path& path); } /** diff --git a/src/Utils/Format.cpp b/src/Utils/Format.cpp index a63befd056..63eaddc759 100644 --- a/src/Utils/Format.cpp +++ b/src/Utils/Format.cpp @@ -105,6 +105,57 @@ namespace Util return oss.str(); } + std::string FormatFileSize(uint64_t bytes) + { + if (bytes >= 1024 * 1024) { + char buffer[32]; + sprintf_s(buffer, "%.1f MB", static_cast(bytes) / (1024 * 1024)); + return buffer; + } else { + char buffer[32]; + sprintf_s(buffer, "%.1f KB", static_cast(bytes) / 1024); + return buffer; + } + } + + std::string FormatTimeAgo(std::filesystem::file_time_type fileTime) + { + try { + // Convert filesystem time to system time correctly + // std::filesystem::file_time_type uses Windows FILETIME epoch (1601-01-01) + // std::chrono::system_clock uses Unix epoch (1970-01-01) + // Difference is 11644473600 seconds (number of seconds between 1601-01-01 and 1970-01-01) + constexpr int64_t WINDOWS_TO_UNIX_EPOCH_SECONDS = 11644473600LL; + auto fileDuration = fileTime.time_since_epoch(); + auto systemDuration = std::chrono::duration_cast( + fileDuration - std::chrono::seconds(WINDOWS_TO_UNIX_EPOCH_SECONDS)); + auto systemTime = std::chrono::system_clock::time_point(systemDuration); + auto fileTimeT = std::chrono::system_clock::to_time_t(systemTime); + auto nowT = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + + // Check if file time is in the future + if (fileTimeT > nowT) { + return "Future"; + } + + // Calculate duration in seconds + auto seconds = static_cast(nowT - fileTimeT); + + // Format based on time difference + if (seconds < 60) { + return std::to_string(seconds) + "s ago"; + } else if (seconds < 3600) { + return std::to_string(seconds / 60) + "m ago"; + } else if (seconds < 86400) { + return std::to_string(seconds / 3600) + "h ago"; + } else { + return std::to_string(seconds / 86400) + "d ago"; + } + } catch (const std::exception&) { + return "Unknown"; + } + } + std::string TimeAgoString(std::chrono::steady_clock::time_point last) { using namespace std::chrono; @@ -153,8 +204,25 @@ namespace Util } else if (b < a && b > 0.0f) { percentDelta = 100.0f * (a - b) / b; } - std::string percentStr = (percentDelta >= threshold) ? std::format(" ({:+.1f}%)", (b < a ? -percentDelta : percentDelta)) : ""; - return (delta > 0.0f ? "+" : "") + FormatMilliseconds(delta) + percentStr; + char buffer[64]; + if (percentDelta >= threshold) { + sprintf_s(buffer, " (+%.1f%%)", (b < a ? -percentDelta : percentDelta)); + } else { + buffer[0] = '\0'; + } + return (delta > 0.0f ? "+" : "") + FormatMilliseconds(delta) + buffer; + } + + std::string FormatDeltaWithPercent(float delta) + { + // Format as percentage with sign + char buffer[32]; + if (delta >= 0.0f) { + std::snprintf(buffer, sizeof(buffer), "+%.1f%%", delta); + } else { + std::snprintf(buffer, sizeof(buffer), "%.1f%%", delta); + } + return buffer; } float CalculatePercentage(float part, float total, float defaultValue) diff --git a/src/Utils/Format.h b/src/Utils/Format.h index 7bd4a195a4..0f253af3e2 100644 --- a/src/Utils/Format.h +++ b/src/Utils/Format.h @@ -36,6 +36,12 @@ namespace Util * Formats a float value as a percentage string with 1 decimal place. */ std::string FormatPercent(float percent); + /** + * Formats a file size in bytes to a human-readable string (KB/MB). + * @param bytes The file size in bytes + * @return Formatted string like "1.2 MB" or "512 KB" + */ + std::string FormatFileSize(uint64_t bytes); /** * Returns a human-readable string for the time elapsed since the given time point (e.g., '5s', '2m', '1h'). */ @@ -51,6 +57,13 @@ namespace Util */ std::string TimeAgoStringQPC(const LARGE_INTEGER& lastTime, const LARGE_INTEGER& frequency); + /** + * Returns a human-readable string for the time elapsed since the given filesystem time point (e.g., '5s ago', '2m ago', '1h ago'). + * @param fileTime The filesystem time point to calculate time ago from + * @return Formatted string showing time elapsed (e.g., "5s ago", "2m ago", "1h ago", or "Unknown" on error) + */ + std::string FormatTimeAgo(std::filesystem::file_time_type fileTime); + /** * Formats a delta value with percentage difference for A/B test comparisons. * Returns a string like "+0.45 ms (+12.3%)" or "-0.23 ms (-8.1%)". diff --git a/src/Utils/Game.cpp b/src/Utils/Game.cpp index 0f1cc0de69..d404433e59 100644 --- a/src/Utils/Game.cpp +++ b/src/Utils/Game.cpp @@ -31,7 +31,7 @@ namespace Util float4 TryGetWaterData(float offsetX, float offsetY) { if (globals::game::shadowState) { - if (auto tes = globals::game::tes) { + if (auto tes = RE::TES::GetSingleton()) { auto position = GetEyePosition(0); position.x += offsetX; position.y += offsetY; @@ -302,10 +302,10 @@ namespace Util bool IsInterior() { - auto tes = globals::game::tes; + auto tes = RE::TES::GetSingleton(); if (tes && !tes->interiorCell) { if (auto worldSpace = tes->GetRuntimeData2().worldSpace) { - if (!worldSpace->flags.all(RE::TESWorldSpace::Flag::kNoSky)) { + if (!worldSpace->flags.any(RE::TESWorldSpace::Flag::kNoSky, RE::TESWorldSpace::Flag::kFixedDimensions)) { return false; } } diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 0d72632f2e..ce2622357c 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1,5 +1,8 @@ #include "UI.h" + +#include "FileSystem.h" #include "Menu.h" +#include "Menu/IconLoader.h" #ifndef DIRECTINPUT_VERSION # define DIRECTINPUT_VERSION 0x0800 @@ -16,21 +19,24 @@ #define STB_IMAGE_IMPLEMENTATION #include +#include #include #include #include #include #include +#include #include #include #include +#include #include namespace Util { HoverTooltipWrapper::HoverTooltipWrapper() { - hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal); + hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled); if (hovered) { ImGui::BeginTooltip(); ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); @@ -70,192 +76,10 @@ namespace Util const auto Size = ImGui::GetMainViewport()->Size; return { Size.x * scale, Size.y * scale }; } - // Icon loading functions (moved from UIIconLoader) - bool LoadTextureFromFile(ID3D11Device* device, - const char* filename, - ID3D11ShaderResourceView** out_srv, - ImVec2& out_size) - { - // Validate input parameters - if (!device || !out_srv) { - logger::warn("LoadTextureFromFile: Invalid parameters - device: {}, out_srv: {}", - device ? "valid" : "null", out_srv ? "valid" : "null"); - return false; - } - - // Initialize output to nullptr - *out_srv = nullptr; - - logger::debug("LoadTextureFromFile: Attempting to load {}", filename); - - // Load from disk into a raw RGBA buffer - int image_width = 0; - int image_height = 0; - int channels_in_file; - unsigned char* image_data = stbi_load(filename, &image_width, &image_height, &channels_in_file, 4); - if (image_data == NULL) { - logger::warn("LoadTextureFromFile: Failed to load image data from {}", filename); - return false; - } - // Creates Textures for Icons with Mipmapping to support high DPI displays. - logger::debug("LoadTextureFromFile: Loaded image {}x{} with {} channels from {}", - image_width, image_height, channels_in_file, filename); - D3D11_TEXTURE2D_DESC desc; - ZeroMemory(&desc, sizeof(desc)); - desc.Width = image_width; - desc.Height = image_height; - desc.MipLevels = 0; // Let D3D11 calculate the full mipmap chain - desc.ArraySize = 1; - desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - desc.SampleDesc.Count = 1; - desc.SampleDesc.Quality = 0; - desc.Usage = D3D11_USAGE_DEFAULT; - desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; - desc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS; - desc.CPUAccessFlags = 0; - - ID3D11Texture2D* pTexture = nullptr; - // Create texture without initial data to enable full mipmap chain - HRESULT hr = device->CreateTexture2D(&desc, nullptr, &pTexture); - if (FAILED(hr) || !pTexture) { - logger::warn("LoadTextureFromFile: Failed to create D3D11 texture, HRESULT: 0x{:08X}", static_cast(hr)); - stbi_image_free(image_data); - return false; - } - - // Upload the base level data using UpdateSubresource - ID3D11DeviceContext* context = nullptr; - device->GetImmediateContext(&context); - if (context) { - context->UpdateSubresource(pTexture, 0, nullptr, image_data, image_width * 4, 0); - } - - // Create simple shader resource view - hr = device->CreateShaderResourceView(pTexture, nullptr, out_srv); - if (FAILED(hr) || !*out_srv) { - logger::warn("LoadTextureFromFile: Failed to create shader resource view, HRESULT: 0x{:08X}", static_cast(hr)); - pTexture->Release(); - stbi_image_free(image_data); - if (context) - context->Release(); - *out_srv = nullptr; - return false; - } - - // Generate mipmaps for better icon quality at different scales - if (context) { - context->GenerateMips(*out_srv); - context->Release(); - } - // Success - clean up intermediate resources - pTexture->Release(); - stbi_image_free(image_data); - out_size = ImVec2((float)image_width, (float)image_height); - logger::debug("LoadTextureFromFile: Successfully loaded {} ({}x{})", filename, image_width, image_height); - return true; - } bool InitializeMenuIcons(Menu* menu) { - if (!menu) { - logger::warn("InitializeMenuIcons: Menu pointer is null"); - return false; - } - - // Get the D3D device from globals - ID3D11Device* device = globals::d3d::device; - if (!device) { - logger::warn("InitializeMenuIcons: D3D device is null"); - return false; - } - // Define path to icons - std::string basePath = Util::PathHelpers::GetIconsPath().string() + "\\"; - logger::info("InitializeMenuIcons: Loading icons from base path: {}", basePath); - - // Initialize all texture pointers to nullptr for safe cleanup - std::array texturePointers = { - &menu->uiIcons.saveSettings.texture, - &menu->uiIcons.loadSettings.texture, - &menu->uiIcons.clearCache.texture, - &menu->uiIcons.logo.texture, - &menu->uiIcons.discord.texture, - &menu->uiIcons.characters.texture, - &menu->uiIcons.display.texture, - &menu->uiIcons.grass.texture, - &menu->uiIcons.lighting.texture, - &menu->uiIcons.sky.texture, - &menu->uiIcons.landscape.texture, - &menu->uiIcons.water.texture, - &menu->uiIcons.debug.texture, - &menu->uiIcons.materials.texture, - &menu->uiIcons.postProcessing.texture - }; - - // Safely release existing textures - for (auto* texturePtr : texturePointers) { - if (*texturePtr) { - (*texturePtr)->Release(); - *texturePtr = nullptr; - } - } - - // Instead of failing completely if one icon fails, try to load each one individually - bool anyIconLoaded = false; - int iconsLoaded = 0; - - // Helper function to load a single icon - auto loadIcon = [&](const std::string& path, ID3D11ShaderResourceView** texture, ImVec2& size) -> bool { - if (LoadTextureFromFile(device, path.c_str(), texture, size)) { - iconsLoaded++; - anyIconLoaded = true; - return true; - } - return false; - }; - - // Helper function to load icon with logging - auto loadIconWithLogging = [&](const std::string& path, ID3D11ShaderResourceView** texture, ImVec2& size, const std::string& name) { - if (!loadIcon(path, texture, size)) { - logger::warn("InitializeMenuIcons: Failed to load {} icon from: {}", name, path); - } - }; - - // Load action icons - loadIconWithLogging(basePath + "Action Icons\\save-settings.png", &menu->uiIcons.saveSettings.texture, menu->uiIcons.saveSettings.size, "save-settings"); - loadIconWithLogging(basePath + "Action Icons\\load-settings.png", &menu->uiIcons.loadSettings.texture, menu->uiIcons.loadSettings.size, "load-settings"); - loadIconWithLogging(basePath + "Action Icons\\clear-cache.png", &menu->uiIcons.clearCache.texture, menu->uiIcons.clearCache.size, "clear-cache"); - loadIconWithLogging(basePath + "Community Shaders Logo\\cs-logo.png", &menu->uiIcons.logo.texture, menu->uiIcons.logo.size, "logo"); - loadIconWithLogging(basePath + "Action Icons\\discord.png", &menu->uiIcons.discord.texture, menu->uiIcons.discord.size, "discord"); - - // Load category icons in a more compact way - struct CategoryIcon - { - const char* filename; - ID3D11ShaderResourceView** texture; - ImVec2& size; - }; - - std::vector categoryIcons = { - { "characters.png", &menu->uiIcons.characters.texture, menu->uiIcons.characters.size }, - { "display.png", &menu->uiIcons.display.texture, menu->uiIcons.display.size }, - { "grass.png", &menu->uiIcons.grass.texture, menu->uiIcons.grass.size }, - { "lighting.png", &menu->uiIcons.lighting.texture, menu->uiIcons.lighting.size }, - { "sky.png", &menu->uiIcons.sky.texture, menu->uiIcons.sky.size }, - { "landscape.png", &menu->uiIcons.landscape.texture, menu->uiIcons.landscape.size }, - { "water.png", &menu->uiIcons.water.texture, menu->uiIcons.water.size }, - { "debug.png", &menu->uiIcons.debug.texture, menu->uiIcons.debug.size }, - { "materials.png", &menu->uiIcons.materials.texture, menu->uiIcons.materials.size }, - { "post-processing.png", &menu->uiIcons.postProcessing.texture, menu->uiIcons.postProcessing.size } - }; - - for (const auto& icon : categoryIcons) { - std::string path = basePath + "Categories\\" + icon.filename; - loadIcon(path, icon.texture, icon.size); - } - - logger::info("InitializeMenuIcons: Loaded {}/15 icons successfully", iconsLoaded); - - return anyIconLoaded; + return IconLoader::InitializeMenuIcons(menu); } // Text rendering helpers @@ -290,7 +114,7 @@ namespace Util return ImVec2(endPos.x - startPos.x, endPos.y - startPos.y); } - ImVec2 DrawAlignedTextWithLogo(ID3D11ShaderResourceView* logoTexture, const ImVec2& logoSize, const char* text, float textScale) + ImVec2 DrawAlignedTextWithLogo(ID3D11ShaderResourceView* logoTexture, const ImVec2& logoSize, const char* text, float textScale, ImU32 logoTint) { // Save current cursor position ImVec2 startPos = ImGui::GetCursorPos(); @@ -305,10 +129,19 @@ namespace Util // Position cursor for logo with vertical alignment ImGui::SetCursorPos(ImVec2(startPos.x, startPos.y + verticalOffset)); - // Render logo - ImGui::Image(logoTexture, logoSize); + // Render logo using draw list with tint color support + ImVec2 logoPos = ImGui::GetCursorScreenPos(); + ImVec2 logoMin = logoPos; + ImVec2 logoMax = ImVec2(logoPos.x + logoSize.x, logoPos.y + logoSize.y); + ImGui::GetWindowDrawList()->AddImage(logoTexture, logoMin, logoMax, ImVec2(0, 0), ImVec2(1, 1), logoTint); + + // Advance cursor past logo + ImGui::Dummy(logoSize); ImGui::SameLine(); + // Add consistent spacing between logo and text + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0f); + // Reset cursor for text with proper vertical alignment ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX(), startPos.y)); // Use windowed font scale for sharper text @@ -325,6 +158,25 @@ namespace Util ImVec2 endPos = ImGui::GetCursorPos(); return ImVec2(endPos.x - startPos.x, endPos.y - startPos.y); } + + float GetCenterOffsetForContent(float contentWidth) + { + // Get full window width for true centering + float fullWindowWidth = ImGui::GetWindowWidth(); + float windowPaddingX = ImGui::GetStyle().WindowPadding.x; + float availableFullWidth = fullWindowWidth - (windowPaddingX * 2.0f); + + // Calculate center position + float centerOffset = (availableFullWidth - contentWidth) * 0.5f; + + // Adjust for current cursor position + float currentX = ImGui::GetCursorPosX(); + float targetX = windowPaddingX + centerOffset; + float offset = targetX - currentX; + + return offset > 0.0f ? offset : 0.0f; + } + // StyledButtonWrapper implementation StyledButtonWrapper::StyledButtonWrapper(const ImVec4& normalColor, const ImVec4& hoveredColor, const ImVec4& activeColor) : m_pushedStyles(0) @@ -440,17 +292,21 @@ namespace Util hovered = ImGui::IsItemHovered(); // Draw the lines and text using Menu theme colors - auto& theme = globals::menu->GetTheme().FeatureHeading; + auto& themeSettings = globals::menu->GetSettings().Theme; + auto& palette = themeSettings.Palette; - // Get the color based on hover state - ImVec4 color = hovered ? theme.ColorHovered : theme.ColorDefault; - // If minimized, apply the minimized factor + // Use theme text color + ImVec4 color = palette.Text; + + // If minimized, apply reduced alpha if (!isExpanded) { - color.w *= theme.MinimizedFactor; + color.w *= 0.7f; // 70% alpha when minimized } - ImU32 headerColor = ImGui::GetColorU32(color); - - // Left line + // If hovered, slightly dim the color + if (hovered) { + color.w *= 0.8f; // 80% alpha when hovered + } + ImU32 headerColor = ImGui::GetColorU32(color); // Left line if (lineLength > 0) { drawList->AddLine(ImVec2(pos.x, lineY), ImVec2(pos.x + lineLength, lineY), headerColor, 1.0f); } @@ -498,7 +354,9 @@ namespace Util // Use Menu theme colors for consistent styling auto& theme = globals::menu->GetTheme().FeatureHeading; - ImVec4 color = useWhiteText ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : theme.ColorDefault; + auto& palette = globals::menu->GetTheme().Palette; + // When useWhiteText is true, use the theme's text color instead of hardcoded white + ImVec4 color = useWhiteText ? palette.Text : theme.ColorDefault; ImU32 headerColor = ImGui::GetColorU32(color); @@ -691,6 +549,43 @@ namespace Util return ascending ? (a < b) : (b < a); } + void RenderTextWithHighlights(const std::string& text, const std::string& searchTerm, ImVec4 highlightColor) + { + if (searchTerm.empty()) { + ImGui::TextUnformatted(text.c_str()); + return; + } + + std::string lowerText = text; + std::string lowerSearch = searchTerm; + std::transform(lowerText.begin(), lowerText.end(), lowerText.begin(), [](unsigned char c) { return static_cast(::tolower(c)); }); + std::transform(lowerSearch.begin(), lowerSearch.end(), lowerSearch.begin(), [](unsigned char c) { return static_cast(::tolower(c)); }); + + size_t pos = 0; + size_t lastPos = 0; + + while ((pos = lowerText.find(lowerSearch, lastPos)) != std::string::npos) { + // Render text before highlight + if (pos > lastPos) { + ImGui::TextUnformatted(text.substr(lastPos, pos - lastPos).c_str()); + ImGui::SameLine(0, 0); + } + + // Render highlighted text + ImGui::PushStyleColor(ImGuiCol_Text, highlightColor); + ImGui::TextUnformatted(text.substr(pos, searchTerm.length()).c_str()); + ImGui::PopStyleColor(); + ImGui::SameLine(0, 0); + + lastPos = pos + searchTerm.length(); + } + + // Render remaining text + if (lastPos < text.length()) { + ImGui::TextUnformatted(text.substr(lastPos).c_str()); + } + } + ImVec4 GetThresholdColor(float value, float good, float warn, ImVec4 goodColor, ImVec4 warnColor, ImVec4 badColor) { if (value < good) @@ -712,15 +607,52 @@ namespace Util std::string query = searchQuery; // Convert all to lowercase for case-insensitive search - std::transform(shortName.begin(), shortName.end(), shortName.begin(), ::tolower); - std::transform(displayName.begin(), displayName.end(), displayName.begin(), ::tolower); - std::transform(query.begin(), query.end(), query.begin(), ::tolower); + std::transform(shortName.begin(), shortName.end(), shortName.begin(), [](unsigned char c) { return static_cast(::tolower(c)); }); + std::transform(displayName.begin(), displayName.end(), displayName.begin(), [](unsigned char c) { return static_cast(::tolower(c)); }); + std::transform(query.begin(), query.end(), query.begin(), [](unsigned char c) { return static_cast(::tolower(c)); }); // Search in both short name and display name return shortName.find(query) != std::string::npos || displayName.find(query) != std::string::npos; } + bool StringMatchesSearch(const std::string& text, const std::string& searchQuery) + { + if (searchQuery.empty()) + return true; + + std::string lowerText = text; + std::string lowerQuery = searchQuery; + + // Convert all to lowercase for case-insensitive search + std::transform(lowerText.begin(), lowerText.end(), lowerText.begin(), ::tolower); + std::transform(lowerQuery.begin(), lowerQuery.end(), lowerQuery.begin(), ::tolower); + + return lowerText.find(lowerQuery) != std::string::npos; + } + + void DrawSearchIcon(const ImVec2& position, float size, float alpha) + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + ImVec2 center = ImVec2(position.x + size * 0.46f, position.y + size * 0.5f); + float radius = size * 0.3f; + + // Use themed text color with reduced alpha for search icon + auto& theme = globals::menu->GetTheme().Palette; + ImVec4 iconColor = theme.Text; + iconColor.w *= alpha; // Apply alpha multiplier for subtler appearance + ImU32 placeholderColor = ImGui::GetColorU32(iconColor); + + // Draw circle + drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); + + // Draw handle + ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); + ImVec2 handleEnd = ImVec2(handleStart.x + size * 0.29f, handleStart.y + size * 0.29f); + drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); + } + void DrawFeatureSearchBar(std::string& searchString, float availableWidth) { ImGui::PushID("FeatureSearchBar"); @@ -738,7 +670,9 @@ namespace Util // Custom style - always transparent background to avoid click blocking ImVec4 bgColor = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); ImVec4 bgColorActive = ImVec4(0.3f, 0.3f, 0.3f, 0.9f); - ImVec4 textColor = ImVec4(0.9f, 0.9f, 0.9f, 1.0f); + // Use theme text color instead of hardcoded color + auto& palette = globals::menu->GetTheme().Palette; + ImVec4 textColor = palette.Text; ImGui::PushStyleColor(ImGuiCol_FrameBg, bgColor); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, bgColor); @@ -758,21 +692,9 @@ namespace Util searchString = buffer; } - // Draw a simple search icon (magnifying glass shape) + // Draw search icon using the reusable function ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - ImVec2 center = ImVec2(iconPos.x + iconSize * 0.46f, iconPos.y + iconSize * 0.5f); - float radius = iconSize * 0.3f; - ImU32 placeholderColor = IM_COL32(140, 140, 140, 180); - - // Draw circle - drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); - - // Draw handle - ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); - ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); - drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); + DrawSearchIcon(iconPos, iconSize, 0.7f); ImGui::PopStyleVar(2); ImGui::PopStyleColor(5); @@ -1192,5 +1114,138 @@ namespace Util return keyboard_keys_international[key]; } + } // namespace Input + + bool ButtonWithFlash(const char* label, const ImVec2& size, int flashDurationMs) + { + static std::unordered_map flashTimers; + static std::mutex flashTimersMutex; + + std::string buttonId = std::string(label); + auto now = std::chrono::steady_clock::now(); + + // Check if this button has active flash (thread-safe) + bool hasActiveFlash = false; + { + std::lock_guard lock(flashTimersMutex); + auto it = flashTimers.find(buttonId); + if (it != flashTimers.end()) { + auto elapsed = std::chrono::duration_cast(now - it->second); + if (elapsed.count() < flashDurationMs) { + hasActiveFlash = true; + } else { + // Flash expired, remove it + flashTimers.erase(it); + } + } + } + + // Style the button with flash effect if active. + bool styleChanged = false; + if (hasActiveFlash) { + // Use subtle white overlay similar to action icon hover effect + ImVec4 normalButton = ImGui::GetStyleColorVec4(ImGuiCol_Button); + ImVec4 flashColor = ImVec4( + normalButton.x + 0.2f, // Brighten slightly + normalButton.y + 0.2f, + normalButton.z + 0.2f, + normalButton.w); + ImVec4 flashHovered = ImVec4(flashColor.x * 1.1f, flashColor.y * 1.1f, flashColor.z * 1.1f, flashColor.w); + ImVec4 flashActive = ImVec4(flashColor.x * 0.9f, flashColor.y * 0.9f, flashColor.z * 0.9f, flashColor.w); + + ImGui::PushStyleColor(ImGuiCol_Button, flashColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, flashHovered); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, flashActive); + styleChanged = true; + } + + bool clicked = ImGui::Button(label, size); + + if (styleChanged) { + ImGui::PopStyleColor(3); + } + + // If clicked, start the flash timer (thread-safe) + if (clicked) { + std::lock_guard lock(flashTimersMutex); + flashTimers[buttonId] = now; + } + + return clicked; } -} // namespace Util \ No newline at end of file + + bool FeatureToggle(const char* label, bool* enabled, const ImVec2& size) + { + if (!enabled) + return false; + + // Calculate appropriate size if not specified - make it smaller + ImVec2 toggleSize = size; + if (toggleSize.x <= 0) { + toggleSize.x = ImGui::GetFrameHeight() * 1.6f; // Smaller 1.6:1 aspect ratio + } + if (toggleSize.y <= 0) { + toggleSize.y = ImGui::GetFrameHeight() * 0.8f; // Smaller height + } + + // Get theme colors for better integration + auto& style = ImGui::GetStyle(); + auto& colors = style.Colors; + + // Use theme header colors instead of bright green/red + ImVec4 toggleBg = *enabled ? + colors[ImGuiCol_Header] : // Use header color when enabled + colors[ImGuiCol_FrameBg]; // Use frame background when disabled + + ImVec4 toggleBgHovered = *enabled ? + colors[ImGuiCol_HeaderHovered] : // Use header hovered when enabled + colors[ImGuiCol_FrameBgHovered]; // Use frame hovered when disabled + + ImVec4 toggleBgActive = *enabled ? + colors[ImGuiCol_HeaderActive] : // Use header active when enabled + colors[ImGuiCol_FrameBgActive]; // Use frame active when disabled + + // Apply toggle styling with border + ImGui::PushStyleColor(ImGuiCol_Button, toggleBg); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, toggleBgHovered); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, toggleBgActive); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, toggleSize.y * 0.5f); // Round ends + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f); // Larger border + + // Create unique ID for the toggle + ImGui::PushID(label); + + // Draw the toggle button + bool clicked = ImGui::Button("", toggleSize); + + // Draw the toggle knob + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 buttonMin = ImGui::GetItemRectMin(); + ImVec2 buttonMax = ImGui::GetItemRectMax(); + + // Calculate knob position and size + float knobRadius = (toggleSize.y - 4.0f) * 0.5f; + float knobPadding = 2.0f; + float knobTravel = toggleSize.x - (knobRadius * 2.0f) - (knobPadding * 2.0f); + float knobX = *enabled ? + buttonMin.x + knobPadding + knobRadius + knobTravel : + buttonMin.x + knobPadding + knobRadius; + float knobY = buttonMin.y + toggleSize.y * 0.5f; + + // Draw knob + ImU32 knobColor = ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + drawList->AddCircleFilled(ImVec2(knobX, knobY), knobRadius, knobColor); + + ImGui::PopID(); + ImGui::PopStyleVar(2); // Pop both FrameRounding and FrameBorderSize + ImGui::PopStyleColor(3); + + // Handle toggle action + if (clicked) { + *enabled = !*enabled; + } + + return clicked; + } + +} // namespace Util diff --git a/src/Utils/UI.h b/src/Utils/UI.h index c589103bb7..d75c5c14f2 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -1,11 +1,14 @@ #pragma once #include +#include // For FLT_MAX #include #include #include #include #include // For WPARAM and virtual key constants +#include "../Menu/Fonts.h" + // Forward declarations struct ID3D11Device; struct ID3D11ShaderResourceView; @@ -109,6 +112,24 @@ namespace Util int m_pushedStyles; }; + /** + * Button with simple flash feedback (matches action icon hover effect style) + * @param label Button text + * @param size Button size (optional) + * @param flashDurationMs How long to show flash effect in milliseconds (default 200ms) + * @return True if the button was clicked + */ + bool ButtonWithFlash(const char* label, const ImVec2& size = ImVec2(0, 0), int flashDurationMs = 200); + + /** + * Clean, minimalist toggle switch for feature enable/disable state + * @param label Label text to display next to the toggle + * @param enabled Reference to the boolean state to toggle + * @param size Toggle size (optional, defaults to automatic sizing) + * @return True if the toggle state was changed + */ + bool FeatureToggle(const char* label, bool* enabled, const ImVec2& size = ImVec2(0, 0)); + /** * RAII wrapper for creating collapsible UI sections. * Automatically handles the TreeNode creation, styling, and cleanup. @@ -161,7 +182,14 @@ namespace Util // Text rendering helpers for clearer title text // These functions modify ImGui rendering state and should be called within ImGui context ImVec2 DrawSharpText(const char* text, bool alignToPixelGrid = true, float scale = 1.0f); - ImVec2 DrawAlignedTextWithLogo(ID3D11ShaderResourceView* logoTexture, const ImVec2& logoSize, const char* text, float textScale = DefaultHeaderTextScale); + ImVec2 DrawAlignedTextWithLogo(ID3D11ShaderResourceView* logoTexture, const ImVec2& logoSize, const char* text, float textScale = DefaultHeaderTextScale, ImU32 logoTint = IM_COL32_WHITE); + + /** + * Calculates the horizontal offset needed to center content within the full window width + * @param contentWidth The width of the content to center + * @return The offset to add to cursor X position to center the content + */ + float GetCenterOffsetForContent(float contentWidth); /** * Draws a custom styled collapsible category header with lines extending from both sides @@ -281,6 +309,7 @@ namespace Util * Each function should compare two rows and return true if the first should come before the second. * @param cellRender Function to render a cell: (rowIdx, colIdx, const T& row). * @param footerRows Optional static footer rows (not sorted, rendered after main rows). + * @param outerSize Optional outer size for the table (0,0 = auto-size). */ template void ShowSortedStringTableCustom( @@ -291,12 +320,20 @@ namespace Util bool ascending, const std::vector>& customSorts, std::function cellRender, - const std::vector& footerRows = {}) + const std::vector& footerRows = {}, + const ImVec2& outerSize = ImVec2(0, 0)) { - ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable; - if (ImGui::BeginTable(table_id, static_cast(headers.size()), flags)) { - for (const auto& header : headers) - ImGui::TableSetupColumn(header.c_str()); + ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp; + ImVec2 tableSize = outerSize; + if (outerSize.y == 0.0f) { + size_t totalRows = rows.size() + footerRows.size(); + tableSize.y = ImGui::GetTextLineHeightWithSpacing() * (static_cast((totalRows < 15) ? totalRows : 15) + 1.2f); + } + if (ImGui::BeginTable(table_id, static_cast(headers.size()), flags, tableSize)) { + // Set up columns with content-based sizing + for (size_t i = 0; i < headers.size(); ++i) { + ImGui::TableSetupColumn(headers[i].c_str()); + } ImGui::TableHeadersRow(); // Interactive sorting @@ -351,6 +388,129 @@ namespace Util } } + /** + * Renders a sortable and filterable ImGui table for custom row types. + * Extends ShowSortedStringTableCustom with filtering capabilities including + * substring highlighting and column-specific search. + * @tparam T The row type. Must be copyable and compatible with the provided functions. + * @param table_id Unique ImGui table ID. + * @param headers Column headers. + * @param originalRows Original table data (not modified - filtering creates a copy). + * @param sortColumn Default sort column index. + * @param ascending Default sort direction. + * @param customSorts Vector of custom comparator functions, one per column. + * Each function should compare two rows and return true if the first should come before the second. + * @param cellRender Function to render a cell: (rowIdx, colIdx, const T& row, const std::string& filterText). + * The filterText parameter enables substring highlighting in the cell renderer. + * @param filterText Reference to filter text string (modified by the input field). + * @param searchColumn Reference to search column index (0 = All Columns, 1+ = specific column). + * @param getFilterableFields Function that extracts filterable strings from a row for each column. + * Should return a vector of strings, one per column, used for filtering. + * @param scrollOnFilterChange If true, scrolls to top when filter changes (default: true). + */ + template + void ShowFilteredStringTableCustom( + const char* table_id, + const std::vector& headers, + const std::vector& originalRows, + size_t sortColumn, + bool ascending, + const std::vector>& customSorts, + std::function cellRender, + std::string& filterText, + int& searchColumn, + std::function(const T&)> getFilterableFields, + bool scrollOnFilterChange = true) + { + // Filter controls + static std::string previousFilterText = ""; + char filterBuffer[256] = { 0 }; + strncpy_s(filterBuffer, filterText.c_str(), sizeof(filterBuffer) - 1); + + ImGui::InputText("Filter", filterBuffer, IM_ARRAYSIZE(filterBuffer)); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Filter shaders by the selected column. Case-insensitive."); + } + + // Create search column options + std::vector searchOptions = { "All Columns" }; + for (const auto& col : headers) { + searchOptions.push_back(col); + } + std::vector searchOptionsCStr; + for (const auto& option : searchOptions) { + searchOptionsCStr.push_back(option.c_str()); + } + + ImGui::Combo("Search In", &searchColumn, searchOptionsCStr.data(), static_cast(searchOptionsCStr.size())); + + // Filter rows based on search column and filter text + std::vector filteredRows; + std::string currentFilterText(filterBuffer); + filterText = currentFilterText; // Update the reference + + if (currentFilterText.empty()) { + filteredRows = originalRows; + } else { + std::string filterLower = currentFilterText; + std::transform(filterLower.begin(), filterLower.end(), filterLower.begin(), ::tolower); + + for (const auto& row : originalRows) { + bool passesFilter = false; + auto filterableFields = getFilterableFields(row); + + if (searchColumn == 0) { // All Columns + for (const auto& field : filterableFields) { + std::string fieldLower = field; + std::transform(fieldLower.begin(), fieldLower.end(), fieldLower.begin(), ::tolower); + if (fieldLower.find(filterLower) != std::string::npos) { + passesFilter = true; + break; + } + } + } else { // Specific column (searchColumn is 1-indexed for columns) + int columnIndex = searchColumn - 1; + if (columnIndex >= 0 && static_cast(columnIndex) < filterableFields.size()) { + std::string fieldLower = filterableFields[columnIndex]; + std::transform(fieldLower.begin(), fieldLower.end(), fieldLower.begin(), ::tolower); + passesFilter = fieldLower.find(filterLower) != std::string::npos; + } + } + + if (passesFilter) { + filteredRows.push_back(row); + } + } + } + + // Handle filter change scrolling + bool filterChanged = (currentFilterText != previousFilterText); + if (filterChanged && scrollOnFilterChange) { + ImGui::SetScrollHereY(0.5f); // Keep the table visible when filter changes + previousFilterText = currentFilterText; + } + + // Constrain table height to prevent infinite scrolling appearance + ImGui::BeginChild("ShaderTableContainer", ImVec2(0, 400), true, ImGuiWindowFlags_HorizontalScrollbar); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(4, 2)); // Tighter cell padding for better fit + + // Use the existing sorted table function + ShowSortedStringTableCustom( + table_id, + headers, + filteredRows, + sortColumn, + ascending, + customSorts, + [&cellRender, ¤tFilterText](int rowIdx, int colIdx, const T& row) { + if (cellRender) { + cellRender(rowIdx, colIdx, row, currentFilterText); + } + }); + + ImGui::PopStyleVar(); // CellPadding + ImGui::EndChild(); + } /** * @brief Compares two version strings (e.g., "1.2.3") numerically. * @param a First version string. @@ -367,6 +527,7 @@ namespace Util // A standard string comparator for use with ShowSortedStringTable bool StringSortComparator(const std::string& a, const std::string& b, bool ascending); + void RenderTextWithHighlights(const std::string& text, const std::string& searchTerm, ImVec4 highlightColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f)); // Performance overlay formatting and color helpers ImVec4 GetThresholdColor(float value, float good, float warn, ImVec4 goodColor, ImVec4 warnColor, ImVec4 badColor); @@ -381,6 +542,22 @@ namespace Util */ bool FeatureMatchesSearch(Feature* feat, const std::string& searchQuery); + /** + * @brief Generic case-insensitive string matching for search functionality. + * @param text The text to search in + * @param searchQuery The search query string + * @return True if the text matches the search query (case-insensitive) + */ + bool StringMatchesSearch(const std::string& text, const std::string& searchQuery); + + /** + * @brief Draws a search icon (magnifying glass) at the specified position. + * @param position The screen position where the icon should be drawn + * @param size The size of the icon (default: 20.0f) + * @param alpha Alpha multiplier for the icon color (default: 0.7f for subtle appearance) + */ + void DrawSearchIcon(const ImVec2& position, float size = 20.0f, float alpha = 0.7f); + /** * @brief Draws the feature search bar with magnifying glass icon. * @param searchString Reference to the search string to modify @@ -515,4 +692,444 @@ namespace Util */ const char* KeyIdToString(uint32_t key); } + + /** + * @brief Renders a searchable combo box with case-insensitive filtering + * + * Provides a reusable ImGui combo box with built-in search functionality. + * When opened, automatically focuses a search input that filters items as you type. + * The search is case-insensitive and clears automatically on selection or close. + * + * @tparam T The value type stored in the map + * @param label The label for the combo box + * @param selectedName Reference to the currently selected item's name (will be updated on selection) + * @param itemMap The map of items to display (key = item name, value = item data) + * @return true if a new item was selected, false otherwise + * + * @note Uses a static search buffer, so only one SearchableCombo should be open at a time + * + * @example + * @code + * std::unordered_map myItems; + * std::string selectedName; + * MyData* selectedItem = nullptr; + * + * if (Util::SearchableCombo("Choose Item", selectedName, myItems)) { + * selectedItem = &myItems[selectedName]; + * } + * @endcode + */ + template + bool SearchableCombo(const char* label, std::string& selectedName, std::unordered_map& itemMap) + { + bool valueChanged = false; + static std::unordered_map searchBuffers; + + std::string comboId = std::string(label); + auto& searchBuffer = searchBuffers[comboId]; + + if (ImGui::BeginCombo(label, selectedName.c_str())) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(24.0f, ImGui::GetStyle().FramePadding.y)); + ImGui::InputText("##search", searchBuffer, IM_ARRAYSIZE(searchBuffer)); + ImGui::PopStyleVar(); + ImVec2 iconPos = ImVec2(ImGui::GetItemRectMin().x + 5.0f, ImGui::GetItemRectMin().y + (ImGui::GetItemRectSize().y - 16.0f) * 0.5f); + DrawSearchIcon(iconPos, 16.0f, 0.5f); + + ImGui::Separator(); + + // Filter and display items + for (auto& [itemName, item] : itemMap) { + // Simple case-insensitive search + if (searchBuffer[0] == '\0' || + std::search(itemName.begin(), itemName.end(), searchBuffer, searchBuffer + strlen(searchBuffer), + [](char a, char b) { return std::tolower(a) == std::tolower(b); }) != itemName.end()) { + if (ImGui::Selectable(itemName.c_str(), itemName == selectedName)) { + selectedName = itemName; + valueChanged = true; + searchBuffer[0] = '\0'; // Clear search on selection + } + } + } + + ImGui::EndCombo(); + } else { + // Reset search when combo is closed + searchBuffer[0] = '\0'; + } + + return valueChanged; + } + + /** + * @brief Renders a table cell with automatic text highlighting and optional tooltip/fallback. + * Convenience function for table cell renderers that combines text rendering with highlighting, + * tooltips, and fallback text for empty content. + * @param text The text to render in the cell (if empty, uses fallbackText) + * @param filterText The search filter text for highlighting + * @param tooltipText Optional tooltip text (if provided, shows on hover) + * @param fallbackText Text to show if primary text is empty (default: "--") + * @param highlightColor Color for highlighting (default: yellow) + * @param enableWrapping Whether to enable text wrapping for multi-line content (default: true) + * @param textColor Optional text color override (default: use default text color) + */ + + inline void RenderTableCell(const std::string& text, const std::string& filterText, + const std::string& tooltipText = "", const char* fallbackText = nullptr, + ImVec4 highlightColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f), bool enableWrapping = true, + ImVec4 textColor = ImVec4(0, 0, 0, 0)) + { + const std::string& displayText = text.empty() && fallbackText ? std::string(fallbackText) : text; + + // Apply custom text color if provided + if (textColor.w > 0.0f) { + ImGui::PushStyleColor(ImGuiCol_Text, textColor); + } + + // Enable text wrapping for the cell content + if (enableWrapping) { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x); + } + + RenderTextWithHighlights(displayText, filterText, highlightColor); + + if (enableWrapping) { + ImGui::PopTextWrapPos(); + } + + if (!tooltipText.empty() && ImGui::IsItemHovered()) { + if (auto _tt = HoverTooltipWrapper()) { + ImGui::Text("%s", tooltipText.c_str()); + } + } + + // Pop text color if we pushed one + if (textColor.w > 0.0f) { + ImGui::PopStyleColor(); + } + } + + /** + * @brief Configuration for a table column (text-only, click handling is row-level) + */ + template + struct TableColumnConfig + { + std::string header; + std::string tooltip; + std::function getValue; + }; + + /** + * @brief Represents different types of input events that can occur on table rows + */ + enum class TableInputEventType + { + MouseClick, + MouseDoubleClick, + KeyPress, + ContextMenu + }; + + /** + * @brief Configuration for a specific input event handler + * @tparam T The row type + */ + template + struct TableInputEvent + { + TableInputEventType type; + int mouseButton = 0; // For mouse events (0=left, 1=right, 2=middle) + ImGuiKey key = ImGuiKey_None; // For keyboard events + std::string label; // Display label for context menus + std::function callback; // Action to perform + bool enabled = true; // Whether this event is currently enabled + + TableInputEvent(TableInputEventType t, std::function cb, + const std::string& lbl = "", int btn = 0, ImGuiKey k = ImGuiKey_None) : + type(t), mouseButton(btn), key(k), label(lbl), callback(cb) {} + }; + + /** + * @brief Manages the state and logic for table filtering + * @tparam T The row type + */ + template + struct TableFilterState + { + std::string filterText; + int searchColumn = 0; // 0 = All Columns, 1+ = specific column + std::function(const T&)> getFilterableFields; + + TableFilterState(std::function(const T&)> fieldsFunc) : + getFilterableFields(fieldsFunc) {} + + /** + * @brief Apply filtering to the original rows and return filtered results + */ + std::vector ApplyFilter(const std::vector& originalRows) const + { + if (filterText.empty()) { + return originalRows; + } + + std::vector filteredRows; + std::string filterLower = filterText; + std::transform(filterLower.begin(), filterLower.end(), filterLower.begin(), ::tolower); + + for (const auto& row : originalRows) { + bool passesFilter = false; + auto filterableFields = getFilterableFields(row); + + if (searchColumn == 0) { // All Columns + for (const auto& field : filterableFields) { + std::string fieldLower = field; + std::transform(fieldLower.begin(), fieldLower.end(), fieldLower.begin(), ::tolower); + if (fieldLower.find(filterLower) != std::string::npos) { + passesFilter = true; + break; + } + } + } else { // Specific column (searchColumn is 1-indexed for columns) + int columnIndex = searchColumn - 1; + if (columnIndex >= 0 && static_cast(columnIndex) < filterableFields.size()) { + std::string fieldLower = filterableFields[columnIndex]; + std::transform(fieldLower.begin(), fieldLower.end(), fieldLower.begin(), ::tolower); + passesFilter = fieldLower.find(filterLower) != std::string::npos; + } + } + + if (passesFilter) { + filteredRows.push_back(row); + } + } + + return filteredRows; + } + + /** + * @brief Render the filter UI controls + */ + void RenderControls(const std::vector& columnHeaders) + { + char filterBuffer[256] = { 0 }; + strncpy_s(filterBuffer, filterText.c_str(), sizeof(filterBuffer) - 1); + + ImGui::InputText("Filter", filterBuffer, IM_ARRAYSIZE(filterBuffer)); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Filter table by the selected column. Case-insensitive."); + } + + // Create search column options + std::vector searchOptions = { "All Columns" }; + for (const auto& col : columnHeaders) { + searchOptions.push_back(col); + } + std::vector searchOptionsCStr; + for (const auto& option : searchOptions) { + searchOptionsCStr.push_back(option.c_str()); + } + + ImGui::Combo("Search In", &searchColumn, searchOptionsCStr.data(), static_cast(searchOptionsCStr.size())); + + // Update filter text from buffer + filterText = filterBuffer; + } + }; + + /** + * @brief Enhanced filtered table with general input event support and theme integration + * @tparam T The row type + * @param table_id Unique ImGui table ID + * @param columns Column configurations (text-only, click handling is row-level) + * @param originalRows Original table data (not modified - filtering creates a copy) + * @param sortColumn Default sort column index + * @param ascending Default sort direction + * @param customSorts Vector of custom comparator functions, one per column + * @param filterState Filter state management + * @param inputEvents Vector of input event handlers for row interactions + * @param getRowTooltip Optional function to get tooltip for entire row + * @param getRowBgColor Optional function to get background color for row (for highlighting blocked/disabled items) + * @param getRowTextColor Optional function to get text color for row (for highlighting blocked/disabled items) + * @param tableHeight Maximum height for the table container (0 = auto) + */ + template + void ShowInteractiveTable( + const char* table_id, + const std::vector>& columns, + const std::vector& originalRows, + size_t sortColumn, + bool ascending, + const std::vector>& customSorts, + TableFilterState& filterState, + const std::vector>& inputEvents = {}, + std::function getRowTooltip = nullptr, + std::function getRowBgColor = nullptr, + std::function getRowTextColor = nullptr, + float tableHeight = 400.0f) + { + // Render filter controls + filterState.RenderControls([&]() { + std::vector headers; + for (const auto& col : columns) { + headers.push_back(col.header); + } + return headers; + }()); + + // Apply filtering + auto filteredRows = filterState.ApplyFilter(originalRows); + + // Handle filter change scrolling + static std::string previousFilterText = ""; + bool filterChanged = (filterState.filterText != previousFilterText); + if (filterChanged) { + ImGui::SetScrollHereY(0.5f); + previousFilterText = filterState.filterText; + } + + // Constrain table height to prevent infinite scrolling appearance + std::string containerId = std::string(table_id) + "_Container"; + ImGui::BeginChild(containerId.c_str(), ImVec2(0, tableHeight), true, ImGuiWindowFlags_HorizontalScrollbar); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(4, 2)); + + ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp; + if (ImGui::BeginTable(table_id, static_cast(columns.size()), flags)) { + // Set up columns + for (size_t i = 0; i < columns.size(); ++i) { + ImGui::TableSetupColumn(columns[i].header.c_str()); + } + ImGui::TableHeadersRow(); + + // Interactive sorting + int sortCol = static_cast(sortColumn); + bool sortAsc = ascending; + if (const ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs()) { + if (sortSpecs->SpecsCount > 0) { + sortCol = sortSpecs->Specs->ColumnIndex; + sortAsc = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; + } + } + if (sortCol >= 0 && static_cast(sortCol) < columns.size()) { + if (sortCol < static_cast(customSorts.size()) && customSorts[sortCol]) { + auto cmp = customSorts[sortCol]; + std::sort(filteredRows.begin(), filteredRows.end(), [sortCol, sortAsc, &cmp](const T& a, const T& b) { + return cmp(a, b, sortAsc); + }); + } + } + + // Render rows with input event support + for (size_t rowIdx = 0; rowIdx < filteredRows.size(); ++rowIdx) { + const auto& row = filteredRows[rowIdx]; + ImGui::TableNextRow(); + + // Set custom row background color if provided (for blocked/disabled items) + if (getRowBgColor) { + ImVec4 bgColor = getRowBgColor(row); + if (bgColor.w > 0.0f) { // Only set if color has alpha > 0 + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(bgColor)); + } + } + + // Render all columns first to establish proper row layout + for (size_t col = 0; col < columns.size(); ++col) { + ImGui::TableSetColumnIndex(static_cast(col)); + const auto& column = columns[col]; + + // All columns are now text-only with highlighting + std::string value = column.getValue(row); + ImVec4 textColor = getRowTextColor ? getRowTextColor(row) : ImVec4(0, 0, 0, 0); + Util::RenderTableCell(value, filterState.filterText, column.tooltip, nullptr, ImVec4(1.0f, 1.0f, 0.0f, 1.0f), true, textColor); + } + + // Now create the invisible button that covers the entire rendered row + // Get the position after all cells are rendered + ImVec2 rowMin = ImGui::GetItemRectMin(); + ImVec2 rowMax = ImGui::GetItemRectMax(); + + // Find the actual row boundaries by checking all columns + float minY = FLT_MAX; + float maxY = -FLT_MAX; + float minX = FLT_MAX; + float maxX = -FLT_MAX; + + for (size_t col = 0; col < columns.size(); ++col) { + ImGui::TableSetColumnIndex(static_cast(col)); + ImVec2 cellMin = ImGui::GetItemRectMin(); + ImVec2 cellMax = ImGui::GetItemRectMax(); + + minX = std::min(minX, cellMin.x); + maxX = std::max(maxX, cellMax.x); + minY = std::min(minY, cellMin.y); + maxY = std::max(maxY, cellMax.y); + } + + ImVec2 rowStartPos = ImVec2(minX, minY); + ImVec2 rowSize = ImVec2(maxX - minX, maxY - minY); + + // Position the button absolutely over the rendered row + ImGui::SetCursorScreenPos(rowStartPos); + ImGui::PushID(static_cast(rowIdx)); + + std::string buttonId = "##row_" + std::to_string(rowIdx); + ImGui::InvisibleButton(buttonId.c_str(), rowSize); + + // Handle input events on the invisible button + for (const auto& event : inputEvents) { + if (!event.enabled) + continue; + + bool shouldTrigger = false; + switch (event.type) { + case TableInputEventType::MouseClick: + shouldTrigger = ImGui::IsItemClicked() && event.mouseButton == 0; // Left click + break; + case TableInputEventType::MouseDoubleClick: + shouldTrigger = ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(event.mouseButton); + break; + case TableInputEventType::KeyPress: + shouldTrigger = ImGui::IsItemFocused() && ImGui::IsKeyPressed(event.key); + break; + case TableInputEventType::ContextMenu: + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(event.mouseButton)) { + std::string popupId = "row_context_" + std::to_string(rowIdx); + ImGui::OpenPopup(popupId.c_str()); + } + break; + } + + if (shouldTrigger && event.callback) { + event.callback(row); + } + } + + // Render context menus + for (const auto& event : inputEvents) { + if (event.type == TableInputEventType::ContextMenu) { + std::string popupId = "row_context_" + std::to_string(rowIdx); + if (ImGui::BeginPopup(popupId.c_str())) { + if (ImGui::MenuItem(event.label.c_str()) && event.callback) { + event.callback(row); + } + ImGui::EndPopup(); + } + } + } + + // Row tooltip + if (getRowTooltip && ImGui::IsItemHovered()) { + if (auto _tt = Util::HoverTooltipWrapper()) { + std::string tooltip = getRowTooltip(row); + ImGui::Text("%s", tooltip.c_str()); + } + } + + ImGui::PopID(); + } + ImGui::EndTable(); + } + + ImGui::PopStyleVar(); + ImGui::EndChild(); + } } // namespace Util diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index 6ade711af4..c3873b3423 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -4,6 +4,7 @@ #include "Globals.h" #include "Hooks.h" #include "Menu.h" +#include "Menu/ThemeManager.h" #include "ShaderCache.h" #include "State.h" #include "TruePBR.h" @@ -145,7 +146,7 @@ bool Load() } if (REL::Module::IsVR()) { - REL::IDDatabase::get().IsVRAddressLibraryAtLeastVersion("0.189.0", true); + REL::IDDatabase::get().IsVRAddressLibraryAtLeastVersion("0.193.0", true); } auto privateProfileRedirectorVersion = Util::GetDllVersion(L"Data/SKSE/Plugins/PrivateProfileRedirector.dll"); @@ -161,6 +162,13 @@ bool Load() auto state = globals::state; state->Load(); + state->LoadTheme(); // Load theme settings from SettingsTheme.json + + // Initialize theme system - create default themes and discover existing ones + globals::menu->CreateDefaultThemes(); // Creates JSON files if they don't exist + auto themeManager = ThemeManager::GetSingleton(); + themeManager->DiscoverThemes(); // Discover all available themes + auto log = spdlog::default_logger(); log->set_level(state->GetLogLevel()); @@ -196,6 +204,7 @@ bool Load() } if (errors.empty()) { + Hooks::InstallEarlyHooks(); logger::info("Calling feature Load methods"); for (auto* feature : Feature::GetFeatureList()) { if (feature->loaded) { diff --git a/vcpkg.json b/vcpkg.json index 94a910acdb..1b7f1b3dd2 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -21,11 +21,11 @@ "name": "imgui", "features": ["dx11-binding", "win32-binding", "docking-experimental"] }, - "intel-xess", "magic-enum", "detours", "nlohmann-json", "pystring", + "renderdoc", "stb", "tracy", "unordered-dense", @@ -45,10 +45,9 @@ "version": "0.11.0" }, { - "name": "magic-enum", - "version": "0.9.6", - "port-version": 1 + "name": "directx-headers", + "version": "1.606.4" } ], - "builtin-baseline": "98aa6396292d57e737a6ef999d4225ca488859d5" + "builtin-baseline": "a62ce77d56ee07513b4b67de1ec2daeaebfae51a" }