├── .clang-format ├── .editorconfig ├── .gitignore ├── .gitmodules ├── .vs ├── launch.vs.json └── launch_with_automation.ps1 ├── CMakeLists.txt ├── CMakePresets.json ├── CMakeUserEnvVars.json ├── README.md ├── make-release.ps1 ├── resources └── SFShaderInjector.ini ├── source ├── CComPtr.h ├── CMakeLists.txt ├── CRHooks.cpp ├── CRHooks.h ├── D3DHooks.cpp ├── D3DHooks.h ├── D3DPipelineStateStream.cpp ├── D3DPipelineStateStream.h ├── D3DShaderReplacement.cpp ├── D3DShaderReplacement.h ├── DebuggingUtil.cpp ├── DebuggingUtil.h ├── Hooking │ ├── Hooks.cpp │ ├── Hooks.h │ ├── Memory.cpp │ ├── Memory.h │ ├── Offsets.cpp │ └── Offsets.h ├── Plugin.cpp ├── Plugin.h ├── RE │ ├── CreationRenderer.cpp │ └── CreationRenderer.h ├── ReShadeHelper.cpp ├── ReShadeHelper.h ├── dllmain.cpp └── pch.h ├── vcpkg-ports ├── detours │ ├── detours-config.cmake │ ├── detours-targets.cmake.in │ ├── find-jmp-bounds-arm64.patch │ ├── portfile.cmake │ ├── usage │ └── vcpkg.json ├── reshade-api │ ├── portfile.cmake │ └── vcpkg.json ├── reshade-imgui │ ├── portfile.cmake │ └── vcpkg.json └── sfse-common │ ├── portfile.cmake │ ├── usage │ └── vcpkg.json └── vcpkg.json /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AlignOperands: Align 3 | AllowAllArgumentsOnNextLine: false 4 | AllowShortBlocksOnASingleLine: Never 5 | AllowShortCaseLabelsOnASingleLine: false 6 | AllowShortEnumsOnASingleLine: false 7 | AllowShortFunctionsOnASingleLine: Empty 8 | AllowShortIfStatementsOnASingleLine: Never 9 | AllowShortLambdasOnASingleLine: None 10 | AllowShortLoopsOnASingleLine: false 11 | AlwaysBreakAfterDefinitionReturnType: None 12 | AlwaysBreakAfterReturnType: None 13 | AlwaysBreakTemplateDeclarations: Yes 14 | AllowAllParametersOfDeclarationOnNextLine: false 15 | BraceWrapping: 16 | AfterCaseLabel: true 17 | AfterClass: true 18 | AfterFunction: true 19 | AfterEnum: true 20 | AfterNamespace: true 21 | AfterStruct: true 22 | AfterObjCDeclaration: true 23 | AfterUnion: true 24 | AfterExternBlock: true 25 | BeforeCatch: true 26 | AfterControlStatement: Always 27 | BeforeLambdaBody: true 28 | BeforeElse: true 29 | BeforeWhile: false 30 | AlignEscapedNewlines: Left 31 | IndentCaseBlocks: false 32 | IndentCaseLabels: false 33 | IndentExternBlock: Indent 34 | InsertNewlineAtEOF: true 35 | IndentRequiresClause: false 36 | IndentPPDirectives: None 37 | LineEnding: DeriveCRLF 38 | PointerAlignment: Right 39 | ReferenceAlignment: Left 40 | RemoveSemicolon: false 41 | RequiresClausePosition: OwnLine 42 | SortIncludes: Never 43 | SpaceAfterCStyleCast: false 44 | SpaceAfterTemplateKeyword: false 45 | SpaceBeforeParens: ControlStatementsExceptControlMacros 46 | Standard: Auto 47 | AlignAfterOpenBracket: AlwaysBreak 48 | AlignTrailingComments: 49 | Kind: Always 50 | OverEmptyLines: 1 51 | BreakBeforeBraces: Custom 52 | AlignConsecutiveMacros: 53 | Enabled: true 54 | SpaceBeforeCtorInitializerColon: true 55 | SpaceBeforeInheritanceColon: true 56 | SpaceBeforeCaseColon: false 57 | SpaceBeforeAssignmentOperators: true 58 | SpaceAroundPointerQualifiers: Default 59 | SpacesInAngles: Never 60 | SpacesInCStyleCastParentheses: false 61 | SpacesInConditionalStatement: false 62 | SpacesInParentheses: false 63 | SpacesInSquareBrackets: false 64 | SpaceBeforeCpp11BracedList: true 65 | RequiresExpressionIndentation: OuterScope 66 | Language: Cpp 67 | Cpp11BracedListStyle: false 68 | IndentWrappedFunctionNames: false 69 | LambdaBodyIndentation: OuterScope 70 | SpaceAfterLogicalNot: false 71 | SpaceBeforeRangeBasedForLoopColon: true 72 | SpaceBeforeSquareBrackets: false 73 | SpaceInEmptyBlock: false 74 | SpaceInEmptyParentheses: false 75 | TabWidth: 4 76 | UseTab: Always 77 | IndentWidth: 4 78 | ColumnLimit: 140 79 | NamespaceIndentation: All 80 | AccessModifierOffset: -4 81 | SpacesBeforeTrailingComments: 1 82 | QualifierAlignment: Leave 83 | FixNamespaceComments: false 84 | BitFieldColonSpacing: Both 85 | BreakBeforeConceptDeclarations: Always 86 | IndentGotoLabels: true 87 | BinPackArguments: false 88 | BinPackParameters: false 89 | PackConstructorInitializers: CurrentLine 90 | AlignConsecutiveBitFields: 91 | Enabled: true 92 | PenaltyBreakAssignment: 900 93 | PenaltyReturnTypeOnItsOwnLine: 9001 94 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | trim_trailing_whitespace = true 4 | insert_final_newline = true 5 | 6 | [*.{c,cmake,cpp,cxx,h,hpp,hxx}] 7 | indent_style = tab 8 | indent_size = 4 9 | 10 | [*.json] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio/Visual Studio Code cache and options directories 2 | .vscode/* 3 | .vs/* 4 | 5 | # Launch.vs.json isn't user-specific in this repo 6 | !.vs/launch.vs.json 7 | !.vs/launch_with_automation.ps1 8 | 9 | # Artifacts 10 | bin/* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nukem9/starfield-shader-injector/9bef841144c6c8364deecb865b70399e48648daf/.gitmodules -------------------------------------------------------------------------------- /.vs/launch.vs.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.1", 3 | "defaults": {}, 4 | "configurations": [ 5 | { 6 | "isDefaultConfiguration": true, 7 | "noDebug": true, 8 | "type": "dll", 9 | "name": "CMakeLists.txt", 10 | "project": "CMakeLists.txt", 11 | "projectTarget": "", 12 | "currentDir": "${env.GAME_ROOT_DIRECTORY}", 13 | "exe": "${env.ComSpec}", 14 | "args": [ 15 | "/C \"powershell.exe -WindowStyle Hidden -nologo -ExecutionPolicy Bypass -File ^\"${workspaceRoot}/.vs/launch_with_automation.ps1^\" Launch\"" 16 | ], 17 | "environment": [ 18 | { 19 | "name": "GAME_DEBUGGER_CMDLINE", 20 | "value": "${env.GAME_DEBUGGER_CMDLINE}" 21 | } 22 | ] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vs/launch_with_automation.ps1: -------------------------------------------------------------------------------- 1 | param ($Action, $VSProcessId, $ChildProcessId) 2 | 3 | # See end of file 4 | 5 | Add-Type -Language Csharp -TypeDefinition @" 6 | using System; 7 | using System.Runtime.InteropServices; 8 | using System.Runtime.InteropServices.ComTypes; 9 | 10 | namespace VSAutomationHelper 11 | { 12 | public class Main 13 | { 14 | [DllImport("ole32.dll")] 15 | private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc); 16 | 17 | public static object FindDTEInstanceByProcessId(int processId) 18 | { 19 | IBindCtx bindContext = null; 20 | IRunningObjectTable objectTable = null; 21 | IEnumMoniker enumMonikers = null; 22 | 23 | try 24 | { 25 | Marshal.ThrowExceptionForHR(CreateBindCtx(0, out bindContext)); 26 | bindContext.GetRunningObjectTable(out objectTable); 27 | objectTable.EnumRunning(out enumMonikers); 28 | 29 | IMoniker[] moniker = new IMoniker[1]; 30 | IntPtr numberFetched = IntPtr.Zero; 31 | 32 | while (enumMonikers.Next(1, moniker, numberFetched) == 0) 33 | { 34 | var runningObjectMoniker = moniker[0]; 35 | 36 | if (runningObjectMoniker == null) 37 | continue; 38 | 39 | try 40 | { 41 | string name = null; 42 | runningObjectMoniker.GetDisplayName(bindContext, null, out name); 43 | 44 | if (name.StartsWith("!VisualStudio.DTE.")) // Consistent from 2010~2022 45 | { 46 | int idIndex = name.IndexOf(':'); 47 | 48 | if (idIndex != -1 && int.Parse(name.Substring(idIndex + 1)) == processId) 49 | { 50 | object runningObject = null; 51 | Marshal.ThrowExceptionForHR(objectTable.GetObject(runningObjectMoniker, out runningObject)); 52 | 53 | return runningObject; 54 | } 55 | } 56 | } 57 | catch (UnauthorizedAccessException) 58 | { 59 | // Inaccessible due to permissions 60 | } 61 | } 62 | } 63 | finally 64 | { 65 | if (enumMonikers != null) 66 | Marshal.ReleaseComObject(enumMonikers); 67 | 68 | if (objectTable != null) 69 | Marshal.ReleaseComObject(objectTable); 70 | 71 | if (bindContext != null) 72 | Marshal.ReleaseComObject(bindContext); 73 | } 74 | 75 | return null; 76 | } 77 | } 78 | } 79 | "@ 80 | 81 | # 82 | # .\script.ps1 Launch Callback from Visual Studio to launch the game 83 | # .\script.ps1 Attach Callback from the game to attach Visual Studio's debugger 84 | # 85 | # GAME_DEBUGGER_CMDLINE Env var holding a command line expression that launches the game 86 | # GAME_DEBUGGER_REQUEST Env var holding a command line expression that runs when the game is ready for debugger attach 87 | # GAME_DEBUGGER_PROC Env var holding kernel32.dll's CreateProcessA import function string to evade anti-viruses 88 | # 89 | if ($Action -eq 'Launch') { 90 | # Visual Studio doesn't necessarily launch the debugee. Keep chasing the parent PID until we find it. 91 | $parentVSProcessId = $PID 92 | 93 | while ($true) { 94 | $wmiProcess = Get-WmiObject Win32_Process -Filter ProcessId=$parentVSProcessId 95 | 96 | if ($wmiProcess.ProcessName.ToLower() -eq "devenv.exe") { 97 | break; 98 | } 99 | 100 | $parentVSProcessId = $wmiProcess.ParentProcessId 101 | } 102 | 103 | # Start process with special environment vars set 104 | $localPSPath = (Get-Process -Id $PID | Get-Item).FullName 105 | $localScriptPath = $MyInvocation.MyCommand.Path 106 | 107 | $env:GAME_DEBUGGER_REQUEST = '"' + $localPSPath + '" -ExecutionPolicy Bypass -File "' + $localScriptPath + '" Attach ' + $parentVSProcessId + ' ' 108 | $env:GAME_DEBUGGER_PROC = 'kernel32!CreateProcessA' 109 | 110 | & $env:GAME_DEBUGGER_CMDLINE 111 | } 112 | elseif ($Action -eq 'Attach') { 113 | # Callback from game DLL side. Tell Visual Studio to attach to its process. 114 | $automationDTE = [VSAutomationHelper.Main]::FindDTEInstanceByProcessId($VSProcessId) 115 | 116 | foreach ($process in $automationDTE.Debugger.LocalProcesses) { 117 | if ($process.ProcessID -eq $ChildProcessId) { 118 | $process.Attach() 119 | break; 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.26) 2 | 3 | project( 4 | sf_shaderinjector 5 | VERSION 1.9 6 | LANGUAGES CXX) 7 | 8 | option(BUILD_FOR_SFSE "Build is meant for the Starfield Script Extender" OFF) 9 | option(BUILD_FOR_ASILOADER "Build is meant for the Microsoft Store ASI loader" OFF) 10 | 11 | if(BUILD_FOR_SFSE AND BUILD_FOR_ASILOADER) 12 | message(FATAL_ERROR "BUILD_FOR_ASILOADER and BUILD_FOR_SFSE cannot be enabled at the same time.") 13 | endif() 14 | 15 | set(PROJECT_DEPENDENCIES_PATH "${CMAKE_CURRENT_LIST_DIR}/dependencies") 16 | set(PROJECT_RESOURCES_PATH "${CMAKE_CURRENT_LIST_DIR}/resources") 17 | set(PROJECT_SOURCE_PATH "${CMAKE_CURRENT_LIST_DIR}/source") 18 | 19 | # 20 | # Store the current git commit hash for later use 21 | # 22 | execute_process( 23 | COMMAND git log -1 --format=%h 24 | WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" 25 | OUTPUT_VARIABLE BUILD_GIT_COMMIT_HASH 26 | OUTPUT_STRIP_TRAILING_WHITESPACE 27 | ) 28 | 29 | # 30 | # Set up the actual library 31 | # 32 | add_subdirectory("${PROJECT_SOURCE_PATH}") 33 | 34 | # 35 | # And finally produce build artifacts 36 | # 37 | if(BUILD_FOR_SFSE) 38 | set(PLUGIN_LOADER_OUTPUT_DIR "Data/SFSE/Plugins/") 39 | set(CPACK_PACKAGE_FILE_NAME "SSI-${CMAKE_PROJECT_VERSION_MAJOR}_${CMAKE_PROJECT_VERSION_MINOR} (SFSE-X_X_XX)") 40 | elseif(BUILD_FOR_ASILOADER) 41 | set(PLUGIN_LOADER_OUTPUT_DIR "/") 42 | set(CPACK_PACKAGE_FILE_NAME "SSI-${CMAKE_PROJECT_VERSION_MAJOR}_${CMAKE_PROJECT_VERSION_MINOR} (GamePassASI-X_X_XX)") 43 | endif() 44 | 45 | install( 46 | FILES 47 | "${PROJECT_RESOURCES_PATH}/SFShaderInjector.ini" 48 | DESTINATION ${PLUGIN_LOADER_OUTPUT_DIR} 49 | ) 50 | 51 | install( 52 | TARGETS 53 | sfse_output_dll 54 | RUNTIME 55 | DESTINATION ${PLUGIN_LOADER_OUTPUT_DIR} 56 | ) 57 | 58 | set(CPACK_GENERATOR "ZIP") 59 | set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF) 60 | include(CPack) 61 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 6, 3 | "include": [ 4 | "CMakeUserEnvVars.json" 5 | ], 6 | "configurePresets": [ 7 | { 8 | "name": "config-base-vcpkg", 9 | "hidden": true, 10 | "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", 11 | "cacheVariables": { 12 | "VCPKG_TARGET_TRIPLET": "x64-windows-static", 13 | "VCPKG_OVERLAY_PORTS": "${sourceDir}/vcpkg-ports" 14 | } 15 | }, 16 | { 17 | "name": "config-base-cmake", 18 | "hidden": true, 19 | "binaryDir": "${sourceDir}/bin/${presetName}", 20 | "cacheVariables": { 21 | "CMAKE_CONFIGURATION_TYPES": "Debug;Release", 22 | "CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE": "TRUE" 23 | } 24 | }, 25 | { 26 | "name": "config-base-ninja", 27 | "hidden": true, 28 | "generator": "Ninja Multi-Config", 29 | "architecture": { 30 | "value": "x64", 31 | "strategy": "external" 32 | }, 33 | "cacheVariables": { 34 | "CMAKE_CXX_FLAGS": "/MP /diagnostics:caret", 35 | "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded$<$:Debug>" 36 | } 37 | }, 38 | { 39 | "name": "config-base-msvc2022", 40 | "hidden": true, 41 | "generator": "Visual Studio 17 2022", 42 | "architecture": { 43 | "value": "x64" 44 | }, 45 | "inherits": [ 46 | "config-base-ninja" 47 | ] 48 | }, 49 | { 50 | "name": "sfse", 51 | "cacheVariables": { 52 | "BUILD_FOR_SFSE": "TRUE", 53 | "BUILD_FOR_ASILOADER": "FALSE" 54 | }, 55 | "inherits": [ 56 | "config-base-vcpkg", 57 | "config-base-cmake", 58 | "config-base-ninja", 59 | "config-envvars" 60 | ] 61 | }, 62 | { 63 | "name": "asiloader", 64 | "cacheVariables": { 65 | "BUILD_FOR_SFSE": "FALSE", 66 | "BUILD_FOR_ASILOADER": "TRUE" 67 | }, 68 | "inherits": [ 69 | "config-base-vcpkg", 70 | "config-base-cmake", 71 | "config-base-ninja", 72 | "config-envvars" 73 | ] 74 | } 75 | ], 76 | "buildPresets": [ 77 | { 78 | "name": "sfse-debug", 79 | "configurePreset": "sfse", 80 | "configuration": "Debug", 81 | "displayName": "SFSE Debug" 82 | }, 83 | { 84 | "name": "sfse-release", 85 | "configurePreset": "sfse", 86 | "configuration": "Release", 87 | "displayName": "SFSE Release" 88 | }, 89 | { 90 | "name": "asiloader-debug", 91 | "configurePreset": "asiloader", 92 | "configuration": "Debug", 93 | "displayName": "ASI Debug" 94 | }, 95 | { 96 | "name": "asiloader-release", 97 | "configurePreset": "asiloader", 98 | "configuration": "Release", 99 | "displayName": "ASI Release" 100 | } 101 | ], 102 | "packagePresets": [ 103 | { 104 | "name": "sfse", 105 | "displayName": "Package SFSE", 106 | "configurePreset": "sfse", 107 | "packageDirectory": "${sourceDir}/bin/built-packages" 108 | }, 109 | { 110 | "name": "asiloader", 111 | "displayName": "Package ASI", 112 | "configurePreset": "asiloader", 113 | "packageDirectory": "${sourceDir}/bin/built-packages" 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /CMakeUserEnvVars.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 6, 3 | "configurePresets": [ 4 | { 5 | "name": "config-envvars", 6 | "hidden": true, 7 | "environment": { 8 | "GAME_ROOT_DIRECTORY": "C:/Program Files (x86)/Steam/steamapps/common/Starfield", 9 | "GAME_DEBUGGER_CMDLINE": "$env{GAME_ROOT_DIRECTORY}/sfse_loader.exe" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starfield Shader Injector 2 | 3 | A plugin that allows people to use custom shaders without needing to rebuild or extract Starfield's BA2 shader package. Live shader updates are also supported for easy debugging at runtime. 4 | 5 | ## Building 6 | 7 | - CMake and vcpkg are expected to be set up beforehand. Visual Studio 2022 is recommended. 8 | - \ is `asiloader` or `sfse`. 9 | - \ is `asiloader-release`, `asiloader-debug`, `sfse-release`, or `sfse-debug`. 10 | 11 | ``` 12 | git clone --recurse-submodules https://github.com/Nukem9/sf-shader-injector.git 13 | cmake --preset 14 | cmake --build --preset 15 | ``` 16 | 17 | ## Installation 18 | 19 | - For developers, edit `CMakeUserEnvVars.json` and set `GAME_ROOT_DIRECTORY` to Starfield's root directory. The build script will automatically copy library files to the game folder. 20 | 21 | - For manual SFSE (Steam) installs, place `SFShaderInjector.dll` in the corresponding `Starfield\Data\SFSE\Plugins` folder. An example path is: `C:\steamapps\common\Starfield\Data\SFSE\Plugins\SFShaderInjector.dll` 22 | 23 | - For manual ASI loader (Game Pass) installs, place `SFShaderInjector.asi` in the game root directory next to `Starfield.exe`. An example path is: `C:\XboxGames\Starfield\Content\SFShaderInjector.asi` 24 | 25 | ## Custom Shader Installation 26 | 27 | - Shaders must be installed under the `Data\shadersfx` folder in Starfield's root directory. 28 | 29 | - A Steam edition path looks like this: `C:\steamapps\common\Starfield\Data\shadersfx\ColorGradingMerge\ColorGradingMerge_FF81_cs.bin` 30 | 31 | - A Game Pass edition path looks like this: `C:\XboxGames\Starfield\Content\Data\shadersfx\ColorGradingMerge\ColorGradingMerge_FF81_cs.bin` 32 | 33 | ## License 34 | 35 | - No license provided. TBD. 36 | - Dependencies are under their respective licenses. 37 | -------------------------------------------------------------------------------- /make-release.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | # Set up powershell equivalent of vcvarsall.bat when CMake/CPack aren't in PATH 4 | if ((Get-Command "cmake" -ErrorAction SilentlyContinue) -eq $null) { 5 | $vsPath = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationpath 6 | 7 | Import-Module (Get-ChildItem $vsPath -Recurse -File -Filter Microsoft.VisualStudio.DevShell.dll).FullName 8 | Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64' 9 | } 10 | 11 | # Then build with VS 12 | & cmake --preset asiloader 13 | & cmake --build --preset asiloader-release 14 | & cpack --preset asiloader 15 | 16 | & cmake --preset sfse 17 | & cmake --build --preset sfse-release 18 | & cpack --preset sfse -------------------------------------------------------------------------------- /resources/SFShaderInjector.ini: -------------------------------------------------------------------------------- 1 | # 2 | # Shader injector development options. Recommended for advanced users only. 3 | # 4 | [Development] 5 | # Set this to 1 to automatically reload custom shaders when .bin file edits are detected. 6 | AllowLiveUpdates = 0 7 | 8 | # Set this to 1 to add D3D12 debug markers for use in tools such as PIX, RenderDoc, or NSight. 9 | InsertDebugMarkers = 0 10 | 11 | # Sets the destination folder to extract Starfield's shader package to on startup. Paths will be 12 | # created if they don't exist and all .bin files will be overwritten. AllowLiveUpdates is disabled 13 | # when this option is used. 14 | # 15 | # Example: ShaderDumpBinPath = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Starfield\\Data\\shadersfx" 16 | ShaderDumpBinPath = "" -------------------------------------------------------------------------------- /source/CComPtr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef __WRL_CLASSIC_COM_STRICT__ 4 | #define __WRL_CLASSIC_COM_STRICT__ 5 | #endif // __WRL_CLASSIC_COM_STRICT__ 6 | 7 | #ifndef __WRL_NO_DEFAULT_LIB__ 8 | #define __WRL_NO_DEFAULT_LIB__ 9 | #endif // __WRL_NO_DEFAULT_LIB__ 10 | 11 | #include 12 | 13 | template 14 | using CComPtr = Microsoft::WRL::ComPtr; 15 | -------------------------------------------------------------------------------- /source/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Set up the source files and output library 3 | # 4 | set(CURRENT_PROJECT sfse_output_dll) 5 | set(CURRENT_PROJECT_FRIENDLY_NAME "SFShaderInjector") 6 | set(SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") 7 | 8 | file( 9 | GLOB HEADER_FILES 10 | LIST_DIRECTORIES FALSE 11 | CONFIGURE_DEPENDS 12 | "${SOURCE_DIR}/Hooking/*.h" 13 | "${SOURCE_DIR}/RE/*.h" 14 | "${SOURCE_DIR}/*.h" 15 | ) 16 | 17 | file( 18 | GLOB SOURCE_FILES 19 | LIST_DIRECTORIES FALSE 20 | CONFIGURE_DEPENDS 21 | "${SOURCE_DIR}/Hooking/*.cpp" 22 | "${SOURCE_DIR}/RE/*.cpp" 23 | "${SOURCE_DIR}/*.cpp" 24 | ) 25 | 26 | source_group( 27 | TREE "${SOURCE_DIR}/.." 28 | FILES 29 | ${HEADER_FILES} 30 | ${SOURCE_FILES} 31 | ) 32 | 33 | add_library( 34 | ${CURRENT_PROJECT} 35 | SHARED 36 | ${HEADER_FILES} 37 | ${SOURCE_FILES} 38 | ) 39 | 40 | target_precompile_headers( 41 | ${CURRENT_PROJECT} 42 | PRIVATE 43 | pch.h 44 | ) 45 | 46 | target_include_directories( 47 | ${CURRENT_PROJECT} 48 | PRIVATE 49 | "${SOURCE_DIR}" 50 | ) 51 | 52 | set_target_properties( 53 | ${CURRENT_PROJECT} 54 | PROPERTIES 55 | OUTPUT_NAME ${CURRENT_PROJECT_FRIENDLY_NAME} 56 | MSVC_DEBUG_INFORMATION_FORMAT "ProgramDatabase" 57 | ) 58 | 59 | if(BUILD_FOR_ASILOADER) 60 | set_target_properties( 61 | ${CURRENT_PROJECT} 62 | PROPERTIES 63 | SUFFIX ".asi" 64 | ) 65 | endif() 66 | 67 | # 68 | # Compiler-specific options 69 | # 70 | target_compile_features( 71 | ${CURRENT_PROJECT} 72 | PRIVATE 73 | cxx_std_23 74 | ) 75 | 76 | if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 77 | target_compile_options( 78 | ${CURRENT_PROJECT} 79 | PRIVATE 80 | "/utf-8" 81 | "/sdl" 82 | "/permissive-" 83 | "/Zc:preprocessor" 84 | "/Zc:inline" 85 | "/EHsc" 86 | 87 | "/W4" 88 | "/wd4100" # '': unreferenced formal parameter 89 | "/wd4200" # nonstandard extension used: zero-sized array in struct/union 90 | "/wd4201" # nonstandard extension used: nameless struct/union 91 | "/wd4324" # '': structure was padded due to alignment specifier 92 | ) 93 | 94 | target_link_options( 95 | ${CURRENT_PROJECT} 96 | PRIVATE 97 | # Force generate release PDBs: https://learn.microsoft.com/en-us/cpp/build/reference/debug-generate-debug-info 98 | "$<$:/DEBUG:FULL;/OPT:REF;/OPT:ICF>" 99 | # Anonymize RSDS PDB paths 100 | "$<$:/PDBALTPATH:$$.pdb>" 101 | ) 102 | endif() 103 | 104 | target_compile_definitions( 105 | ${CURRENT_PROJECT} 106 | PRIVATE 107 | NOMINMAX 108 | VC_EXTRALEAN 109 | WIN32_LEAN_AND_MEAN 110 | 111 | BUILD_PROJECT_NAME="${CURRENT_PROJECT_FRIENDLY_NAME}" 112 | BUILD_VERSION_MAJOR=${CMAKE_PROJECT_VERSION_MAJOR} 113 | BUILD_VERSION_MINOR=${CMAKE_PROJECT_VERSION_MINOR} 114 | BUILD_FOR_ASILOADER=$ 115 | BUILD_FOR_SFSE=$ 116 | ) 117 | 118 | # 119 | # Dependencies 120 | # 121 | # tomlplusplus 122 | find_package(PkgConfig REQUIRED) 123 | pkg_check_modules(tomlplusplus REQUIRED IMPORTED_TARGET tomlplusplus) 124 | 125 | # Spdlog 126 | find_package(spdlog CONFIG REQUIRED) 127 | target_link_libraries(${CURRENT_PROJECT} PRIVATE spdlog::spdlog) 128 | 129 | # Detours 130 | find_package(detours CONFIG REQUIRED) 131 | target_link_libraries(${CURRENT_PROJECT} PRIVATE detours::detours) 132 | 133 | # Xbyak 134 | find_package(xbyak CONFIG REQUIRED) 135 | 136 | # SFSE 137 | if(BUILD_FOR_SFSE) 138 | find_package(sfse-common CONFIG REQUIRED) 139 | endif() 140 | 141 | # 142 | # Commands 143 | # 144 | if(NOT $ENV{GAME_ROOT_DIRECTORY} STREQUAL "") 145 | add_custom_command( 146 | TARGET ${CURRENT_PROJECT} 147 | POST_BUILD 148 | COMMAND 149 | "${CMAKE_COMMAND}" -E copy_if_different 150 | "$" 151 | "$ENV{GAME_ROOT_DIRECTORY}/Data/SFSE/Plugins/$" 152 | ) 153 | endif() 154 | -------------------------------------------------------------------------------- /source/CRHooks.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "D3DShaderReplacement.h" 3 | #include "DebuggingUtil.h" 4 | #include "CRHooks.h" 5 | #include "Plugin.h" 6 | #include "ReShadeHelper.h" 7 | 8 | namespace CRHooks 9 | { 10 | struct TrackedDataEntry 11 | { 12 | CreationRenderer::TechniqueData *Technique; 13 | D3DPipelineStateStream::Copy StreamCopy; 14 | }; 15 | 16 | std::mutex TrackedShaderDataLock; 17 | std::vector TrackedPipelineData; 18 | std::unordered_map> TrackedTechniqueIdToRootSignature; 19 | 20 | void LiveUpdateFilesystemWatcherThread(CComPtr Device) 21 | { 22 | const auto changeHandle = FindFirstChangeNotificationW( 23 | D3DShaderReplacement::GetShaderBinDirectory().c_str(), 24 | true, 25 | FILE_NOTIFY_CHANGE_LAST_WRITE); 26 | 27 | if (changeHandle == INVALID_HANDLE_VALUE) 28 | { 29 | spdlog::error("Live update: FindFirstChangeNotification failed with error code {:X}.", GetLastError()); 30 | return; 31 | } 32 | 33 | spdlog::info("Live update: Initialized."); 34 | 35 | while (true) 36 | { 37 | const auto status = WaitForSingleObject(changeHandle, INFINITE); 38 | 39 | if (status != WAIT_OBJECT_0) 40 | break; 41 | 42 | // Update all known shaders in the directory. The loop might run multiple times if multiple files are 43 | // changed but that's okay. 44 | TrackedShaderDataLock.lock(); 45 | { 46 | size_t patchCounter = 0; 47 | 48 | for (auto& data : TrackedPipelineData) 49 | { 50 | const bool newPipelineRequired = D3DShaderReplacement::PatchPipelineStateStream( 51 | data.StreamCopy, 52 | Device.Get(), 53 | nullptr, 54 | data.Technique->m_Name, 55 | data.Technique->m_Id); 56 | 57 | if (!newPipelineRequired) 58 | continue; 59 | 60 | CComPtr pipelineState; 61 | if (auto hr = Device->CreatePipelineState(data.StreamCopy.GetDesc(), IID_PPV_ARGS(&pipelineState)); FAILED(hr)) 62 | { 63 | spdlog::error( 64 | "Live update: Failed to compile pipeline: {:X}. Shader technique: {:X}.", 65 | static_cast(hr), 66 | data.Technique->m_Id); 67 | 68 | continue; 69 | } 70 | 71 | DebuggingUtil::SetObjectDebugName(pipelineState.Get(), data.Technique->m_Name); 72 | 73 | // pipelineState->AddRef() is needed due to CComPtr's destructor. Luckily for us, the game keeps 74 | // exactly 1 reference to the old state so we don't have to fix mismatched reference counts. 75 | // 76 | // WARNING: This'll never be thread safe. It's meant as a developer tool, not for production. 77 | // 78 | // HACK: oldValue is never released. It's not stable and leaks memory for now. 79 | pipelineState->AddRef(); 80 | 81 | auto targetPointer = reinterpret_cast(&data.Technique->m_PipelineState); 82 | auto oldValue = InterlockedExchangePointer(targetPointer, pipelineState.Get()); 83 | (void)oldValue; // ->Release(); 84 | 85 | patchCounter++; 86 | } 87 | 88 | if (patchCounter > 0) 89 | spdlog::info("Live update: Created pipelines for {} technique(s).", patchCounter); 90 | } 91 | TrackedShaderDataLock.unlock(); 92 | 93 | FindNextChangeNotification(changeHandle); 94 | } 95 | 96 | FindCloseChangeNotification(changeHandle); 97 | } 98 | 99 | void TrackDevice(CComPtr Device) 100 | { 101 | static bool once = [&] 102 | { 103 | if (Plugin::AllowLiveUpdates) 104 | std::thread(LiveUpdateFilesystemWatcherThread, Device).detach(); 105 | 106 | ReShadeHelper::Initialize(); 107 | return true; 108 | }(); 109 | } 110 | 111 | void TrackCompiledTechnique( 112 | CComPtr Device, 113 | CreationRenderer::TechniqueData *Technique, 114 | D3DPipelineStateStream::Copy&& StreamCopy, 115 | bool WasPatchedUpfront) 116 | { 117 | // Root signature override has to be tracked 118 | if (WasPatchedUpfront) 119 | { 120 | for (D3DPipelineStateStream::Iterator iter(StreamCopy.GetDesc()); !iter.AtEnd(); iter.Advance()) 121 | { 122 | switch (auto obj = iter.GetObj(); obj->Type) 123 | { 124 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_ROOT_SIGNATURE: 125 | std::scoped_lock lock(TrackedShaderDataLock); 126 | TrackedTechniqueIdToRootSignature.emplace(Technique->m_Id, obj->RootSignature); 127 | break; 128 | } 129 | } 130 | } 131 | 132 | if (Plugin::AllowLiveUpdates) 133 | { 134 | std::scoped_lock lock(TrackedShaderDataLock); 135 | TrackedPipelineData.emplace_back(TrackedDataEntry { 136 | .Technique = Technique, 137 | .StreamCopy = std::move(StreamCopy), 138 | }); 139 | } 140 | } 141 | 142 | bool OverridePipelineLayoutDx12( 143 | ID3D12GraphicsCommandList4 *CommandList, 144 | CreationRenderer::PipelineLayoutDx12 *CurrentLayout, 145 | CreationRenderer::PipelineLayoutDx12 *TargetLayout, 146 | CreationRenderer::TechniqueData **CurrentTech, 147 | CreationRenderer::TechniqueData **TargetTech) 148 | { 149 | // 150 | // Return false when absolutely nothing has changed. 151 | // Return true when a new root signature is required. The command list MUST be updated before returning. 152 | // 153 | // Vanilla game code uses the following logic: 154 | // if (CurrentLayout == TargetLayout) 155 | // return false; 156 | // 157 | bool updateRequired = CurrentLayout != TargetLayout; 158 | auto rootSignature = TargetLayout->m_RootSignature; 159 | 160 | // If the target technique requires an override OR the previous technique was overridden, force a flush 161 | if (auto itr = TrackedTechniqueIdToRootSignature.find((*TargetTech)->m_Id); itr != TrackedTechniqueIdToRootSignature.end()) 162 | { 163 | updateRequired = true; 164 | rootSignature = itr->second.Get(); 165 | } 166 | else if (!updateRequired && CurrentTech) 167 | { 168 | updateRequired = TrackedTechniqueIdToRootSignature.contains((*CurrentTech)->m_Id); 169 | } 170 | 171 | if (updateRequired) 172 | { 173 | const auto type = *reinterpret_cast( 174 | reinterpret_cast(TargetLayout->m_LayoutConfigurationData) + 0x4); 175 | 176 | switch (type) 177 | { 178 | case CreationRenderer::ShaderType::Graphics: 179 | CommandList->SetGraphicsRootSignature(rootSignature); 180 | break; 181 | 182 | case CreationRenderer::ShaderType::Compute: 183 | case CreationRenderer::ShaderType::RayTracing: 184 | CommandList->SetComputeRootSignature(rootSignature); 185 | break; 186 | } 187 | } 188 | 189 | return updateRequired; 190 | } 191 | 192 | class SetPipelineLayoutDx12HookGen : Xbyak::CodeGenerator 193 | { 194 | private: 195 | const uintptr_t m_TargetAddress; 196 | 197 | public: 198 | SetPipelineLayoutDx12HookGen(uintptr_t TargetAddress) : m_TargetAddress(TargetAddress) 199 | { 200 | Xbyak::Label emulateSetNewSignature; 201 | 202 | lea(r9, ptr[rsi + 0x8]); 203 | mov(ptr[rsp + 0x20], r9); // a5: Target Technique** 204 | mov(r9, r15); // a4: Current Technique** 205 | mov(r8, r13); // a3: Target PipelineLayoutDx12 206 | mov(rdx, ptr[rcx + 0x18]); // a2: Current PipelineLayoutDx12 207 | mov(rcx, ptr[r14 + 0x10]); // a1: ID3D12GraphicsCommandList 208 | mov(rax, reinterpret_cast(&OverridePipelineLayoutDx12)); 209 | call(rax); 210 | 211 | test(al, al); 212 | jnz(emulateSetNewSignature); 213 | 214 | // Run the original code 215 | mov(rax, m_TargetAddress + 0x73); 216 | jmp(rax); 217 | 218 | // New signature required. OverridePipelineLayoutDx12() is expected to pass a signature to the D3D12 API 219 | // before we get here. This bypasses Starfield's calls to ID3D12CommandList::SetXXXRootSignature(). 220 | L(emulateSetNewSignature); 221 | mov(rax, m_TargetAddress + 0x60); 222 | jmp(rax); 223 | } 224 | 225 | void Patch() 226 | { 227 | Hooks::WriteJump(m_TargetAddress, getCode()); 228 | } 229 | }; 230 | 231 | DECLARE_HOOK_TRANSACTION(CRHooks) 232 | { 233 | static SetPipelineLayoutDx12HookGen setPipelineLayoutDx12Hook( 234 | Offsets::Signature("4C 39 69 18 74 6D 41 8B C8 83 E9 01 74 41 83 E9 01 74 29 83 F9 01 74 24 41 8B C8 83 E9 01 74 40")); 235 | setPipelineLayoutDx12Hook.Patch(); 236 | }; 237 | } 238 | -------------------------------------------------------------------------------- /source/CRHooks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RE/CreationRenderer.h" 4 | #include "CComPtr.h" 5 | #include "D3DPipelineStateStream.h" 6 | 7 | namespace CRHooks 8 | { 9 | void TrackDevice(CComPtr Device); 10 | 11 | void TrackCompiledTechnique( 12 | CComPtr Device, 13 | CreationRenderer::TechniqueData *Technique, 14 | D3DPipelineStateStream::Copy&& StreamCopy, 15 | bool WasPatchedUpfront); 16 | } 17 | -------------------------------------------------------------------------------- /source/D3DHooks.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "RE/CreationRenderer.h" 3 | #include "CComPtr.h" 4 | #include "CRHooks.h" 5 | #include "D3DShaderReplacement.h" 6 | #include "DebuggingUtil.h" 7 | #include "D3Dhooks.h" 8 | 9 | namespace D3DHooks 10 | { 11 | // 12 | // Skip the early LoadPipeline() call and move it down into CreatePipelineStateForTechnique. 13 | // 14 | // Starfield calls CreatePipelineState with the same parameters as here if we return an error code. There's not 15 | // many choices since we have to query multiple files on disk each time. 7000 pipelines * 8 shaders * 3 calls. 16 | // 17 | // Instead we get 7000 pipelines * 8 shaders * 1 call. 18 | // 19 | // 20 | // This method returns an HRESULT success or error code, which can include E_INVALIDARG if the name doesn't 21 | // exist or the stream description doesn't match the data in the library, and E_OUTOFMEMORY if the function 22 | // is unable to allocate the resulting PSO. 23 | // 24 | thread_local CComPtr TLLastRequestedPipelineLibrary; 25 | thread_local CreationRenderer::TechniqueData *TLLastRequestedShaderTechnique; 26 | thread_local wchar_t TLLastRequestedPipelineName[64]; 27 | 28 | HRESULT LoadPipelineForTechnique( 29 | ID3D12PipelineLibrary1 *Thisptr, 30 | LPCWSTR Name, 31 | const D3D12_PIPELINE_STATE_STREAM_DESC *Desc, 32 | REFIID Riid, 33 | void **PipelineState, 34 | CreationRenderer::TechniqueData *Tech) 35 | { 36 | TLLastRequestedPipelineLibrary = Thisptr; 37 | TLLastRequestedShaderTechnique = Tech; 38 | wcscpy_s(TLLastRequestedPipelineName, Name); 39 | 40 | return E_INVALIDARG; 41 | } 42 | 43 | class LoadPipelineHookGen : Xbyak::CodeGenerator 44 | { 45 | private: 46 | const uintptr_t m_TargetAddress; 47 | 48 | public: 49 | LoadPipelineHookGen(uintptr_t TargetAddress) : m_TargetAddress(TargetAddress) 50 | { 51 | mov(ptr[rsp + 0x28], r12); // a6: Technique pointer 52 | mov(rax, reinterpret_cast(&LoadPipelineForTechnique)); 53 | call(rax); 54 | test(eax, eax); 55 | 56 | jmp(ptr[rip]); 57 | dq(m_TargetAddress + 0x5); 58 | } 59 | 60 | void Patch() 61 | { 62 | Hooks::WriteJump(m_TargetAddress, getCode()); 63 | } 64 | }; 65 | 66 | // 67 | // Similar to LoadPipeline above, but for storing pipeline state. CreatePipelineStateForTechnique determines 68 | // whether this gets called by setting TLNextShaderTechniqueToSkipCaching. We don't want modified shaders to 69 | // be cached. 70 | // 71 | // 72 | // This method returns an HRESULT success or error code, including E_INVALIDARG if the name already exists, 73 | // E_OUTOFMEMORY if unable to allocate storage in the library. 74 | // 75 | thread_local CreationRenderer::TechniqueData *TLNextShaderTechniqueToSkipCaching; 76 | 77 | HRESULT StorePipelineForTechnique( 78 | ID3D12PipelineLibrary1 *Thisptr, 79 | LPCWSTR Name, 80 | ID3D12PipelineState *Pipeline, 81 | CreationRenderer::TechniqueData *Tech) 82 | { 83 | if (TLNextShaderTechniqueToSkipCaching == Tech) 84 | { 85 | TLNextShaderTechniqueToSkipCaching = nullptr; 86 | return S_OK; 87 | } 88 | 89 | return Thisptr->StorePipeline(Name, Pipeline); 90 | } 91 | 92 | class StorePipelineHookGen : Xbyak::CodeGenerator 93 | { 94 | private: 95 | const uintptr_t m_TargetAddress; 96 | 97 | public: 98 | StorePipelineHookGen(uintptr_t TargetAddress) : m_TargetAddress(TargetAddress) 99 | { 100 | mov(r9, r12); // a4: Technique pointer 101 | mov(rax, reinterpret_cast(&StorePipelineForTechnique)); 102 | call(rax); 103 | mov(ebx, eax); 104 | 105 | jmp(ptr[rip]); 106 | dq(m_TargetAddress + 0x5); 107 | } 108 | 109 | void Patch() 110 | { 111 | Hooks::WriteJump(m_TargetAddress, getCode()); 112 | } 113 | }; 114 | 115 | // 116 | // Pipeline state object creation. This is the main hook where shader bytecode gets replaced. 117 | // 118 | // 119 | // This method returns E_OUTOFMEMORY if there is insufficient memory to create the pipeline state object. 120 | // See Direct3D 12 Return Codes for other possible return values. 121 | // 122 | HRESULT CreatePipelineStateForTechnique( 123 | ID3D12Device2 *Thisptr, 124 | const D3D12_PIPELINE_STATE_STREAM_DESC *Desc, 125 | REFIID Riid, 126 | void **PipelineState, 127 | CreationRenderer::TechniqueData *Tech) 128 | { 129 | CRHooks::TrackDevice(Thisptr); 130 | 131 | if (Riid != __uuidof(ID3D12PipelineState)) 132 | return E_NOINTERFACE; 133 | 134 | *PipelineState = nullptr; 135 | 136 | // Note that streamCopy is initially a 1:1 copy since Desc is const. We don't know if a modification 137 | // is applied until PatchPipelineStateStream returns. 138 | D3DPipelineStateStream::Copy streamCopy(Desc); 139 | const std::span rootSignatureData(Tech->m_Inputs->m_RootSignatureBlob, Tech->m_Inputs->m_RootSignatureBlobSize); 140 | 141 | // shaderWasPatched will be true if ANY part of the pipeline state stream is modified by code. If so, 142 | // the pipeline state has to be created from scratch. Otherwise ask the pipeline library interface for 143 | // a precompiled copy. 144 | bool shaderWasPatched = false; 145 | bool shaderWasLoadedFromCache = false; 146 | 147 | if (D3DShaderReplacement::PatchPipelineStateStream(streamCopy, Thisptr, &rootSignatureData, Tech->m_Name, Tech->m_Id)) 148 | { 149 | shaderWasPatched = true; 150 | } 151 | else 152 | { 153 | if (TLLastRequestedPipelineLibrary && TLLastRequestedShaderTechnique == Tech) 154 | { 155 | if (SUCCEEDED(TLLastRequestedPipelineLibrary->LoadPipeline( 156 | TLLastRequestedPipelineName, 157 | streamCopy.GetDesc(), 158 | Riid, 159 | PipelineState))) 160 | shaderWasLoadedFromCache = true; 161 | } 162 | } 163 | 164 | TLLastRequestedPipelineLibrary = nullptr; 165 | TLLastRequestedShaderTechnique = nullptr; 166 | TLNextShaderTechniqueToSkipCaching = (shaderWasLoadedFromCache || shaderWasPatched) ? Tech : nullptr; 167 | 168 | if (!shaderWasLoadedFromCache) 169 | { 170 | const auto hr = Thisptr->CreatePipelineState(streamCopy.GetDesc(), Riid, PipelineState); 171 | 172 | if (FAILED(hr)) 173 | { 174 | spdlog::error( 175 | "CreatePipelineState failed and returned {:X}. Shader technique: {:X}.", 176 | static_cast(hr), 177 | Tech->m_Id); 178 | 179 | if (hr == E_INVALIDARG) 180 | spdlog::error("Please check that all custom shaders have matching input semantics, root signatures, and are digitally " 181 | "signed by dxc.exe."); 182 | 183 | return hr; 184 | } 185 | } 186 | 187 | // Tech can't be used because it's allocated on the stack and quickly discarded. PipelineState is a 188 | // pointer within another TechniqueData struct that's stored in a global array - a suitable alternative. 189 | auto globalTech = reinterpret_cast(PipelineState) - offsetof(CreationRenderer::TechniqueData, m_PipelineState); 190 | 191 | CRHooks::TrackCompiledTechnique( 192 | Thisptr, 193 | reinterpret_cast(globalTech), 194 | std::move(streamCopy), 195 | shaderWasPatched); 196 | 197 | DebuggingUtil::SetObjectDebugName(static_cast(*PipelineState), Tech->m_Name); 198 | return S_OK; 199 | } 200 | 201 | class CreatePipelineStateHookGen : Xbyak::CodeGenerator 202 | { 203 | private: 204 | const uintptr_t m_TargetAddress; 205 | 206 | public: 207 | CreatePipelineStateHookGen(uintptr_t TargetAddress) : m_TargetAddress(TargetAddress) 208 | { 209 | mov(ptr[rsp + 0x20], r12); // a5: Technique pointer 210 | mov(rax, reinterpret_cast(&CreatePipelineStateForTechnique)); 211 | call(rax); 212 | 213 | jmp(ptr[rip]); 214 | dq(m_TargetAddress + 0x6); 215 | } 216 | 217 | void Patch() 218 | { 219 | Hooks::WriteJump(m_TargetAddress, getCode()); 220 | } 221 | }; 222 | 223 | // 224 | // Identical to CreatePipelineStateForTechnique but intercepts the call to CreateGraphicsPipelineState that 225 | // FidelityFX's SDK uses. 226 | // 227 | HRESULT FFXCreateGraphicsPipelineStateForTechnique( 228 | ID3D12Device2 *Thisptr, 229 | const D3D12_GRAPHICS_PIPELINE_STATE_DESC *Desc, 230 | REFIID Riid, 231 | void **PipelineState) 232 | { 233 | CRHooks::TrackDevice(Thisptr); 234 | 235 | if (Riid != __uuidof(ID3D12PipelineState)) 236 | return E_NOINTERFACE; 237 | 238 | *PipelineState = nullptr; 239 | 240 | // FFX doesn't have debug names so we have to fake one 241 | const auto fakeTechniqueId = static_cast(DebuggingUtil::FNV1A32(Desc->VS.pShaderBytecode, Desc->VS.BytecodeLength)) << 32ull | 242 | static_cast(DebuggingUtil::FNV1A32(Desc->PS.pShaderBytecode, Desc->PS.BytecodeLength)); 243 | 244 | char fakeTechniqueName[128]; 245 | sprintf_s(fakeTechniqueName, "FidelityFX3FI- (%llX)", fakeTechniqueId); 246 | 247 | // Upgrade CreateGraphicsPipelineState's legacy structure to CreatePipelineState's bytestream description 248 | struct 249 | { 250 | #define MAKE_PSS_ENTRY(HeaderType, FullType) \ 251 | struct alignas(void *) \ 252 | { \ 253 | D3D12_PIPELINE_STATE_SUBOBJECT_TYPE m_Type_##HeaderType = D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_##HeaderType; \ 254 | FullType = {}; \ 255 | } 256 | 257 | MAKE_PSS_ENTRY(ROOT_SIGNATURE, ID3D12RootSignature *m_RootSignature); 258 | MAKE_PSS_ENTRY(VS, D3D12_SHADER_BYTECODE m_VS); 259 | MAKE_PSS_ENTRY(PS, D3D12_SHADER_BYTECODE m_PS); 260 | MAKE_PSS_ENTRY(DS, D3D12_SHADER_BYTECODE m_DS); 261 | MAKE_PSS_ENTRY(HS, D3D12_SHADER_BYTECODE m_HS); 262 | MAKE_PSS_ENTRY(GS, D3D12_SHADER_BYTECODE m_GS); 263 | MAKE_PSS_ENTRY(STREAM_OUTPUT, D3D12_STREAM_OUTPUT_DESC m_StreamOutput); 264 | MAKE_PSS_ENTRY(BLEND, D3D12_BLEND_DESC m_BlendState); 265 | MAKE_PSS_ENTRY(SAMPLE_MASK, UINT m_SampleMask); 266 | MAKE_PSS_ENTRY(RASTERIZER, D3D12_RASTERIZER_DESC m_RasterizerState); 267 | MAKE_PSS_ENTRY(DEPTH_STENCIL, D3D12_DEPTH_STENCIL_DESC m_DepthStencilState); 268 | MAKE_PSS_ENTRY(INPUT_LAYOUT, D3D12_INPUT_LAYOUT_DESC m_InputLayout); 269 | MAKE_PSS_ENTRY(IB_STRIP_CUT_VALUE, D3D12_INDEX_BUFFER_STRIP_CUT_VALUE m_IBStripCutValue); 270 | MAKE_PSS_ENTRY(PRIMITIVE_TOPOLOGY, D3D12_PRIMITIVE_TOPOLOGY_TYPE m_PrimitiveTopologyType); 271 | MAKE_PSS_ENTRY(RENDER_TARGET_FORMATS, D3D12_RT_FORMAT_ARRAY m_RTVFormats); 272 | MAKE_PSS_ENTRY(DEPTH_STENCIL_FORMAT, DXGI_FORMAT m_DSVFormat); 273 | MAKE_PSS_ENTRY(SAMPLE_DESC, DXGI_SAMPLE_DESC m_SampleDesc); 274 | MAKE_PSS_ENTRY(NODE_MASK, UINT m_NodeMask); 275 | MAKE_PSS_ENTRY(CACHED_PSO, D3D12_CACHED_PIPELINE_STATE m_CachedPSO); 276 | MAKE_PSS_ENTRY(FLAGS, D3D12_PIPELINE_STATE_FLAGS m_Flags); 277 | 278 | #undef MAKE_PSS_ENTRY 279 | } upgradedStreamData; 280 | // Designated initializers generate an internal compiler error 281 | upgradedStreamData.m_RootSignature = Desc->pRootSignature; 282 | upgradedStreamData.m_VS = Desc->VS; 283 | upgradedStreamData.m_PS = Desc->PS; 284 | upgradedStreamData.m_DS = Desc->DS; 285 | upgradedStreamData.m_HS = Desc->HS; 286 | upgradedStreamData.m_GS = Desc->GS; 287 | upgradedStreamData.m_StreamOutput = Desc->StreamOutput; 288 | upgradedStreamData.m_BlendState = Desc->BlendState; 289 | upgradedStreamData.m_SampleMask = Desc->SampleMask; 290 | upgradedStreamData.m_RasterizerState = Desc->RasterizerState; 291 | upgradedStreamData.m_DepthStencilState = Desc->DepthStencilState; 292 | upgradedStreamData.m_InputLayout = Desc->InputLayout; 293 | upgradedStreamData.m_IBStripCutValue = Desc->IBStripCutValue; 294 | upgradedStreamData.m_PrimitiveTopologyType = Desc->PrimitiveTopologyType; 295 | upgradedStreamData.m_RTVFormats.NumRenderTargets = Desc->NumRenderTargets; 296 | memcpy(upgradedStreamData.m_RTVFormats.RTFormats, Desc->RTVFormats, sizeof(Desc->RTVFormats)); 297 | upgradedStreamData.m_DSVFormat = Desc->DSVFormat; 298 | upgradedStreamData.m_SampleDesc = Desc->SampleDesc; 299 | upgradedStreamData.m_NodeMask = Desc->NodeMask; 300 | upgradedStreamData.m_CachedPSO = Desc->CachedPSO; 301 | upgradedStreamData.m_Flags = Desc->Flags; 302 | 303 | const D3D12_PIPELINE_STATE_STREAM_DESC upgradedStreamDesc = { 304 | .SizeInBytes = sizeof(upgradedStreamData), 305 | .pPipelineStateSubobjectStream = &upgradedStreamData, 306 | }; 307 | 308 | D3DPipelineStateStream::Copy streamCopy(&upgradedStreamDesc); 309 | D3DShaderReplacement::PatchPipelineStateStream(streamCopy, Thisptr, nullptr, fakeTechniqueName, fakeTechniqueId); 310 | 311 | const auto hr = Thisptr->CreatePipelineState(streamCopy.GetDesc(), Riid, PipelineState); 312 | 313 | if (FAILED(hr)) 314 | { 315 | spdlog::error( 316 | "CreatePipelineState failed and returned {:X}. Shader technique: {:X}.", 317 | static_cast(hr), 318 | fakeTechniqueId); 319 | 320 | if (hr == E_INVALIDARG) 321 | spdlog::error("Please check that all custom shaders have matching input semantics, root signatures, and are digitally " 322 | "signed by dxc.exe."); 323 | 324 | return hr; 325 | } 326 | 327 | return S_OK; 328 | } 329 | 330 | class FFXCreateGraphicsPipelineStateHookGen : Xbyak::CodeGenerator 331 | { 332 | private: 333 | const uintptr_t m_TargetAddress; 334 | 335 | public: 336 | FFXCreateGraphicsPipelineStateHookGen(uintptr_t TargetAddress) : m_TargetAddress(TargetAddress) 337 | { 338 | mov(rax, reinterpret_cast(&FFXCreateGraphicsPipelineStateForTechnique)); 339 | call(rax); 340 | test(eax, eax); 341 | 342 | jmp(ptr[rip]); 343 | dq(m_TargetAddress + 0x5); 344 | } 345 | 346 | void Patch() 347 | { 348 | Hooks::WriteJump(m_TargetAddress, getCode()); 349 | } 350 | }; 351 | 352 | DECLARE_HOOK_TRANSACTION(D3DHooks) 353 | { 354 | static LoadPipelineHookGen loadPipelineHook( 355 | Offsets::Signature("FF 50 68 85 C0 0F 89 ? ? ? ? 49 8B 8F ? ? ? ? 48 8B 01 4C 8B CF 4C 8D")); 356 | loadPipelineHook.Patch(); 357 | 358 | static StorePipelineHookGen storePipelineHook(Offsets::Signature("FF 50 40 8B D8 85 C0 0F 89 ? ? ? ? 45 33 E4 4C 89 64 24 58")); 359 | storePipelineHook.Patch(); 360 | 361 | static CreatePipelineStateHookGen createPipelineStateHook1( 362 | Offsets::Signature("FF 90 78 01 00 00 8B D8 41 BD FF FF FF FF 85 C0 0F 89 ? ? ? ? 33 C0")); 363 | createPipelineStateHook1.Patch(); 364 | 365 | static CreatePipelineStateHookGen createPipelineStateHook2( 366 | Offsets::Signature("FF 90 78 01 00 00 8B D8 85 C0 0F 89 ? ? ? ? 4C 89 6C 24 68")); 367 | createPipelineStateHook2.Patch(); 368 | 369 | static FFXCreateGraphicsPipelineStateHookGen createGraphicsPipelineStateHook( 370 | Offsets::Signature("FF 50 50 85 C0 78 04 33 C0 EB 05 B8 0D 00 00 80")); 371 | createGraphicsPipelineStateHook.Patch(); 372 | }; 373 | } 374 | -------------------------------------------------------------------------------- /source/D3DHooks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace D3DHooks 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /source/D3DPipelineStateStream.cpp: -------------------------------------------------------------------------------- 1 | #include "D3DPipelineStateStream.h" 2 | 3 | namespace D3DPipelineStateStream 4 | { 5 | Iterator::Iterator(const D3D12_PIPELINE_STATE_STREAM_DESC *Description) 6 | { 7 | m_Start = reinterpret_cast(Description->pPipelineStateSubobjectStream); 8 | m_End = m_Start + Description->SizeInBytes; 9 | } 10 | 11 | void Iterator::Advance() 12 | { 13 | const auto type = GetObj()->Type; 14 | 15 | m_Start += sizeof(D3D12_PTR_PSO_SUBOBJECT::Type); 16 | m_Start = reinterpret_cast(AlignUp(reinterpret_cast(m_Start), GetAlignmentForType(type))); 17 | 18 | m_Start += GetSizeForType(type); 19 | m_Start = reinterpret_cast(AlignUp(reinterpret_cast(m_Start), alignof(void *))); 20 | } 21 | 22 | bool Iterator::AtEnd() const 23 | { 24 | return m_Start >= m_End; 25 | } 26 | 27 | Iterator::D3D12_PTR_PSO_SUBOBJECT *Iterator::GetObj() const 28 | { 29 | return reinterpret_cast(m_Start); 30 | } 31 | 32 | size_t Iterator::GetSizeForType(D3D12_PIPELINE_STATE_SUBOBJECT_TYPE Type) const 33 | { 34 | switch (Type) 35 | { 36 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_VS: 37 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_PS: 38 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_HS: 39 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DS: 40 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_GS: 41 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CS: 42 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_AS: 43 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_MS: 44 | return sizeof(D3D12_SHADER_BYTECODE); 45 | 46 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CACHED_PSO: 47 | return sizeof(D3D12_CACHED_PIPELINE_STATE); 48 | 49 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_ROOT_SIGNATURE: 50 | return sizeof(ID3D12RootSignature *); 51 | 52 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_STREAM_OUTPUT: 53 | return sizeof(D3D12_STREAM_OUTPUT_DESC); 54 | 55 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_BLEND: 56 | return sizeof(D3D12_BLEND_DESC); 57 | 58 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_SAMPLE_MASK: 59 | return sizeof(UINT); 60 | 61 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_RASTERIZER: 62 | return sizeof(D3D12_RASTERIZER_DESC); 63 | 64 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DEPTH_STENCIL: 65 | return sizeof(D3D12_DEPTH_STENCIL_DESC); 66 | 67 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_INPUT_LAYOUT: 68 | return sizeof(D3D12_INPUT_LAYOUT_DESC); 69 | 70 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_IB_STRIP_CUT_VALUE: 71 | return sizeof(D3D12_INDEX_BUFFER_STRIP_CUT_VALUE); 72 | 73 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_PRIMITIVE_TOPOLOGY: 74 | return sizeof(D3D12_PRIMITIVE_TOPOLOGY_TYPE); 75 | 76 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_RENDER_TARGET_FORMATS: 77 | return sizeof(D3D12_RT_FORMAT_ARRAY); 78 | 79 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DEPTH_STENCIL_FORMAT: 80 | return sizeof(DXGI_FORMAT); 81 | 82 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_SAMPLE_DESC: 83 | return sizeof(DXGI_SAMPLE_DESC); 84 | 85 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_NODE_MASK: 86 | return sizeof(UINT); 87 | 88 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_FLAGS: 89 | return sizeof(D3D12_PIPELINE_STATE_FLAGS); 90 | 91 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DEPTH_STENCIL1: 92 | return sizeof(D3D12_DEPTH_STENCIL_DESC1); 93 | 94 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_VIEW_INSTANCING: 95 | return sizeof(D3D12_VIEW_INSTANCING_DESC); 96 | } 97 | 98 | std::unreachable(); 99 | } 100 | 101 | size_t Iterator::GetAlignmentForType(D3D12_PIPELINE_STATE_SUBOBJECT_TYPE Type) const 102 | { 103 | switch (Type) 104 | { 105 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_VS: 106 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_PS: 107 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_HS: 108 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DS: 109 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_GS: 110 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CS: 111 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_AS: 112 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_MS: 113 | return alignof(D3D12_SHADER_BYTECODE); 114 | 115 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CACHED_PSO: 116 | return alignof(D3D12_CACHED_PIPELINE_STATE); 117 | 118 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_ROOT_SIGNATURE: 119 | return alignof(ID3D12RootSignature *); 120 | 121 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_STREAM_OUTPUT: 122 | return alignof(D3D12_STREAM_OUTPUT_DESC); 123 | 124 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_BLEND: 125 | return alignof(D3D12_BLEND_DESC); 126 | 127 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_SAMPLE_MASK: 128 | return alignof(UINT); 129 | 130 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_RASTERIZER: 131 | return alignof(D3D12_RASTERIZER_DESC); 132 | 133 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DEPTH_STENCIL: 134 | return alignof(D3D12_DEPTH_STENCIL_DESC); 135 | 136 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_INPUT_LAYOUT: 137 | return alignof(D3D12_INPUT_LAYOUT_DESC); 138 | 139 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_IB_STRIP_CUT_VALUE: 140 | return alignof(D3D12_INDEX_BUFFER_STRIP_CUT_VALUE); 141 | 142 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_PRIMITIVE_TOPOLOGY: 143 | return alignof(D3D12_PRIMITIVE_TOPOLOGY_TYPE); 144 | 145 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_RENDER_TARGET_FORMATS: 146 | return alignof(D3D12_RT_FORMAT_ARRAY); 147 | 148 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DEPTH_STENCIL_FORMAT: 149 | return alignof(DXGI_FORMAT); 150 | 151 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_SAMPLE_DESC: 152 | return alignof(DXGI_SAMPLE_DESC); 153 | 154 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_NODE_MASK: 155 | return alignof(UINT); 156 | 157 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_FLAGS: 158 | return alignof(D3D12_PIPELINE_STATE_FLAGS); 159 | 160 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DEPTH_STENCIL1: 161 | return alignof(D3D12_DEPTH_STENCIL_DESC1); 162 | 163 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_VIEW_INSTANCING: 164 | return alignof(D3D12_VIEW_INSTANCING_DESC); 165 | } 166 | 167 | std::unreachable(); 168 | } 169 | 170 | Copy::Copy(const D3D12_PIPELINE_STATE_STREAM_DESC *Description) 171 | { 172 | CreateCopy(Description); 173 | } 174 | 175 | Copy::Copy(Copy&& Other) noexcept 176 | { 177 | m_TempBuffers = std::move(Other.m_TempBuffers); 178 | m_RefCountedObjects = std::move(Other.m_RefCountedObjects); 179 | 180 | m_CopiedDesc = Other.m_CopiedDesc; 181 | Other.m_CopiedDesc = {}; 182 | } 183 | 184 | void Copy::CreateCopy(const D3D12_PIPELINE_STATE_STREAM_DESC *InputDesc) 185 | { 186 | // Do a memcpy up front and then patch the pointers as needed 187 | m_CopiedDesc.pPipelineStateSubobjectStream = memdup(InputDesc->pPipelineStateSubobjectStream, InputDesc->SizeInBytes); 188 | m_CopiedDesc.SizeInBytes = InputDesc->SizeInBytes; 189 | 190 | for (Iterator iter(GetDesc()); !iter.AtEnd(); iter.Advance()) 191 | { 192 | switch (auto obj = iter.GetObj(); obj->Type) 193 | { 194 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_VS: 195 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_PS: 196 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_HS: 197 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DS: 198 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_GS: 199 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CS: 200 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_AS: 201 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_MS: 202 | obj->Shader.pShaderBytecode = memdup(obj->Shader.pShaderBytecode, obj->Shader.BytecodeLength); 203 | break; 204 | 205 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CACHED_PSO: 206 | obj->CachedPSO.pCachedBlob = memdup(obj->CachedPSO.pCachedBlob, obj->CachedPSO.CachedBlobSizeInBytes); 207 | break; 208 | 209 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_ROOT_SIGNATURE: 210 | if (obj->RootSignature) 211 | { 212 | CComPtr rsCopy(obj->RootSignature); 213 | TrackObject(std::move(rsCopy)); 214 | } 215 | break; 216 | 217 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_STREAM_OUTPUT: 218 | obj->StreamOutput.pSODeclaration = memdup( 219 | obj->StreamOutput.pSODeclaration, 220 | obj->StreamOutput.NumEntries * sizeof(D3D12_SO_DECLARATION_ENTRY)); 221 | obj->StreamOutput.pBufferStrides = memdup(obj->StreamOutput.pBufferStrides, obj->StreamOutput.NumStrides * sizeof(UINT)); 222 | break; 223 | 224 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_INPUT_LAYOUT: 225 | obj->InputLayout.pInputElementDescs = memdup( 226 | obj->InputLayout.pInputElementDescs, 227 | obj->InputLayout.NumElements * sizeof(D3D12_INPUT_ELEMENT_DESC)); 228 | break; 229 | 230 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_VIEW_INSTANCING: 231 | obj->ViewInstancing.pViewInstanceLocations = memdup( 232 | obj->ViewInstancing.pViewInstanceLocations, 233 | obj->ViewInstancing.ViewInstanceCount * sizeof(D3D12_VIEW_INSTANCE_LOCATION)); 234 | break; 235 | } 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /source/D3DPipelineStateStream.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CComPtr.h" 4 | 5 | namespace D3DPipelineStateStream 6 | { 7 | // Thanks to RenderDoc source code for providing some insight. What a mess this was... 8 | // 9 | // NOTE: D3DX12ParsePipelineStream is close to what I want. However, what I don't want 10 | // is to have to implement five billion interface callbacks. 11 | class Iterator 12 | { 13 | private: 14 | struct D3D12_PSO_SUBOBJECT 15 | { 16 | D3D12_PIPELINE_STATE_SUBOBJECT_TYPE Type; 17 | }; 18 | 19 | struct D3D12_PTR_PSO_SUBOBJECT 20 | { 21 | D3D12_PIPELINE_STATE_SUBOBJECT_TYPE Type; 22 | UINT Padding; 23 | 24 | union 25 | { 26 | ID3D12RootSignature *RootSignature; 27 | D3D12_SHADER_BYTECODE Shader; 28 | D3D12_STREAM_OUTPUT_DESC StreamOutput; 29 | D3D12_INPUT_LAYOUT_DESC InputLayout; 30 | D3D12_CACHED_PIPELINE_STATE CachedPSO; 31 | D3D12_VIEW_INSTANCING_DESC ViewInstancing; 32 | }; 33 | }; 34 | 35 | uint8_t *m_Start = nullptr; 36 | uint8_t *m_End = nullptr; 37 | 38 | public: 39 | Iterator(const D3D12_PIPELINE_STATE_STREAM_DESC *Description); 40 | Iterator(const Iterator& Other) = delete; 41 | Iterator& operator=(const Iterator& Other) = delete; 42 | 43 | void Advance(); 44 | 45 | bool AtEnd() const; 46 | D3D12_PTR_PSO_SUBOBJECT *GetObj() const; 47 | 48 | size_t GetSizeForType(D3D12_PIPELINE_STATE_SUBOBJECT_TYPE Type) const; 49 | size_t GetAlignmentForType(D3D12_PIPELINE_STATE_SUBOBJECT_TYPE Type) const; 50 | 51 | private: 52 | template 53 | static T AlignUp(T X, T A) 54 | { 55 | return (X + (A - 1)) & (~(A - 1)); 56 | } 57 | }; 58 | 59 | class Copy 60 | { 61 | private: 62 | std::vector> m_TempBuffers; 63 | std::vector> m_RefCountedObjects; 64 | D3D12_PIPELINE_STATE_STREAM_DESC m_CopiedDesc = {}; 65 | 66 | public: 67 | Copy(const D3D12_PIPELINE_STATE_STREAM_DESC *Description); 68 | Copy(const Copy& Other) = delete; 69 | Copy(Copy&& Other) noexcept; 70 | 71 | void TrackAllocation(std::unique_ptr&& Allocation) 72 | { 73 | m_TempBuffers.emplace_back(std::forward>(Allocation)); 74 | } 75 | 76 | template 77 | void TrackObject(CComPtr&& Object) 78 | { 79 | m_RefCountedObjects.emplace_back(std::forward>(Object)); 80 | } 81 | 82 | const D3D12_PIPELINE_STATE_STREAM_DESC *GetDesc() const 83 | { 84 | return &m_CopiedDesc; 85 | } 86 | 87 | private: 88 | void CreateCopy(const D3D12_PIPELINE_STATE_STREAM_DESC *InputDesc); 89 | 90 | template 91 | T *memdup(const T *Data, size_t Size) 92 | { 93 | if (!Data) 94 | return nullptr; 95 | 96 | auto& ptr = m_TempBuffers.emplace_back(std::make_unique(Size)); 97 | return reinterpret_cast(memcpy(ptr.get(), Data, Size)); 98 | } 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /source/D3DShaderReplacement.cpp: -------------------------------------------------------------------------------- 1 | #include "CComPtr.h" 2 | #include "D3DPipelineStateStream.h" 3 | #include "D3DShaderReplacement.h" 4 | #include "DebuggingUtil.h" 5 | #include "Plugin.h" 6 | 7 | namespace D3DShaderReplacement 8 | { 9 | const std::filesystem::path& GetShaderBinDirectory() 10 | { 11 | const static auto path = []() 12 | { 13 | auto temp = Plugin::ShaderDumpBinPath; 14 | 15 | if (temp.empty()) 16 | temp = std::filesystem::current_path() / "Data" / "shadersfx"; 17 | 18 | spdlog::info("Using custom shader root directory: {}", temp.string()); 19 | return temp; 20 | }(); 21 | 22 | return path; 23 | } 24 | 25 | const char *GetShaderTypePrefix(D3D12_PIPELINE_STATE_SUBOBJECT_TYPE Type) 26 | { 27 | switch (Type) 28 | { 29 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_VS: 30 | return "vs"; 31 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_PS: 32 | return "ps"; 33 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_HS: 34 | return "hs"; 35 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DS: 36 | return "ds"; 37 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_GS: 38 | return "gs"; 39 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CS: 40 | return "cs"; 41 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_AS: 42 | return "as"; 43 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_MS: 44 | return "ms"; 45 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_ROOT_SIGNATURE: 46 | return "rsg"; 47 | } 48 | 49 | return "unknown"; 50 | } 51 | 52 | bool ExtractOrReplaceShader( 53 | D3DPipelineStateStream::Copy& StreamCopy, 54 | D3D12_PIPELINE_STATE_SUBOBJECT_TYPE Type, 55 | D3D12_SHADER_BYTECODE *Bytecode, 56 | const char *TechniqueName, 57 | uint64_t TechniqueId) 58 | { 59 | // Techniques have to be trimmed as they're too long to be used in file names 60 | const auto prefix = GetShaderTypePrefix(Type); 61 | 62 | char techniqueShortName[512] = {}; 63 | strncpy_s(techniqueShortName, TechniqueName, _TRUNCATE); 64 | 65 | if (auto s = strchr(techniqueShortName, '-')) 66 | *s = '\0'; 67 | 68 | char shaderBinFileName[512]; 69 | sprintf_s(shaderBinFileName, "%s_%llX_%s.bin", techniqueShortName, TechniqueId, prefix); 70 | 71 | const auto shaderBinFullPath = GetShaderBinDirectory() / techniqueShortName / shaderBinFileName; 72 | 73 | if (!Plugin::ShaderDumpBinPath.empty()) 74 | { 75 | // Extract it 76 | if (Bytecode->pShaderBytecode && Bytecode->BytecodeLength != 0) 77 | { 78 | // Calculate the shader data hash, dump it, then map it to a technique name in a dedicated CSV file. It's 79 | // far from efficient but it's usually a one-time operation. 80 | std::filesystem::create_directories(shaderBinFullPath.parent_path()); 81 | 82 | const auto hash = DebuggingUtil::FNV1A32(Bytecode->pShaderBytecode, Bytecode->BytecodeLength); 83 | spdlog::info("Dumping shader with hash {} to {}", hash, shaderBinFullPath.string()); 84 | 85 | static std::mutex fileDumpMutex; 86 | fileDumpMutex.lock(); 87 | { 88 | // Dump binary 89 | if (std::ofstream f(shaderBinFullPath, std::ios::binary); f.good()) 90 | f.write(reinterpret_cast(Bytecode->pShaderBytecode), Bytecode->BytecodeLength); 91 | 92 | // Append to CSV 93 | const static auto csvPath = Plugin::ShaderDumpBinPath / "ShaderTechniqueMap.csv"; 94 | 95 | if (std::ofstream f(csvPath, std::ios::app); f.good()) 96 | { 97 | char csvLine[2048]; 98 | auto length = sprintf_s( 99 | csvLine, 100 | "%s,%s,%u,%llX,\"%s\"\n", 101 | techniqueShortName, 102 | prefix, 103 | hash, 104 | TechniqueId, 105 | TechniqueName); 106 | 107 | f.write(csvLine, length); 108 | } 109 | } 110 | fileDumpMutex.unlock(); 111 | } 112 | } 113 | else 114 | { 115 | // Replace it 116 | if (std::ifstream f(shaderBinFullPath, std::ios::binary | std::ios::ate); f.good()) 117 | { 118 | static bool once = [&]() 119 | { 120 | spdlog::info("Trying to replace at least one shader: {}", shaderBinFullPath.string()); 121 | return true; 122 | }(); 123 | 124 | auto fileSize = static_cast(f.tellg()); 125 | auto fileData = std::make_unique(fileSize); 126 | 127 | f.seekg(0, std::ios::beg); 128 | f.read(reinterpret_cast(fileData.get()), fileSize); 129 | 130 | // Only replace if the on-disk data is different 131 | if (fileSize != Bytecode->BytecodeLength || memcmp(fileData.get(), Bytecode->pShaderBytecode, fileSize) != 0) 132 | { 133 | Bytecode->BytecodeLength = fileSize; 134 | Bytecode->pShaderBytecode = fileData.get(); 135 | StreamCopy.TrackAllocation(std::move(fileData)); 136 | 137 | spdlog::trace("Used file replacement: {}", shaderBinFullPath.string()); 138 | return true; 139 | } 140 | } 141 | } 142 | 143 | return false; 144 | } 145 | 146 | bool PatchPipelineStateStream( 147 | D3DPipelineStateStream::Copy& StreamCopy, 148 | ID3D12Device2 *Device, 149 | const std::span *RootSignatureData, 150 | const char *TechniqueName, 151 | uint64_t TechniqueId) 152 | { 153 | bool modified = false; 154 | 155 | for (D3DPipelineStateStream::Iterator iter(StreamCopy.GetDesc()); !iter.AtEnd(); iter.Advance()) 156 | { 157 | switch (auto obj = iter.GetObj(); obj->Type) 158 | { 159 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_VS: 160 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_PS: 161 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_HS: 162 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DS: 163 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_GS: 164 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CS: 165 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_AS: 166 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_MS: 167 | if (ExtractOrReplaceShader(StreamCopy, obj->Type, &obj->Shader, TechniqueName, TechniqueId)) 168 | modified = true; 169 | break; 170 | 171 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_ROOT_SIGNATURE: 172 | if (RootSignatureData) 173 | { 174 | D3D12_SHADER_BYTECODE bytecode { 175 | .pShaderBytecode = RootSignatureData->data(), 176 | .BytecodeLength = RootSignatureData->size(), 177 | }; 178 | 179 | if (ExtractOrReplaceShader(StreamCopy, obj->Type, &bytecode, TechniqueName, TechniqueId)) 180 | { 181 | CComPtr newSignature; 182 | const auto hr = Device->CreateRootSignature( 183 | 0, 184 | bytecode.pShaderBytecode, 185 | bytecode.BytecodeLength, 186 | IID_PPV_ARGS(&newSignature)); 187 | 188 | if (FAILED(hr)) 189 | { 190 | // Somebody passed in malformed data 191 | spdlog::error( 192 | "Failed to create root signature: {:X}. Shader technique: {:X}.", 193 | static_cast(hr), 194 | TechniqueId); 195 | } 196 | else 197 | { 198 | obj->RootSignature = newSignature.Get(); 199 | StreamCopy.TrackObject(std::move(newSignature)); 200 | 201 | modified = true; 202 | } 203 | } 204 | } 205 | break; 206 | } 207 | } 208 | 209 | // Loop around once again to disable PSO cache entries 210 | if (modified) 211 | { 212 | for (D3DPipelineStateStream::Iterator iter(StreamCopy.GetDesc()); !iter.AtEnd(); iter.Advance()) 213 | { 214 | switch (auto obj = iter.GetObj(); obj->Type) 215 | { 216 | case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CACHED_PSO: 217 | obj->CachedPSO.pCachedBlob = nullptr; 218 | obj->CachedPSO.CachedBlobSizeInBytes = 0; 219 | break; 220 | } 221 | } 222 | } 223 | 224 | return modified; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /source/D3DShaderReplacement.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "D3DPipelineStateStream.h" 4 | 5 | namespace D3DShaderReplacement 6 | { 7 | const std::filesystem::path& GetShaderBinDirectory(); 8 | 9 | bool PatchPipelineStateStream( 10 | D3DPipelineStateStream::Copy& StreamCopy, 11 | ID3D12Device2 *Device, 12 | const std::span *RootSignatureData, 13 | const char *TechniqueName, 14 | uint64_t TechniqueId); 15 | } 16 | -------------------------------------------------------------------------------- /source/DebuggingUtil.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "DebuggingUtil.h" 3 | #include "Plugin.h" 4 | 5 | namespace DebuggingUtil 6 | { 7 | uint32_t FNV1A32(const void *Input, size_t Length) 8 | { 9 | constexpr uint32_t FNV1_PRIME_32 = 0x01000193; 10 | constexpr uint32_t FNV1_BASE_32 = 2166136261U; 11 | 12 | auto data = reinterpret_cast(Input); 13 | auto end = data + Length; 14 | 15 | auto hash = FNV1_BASE_32; 16 | 17 | for (; data != end; data++) 18 | { 19 | hash ^= *data; 20 | hash *= FNV1_PRIME_32; 21 | } 22 | 23 | return hash; 24 | } 25 | 26 | void SetObjectDebugName(ID3D12Object *Object, const char *Name) 27 | { 28 | if (!Plugin::InsertDebugMarkers) 29 | return; 30 | 31 | if (!Object || !Name || strlen(Name) <= 0) 32 | return; 33 | 34 | wchar_t tempOut[1024]; 35 | if (mbstowcs_s(nullptr, tempOut, Name, _TRUNCATE) == 0) 36 | Object->SetName(tempOut); 37 | } 38 | 39 | void (*OriginalCreateTexture)(void *, void *, void *, void *, const char *, void *, void *); 40 | void HookedCreateTexture(void *a1, void *a2, void *a3, void *a4, const char *DebugName, void *a6, void *a7) 41 | { 42 | OriginalCreateTexture(a1, a2, a3, a4, DebugName, a6, a7); 43 | 44 | auto textureResource = *reinterpret_cast(a4); 45 | auto dx12TextureResource = *reinterpret_cast(reinterpret_cast(textureResource) + 0x78); 46 | 47 | SetObjectDebugName(dx12TextureResource, DebugName); 48 | } 49 | 50 | void (*OriginalCmdBeginProfilingMarker)(void *, void *, const char *); 51 | void HookedCmdBeginProfilingMarker(void *a1, void *a2, const char *MarkerText) 52 | { 53 | auto commandList = *reinterpret_cast(reinterpret_cast(a1) + 0x10); 54 | commandList->BeginEvent(1, MarkerText, static_cast(strlen(MarkerText) + 1)); 55 | 56 | OriginalCmdBeginProfilingMarker(a1, a2, MarkerText); 57 | } 58 | 59 | void (*OriginalCmdEndProfilingMarker)(void *); 60 | void HookedCmdEndProfilingMarker(void *a1) 61 | { 62 | OriginalCmdEndProfilingMarker(a1); 63 | 64 | auto commandList = *reinterpret_cast(reinterpret_cast(a1) + 0x10); 65 | commandList->EndEvent(); 66 | } 67 | 68 | DECLARE_HOOK_TRANSACTION(DebuggingUtil) 69 | { 70 | if (!Plugin::InsertDebugMarkers) 71 | return; 72 | 73 | Hooks::WriteJump( 74 | Offsets::Signature( 75 | "4C 89 4C 24 20 4C 89 44 24 18 48 89 54 24 10 48 89 4C 24 08 53 56 57 41 54 41 55 41 56 41 57 48 81 EC D0 02 00 00"), 76 | &HookedCreateTexture, 77 | &OriginalCreateTexture); 78 | 79 | Hooks::WriteJump( 80 | Offsets::Signature("48 89 5C 24 08 48 89 74 24 10 44 88 4C 24 20 57 48 83 EC 20"), 81 | &HookedCmdBeginProfilingMarker, 82 | &OriginalCmdBeginProfilingMarker); 83 | 84 | Hooks::WriteJump( 85 | Offsets::Signature("48 89 5C 24 08 88 54 24 10 57 48 83 EC 20 48 8B F9 E8 ? ? ? ? 8B D8 89 44 24 38 B9 1A 00 00 00"), 86 | &HookedCmdEndProfilingMarker, 87 | &OriginalCmdEndProfilingMarker); 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /source/DebuggingUtil.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace DebuggingUtil 4 | { 5 | uint32_t FNV1A32(const void *Input, size_t Length); 6 | void SetObjectDebugName(ID3D12Object *Object, const char *Name); 7 | } 8 | -------------------------------------------------------------------------------- /source/Hooking/Hooks.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "Memory.h" 4 | 5 | namespace Hooks 6 | { 7 | struct CallbackEntry 8 | { 9 | const char *Name = nullptr; 10 | std::variant Callback; 11 | }; 12 | 13 | struct HookTransactionEntry 14 | { 15 | void *DetourStubFunction = nullptr; 16 | void *TargetFunction = nullptr; 17 | bool RequiresCallFixup = false; 18 | }; 19 | 20 | struct IATEnumContext 21 | { 22 | const char *ModuleName = nullptr; 23 | std::variant ImportName; 24 | const void *CallbackFunction = nullptr; 25 | void **OriginalFunction = nullptr; 26 | bool ModuleFound = false; 27 | bool Succeeded = false; 28 | }; 29 | 30 | std::vector& GetInitializationEntries() 31 | { 32 | // Has to be a function-local static to avoid initialization order issues 33 | static std::vector entries; 34 | return entries; 35 | } 36 | 37 | std::vector>& GetTransactionEntries() 38 | { 39 | static std::vector> entries; 40 | return entries; 41 | } 42 | 43 | bool Initialize() 44 | { 45 | spdlog::info("{}():", __FUNCTION__); 46 | 47 | auto& initEntries = GetInitializationEntries(); 48 | auto& transactionEntries = GetTransactionEntries(); 49 | 50 | DetourSetIgnoreTooSmall(true); 51 | 52 | if (DetourTransactionBegin() != NO_ERROR) 53 | return false; 54 | 55 | DetourUpdateThread(GetCurrentThread()); 56 | 57 | for (const auto& entry : initEntries) 58 | { 59 | spdlog::info("Setting up hooks for {}...", entry.Name); 60 | 61 | auto visitor = [](auto&& F) 62 | { 63 | if constexpr (std::is_same_v) 64 | return F(), true; 65 | else if constexpr (std::is_same_v) 66 | return F(); 67 | else 68 | static_assert(!sizeof(decltype(F)), "Invalid callback type"); 69 | }; 70 | 71 | // Bail on the whole process when a callback returns false 72 | if (!std::visit(visitor, entry.Callback)) 73 | { 74 | DetourTransactionAbort(); 75 | 76 | spdlog::error("Transaction aborted."); 77 | return false; 78 | } 79 | } 80 | 81 | if (DetourTransactionCommit() != NO_ERROR) 82 | return false; 83 | 84 | // Apply call fixups 85 | for (const auto& entry : transactionEntries) 86 | { 87 | if (entry->RequiresCallFixup) 88 | Memory::Patch(reinterpret_cast(entry->TargetFunction), { 0xE8 }); 89 | } 90 | 91 | initEntries.clear(); 92 | transactionEntries.clear(); 93 | 94 | spdlog::info("Done!"); 95 | return true; 96 | } 97 | 98 | bool WriteJump(std::uintptr_t TargetAddress, const void *CallbackFunction, void **OriginalFunction) 99 | { 100 | if (!TargetAddress) 101 | return false; 102 | 103 | auto ptr = std::make_unique(); 104 | ptr->TargetFunction = reinterpret_cast(TargetAddress); 105 | 106 | // We need a temporary pointer until the transaction is committed 107 | if (!OriginalFunction) 108 | OriginalFunction = &ptr->DetourStubFunction; 109 | 110 | // Detours needs the real target function stored in said pointer 111 | *OriginalFunction = ptr->TargetFunction; 112 | 113 | if (DetourAttach(OriginalFunction, const_cast(CallbackFunction)) != NO_ERROR) 114 | return false; 115 | 116 | GetTransactionEntries().emplace_back(std::move(ptr)); 117 | return true; 118 | } 119 | 120 | bool WriteCall(std::uintptr_t TargetAddress, const void *CallbackFunction, void **OriginalFunction) 121 | { 122 | // Identical to WriteJump, but with a call 123 | if (!TargetAddress) 124 | return false; 125 | 126 | auto ptr = std::make_unique(); 127 | ptr->TargetFunction = reinterpret_cast(TargetAddress); 128 | ptr->RequiresCallFixup = true; 129 | 130 | if (!OriginalFunction) 131 | OriginalFunction = &ptr->DetourStubFunction; 132 | 133 | *OriginalFunction = ptr->TargetFunction; 134 | 135 | if (DetourAttach(OriginalFunction, const_cast(CallbackFunction)) != NO_ERROR) 136 | return false; 137 | 138 | GetTransactionEntries().emplace_back(std::move(ptr)); 139 | return true; 140 | } 141 | 142 | bool WriteVirtualFunction(std::uintptr_t TableAddress, uint32_t Index, const void *CallbackFunction, void **OriginalFunction) 143 | { 144 | if (!TableAddress) 145 | return false; 146 | 147 | const auto calculatedAddress = TableAddress + (sizeof(void *) * Index); 148 | 149 | if (OriginalFunction) 150 | *OriginalFunction = *reinterpret_cast(calculatedAddress); 151 | 152 | Memory::Patch(calculatedAddress, reinterpret_cast(&CallbackFunction), sizeof(void *)); 153 | return true; 154 | } 155 | 156 | bool RedirectImport( 157 | void *ModuleHandle, 158 | const char *ImportModuleName, 159 | std::variant ImportName, 160 | const void *CallbackFunction, 161 | void **OriginalFunction) 162 | { 163 | auto moduleCallback = [](PVOID Context, HMODULE, LPCSTR Name) -> BOOL 164 | { 165 | auto c = static_cast(Context); 166 | 167 | c->ModuleFound = Name && _stricmp(Name, c->ModuleName) == 0; 168 | return !c->Succeeded; 169 | }; 170 | 171 | auto importCallback = [](PVOID Context, ULONG Ordinal, PCSTR Name, PVOID *Func) -> BOOL 172 | { 173 | auto c = static_cast(Context); 174 | 175 | if (!c->ModuleFound) 176 | return false; 177 | 178 | // If the import name matches... 179 | const bool matches = [&]() 180 | { 181 | if (!Func) 182 | return false; 183 | 184 | if (std::holds_alternative(c->ImportName)) 185 | return _stricmp(Name, std::get(c->ImportName)) == 0; 186 | 187 | return std::cmp_equal(Ordinal, std::get(c->ImportName)); 188 | }(); 189 | 190 | if (matches) 191 | { 192 | // ...swap out the IAT pointer 193 | if (c->OriginalFunction) 194 | *c->OriginalFunction = *Func; 195 | 196 | Memory::Patch( 197 | reinterpret_cast(Func), 198 | reinterpret_cast(&c->CallbackFunction), 199 | sizeof(void *)); 200 | 201 | c->Succeeded = true; 202 | return false; 203 | } 204 | 205 | return true; 206 | }; 207 | 208 | IATEnumContext context { 209 | .ModuleName = ImportModuleName, 210 | .ImportName = ImportName, 211 | .CallbackFunction = CallbackFunction, 212 | .OriginalFunction = OriginalFunction, 213 | }; 214 | 215 | if (!ModuleHandle) 216 | ModuleHandle = GetModuleHandleA(nullptr); 217 | 218 | DetourEnumerateImportsEx(reinterpret_cast(ModuleHandle), &context, moduleCallback, importCallback); 219 | return context.Succeeded; 220 | } 221 | } 222 | 223 | namespace Hooks::Impl 224 | { 225 | TxnBase::TxnBase(void (*Initializer)(), const char *Name) 226 | { 227 | GetInitializationEntries().emplace_back(CallbackEntry { 228 | .Name = Name, 229 | .Callback = Initializer, 230 | }); 231 | } 232 | 233 | TxnBase::TxnBase(bool (*Initializer)(), const char *Name) 234 | { 235 | GetInitializationEntries().emplace_back(CallbackEntry { 236 | .Name = Name, 237 | .Callback = Initializer, 238 | }); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /source/Hooking/Hooks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Hooks 4 | { 5 | bool Initialize(); 6 | bool WriteJump(std::uintptr_t TargetAddress, const void *CallbackFunction, void **OriginalFunction = nullptr); 7 | bool WriteCall(std::uintptr_t TargetAddress, const void *CallbackFunction, void **OriginalFunction = nullptr); 8 | bool WriteVirtualFunction(std::uintptr_t TableAddress, uint32_t Index, const void *CallbackFunction, void **OriginalFunction = nullptr); 9 | bool RedirectImport(void *ModuleHandle, 10 | const char *ImportModuleName, 11 | std::variant ImportName, 12 | const void *CallbackFunction, 13 | void **OriginalFunction); 14 | 15 | template 16 | bool WriteJump(std::uintptr_t TargetAddress, U (*CallbackFunction)(Args...), U (**OriginalFunction)(Args...) = nullptr) 17 | { 18 | return WriteJump(TargetAddress, *reinterpret_cast(&CallbackFunction), reinterpret_cast(OriginalFunction)); 19 | } 20 | 21 | template 22 | bool WriteJump(std::uintptr_t TargetAddress, U (T::*CallbackFunction)(Args...), U (T::**OriginalFunction)(Args...) = nullptr) 23 | { 24 | return WriteJump(TargetAddress, *reinterpret_cast(&CallbackFunction), reinterpret_cast(OriginalFunction)); 25 | } 26 | 27 | template 28 | bool WriteCall(std::uintptr_t TargetAddress, U (*CallbackFunction)(Args...), U (**OriginalFunction)(Args...) = nullptr) 29 | { 30 | return WriteCall(TargetAddress, *reinterpret_cast(&CallbackFunction), reinterpret_cast(OriginalFunction)); 31 | } 32 | 33 | template 34 | bool WriteCall(std::uintptr_t TargetAddress, U (T::*CallbackFunction)(Args...), U (T::**OriginalFunction)(Args...) = nullptr) 35 | { 36 | return WriteCall(TargetAddress, *reinterpret_cast(&CallbackFunction), reinterpret_cast(OriginalFunction)); 37 | } 38 | 39 | template 40 | bool WriteVirtualFunction( 41 | std::uintptr_t TableAddress, 42 | uint32_t Index, 43 | U (*CallbackFunction)(Args...), 44 | U (**OriginalFunction)(Args...) = nullptr) 45 | { 46 | return WriteVirtualFunction( 47 | TableAddress, 48 | Index, 49 | *reinterpret_cast(&CallbackFunction), 50 | reinterpret_cast(OriginalFunction)); 51 | } 52 | 53 | template 54 | bool WriteVirtualFunction( 55 | std::uintptr_t TableAddress, 56 | uint32_t Index, 57 | U (T::*CallbackFunction)(Args...), 58 | U (T::**OriginalFunction)(Args...) = nullptr) 59 | { 60 | return WriteVirtualFunction( 61 | TableAddress, 62 | Index, 63 | *reinterpret_cast(&CallbackFunction), 64 | reinterpret_cast(OriginalFunction)); 65 | } 66 | 67 | namespace Impl 68 | { 69 | class TxnBase 70 | { 71 | protected: 72 | TxnBase(void (*Initializer)(), const char *Name); 73 | TxnBase(bool (*Initializer)(), const char *Name); 74 | }; 75 | } 76 | }; 77 | 78 | #define DECLARE_HOOK_TRANSACTION(Name) \ 79 | class ___HookTxnInit##Name : Hooks::Impl::TxnBase \ 80 | { \ 81 | public: \ 82 | ___HookTxnInit##Name(auto Initializer) : TxnBase(Initializer, #Name) {} \ 83 | } const static ___HookTxnCallbackDoNotUse = +[] 84 | -------------------------------------------------------------------------------- /source/Hooking/Memory.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Memory.h" 3 | 4 | namespace Memory 5 | { 6 | void Patch(std::uintptr_t Address, const std::uint8_t *Data, std::size_t Size) 7 | { 8 | DWORD d = 0; 9 | VirtualProtect(reinterpret_cast(Address), Size, PAGE_EXECUTE_READWRITE, &d); 10 | 11 | memcpy(reinterpret_cast(Address), Data, Size); 12 | 13 | VirtualProtect(reinterpret_cast(Address), Size, d, &d); 14 | FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(Address), Size); 15 | } 16 | 17 | void Patch(std::uintptr_t Address, std::initializer_list Data) 18 | { 19 | Patch(Address, Data.begin(), Data.size()); 20 | } 21 | 22 | void Fill(std::uintptr_t Address, std::uint8_t Value, std::size_t Size) 23 | { 24 | DWORD d = 0; 25 | VirtualProtect(reinterpret_cast(Address), Size, PAGE_EXECUTE_READWRITE, &d); 26 | 27 | memset(reinterpret_cast(Address), Value, Size); 28 | 29 | VirtualProtect(reinterpret_cast(Address), Size, d, &d); 30 | FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(Address), Size); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/Hooking/Memory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Memory 4 | { 5 | void Patch(std::uintptr_t Address, const std::uint8_t *Data, std::size_t Size); 6 | void Patch(std::uintptr_t Address, std::initializer_list Data); 7 | void Fill(std::uintptr_t Address, std::uint8_t Value, std::size_t Size); 8 | } 9 | -------------------------------------------------------------------------------- /source/Hooking/Offsets.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace Offsets::Impl 6 | { 7 | std::vector& GetInitializationEntries() 8 | { 9 | // Has to be a function-local static to avoid initialization order issues 10 | static std::vector entries; 11 | return entries; 12 | } 13 | 14 | SignatureStorageWrapper::SignatureStorageWrapper(PatternSpan Signature) : m_Signature(Signature) 15 | { 16 | GetInitializationEntries().emplace_back(this); 17 | } 18 | 19 | ByteSpan::iterator SignatureStorageWrapper::ScanRegion(ByteSpan Region) const 20 | { 21 | if (m_Signature.empty() || m_Signature.size() > Region.size()) 22 | return Region.end(); 23 | 24 | const auto nonWildcardSubrange = FindLongestNonWildcardRun(); 25 | const auto subrangeAdjustment = nonWildcardSubrange.data() - m_Signature.data(); 26 | 27 | if (nonWildcardSubrange.empty()) // if (all wildcards) 28 | return Region.begin(); 29 | 30 | const auto scanStart = Region.begin() + subrangeAdjustment; // Seek forward to prevent underflow 31 | const auto scanEnd = (Region.end() - m_Signature.size()) + subrangeAdjustment; // Seek backward to prevent overflow 32 | auto pos = scanStart; 33 | 34 | #if 0 35 | // Use a Boyer-Moore-Horspool search for each signature. 36 | // 37 | // While BMH itself doesn't support wildcards, we can still use the largest contiguous signature 38 | // byte range that excludes wildcards, and then do a linear scan to match the rest. 39 | const auto lastByteIndex = static_cast(nonWildcardSubrange.size() - 1); 40 | const auto diff = std::max(lastByteIndex, 1); 41 | 42 | // Prime the skip lookup table 43 | std::array skipLUT; 44 | skipLUT.fill(static_cast(diff)); 45 | 46 | for (ptrdiff_t i = lastByteIndex - diff; i < lastByteIndex; i++) 47 | skipLUT[nonWildcardSubrange[i].Value] = static_cast(lastByteIndex - i); 48 | 49 | for (; pos <= scanEnd; pos += skipLUT[pos[lastByteIndex]]) 50 | { 51 | // Match the BMH-only subrange first, then run the full check if it succeeds 52 | for (ptrdiff_t i = lastByteIndex; i >= 0; i--) 53 | { 54 | if (nonWildcardSubrange[i].Value != pos[i]) 55 | goto nextIter; 56 | } 57 | 58 | if (MatchPattern(pos - subrangeAdjustment)) 59 | return pos - subrangeAdjustment; 60 | 61 | nextIter:; 62 | } 63 | #else 64 | // Linear vectorized search. Turns out CPUs are 2-3x faster at this than BMH. 65 | // 66 | // Unrolled version of http://0x80.pl/articles/simd-strfind.html#generic-sse-avx2 since AVX2 support 67 | // can't be assumed. iterCount is used to avoid three extra branches per loop instead of comparing pos. 68 | const __m128i firstBlockMask = _mm_set1_epi8(nonWildcardSubrange.front().Value); 69 | const __m128i lastBlockMask = _mm_set1_epi8(nonWildcardSubrange.back().Value); 70 | 71 | auto loadMask = [&](const size_t Offset) 72 | { 73 | const __m128i firstBlock = _mm_loadu_si128(reinterpret_cast(&pos[Offset])); 74 | const __m128i lastBlock = _mm_loadu_si128(reinterpret_cast(&pos[Offset + nonWildcardSubrange.size() - 1])); 75 | const __m128i mask = _mm_and_si128(_mm_cmpeq_epi8(firstBlockMask, firstBlock), _mm_cmpeq_epi8(lastBlockMask, lastBlock)); 76 | 77 | return static_cast(_mm_movemask_epi8(mask)) << Offset; 78 | }; 79 | 80 | const ptrdiff_t perIterSize = sizeof(__m128i) * 2; 81 | ptrdiff_t iterCount = (scanEnd - scanStart) / perIterSize; 82 | 83 | for (; iterCount > 0; iterCount--, pos += perIterSize) 84 | { 85 | auto mask = loadMask(0) | loadMask(sizeof(__m128i)); 86 | 87 | // The indices of 1-bits in mask map to indices of byte matches in pos. Each iteration finds the 88 | // lowest (LSB) index of a 1-bit in mask, clears it, and tests the full signature at that index. 89 | while (mask != 0) 90 | { 91 | auto bitIndex = std::countr_zero(mask); 92 | mask &= (mask - 1); 93 | 94 | if (MatchPattern(pos + bitIndex - subrangeAdjustment)) 95 | return pos + bitIndex - subrangeAdjustment; 96 | } 97 | } 98 | 99 | for (; pos <= scanEnd; pos++) 100 | { 101 | if (MatchPattern(pos - subrangeAdjustment)) 102 | return pos - subrangeAdjustment; 103 | } 104 | #endif 105 | 106 | return Region.end(); 107 | } 108 | 109 | bool SignatureStorageWrapper::MatchPattern(ByteSpan::iterator Iterator) const 110 | { 111 | return std::equal(m_Signature.begin(), m_Signature.end(), Iterator, [](const auto& A, const auto& B) 112 | { 113 | return A.Wildcard || A.Value == B; 114 | }); 115 | } 116 | 117 | PatternSpan SignatureStorageWrapper::FindLongestNonWildcardRun() const 118 | { 119 | PatternSpan largestRange = {}; 120 | 121 | for (size_t i = 0; i < m_Signature.size(); i++) 122 | { 123 | if (m_Signature[i].Wildcard) 124 | continue; 125 | 126 | auto getEnd = [this](size_t j) 127 | { 128 | for (j += 1; (j < m_Signature.size()) && !m_Signature[j].Wildcard; j++) 129 | /**/; 130 | 131 | return j; 132 | }; 133 | 134 | if (auto len = getEnd(i) - i; len > largestRange.size()) 135 | largestRange = m_Signature.subspan(i, len); 136 | } 137 | 138 | return largestRange; 139 | } 140 | } 141 | 142 | namespace Offsets 143 | { 144 | using namespace Impl; 145 | 146 | bool Initialize() 147 | { 148 | spdlog::info("{}():", __FUNCTION__); 149 | 150 | auto dosHeader = reinterpret_cast(GetModuleHandleW(nullptr)); 151 | auto ntHeaders = reinterpret_cast(reinterpret_cast(dosHeader) + dosHeader->e_lfanew); 152 | auto region = std::span { reinterpret_cast(dosHeader), ntHeaders->OptionalHeader.SizeOfImage }; 153 | 154 | auto& entries = GetInitializationEntries(); 155 | 156 | // Run all scans in parallel 157 | std::for_each(std::execution::par, entries.begin(), entries.end(), [&](auto& P) 158 | { 159 | auto itr = P->ScanRegion(region); 160 | 161 | if (itr != region.end()) 162 | { 163 | P->m_Address = reinterpret_cast(std::to_address(itr)); 164 | P->m_IsResolved = true; 165 | } 166 | }); 167 | 168 | const auto failedSignatureCount = std::count_if(entries.begin(), entries.end(), [](const auto& P) 169 | { 170 | return !P->IsValid(); 171 | }); 172 | 173 | if (failedSignatureCount > 0) 174 | { 175 | spdlog::info("Failed to resolve {} out of {} signatures.", failedSignatureCount, entries.size()); 176 | return false; 177 | } 178 | 179 | entries.clear(); 180 | spdlog::info("Done!"); 181 | return true; 182 | } 183 | 184 | Offset Relative(std::uintptr_t RelAddress) 185 | { 186 | return Offset(reinterpret_cast(GetModuleHandleW(nullptr)) + RelAddress); 187 | } 188 | 189 | Offset Absolute(std::uintptr_t AbsAddress) 190 | { 191 | return Offset(AbsAddress); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /source/Hooking/Offsets.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Offsets 4 | { 5 | namespace Impl 6 | { 7 | struct PatternEntry 8 | { 9 | uint8_t Value = 0; 10 | bool Wildcard = false; 11 | }; 12 | 13 | using ByteSpan = std::span; 14 | using PatternSpan = std::span; 15 | 16 | template 17 | class PatternLiteral 18 | { 19 | static_assert(PatternLength >= 3, "Signature must be at least 1 byte long"); 20 | 21 | public: 22 | PatternEntry m_Signature[(PatternLength / 2) + 1]; 23 | size_t m_SignatureLength = 0; 24 | 25 | consteval PatternLiteral(const char (&Pattern)[PatternLength]) 26 | { 27 | for (size_t i = 0; i < PatternLength - 1;) 28 | { 29 | switch (Pattern[i]) 30 | { 31 | case ' ': 32 | i++; 33 | continue; 34 | 35 | case '?': 36 | if ((i + 2) < PatternLength && Pattern[i + 1] != ' ') 37 | throw "Invalid wildcard"; 38 | 39 | m_Signature[m_SignatureLength].Wildcard = true; 40 | break; 41 | 42 | default: 43 | m_Signature[m_SignatureLength].Value = AsciiHexToBytes(Pattern + i); 44 | break; 45 | } 46 | 47 | i += 2; 48 | m_SignatureLength++; 49 | } 50 | } 51 | 52 | consteval PatternSpan GetSignature() const 53 | { 54 | return { m_Signature, m_SignatureLength }; 55 | } 56 | 57 | private: 58 | template 59 | consteval static T AsciiHexToBytes(const char *Hex) 60 | { 61 | auto charToByte = [](char C) consteval -> T 62 | { 63 | if (C >= 'A' && C <= 'F') 64 | return C - 'A' + 10; 65 | else if (C >= 'a' && C <= 'f') 66 | return C - 'a' + 10; 67 | else if (C >= '0' && C <= '9') 68 | return C - '0'; 69 | 70 | throw "Invalid hexadecimal digit"; 71 | }; 72 | 73 | T value = {}; 74 | 75 | for (size_t i = 0; i < Digits; i++) 76 | value |= charToByte(Hex[i]) << (4 * (Digits - i - 1)); 77 | 78 | return value; 79 | } 80 | }; 81 | 82 | class SignatureStorageWrapper 83 | { 84 | public: 85 | const PatternSpan m_Signature; 86 | uintptr_t m_Address = 0; 87 | bool m_IsResolved = false; 88 | 89 | SignatureStorageWrapper(PatternSpan Signature); 90 | 91 | bool IsValid() const 92 | { 93 | return m_IsResolved; 94 | } 95 | 96 | uintptr_t Address() const 97 | { 98 | return m_Address; 99 | } 100 | 101 | ByteSpan::iterator ScanRegion(ByteSpan Region) const; 102 | 103 | private: 104 | bool MatchPattern(ByteSpan::iterator Iterator) const; 105 | PatternSpan FindLongestNonWildcardRun() const; 106 | }; 107 | 108 | class Offset 109 | { 110 | private: 111 | const uintptr_t m_Address; 112 | 113 | public: 114 | Offset(uintptr_t Address) : m_Address(Address) {} 115 | 116 | operator uintptr_t() const 117 | { 118 | return m_Address; 119 | } 120 | }; 121 | 122 | template 123 | class Signature 124 | { 125 | private: 126 | const static inline SignatureStorageWrapper m_Storage { Literal.GetSignature() }; 127 | 128 | public: 129 | static Offset GetOffset() 130 | { 131 | return Offset(m_Storage.Address()); 132 | } 133 | }; 134 | } 135 | 136 | bool Initialize(); 137 | Impl::Offset Relative(std::uintptr_t RelAddress); 138 | Impl::Offset Absolute(std::uintptr_t AbsAddress); 139 | #define Signature(X) Impl::Signature::GetOffset() 140 | } 141 | -------------------------------------------------------------------------------- /source/Plugin.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "Plugin.h" 5 | 6 | namespace Plugin 7 | { 8 | bool AllowLiveUpdates = false; 9 | bool InsertDebugMarkers = false; 10 | std::filesystem::path ShaderDumpBinPath; 11 | 12 | bool Initialize(bool UseASI) 13 | { 14 | InitializeSettings(); 15 | 16 | if (!InitializeLog(UseASI)) 17 | return false; 18 | 19 | if (!Offsets::Initialize()) 20 | return false; 21 | 22 | if (!Hooks::Initialize()) 23 | return false; 24 | 25 | return true; 26 | } 27 | 28 | bool InitializeLog(bool UseASI) 29 | { 30 | // Initialize logging in the documents folder 31 | wchar_t *documentsPath = nullptr; 32 | 33 | if (FAILED(SHGetKnownFolderPath(FOLDERID_Documents, 0, nullptr, &documentsPath))) 34 | return false; 35 | 36 | std::filesystem::path logPath(documentsPath); 37 | logPath.append(UseASI ? L"My Games\\Starfield\\Logs" : L"My Games\\Starfield\\SFSE\\Logs"); 38 | logPath.append(BUILD_PROJECT_NAME ".log"); 39 | 40 | auto logger = spdlog::basic_logger_mt("file_logger", logPath.string(), true); 41 | logger->set_level(spdlog::level::level_enum::trace); 42 | logger->set_pattern("[%H:%M:%S] [%l] %v"); // [HH:MM:SS] [Level] Message 43 | logger->flush_on(logger->level()); 44 | spdlog::set_default_logger(std::move(logger)); 45 | 46 | spdlog::info( 47 | "Starfield Shader Injector {} version {}.{} by Nukem. Mod URL: https://www.nexusmods.com/starfield/mods/5562", 48 | UseASI ? "ASI" : "SFSE", 49 | BUILD_VERSION_MAJOR, 50 | BUILD_VERSION_MINOR); 51 | 52 | CoTaskMemFree(documentsPath); 53 | return true; 54 | } 55 | 56 | bool InitializeSettings() 57 | { 58 | // Grab the full path of this dll and change the extension to .ini 59 | wchar_t dllPath[1024] = {}; 60 | if (GetModuleFileNameW(static_cast(GetThisModuleHandle()), dllPath, static_cast(std::size(dllPath))) == 0) 61 | return false; 62 | 63 | auto iniPath = std::filesystem::path(dllPath).parent_path(); 64 | iniPath.append(BUILD_PROJECT_NAME ".ini"); 65 | 66 | // Then parse the fake .ini as TOML 67 | try 68 | { 69 | auto toml = toml::parse_file(iniPath.string()); 70 | 71 | if (toml.get("Development")) 72 | { 73 | AllowLiveUpdates = toml["Development"]["AllowLiveUpdates"].value_or(false); 74 | InsertDebugMarkers = toml["Development"]["InsertDebugMarkers"].value_or(false); 75 | ShaderDumpBinPath = toml["Development"]["ShaderDumpBinPath"].value_or(L""); 76 | } 77 | 78 | if (!ShaderDumpBinPath.empty()) 79 | AllowLiveUpdates = false; 80 | } 81 | catch (const toml::parse_error&) 82 | { 83 | return false; 84 | } 85 | 86 | return true; 87 | } 88 | 89 | void *GetThisModuleHandle() 90 | { 91 | HMODULE dllHandle = nullptr; 92 | 93 | GetModuleHandleExW( 94 | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, 95 | reinterpret_cast(&Initialize), 96 | &dllHandle); 97 | 98 | return dllHandle; 99 | } 100 | } 101 | 102 | #if BUILD_FOR_SFSE 103 | #include 104 | #include 105 | 106 | extern "C" __declspec(dllexport) const SFSEPluginVersionData SFSEPlugin_Version { 107 | SFSEPluginVersionData::kVersion, 108 | 109 | (100 * BUILD_VERSION_MAJOR) + BUILD_VERSION_MINOR, // Plugin version 110 | BUILD_PROJECT_NAME, // Name 111 | "Nukem", // Author 112 | 113 | SFSEPluginVersionData::kAddressIndependence_Signatures, // Address independent as of 1.8.86 114 | SFSEPluginVersionData::kStructureIndependence_NoStructs, // Structure independent as 1.8.86 115 | // Compatible with 1.8.86 and beyond 116 | { 117 | RUNTIME_VERSION_1_8_86, 118 | 0, 119 | }, 120 | 121 | 0, // Works with any version of the script extender 122 | 0, 123 | 0, // Reserved 124 | }; 125 | 126 | extern "C" __declspec(dllexport) bool SFSEPlugin_Load(const SFSEInterface *Interface) 127 | { 128 | return true; 129 | } 130 | 131 | extern "C" __declspec(dllexport) bool SFSEPlugin_Preload(const SFSEInterface *Interface) 132 | { 133 | return Plugin::Initialize(false); 134 | } 135 | #endif // BUILD_FOR_SFSE 136 | 137 | #if BUILD_FOR_ASILOADER 138 | extern "C" __declspec(dllexport) void InitializeASI() 139 | { 140 | auto module = reinterpret_cast(GetModuleHandleA(nullptr)); 141 | auto ntHeaders = reinterpret_cast(module + reinterpret_cast(module)->e_lfanew); 142 | 143 | auto& directory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; 144 | auto descriptor = reinterpret_cast(module + directory.VirtualAddress); 145 | 146 | // Plugin dlls can be loaded into non-game processes when people use broken ASI loader setups. The only 147 | // version-agnostic and file-name-agnostic method to detect Starfield.exe is to check the export directory 148 | // name. 149 | if (directory.VirtualAddress == 0 || directory.Size == 0 || 150 | memcmp(reinterpret_cast(module + descriptor->Name), "Starfield.exe", 14) != 0) 151 | return; 152 | 153 | Plugin::Initialize(true); 154 | } 155 | #endif // BUILD_FOR_ASILOADER 156 | -------------------------------------------------------------------------------- /source/Plugin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Plugin 4 | { 5 | extern bool AllowLiveUpdates; 6 | extern bool InsertDebugMarkers; 7 | extern std::filesystem::path ShaderDumpBinPath; 8 | 9 | bool Initialize(bool UseASI); 10 | bool InitializeLog(bool UseASI); 11 | bool InitializeSettings(); 12 | void *GetThisModuleHandle(); 13 | } 14 | -------------------------------------------------------------------------------- /source/RE/CreationRenderer.cpp: -------------------------------------------------------------------------------- 1 | #include "CreationRenderer.h" 2 | 3 | namespace CreationRenderer 4 | { 5 | ID3D12CommandList *GetRenderGraphCommandList(void *RenderGraphData) 6 | { 7 | auto addr = Offsets::Signature("48 83 EC 28 48 8B 89 38 01 00 00 33 C0 48 85 C9 74 05 E8"); 8 | auto func = reinterpret_cast(addr.operator size_t()); 9 | 10 | return *reinterpret_cast(reinterpret_cast(func(RenderGraphData)) + 0x10); 11 | } 12 | 13 | Dx12Unknown *AcquireRenderPassRenderTarget(void *RenderPassData, uint32_t RenderTargetId) 14 | { 15 | // Leads to a call instruction 16 | auto addr = Offsets::Signature("E8 ? ? ? ? 48 8B 0D ? ? ? ? 48 8B D8 8B ? F0 00 00 00 48 89 84 24 ? 00 00 00").operator size_t(); 17 | addr = addr + *reinterpret_cast(addr + 1) + 5; 18 | auto func = reinterpret_cast(addr); 19 | 20 | return func(RenderPassData, RenderTargetId); 21 | } 22 | 23 | Dx12Unknown *AcquireRenderPassSingleInput(void *RenderPassData) 24 | { 25 | auto addr = Offsets::Signature("48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 48 89 7C 24 20 48 8B 01 48 8B 79 08 83 78 08 00 48 8D " 26 | "48 10 7C 03 48 8B 09 44 8B 59 24 8B 41 20 8B 5F 08"); 27 | auto func = reinterpret_cast(addr.operator size_t()); 28 | 29 | return func(RenderPassData); 30 | } 31 | 32 | Dx12Unknown *AcquireRenderPassSingleOutput(void *RenderPassData) 33 | { 34 | auto addr = Offsets::Signature("48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 48 89 7C 24 20 48 8B 01 48 8B 79 08 83 78 08 00 48 8D " 35 | "48 10 7C 03 48 8B 09 44 8B 59 04 8B 01 8B 5F 08"); 36 | auto func = reinterpret_cast(addr.operator size_t()); 37 | 38 | return func(RenderPassData); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /source/RE/CreationRenderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct ID3D12RootSignature; 4 | struct ID3D12PipelineState; 5 | 6 | namespace CreationRenderer 7 | { 8 | constexpr uint32_t TotalTechniqueTypeCount = 234; 9 | 10 | enum class ShaderType : uint8_t 11 | { 12 | Invalid = 0, 13 | Graphics = 1, 14 | Compute = 2, 15 | RayTracing = 3, 16 | }; 17 | 18 | class PipelineLayoutDx12 19 | { 20 | private: 21 | PipelineLayoutDx12() = delete; 22 | virtual ~PipelineLayoutDx12(); 23 | 24 | public: 25 | void *m_LayoutConfigurationData; // 0x8 26 | char _pad0[0x68]; // 0x10 27 | ID3D12RootSignature *m_RootSignature; // 0x78 Ref counted 28 | char _pad1[0x8]; // 0x80 29 | }; 30 | static_assert(sizeof(PipelineLayoutDx12) == 0x88); 31 | static_assert(offsetof(PipelineLayoutDx12, m_RootSignature) == 0x78); 32 | 33 | class ShaderInputsContainerDx12 34 | { 35 | private: 36 | ShaderInputsContainerDx12() = delete; 37 | 38 | public: 39 | char _pad0[0x38]; // 0x0 40 | const uint8_t *m_RootSignatureBlob; // 0x38 41 | char _pad1[0x8]; // 0x40 42 | uint32_t m_RootSignatureBlobSize; // 0x48 43 | }; 44 | static_assert(offsetof(ShaderInputsContainerDx12, m_RootSignatureBlob) == 0x38); 45 | static_assert(offsetof(ShaderInputsContainerDx12, m_RootSignatureBlobSize) == 0x48); 46 | 47 | class TechniqueData 48 | { 49 | private: 50 | TechniqueData() = delete; 51 | 52 | public: 53 | uint32_t m_Type; // 0x0 ShaderType 54 | ShaderInputsContainerDx12 *m_Inputs; // 0x8 55 | char _pad1[0x50]; // 0x10 56 | uint64_t m_Id; // 0x60 57 | char _pad4[0x8]; // 0x68 58 | const char *m_Name; // 0x70 59 | ID3D12PipelineState *m_PipelineState; // 0x78 60 | }; 61 | static_assert(offsetof(TechniqueData, m_Inputs) == 0x8); 62 | static_assert(offsetof(TechniqueData, m_Id) == 0x60); 63 | static_assert(offsetof(TechniqueData, m_Name) == 0x70); 64 | 65 | class TechniqueInfoTable 66 | { 67 | public: 68 | struct ConfigurationEntry 69 | { 70 | uint64_t m_Unknown1; // 0x0 Contains indices for tertiary arrays 71 | uint64_t m_Unknown2; // 0x8 72 | const char *m_FullName; // 0x10 73 | }; 74 | static_assert(sizeof(ConfigurationEntry) == 0x18); 75 | 76 | uint8_t m_BaseTypeIndex; // 0x0 77 | uint8_t m_Unknown1; // 0x1 78 | uint16_t m_TechniqueCount; // 0x2 79 | uint8_t m_Unknown2; // 0x4 80 | bool m_Unknown3; // 0x5 81 | bool m_IsPrecompiled; // 0x6 82 | uint8_t m_Unknown4; // 0x7 83 | uint64_t *m_TechniqueIds; // 0x8 84 | ConfigurationEntry *m_ConfigurationData; // 0x10 85 | const char *m_BaseTypeName; // 0x18 86 | 87 | #if 0 88 | static TechniqueInfoTable *LookupTable(uint32_t TechniqueTypeIndex); 89 | #endif 90 | }; 91 | 92 | struct Dx12Unknown 93 | { 94 | char _pad0[0x48]; // 00 95 | class Dx12Resource *m_Resource; // 48 96 | char _pad1[0x18]; // 50 97 | D3D12_CPU_DESCRIPTOR_HANDLE *m_RTVCpuDescriptors; // 68 98 | }; 99 | 100 | ID3D12CommandList *GetRenderGraphCommandList(void *RenderGraphData); 101 | Dx12Unknown *AcquireRenderPassRenderTarget(void *RenderPassData, uint32_t RenderTargetId); 102 | Dx12Unknown *AcquireRenderPassSingleInput(void *RenderPassData); 103 | Dx12Unknown *AcquireRenderPassSingleOutput(void *RenderPassData); 104 | } 105 | -------------------------------------------------------------------------------- /source/ReShadeHelper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "RE/CreationRenderer.h" 3 | #include "CComPtr.h" 4 | #include "Plugin.h" 5 | #include "ReShadeHelper.h" 6 | 7 | extern "C" __declspec(dllexport) const char *NAME = BUILD_PROJECT_NAME; 8 | extern "C" __declspec(dllexport) const char *DESCRIPTION = "ReShade helper integration via Starfield Shader Injector."; 9 | 10 | namespace ReShadeHelper 11 | { 12 | struct ID3D12ReShadeGraphicsCommandList : ID3D12GraphicsCommandList 13 | { 14 | using CallbackPre = std::move_only_function; 15 | using CallbackPost = std::move_only_function; 16 | 17 | inline static std::unordered_map SplitCommandLists; 18 | 19 | reshade::api::command_list *GetReShadeInterface() 20 | { 21 | return GetImplData(this, IID_NativeToReShade); 22 | } 23 | 24 | template 25 | void QueuePreSubmit(F&& Callback) 26 | { 27 | auto reshadeInterface = GetReShadeInterface(); 28 | SetImplData(reshadeInterface, IID_CommandListSubmitCallback, new CallbackPre(Callback)); 29 | 30 | CommandListLock l; 31 | SplitCommandLists.emplace(this, reshadeInterface); 32 | } 33 | 34 | auto GetPendingPreSubmitCallback() 35 | { 36 | auto reshadeInterface = GetReShadeInterface(); 37 | auto callback = GetImplData(reshadeInterface, IID_CommandListSubmitCallback); 38 | 39 | if (callback) 40 | SetImplData(reshadeInterface, IID_CommandListSubmitCallback, nullptr); 41 | 42 | return callback; 43 | } 44 | 45 | template 46 | void QueuePostSubmit(F&& Callback) 47 | { 48 | SetImplData(this, IID_CommandListSubmitCallback, new CallbackPost(Callback)); 49 | 50 | CommandListLock l; 51 | SplitCommandLists.try_emplace(this, nullptr); 52 | } 53 | 54 | auto GetPendingPostSubmitCallback() 55 | { 56 | const auto callback = GetImplData(this, IID_CommandListSubmitCallback); 57 | 58 | if (callback) 59 | SetImplData(this, IID_CommandListSubmitCallback, nullptr); 60 | 61 | return callback; 62 | } 63 | 64 | void Init(reshade::api::command_list *ReShadeInterface) 65 | { 66 | SetImplData(this, IID_NativeToReShade, ReShadeInterface); 67 | } 68 | 69 | void Destroy() 70 | { 71 | auto reshadeInterface = GetReShadeInterface(); 72 | delete GetPendingPreSubmitCallback(); 73 | delete GetPendingPostSubmitCallback(); 74 | SetImplData(this, IID_NativeToReShade, nullptr); 75 | 76 | CommandListLock l; 77 | std::erase_if( 78 | SplitCommandLists, 79 | [&](const auto& Pair) 80 | { 81 | return Pair.second == reshadeInterface; 82 | }); 83 | } 84 | }; 85 | static_assert(sizeof(ID3D12ReShadeGraphicsCommandList) == sizeof(ID3D12CommandList)); 86 | static_assert(alignof(ID3D12ReShadeGraphicsCommandList) == alignof(ID3D12CommandList)); 87 | 88 | extern void(WINAPI *D3D12CommandQueueExecuteCommandLists)(ID3D12CommandQueue *, UINT, ID3D12CommandList *const *); 89 | void WINAPI HookedD3D12CommandQueueExecuteCommandLists(ID3D12CommandQueue *This, UINT NumCommandLists, ID3D12CommandList *const *ppCommandLists); 90 | 91 | EffectRuntimeConfiguration::EffectRuntimeConfiguration(reshade::api::effect_runtime *Runtime) 92 | { 93 | Load(Runtime); 94 | } 95 | 96 | void EffectRuntimeConfiguration::Load(reshade::api::effect_runtime *Runtime) 97 | { 98 | reshade::get_config_value(Runtime, NAME, "DrawEffectsBeforeUI", m_DrawEffectsBeforeUI); 99 | reshade::get_config_value(Runtime, NAME, "AutomaticDepthBufferSelection", m_AutomaticDepthBufferSelection); 100 | } 101 | 102 | void EffectRuntimeConfiguration::Save(reshade::api::effect_runtime *Runtime) 103 | { 104 | reshade::set_config_value(Runtime, NAME, "DrawEffectsBeforeUI", m_DrawEffectsBeforeUI); 105 | reshade::set_config_value(Runtime, NAME, "AutomaticDepthBufferSelection", m_AutomaticDepthBufferSelection); 106 | } 107 | 108 | void OnInitEffectRuntime(reshade::api::effect_runtime *Runtime) 109 | { 110 | if (auto type = Runtime->get_device()->get_api(); type != reshade::api::device_api::d3d12) 111 | { 112 | spdlog::error( 113 | "Unsupported configuration. Somehow a non-D3D12 device was passed to {}. Device type is: {:X}.", 114 | __FUNCTION__, 115 | static_cast(type)); 116 | 117 | return; 118 | } 119 | 120 | // Devices can have multiple swap chains which means multiple effect runtimes. I guess we'll have to 121 | // use the last one created. 122 | SetImplData(Runtime, __uuidof(EffectRuntimeConfiguration), new EffectRuntimeConfiguration(Runtime)); 123 | SetImplData(Runtime->get_device(), IID_ReShadeEffectRuntime, Runtime); 124 | 125 | // 126 | // ReShade triggers execute_command_list events for each command list prior to calling ExecuteCommandLists. 127 | // These are okay enough when simply listening, but this plugin requires more control over the execution 128 | // order. Effects need to be rendered between or after command list submissions. ReShade doesn't offer such 129 | // functionality. 130 | // 131 | // That leaves two options: 132 | // 1. Hook the command queue's ExecuteCommandLists, split up submission groups, and inject our own lists. 133 | // 2. Hook game code to split up submission groups and then inject our own lists. 134 | // 135 | // Option 1 is implemented. 136 | // 137 | auto nativeD3D12Device = reinterpret_cast(Runtime->get_device()->get_native()); 138 | auto wrappedD3D12Device = GetImplData(nativeD3D12Device, IID_ReShadeD3D12DevicePrivateData); 139 | 140 | // Create a graphics command queue using ReShade's wrapped device which should hopefully return a 141 | // vtable pointing to ReShade's implementation. If it doesn't, it means third party code likely added 142 | // their own hooks. We'll have to skip integrity checks and blindly hook its vtable. 143 | const static bool once = [&] 144 | { 145 | const D3D12_COMMAND_QUEUE_DESC queueDesc = { 146 | .Type = D3D12_COMMAND_LIST_TYPE_DIRECT, 147 | .Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL, 148 | .Flags = D3D12_COMMAND_QUEUE_FLAG_NONE, 149 | .NodeMask = 0, 150 | }; 151 | CComPtr commandQueue; 152 | 153 | if (SUCCEEDED(wrappedD3D12Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue)))) 154 | { 155 | const auto vtableBase = *reinterpret_cast(commandQueue.Get()); 156 | 157 | return Hooks::WriteVirtualFunction( 158 | vtableBase, 159 | 10, 160 | &HookedD3D12CommandQueueExecuteCommandLists, 161 | &D3D12CommandQueueExecuteCommandLists); 162 | } 163 | 164 | return false; 165 | }(); 166 | 167 | if (!once) 168 | spdlog::error("ReShade initialized, but we failed to hook the D3D12 command queue. Crash imminent."); 169 | } 170 | 171 | void OnDestroyEffectRuntime(reshade::api::effect_runtime *Runtime) 172 | { 173 | SetImplData(Runtime->get_device(), IID_ReShadeEffectRuntime, nullptr); 174 | delete GetImplData(Runtime, __uuidof(EffectRuntimeConfiguration)); 175 | } 176 | 177 | void OnInitCommandList(reshade::api::command_list *CommandList) 178 | { 179 | reinterpret_cast(CommandList->get_native())->Init(CommandList); 180 | } 181 | 182 | void OnDestroyCommandList(reshade::api::command_list *CommandList) 183 | { 184 | reinterpret_cast(CommandList->get_native())->Destroy(); 185 | } 186 | 187 | void OnExecuteCommandList(reshade::api::command_queue *Queue, reshade::api::command_list *CommandList) 188 | { 189 | if (auto cb = reinterpret_cast(CommandList->get_native())->GetPendingPreSubmitCallback()) 190 | { 191 | (*cb)(Queue->get_immediate_command_list()); 192 | delete cb; 193 | } 194 | } 195 | 196 | void OnDrawSettingsOverlay(reshade::api::effect_runtime *Runtime) 197 | { 198 | auto effectConfig = GetImplData(Runtime, __uuidof(EffectRuntimeConfiguration)); 199 | bool updated = false; 200 | 201 | updated |= ImGui::Checkbox("Draw ReShade effects before UI", &effectConfig->m_DrawEffectsBeforeUI); 202 | updated |= ImGui::Checkbox("Automatic depth buffer selection", &effectConfig->m_AutomaticDepthBufferSelection); 203 | 204 | if (effectConfig->m_AutomaticDepthBufferSelection) 205 | ImGui::TextColored({ 1.0f, 0.0f, 0.0f, 1.0f }, "Warning: Generic Depth must be disabled while automatic depth is in use."); 206 | 207 | if (updated) 208 | effectConfig->Save(Runtime); 209 | } 210 | 211 | void Initialize() 212 | { 213 | if (!reshade::register_addon(static_cast(Plugin::GetThisModuleHandle()))) 214 | return; 215 | 216 | reshade::register_overlay(nullptr, OnDrawSettingsOverlay); 217 | reshade::register_event(OnInitEffectRuntime); 218 | reshade::register_event(OnDestroyEffectRuntime); 219 | reshade::register_event(OnInitCommandList); 220 | reshade::register_event(OnDestroyCommandList); 221 | reshade::register_event(OnExecuteCommandList); 222 | 223 | spdlog::info("Registered ReShade addon."); 224 | } 225 | 226 | void(WINAPI *D3D12CommandQueueExecuteCommandLists)(ID3D12CommandQueue *, UINT, ID3D12CommandList *const *); 227 | void WINAPI HookedD3D12CommandQueueExecuteCommandLists(ID3D12CommandQueue *This, UINT NumCommandLists, ID3D12CommandList *const *ppCommandLists) 228 | { 229 | const auto localSplits = [&]() 230 | { 231 | std::vector splits; 232 | 233 | if (NumCommandLists <= 0) 234 | return splits; 235 | 236 | CommandListLock l; 237 | 238 | if (ID3D12ReShadeGraphicsCommandList::SplitCommandLists.empty()) 239 | return splits; 240 | 241 | for (uint32_t i = 0; i < NumCommandLists; i++) 242 | { 243 | auto node = ID3D12ReShadeGraphicsCommandList::SplitCommandLists.extract(ppCommandLists[i]); 244 | 245 | if (!node.empty()) 246 | splits.emplace_back(node.key()); 247 | } 248 | 249 | return splits; 250 | }(); 251 | 252 | // Zero matches => forward to original function by default 253 | uint32_t i = NumCommandLists; 254 | uint32_t start = 0; 255 | 256 | if (!localSplits.empty()) 257 | { 258 | // Batch prior command list submissions together until a match is found in localSplits 259 | for (i = 0; i < NumCommandLists; i++) 260 | { 261 | if (std::find(localSplits.begin(), localSplits.end(), ppCommandLists[i]) != localSplits.end()) 262 | { 263 | if (i > start) 264 | D3D12CommandQueueExecuteCommandLists(This, i - start, &ppCommandLists[start]); 265 | 266 | D3D12CommandQueueExecuteCommandLists(This, 1, &ppCommandLists[i]); 267 | start = i + 1; 268 | 269 | if (auto cb = static_cast(ppCommandLists[i])->GetPendingPostSubmitCallback()) 270 | { 271 | (*cb)(This); 272 | delete cb; 273 | } 274 | } 275 | } 276 | } 277 | 278 | if (i > start) 279 | D3D12CommandQueueExecuteCommandLists(This, i - start, &ppCommandLists[start]); 280 | } 281 | 282 | void (*OriginalUpdatePreviousDepthBufferRenderPass)(void *, void *, void *); 283 | void HookedUpdatePreviousDepthBufferRenderPass(void *a1, void *a2, void *a3) 284 | { 285 | OriginalUpdatePreviousDepthBufferRenderPass(a1, a2, a3); 286 | 287 | auto commandList = static_cast(CreationRenderer::GetRenderGraphCommandList(a2)); 288 | auto reshadeInterface = commandList->GetReShadeInterface(); 289 | 290 | if (!reshadeInterface) 291 | return; 292 | 293 | auto source = CreationRenderer::AcquireRenderPassSingleInput(a3); 294 | //auto dest = CreationRenderer::AcquireRenderPassSingleOutput(a3); 295 | 296 | // If automatic depth buffer selection is enabled, we need to copy the game's depth buffer to a 297 | // separate texture so that ReShade can use it in effects. ReShade's API can handle resource 298 | // creation, but the copy has to be done on the game's command list. 299 | auto effectRuntime = GetImplData(reshadeInterface->get_device(), IID_ReShadeEffectRuntime); 300 | auto effectConfig = GetImplData(effectRuntime, __uuidof(EffectRuntimeConfiguration)); 301 | 302 | if (effectConfig->m_AutomaticDepthBufferSelection) 303 | { 304 | auto device = reshadeInterface->get_device(); 305 | 306 | // First determine if the depth buffer format changed between frames 307 | const auto depthResource = device->get_resource_from_view({ source->m_RTVCpuDescriptors[0].ptr }); 308 | const auto depthResourceDesc = device->get_resource_desc(depthResource); 309 | 310 | auto copyInfo = [&]() 311 | { 312 | EffectDepthCopy info = { 313 | .Format = depthResourceDesc.texture, 314 | .LastFrameIndex = effectConfig->DepthTrackingFrameIndex.fetch_add(1) + 1, 315 | }; 316 | 317 | std::scoped_lock lock(effectConfig->DepthBufferListMutex); 318 | auto itr = std::find_if( 319 | effectConfig->DepthBufferCopies.begin(), 320 | effectConfig->DepthBufferCopies.end(), 321 | [&](const auto& Copy) 322 | { 323 | return Copy.Format.width == info.Format.width && Copy.Format.height == info.Format.height && 324 | Copy.Format.depth_or_layers == info.Format.depth_or_layers && Copy.Format.levels == info.Format.levels && 325 | Copy.Format.format == info.Format.format; 326 | }); 327 | 328 | if (itr == effectConfig->DepthBufferCopies.end()) 329 | return info; 330 | 331 | itr->LastFrameIndex = info.LastFrameIndex; 332 | return *itr; 333 | }(); 334 | 335 | // A null handle means a suitable copy texture hasn't been created yet. Create one now. 336 | if (!copyInfo.Resource.handle) 337 | { 338 | auto copyResourceDesc = depthResourceDesc; 339 | copyResourceDesc.heap = reshade::api::memory_heap::gpu_only; 340 | copyResourceDesc.usage = reshade::api::resource_usage::depth_stencil | reshade::api::resource_usage::shader_resource | 341 | reshade::api::resource_usage::copy_dest; 342 | 343 | auto copyViewDesc = device->get_resource_view_desc({ source->m_RTVCpuDescriptors[0].ptr }); 344 | copyViewDesc.format = reshade::api::format_to_default_typed(copyViewDesc.format); 345 | 346 | // We only need a texture and its shader resource view; stencils don't matter 347 | device->create_resource(copyResourceDesc, nullptr, reshade::api::resource_usage::shader_resource, ©Info.Resource); 348 | device->create_resource_view( 349 | copyInfo.Resource, 350 | reshade::api::resource_usage::shader_resource, 351 | copyViewDesc, 352 | ©Info.ResourceView); 353 | 354 | std::scoped_lock lock(effectConfig->DepthBufferListMutex); 355 | effectConfig->DepthBufferCopies.emplace_back(copyInfo); 356 | } 357 | 358 | // Using the game's command list, copy the game's final depth buffer to our copy texture 359 | reshadeInterface->barrier( 360 | copyInfo.Resource, 361 | reshade::api::resource_usage::shader_resource, 362 | reshade::api::resource_usage::copy_dest); 363 | reshadeInterface->copy_resource(depthResource, copyInfo.Resource); 364 | reshadeInterface->barrier( 365 | copyInfo.Resource, 366 | reshade::api::resource_usage::copy_dest, 367 | reshade::api::resource_usage::shader_resource); 368 | 369 | // Schedule a fence signal to release old copies 370 | commandList->QueuePostSubmit( 371 | [device, effectRuntime, effectConfig, index = copyInfo.LastFrameIndex, view = copyInfo.ResourceView](ID3D12CommandQueue *Queue) 372 | { 373 | auto nativeDevice = reinterpret_cast(device->get_native()); 374 | 375 | // GPU-side fence 376 | if (!effectConfig->DepthTrackingFence) 377 | nativeDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&effectConfig->DepthTrackingFence)); 378 | 379 | Queue->Signal(effectConfig->DepthTrackingFence.Get(), index); 380 | const auto currentFenceIndex = effectConfig->DepthTrackingFence->GetCompletedValue(); 381 | 382 | // Update ReShade effects 383 | if (std::exchange(effectConfig->UpdateHint, view) != view) 384 | { 385 | effectRuntime->update_texture_bindings("DEPTH", view); 386 | 387 | effectRuntime->enumerate_uniform_variables( 388 | nullptr, 389 | [](auto Runtime, auto Variable) 390 | { 391 | char source[32] = {}; 392 | if (Runtime->get_annotation_string_from_uniform_variable(Variable, "source", source) && 393 | std::strcmp(source, "bufready_depth") == 0) 394 | Runtime->set_uniform_value_bool(Variable, true); 395 | }); 396 | } 397 | 398 | std::lock_guard lock(effectConfig->DepthBufferListMutex); 399 | std::erase_if( 400 | effectConfig->DepthBufferCopies, 401 | [&](const auto& Copy) 402 | { 403 | if (Copy.LastFrameIndex >= currentFenceIndex) 404 | return false; 405 | 406 | device->destroy_resource_view(Copy.ResourceView); 407 | device->destroy_resource(Copy.Resource); 408 | return true; 409 | }); 410 | }); 411 | } 412 | } 413 | 414 | void (*OriginalScaleformCompositeDrawPass)(void *, void *, void *); 415 | void HookedScaleformCompositeDrawPass(void *a1, void *a2, void *a3) 416 | { 417 | OriginalScaleformCompositeDrawPass(a1, a2, a3); 418 | 419 | auto commandList = static_cast(CreationRenderer::GetRenderGraphCommandList(a2)); 420 | auto reshadeInterface = commandList->GetReShadeInterface(); 421 | 422 | if (!reshadeInterface) 423 | return; 424 | 425 | // Tell ReShade to render effects before this UI command list is submitted. A separate command 426 | // list is required because of state tracking reasons. 427 | auto effectRuntime = GetImplData(reshadeInterface->get_device(), IID_ReShadeEffectRuntime); 428 | auto effectConfig = GetImplData(effectRuntime, __uuidof(EffectRuntimeConfiguration)); 429 | 430 | if (effectConfig->m_DrawEffectsBeforeUI) 431 | { 432 | auto renderTarget = CreationRenderer::AcquireRenderPassRenderTarget(a3, 0x6701701); 433 | 434 | commandList->QueuePreSubmit( 435 | [effectRuntime, rtvHandle = renderTarget->m_RTVCpuDescriptors[0].ptr](reshade::api::command_list *ImmediateCommandList) 436 | { 437 | effectRuntime->render_effects(ImmediateCommandList, { rtvHandle }); 438 | }); 439 | } 440 | } 441 | 442 | DECLARE_HOOK_TRANSACTION(ReShadeHelper) 443 | { 444 | Hooks::WriteJump( 445 | Offsets::Signature( 446 | "48 89 5C 24 08 48 89 6C 24 18 48 89 74 24 20 57 41 54 41 55 41 56 41 57 48 81 EC A0 00 00 00 8B 82 40 01 00 00"), 447 | &HookedScaleformCompositeDrawPass, 448 | &OriginalScaleformCompositeDrawPass); 449 | 450 | Hooks::WriteJump( 451 | Offsets::Signature("48 89 5C 24 08 48 89 74 24 10 48 89 7C 24 18 55 48 8B EC 48 83 EC 60 48 8B CA"), 452 | &HookedUpdatePreviousDepthBufferRenderPass, 453 | &OriginalUpdatePreviousDepthBufferRenderPass); 454 | }; 455 | } 456 | -------------------------------------------------------------------------------- /source/ReShadeHelper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "CComPtr.h" 6 | 7 | namespace ReShadeHelper 8 | { 9 | // Maps a reshade::api::* object to a native D3D12 object via ID3D12Object::GetPrivateData() 10 | constexpr GUID IID_NativeToReShade = { 0x8e315253, 0x21a9, 0x4309, { 0xbc, 0x17, 0x8e, 0xcc, 0x76, 0x24, 0x33, 0x95 } }; 11 | 12 | // ReShade internal https://github.com/crosire/reshade/blob/main/source/d3d12/d3d12_device.hpp#L12 via ID3D12Object::GetPrivateData() 13 | constexpr GUID IID_ReShadeD3D12DevicePrivateData = { 0x2523aff4, 0x978b, 0x4939, { 0xba, 0x16, 0x8e, 0xe8, 0x76, 0xa4, 0xcb, 0x2a } }; 14 | 15 | // Maps a reshade::api::effect_runtime to a native D3D12 device via reshade::api_object::get_private_data() 16 | constexpr GUID IID_ReShadeEffectRuntime = { 0x358d5ebf, 0x15aa, 0x4bae, { 0x89, 0x50, 0x12, 0x01, 0xa4, 0x25, 0xbc, 0x2f } }; 17 | 18 | // Command list submission callback via reshade::api_object::get_private_data() 19 | constexpr GUID IID_CommandListSubmitCallback = { 0xec8960f3, 0x328c, 0x4091, { 0x7f, 0x17, 0x10, 0x23, 0x22, 0xee, 0x51, 0x9e } }; 20 | 21 | struct EffectDepthCopy 22 | { 23 | decltype(reshade::api::resource_desc::texture) Format = {}; 24 | reshade::api::resource Resource = {}; 25 | reshade::api::resource_view ResourceView = {}; 26 | uint64_t LastFrameIndex = 0; 27 | }; 28 | 29 | struct __declspec(uuid("fdc21f1f-7bef-418f-8e34-10eb86add2e4")) EffectRuntimeConfiguration 30 | { 31 | bool m_DrawEffectsBeforeUI = false; 32 | bool m_AutomaticDepthBufferSelection = false; 33 | 34 | std::mutex DepthBufferListMutex; 35 | CComPtr DepthTrackingFence; 36 | reshade::api::resource_view UpdateHint = {}; 37 | std::atomic_uint64_t DepthTrackingFrameIndex = 0; 38 | std::vector DepthBufferCopies; 39 | 40 | EffectRuntimeConfiguration(reshade::api::effect_runtime *Runtime); 41 | void Load(reshade::api::effect_runtime *Runtime); 42 | void Save(reshade::api::effect_runtime *Runtime); 43 | }; 44 | 45 | class CommandListLock 46 | { 47 | private: 48 | static inline std::atomic_flag GlobalLock; 49 | 50 | CommandListLock(const CommandListLock&) = delete; 51 | CommandListLock& operator=(const CommandListLock&) = delete; 52 | 53 | public: 54 | CommandListLock() 55 | { 56 | while (GlobalLock.test_and_set(std::memory_order_acquire)) 57 | { 58 | while (GlobalLock.test(std::memory_order_relaxed)) 59 | _mm_pause(); 60 | } 61 | } 62 | 63 | ~CommandListLock() 64 | { 65 | GlobalLock.clear(std::memory_order_release); 66 | } 67 | }; 68 | 69 | template 70 | requires(sizeof(T) <= sizeof(uint64_t) && std::is_trivially_copyable_v) 71 | void SetImplData(ID3D12Object *Object, const GUID& Guid, const T& Data) 72 | { 73 | if constexpr (std::is_same_v) 74 | Object->SetPrivateData(Guid, 0, nullptr); 75 | else 76 | Object->SetPrivateData(Guid, sizeof(T), std::addressof(Data)); 77 | } 78 | 79 | template 80 | requires(sizeof(T) <= sizeof(uint64_t) && std::is_trivially_copyable_v) 81 | void SetImplData(reshade::api::api_object *Object, const GUID& Guid, const T& Data) 82 | { 83 | uint64_t value = {}; 84 | 85 | if constexpr (!std::is_same_v) 86 | memcpy(&value, std::addressof(Data), sizeof(T)); 87 | 88 | Object->set_private_data(reinterpret_cast(&Guid), value); 89 | } 90 | 91 | template 92 | requires(sizeof(T) <= sizeof(uint64_t) && std::is_trivially_copyable_v) 93 | T GetImplData(ID3D12Object *Object, const GUID& Guid) 94 | { 95 | T value = {}; 96 | uint32_t size = sizeof(T); 97 | 98 | Object->GetPrivateData(Guid, &size, std::addressof(value)); 99 | return value; 100 | } 101 | 102 | template 103 | requires(sizeof(T) <= sizeof(uint64_t) && std::is_trivially_copyable_v) 104 | T GetImplData(reshade::api::api_object *Object, const GUID& Guid) 105 | { 106 | uint64_t temp = {}; 107 | Object->get_private_data(reinterpret_cast(&Guid), &temp); 108 | 109 | T value = {}; 110 | memcpy(std::addressof(value), &temp, sizeof(T)); 111 | 112 | return value; 113 | } 114 | 115 | void Initialize(); 116 | } 117 | -------------------------------------------------------------------------------- /source/dllmain.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | BOOL WINAPI RawDllMain(HINSTANCE hInstDLL, DWORD fdwReason, LPVOID lpvReserved) 4 | { 5 | if (fdwReason == DLL_PROCESS_ATTACH) 6 | { 7 | // The child process debugging extension for Visual Studio conflicts with SKSE's injection mechanism. 8 | // No choice but to use a hack. 9 | // 10 | // Start vsjitdebugger.exe if a debugger isn't already attached. GAME_DEBUGGER_REQUEST determines the 11 | // command line and GAME_DEBUGGER_PROC is used to hide the CreateProcessA IAT entry. 12 | if (char cmd[512] = {}, proc[512] = {}; !IsDebuggerPresent() && 13 | GetEnvironmentVariableA("GAME_DEBUGGER_REQUEST", cmd, ARRAYSIZE(cmd)) > 0 && 14 | GetEnvironmentVariableA("GAME_DEBUGGER_PROC", proc, ARRAYSIZE(proc)) > 0) 15 | { 16 | std::to_chars(cmd + strlen(cmd), std::end(cmd), GetCurrentProcessId()); 17 | auto moduleName = proc; 18 | auto importName = strchr(proc, '!') + 1; 19 | importName[-1] = '\0'; 20 | 21 | PROCESS_INFORMATION pi = {}; 22 | 23 | STARTUPINFOA si = {}; 24 | si.cb = sizeof(si); 25 | si.dwFlags = STARTF_USESHOWWINDOW; 26 | si.wShowWindow = SW_HIDE; 27 | 28 | auto c = reinterpret_cast(GetProcAddress(GetModuleHandleA(moduleName), importName)); 29 | c(nullptr, cmd, nullptr, nullptr, false, 0, nullptr, nullptr, &si, &pi); 30 | 31 | WaitForSingleObject(pi.hProcess, INFINITE); 32 | CloseHandle(pi.hProcess); 33 | CloseHandle(pi.hThread); 34 | } 35 | } 36 | 37 | return TRUE; 38 | } 39 | 40 | BOOL WINAPI DllMain(HINSTANCE hInstDLL, DWORD fdwReason, LPVOID lpvReserved) 41 | { 42 | return TRUE; 43 | } 44 | 45 | extern "C" extern decltype(&RawDllMain) const _pRawDllMain = RawDllMain; 46 | -------------------------------------------------------------------------------- /source/pch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "Hooking/Hooks.h" 17 | #include "Hooking/Offsets.h" 18 | -------------------------------------------------------------------------------- /vcpkg-ports/detours/detours-config.cmake: -------------------------------------------------------------------------------- 1 | if(TARGET detours::detours) 2 | return() 3 | endif() 4 | 5 | get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH) # detours 6 | get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) # share 7 | get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) # package root 8 | add_library(detours::detours STATIC IMPORTED) 9 | set_target_properties(detours::detours PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include") 10 | set(detours_FOUND 1) 11 | 12 | get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) 13 | file(GLOB CONFIG_FILES "${_DIR}/detours-targets-*.cmake") 14 | foreach(f ${CONFIG_FILES}) 15 | include(${f}) 16 | endforeach() 17 | -------------------------------------------------------------------------------- /vcpkg-ports/detours/detours-targets.cmake.in: -------------------------------------------------------------------------------- 1 | set_property(TARGET detours::detours APPEND PROPERTY IMPORTED_CONFIGURATIONS @MS_DETOURS_CONFIGURATION@) 2 | set_target_properties(detours::detours PROPERTIES 3 | IMPORTED_LOCATION_@MS_DETOURS_CONFIGURATION@ "${_IMPORT_PREFIX}/@MS_DETOURS_LOCATION@" 4 | ) 5 | -------------------------------------------------------------------------------- /vcpkg-ports/detours/find-jmp-bounds-arm64.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/detours.cpp b/src/detours.cpp 2 | index 8345c4d..3cd0e9d 100644 3 | --- a/src/detours.cpp 4 | +++ b/src/detours.cpp 5 | @@ -974,6 +974,19 @@ inline PBYTE detour_skip_jmp(PBYTE pbCode, PVOID *ppGlobals) 6 | return pbCode; 7 | } 8 | 9 | +inline void detour_find_jmp_bounds(PBYTE pbCode, 10 | + PDETOUR_TRAMPOLINE *ppLower, 11 | + PDETOUR_TRAMPOLINE *ppUpper) 12 | +{ 13 | + // We have to place trampolines within +/- 2GB of code. 14 | + ULONG_PTR lo = detour_2gb_below((ULONG_PTR)pbCode); 15 | + ULONG_PTR hi = detour_2gb_above((ULONG_PTR)pbCode); 16 | + DETOUR_TRACE(("[%p..%p..%p]\n", lo, pbCode, hi)); 17 | + 18 | + *ppLower = (PDETOUR_TRAMPOLINE)lo; 19 | + *ppUpper = (PDETOUR_TRAMPOLINE)hi; 20 | +} 21 | + 22 | inline BOOL detour_does_code_end_function(PBYTE pbCode) 23 | { 24 | ULONG Opcode = fetch_opcode(pbCode); 25 | -------------------------------------------------------------------------------- /vcpkg-ports/detours/portfile.cmake: -------------------------------------------------------------------------------- 1 | vcpkg_check_linkage(ONLY_STATIC_LIBRARY) 2 | 3 | vcpkg_from_github( 4 | OUT_SOURCE_PATH SOURCE_PATH 5 | REPO microsoft/Detours 6 | REF v4.0.1 7 | SHA512 0a9c21b8222329add2de190d2e94d99195dfa55de5a914b75d380ffe0fb787b12e016d0723ca821001af0168fd1643ffd2455298bf3de5fdc155b3393a3ccc87 8 | HEAD_REF master 9 | PATCHES 10 | find-jmp-bounds-arm64.patch 11 | ) 12 | 13 | vcpkg_build_nmake( 14 | SOURCE_PATH "${SOURCE_PATH}" 15 | PROJECT_SUBPATH "src" 16 | PROJECT_NAME "Makefile" 17 | OPTIONS "PROCESSOR_ARCHITECTURE=${VCPKG_TARGET_ARCHITECTURE}" 18 | OPTIONS_RELEASE "DETOURS_CONFIG=Release" 19 | OPTIONS_DEBUG "DETOURS_CONFIG=Debug" 20 | ) 21 | 22 | # Debug library 23 | if (NOT VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") 24 | file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg/lib.${VCPKG_TARGET_ARCHITECTURE}Debug/" DESTINATION "${CURRENT_PACKAGES_DIR}/debug/lib") 25 | file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg/include/" DESTINATION "${CURRENT_PACKAGES_DIR}/include" RENAME detours) 26 | 27 | set(MS_DETOURS_CONFIGURATION "DEBUG") 28 | set(MS_DETOURS_LOCATION "debug/lib/detours.lib") 29 | configure_file("${CMAKE_CURRENT_LIST_DIR}/detours-targets.cmake.in" "${CURRENT_PACKAGES_DIR}/share/${PORT}/detours-targets-debug.cmake" @ONLY) 30 | endif() 31 | 32 | # Release library 33 | if (NOT VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") 34 | file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/lib.${VCPKG_TARGET_ARCHITECTURE}Release/" DESTINATION "${CURRENT_PACKAGES_DIR}/lib") 35 | file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/include/" DESTINATION "${CURRENT_PACKAGES_DIR}/include" RENAME detours) 36 | 37 | set(MS_DETOURS_CONFIGURATION "RELEASE") 38 | set(MS_DETOURS_LOCATION "lib/detours.lib") 39 | configure_file("${CMAKE_CURRENT_LIST_DIR}/detours-targets.cmake.in" "${CURRENT_PACKAGES_DIR}/share/${PORT}/detours-targets-release.cmake" @ONLY) 40 | endif() 41 | 42 | file(COPY "${CMAKE_CURRENT_LIST_DIR}/detours-config.cmake" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") 43 | file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") 44 | vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.md") 45 | -------------------------------------------------------------------------------- /vcpkg-ports/detours/usage: -------------------------------------------------------------------------------- 1 | The package detours is compatible with built-in CMake targets: 2 | 3 | find_package(detours CONFIG REQUIRED) 4 | target_link_libraries(main PRIVATE detours::detours) 5 | -------------------------------------------------------------------------------- /vcpkg-ports/detours/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "detours", 3 | "version": "4.0.1", 4 | "port-version": 9, 5 | "description": "Detours is a software package for monitoring and instrumenting API calls on Windows.", 6 | "homepage": "https://github.com/microsoft/Detours", 7 | "license": "MIT", 8 | "supports": "windows & !uwp" 9 | } 10 | -------------------------------------------------------------------------------- /vcpkg-ports/reshade-api/portfile.cmake: -------------------------------------------------------------------------------- 1 | vcpkg_from_github( 2 | OUT_SOURCE_PATH SOURCE_PATH 3 | REPO crosire/reshade 4 | REF v5.9.2 5 | SHA512 548be6b0bde6aadf988c332814b0ea72a3efcff6f47503fba2e49de94756639eafd1e76b6ca47373968f0beb1b31601422e428644ab88181785ba9ef1eac910e 6 | HEAD_REF master 7 | ) 8 | 9 | file(INSTALL ${SOURCE_PATH}/include DESTINATION ${CURRENT_PACKAGES_DIR}/include RENAME reshade-api) 10 | 11 | # Handle copyright 12 | vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.md") -------------------------------------------------------------------------------- /vcpkg-ports/reshade-api/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reshade-api", 3 | "version": "5.9.2", 4 | "port-version": 1, 5 | "description": "Header-only C++ API for ReShade, a generic post-processing injector for games and video software.", 6 | "homepage": "https://github.com/crosire/reshade", 7 | "license": "MIT", 8 | "supports": "windows" 9 | } 10 | -------------------------------------------------------------------------------- /vcpkg-ports/reshade-imgui/portfile.cmake: -------------------------------------------------------------------------------- 1 | vcpkg_from_github( 2 | OUT_SOURCE_PATH SOURCE_PATH 3 | REPO ocornut/imgui 4 | REF v1.89.7-docking 5 | SHA512 d5f4433da365961916267e80a82234e439549a997578b684bbcf8970cdae7ab1f284da22ef1469f419e091c30578daadbd437bfa82a031fc6b2cee2e7a048418 6 | HEAD_REF docking 7 | ) 8 | 9 | file(INSTALL ${SOURCE_PATH}/imgui.h DESTINATION ${CURRENT_PACKAGES_DIR}/include/${PORT}) 10 | file(INSTALL ${SOURCE_PATH}/imconfig.h DESTINATION ${CURRENT_PACKAGES_DIR}/include/${PORT}) 11 | 12 | # Handle copyright 13 | vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt") -------------------------------------------------------------------------------- /vcpkg-ports/reshade-imgui/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reshade-imgui", 3 | "version": "1.89.7", 4 | "port-version": 1, 5 | "description": "Bloat-free Immediate Mode Graphical User interface for C++ with minimal dependencies.", 6 | "homepage": "https://github.com/ocornut/imgui", 7 | "license": "MIT" 8 | } 9 | -------------------------------------------------------------------------------- /vcpkg-ports/sfse-common/portfile.cmake: -------------------------------------------------------------------------------- 1 | vcpkg_minimum_required(VERSION 2022-10-12) 2 | 3 | vcpkg_from_github( 4 | OUT_SOURCE_PATH SOURCE_PATH 5 | REPO ianpatt/sfse 6 | REF v0.2.0 7 | SHA512 0a7287f7a09b48e08cb5da892b80b5c7613d6320354236de9ab45319ca2660bec10797fc0e130354a5013d51e992b9601ae459ff42c230c3399c4390b84944d2 8 | HEAD_REF master 9 | ) 10 | 11 | vcpkg_cmake_configure( 12 | SOURCE_PATH "${SOURCE_PATH}/sfse_common" 13 | ) 14 | 15 | vcpkg_cmake_install() 16 | 17 | vcpkg_cmake_config_fixup( 18 | CONFIG_PATH "lib/cmake/sfse_common" 19 | ) 20 | 21 | file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") 22 | 23 | # Vcpkg doesn't allow underscores. find_package() has to match. 24 | file(RENAME "${CURRENT_PACKAGES_DIR}/share/${PORT}/sfse_common-config.cmake" "${CURRENT_PACKAGES_DIR}/share/${PORT}/${PORT}-config.cmake") 25 | 26 | # Manually copy PluginAPI.h over since it should be part of "sfse_common". Alternatively sfse_version.h could be moved 27 | # to the "sfse" project. 28 | file(INSTALL "${SOURCE_PATH}/sfse/PluginAPI.h" DESTINATION "${CURRENT_PACKAGES_DIR}/include/sfse_common") 29 | 30 | # Handle copyright 31 | vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") 32 | 33 | # Handle usage 34 | file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") -------------------------------------------------------------------------------- /vcpkg-ports/sfse-common/usage: -------------------------------------------------------------------------------- 1 | The package sfse-common is compatible with built-in CMake targets: 2 | 3 | find_package(sfse-common CONFIG REQUIRED) 4 | target_link_libraries(main PRIVATE sfse::sfse_common) 5 | -------------------------------------------------------------------------------- /vcpkg-ports/sfse-common/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sfse-common", 3 | "version": "0.2.0", 4 | "port-version": 1, 5 | "description": "Starfield Script Extender common components.", 6 | "homepage": "https://github.com/ianpatt/sfse", 7 | "license": "MIT", 8 | "supports": "windows & !uwp", 9 | "dependencies": [ 10 | { 11 | "name": "vcpkg-cmake", 12 | "host": true 13 | }, 14 | { 15 | "name": "vcpkg-cmake-config", 16 | "host": true 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", 3 | "supports": "windows", 4 | "dependencies": [ 5 | "detours", 6 | "pkgconf", 7 | "reshade-api", 8 | "reshade-imgui", 9 | "spdlog", 10 | "sfse-common", 11 | "tomlplusplus", 12 | "xbyak" 13 | ], 14 | "builtin-baseline": "a39a74405f277773aba08018bb797cb4a6614d0c" 15 | } 16 | --------------------------------------------------------------------------------