diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index d6f2036ee9..0000000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1,427 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build Commands - -### WSL/Linux Environment Note - -This is a Windows-specific project requiring Visual Studio and Windows SDK. If working in WSL, use PowerShell to execute build commands: - -```bash -# In WSL, use powershell.exe to run Windows commands -powershell.exe -Command "./BuildRelease.bat [PRESET_NAME]" -``` - -### Primary Build Command - -```bash -./BuildRelease.bat [PRESET_NAME] -``` - -**Available Presets** (from CMakePresets.json): - -- `ALL` (default) - Builds for SE/AE/VR in single binary -- `SE` - Skyrim Special Edition only -- `AE` - Anniversary Edition only -- `VR` - Skyrim VR only -- `ALL-TRACY` - Includes Tracy profiler support -- `ALL-WITH-AUTO-DEPLOYMENT` - Auto-deploys to configured Skyrim directories when template used. - -### Development Setup - -1. Copy `CMakeUserPresets.json.template` → `CMakeUserPresets.json` -2. Configure `CommunityShadersOutputDir` for auto-deployment to Skyrim installations -3. Set build options in user preset: - - `AUTO_PLUGIN_DEPLOYMENT`: Auto-copy to Skyrim dirs - - `AIO_ZIP_TO_DIST`: Creates all-in-one distribution package - - `ZIP_TO_DIST`: Creates individual feature packages - - `TRACY_SUPPORT`: Enables performance profiling - -### Shader Development and Testing - -```bash -# Install hlslkit (external dependency) -pip install git+https://github.com/alandtse/hlslkit.git - -# Prepare shaders for validation (builds shader directory structure) -cmake --build ./build/ALL --target prepare_shaders - -# Full shader suite validation (can be time-consuming) -hlslkit-compile --shader-dir build/ALL/aio/Shaders --output-dir build/ShaderCache --config .github/configs/shader-validation.yaml --max-warnings 0 --suppress-warnings X1519 - -# VR-specific validation -hlslkit-compile --shader-dir build/ALL/aio/Shaders --output-dir build/ShaderCache --config .github/configs/shader-validation-vr.yaml --max-warnings 0 --suppress-warnings X1519 - -# Targeted testing for faster development (recommended during development) -# Test specific base shader -hlslkit-compile --shader-dir build/ALL/aio/Shaders/Lighting.hlsl --output-dir build/ShaderCache --config .github/configs/shader-validation.yaml - -# Test specific compute shader -hlslkit-compile --shader-dir build/ALL/aio/Shaders/DeferredCompositeCS.hlsl --output-dir build/ShaderCache --config .github/configs/shader-validation.yaml - -# Test specific feature directory -hlslkit-compile --shader-dir build/ALL/aio/Shaders/ScreenSpaceGI/ --output-dir build/ShaderCache --config .github/configs/shader-validation.yaml - -# Test feature-specific compute shader -hlslkit-compile --shader-dir build/ALL/aio/Shaders/LightLimitFix/ClusterBuildingCS.hlsl --output-dir build/ShaderCache --config .github/configs/shader-validation.yaml - -# Generate shader defines from game log (requires CommunityShaders.log from game) -hlslkit-generate-defines --log CommunityShaders.log - -# Scan for buffer conflicts across features -hlslkit-buffer-scan --features-dir features/ -``` - -## Architecture Overview - -### Plugin Architecture - -**Core Pattern**: Feature-driven modular system where each graphics enhancement is an independent `Feature` class that can be enabled/disabled at runtime. - -**Key Classes**: - -- `Feature` (src/Feature.h) - Base class for all graphics features -- `State` (src/State.h) - Global singleton managing feature lifecycle -- `ShaderCache` (src/ShaderCache.h) - Runtime shader compilation and caching -- `Menu` (src/Menu.h) - ImGui-based in-game configuration interface - -### Feature Implementation Pattern - -Each feature follows consistent structure: - -1. **C++ Implementation**: `src/Features/FeatureName.cpp/h` inheriting from `Feature` -2. **Shader Assets**: `features/FeatureName/Shaders/` containing HLSL shaders -3. **Configuration**: `features/FeatureName/Shaders/Features/FeatureName.ini` with versioned settings -4. **Core Features**: Features with `CORE` marker file bundle with main mod - -### DirectX Integration - -**Hooking System**: Uses Detours library to intercept DirectX 11 API calls in `src/Hooks.cpp` -**Deferred Rendering**: Custom deferred pipeline in `src/Deferred.cpp` with feature integration points -**Shader Management**: Runtime compilation with include system (`package/Shaders/Common/`) for shared utilities -**Base Shader Library**: `package/Shaders/` contains Skyrim's core rendering shaders (Lighting.hlsl, Water.hlsl, Sky.hlsl, etc.) - -### Cross-Platform Support - -**Single Binary**: Supports SE/AE/VR through CommonLibSSE-NG runtime detection -**VR Adaptations**: Specialized rendering paths in `src/Features/VR/` -**API Abstraction**: Dual DirectX 11 support with feature-specific rendering strategies - -## Critical Dependencies - -### CommonLibSSE-NG (`extern/CommonLibSSE-NG`) - -**Essential reverse engineering library** providing reverse-engineered interfaces to interact with Skyrim's game engine safely. - -**Core Functionality**: - -- **Game Object Access**: RE namespace with Skyrim's internal classes and structures -- **Memory Management**: Safe access to game memory with proper lifetime management -- **Event System**: Hook into Skyrim's event dispatching (rendering, input, etc.) -- **Address Library Integration**: Runtime address resolution for different game versions - -**Key Namespaces**: - -- `RE::` - Skyrim game objects and classes (BSShader, TESObjectREFR, etc.) -- `REL::` - Relative addressing and version management -- `SKSE::` - SKSE plugin interfaces and utilities - -### Runtime Targeting System - -CommonLibSSE-NG supports multiple Skyrim versions through sophisticated runtime targeting. Further information is available at https://github.com/CharmedBaryon/CommonLibSSE-NG/wiki/Runtime-Targeting - -**Build Presets**: - -- `SE` - Skyrim Special Edition only -- `AE` - Anniversary Edition only -- `VR` - Skyrim VR only -- `ALL` - Multi-runtime support (default for this project) - -**Compile-Time vs Runtime Patterns**: - -**Single Runtime (compile-time)**: When targeting one version, `#ifdef ENABLE_SKYRIM_VR` conditionally compiles VR-specific code: - -```cpp -#ifdef ENABLE_SKYRIM_VR - virtual void Unk_09(UI_MENU_Unk09 a_unk); // VR-only vfunc -#endif -``` - -**Multi-Runtime (runtime detection)**: When targeting ALL, uses runtime accessors: - -```cpp -// Runtime member access with different offsets per version -auto& GetRuntimeData() { - return REL::RelocateMemberIfNewer( - SKSE::RUNTIME_SSE_1_6_629, this, 0x3D8, 0x3E0); -} - -// VR-specific runtime data (only exists in VR) -auto& GetVRRuntimeData() { - return REL::RelocateMember(this, 0, 0x3D8); -} - -// Runtime detection -if (REL::Module::IsVR()) { - // VR-specific code path -} -``` - -**Key Runtime Utilities**: - -- `REL::RelocateMember()` - Access members with different offsets -- `REL::RelocateVirtual()` - Call virtual functions with variant vtables -- `REL::Module::IsVR()`, `IsAE()`, `IsSE()` - Runtime version detection -- `REL::RelocationID()` - Dynamic address resolution based on version - -**Critical for Development**: When modifying classes that inherit from game objects, always check if they have runtime-specific variations and use appropriate accessor patterns. - -## Core Architecture - -### Global System (`src/Globals.h`) - -Central coordination point providing access to all major subsystems: - -**Core Systems**: - -- `globals::state` - Main plugin state and feature lifecycle management -- `globals::deferred` - Deferred rendering pipeline coordinator -- `globals::menu` - ImGui-based in-game configuration interface -- `globals::shaderCache` - Runtime shader compilation and caching - -**Graphics Integration**: - -- `globals::d3d::*` - DirectX 11 device, context, and swapchain access -- `globals::game::*` - Skyrim graphics state (shadowState, renderer, shaders) -- `globals::upscaling` - FidelityFX and Streamline integration -- `globals::dx12SwapChain` - DirectX 12 support for advanced features - -**Feature Registry** (`globals::features::`): -All graphics features are globally accessible for cross-feature coordination: - -- Lighting: `lightLimitFix`, `volumetricLighting`, `skylighting`, `ibl` -- Terrain: `terrainShadows`, `terrainBlending`, `terrainVariation`, `terrainHelper` -- Materials: `extendedMaterials`, `hairSpecular`, `subsurfaceScattering` -- Effects: `screenSpaceGI`, `screenSpaceShadows`, `waterEffects`, `wetnessEffects` -- Environment: `cloudShadows`, `dynamicCubemaps`, `weatherPicker`, `skySync` -- VR: `vr` - VR-specific adaptations and coordinate transformations - -### Shared Utilities (`src/Utils/`) - -Common functionality organized by domain: - -- `UI.h/cpp` - ImGui utilities, input mapping, and UI helper functions -- `D3D.h/cpp` - DirectX utilities and helper functions -- `Game.h/cpp` - Skyrim-specific game state and object utilities -- `VRUtils.h/cpp` - VR-specific utilities and coordinate transformations -- `FileSystem.h/cpp` - File I/O and path manipulation helpers -- `Format.h/cpp` - String formatting and conversion utilities -- `Serialize.h/cpp` - JSON serialization helpers - -### Shader Architecture - -**Base Shader Library** (`package/Shaders/`): - -- **Core Rendering**: `Lighting.hlsl`, `Water.hlsl`, `Sky.hlsl`, `Particle.hlsl` - Skyrim's main rendering pipeline -- **Image Space Effects**: `IS*.hlsl` files - Post-processing effects (blur, depth of field, volumetric lighting) -- **Compute Shaders**: `*CS.hlsl` files - GPU parallel processing (deferred composite, ambient composite) -- **Common Utilities**: `Common/` directory with shared includes (BRDF.hlsli, Math.hlsli, GBuffer.hlsli) - -**Feature Shaders** (`features/*/Shaders/`): - -- **Feature-Specific**: Each feature has its own shader directory (e.g., `ScreenSpaceGI/`, `LightLimitFix/`) -- **Compute-Heavy Features**: Many use compute shaders for performance (ClusterBuildingCS.hlsl, gi.cs.hlsl) -- **Include Integration**: Features can use shared utilities from `package/Shaders/Common/` - -### Menu System - -Modular ImGui-based configuration interface with specialized renderers for different UI sections and centralized constants in `ThemeManager::Constants`. - -## Feature Development Workflow - -### Adding New Features - -1. Use template in `template/` directory as starting point -2. Implement `Feature` interface with required methods: - - `DrawSettings()` - ImGui configuration UI with performance impact warnings - - `LoadSettings()` - JSON deserialization - - `SaveSettings()` - JSON serialization - - Feature-specific rendering hooks with performance considerations -3. Add shader files to `features/NewFeature/Shaders/` with compute shader optimization -4. Create versioned `.ini` configuration file with performance-related settings -5. Register feature in appropriate source files and `globals::features` -6. **Performance Testing**: Measure GPU impact and provide user toggles for heavy features - -### Testing and Validation - -- **Shader Compilation**: Use hlslkit tools for validation before commit -- **Buffer Conflicts**: Run buffer_scan.py to detect register conflicts -- **Integration Testing**: Build and test in-game with various Skyrim editions -- **A/B Testing**: Use built-in A/B testing framework for performance comparisons - -### Version Management - -Feature versions are automatically extracted from `.ini` files and compiled into `FeatureVersions.h` at build time for backward compatibility checking. - -## Key Development Patterns - -### Memory Management - -- Modern C++23 with RAII principles -- Smart pointers for automatic resource management -- Thread pool (bshoshany-thread-pool) for parallel operations - -### Configuration System - -- JSON-based settings with nlohmann_json -- Hot-reload capability through ImGui interface -- Versioned feature configurations for compatibility - -### Error Handling - -- **Comprehensive Logging**: Integrated with SKSE logging system with different severity levels -- **Graceful Degradation**: Features should disable cleanly on shader compilation failures -- **User-Friendly Errors**: Report errors through ImGui interface with actionable guidance -- **Graphics-Specific Errors**: Handle DirectX device lost scenarios and shader compilation failures -- **Recovery Mechanisms**: Provide fallback rendering paths when advanced features fail -- **Error Context**: Include relevant graphics state (current shader, buffer sizes) in error messages - -### Performance Considerations - -**Runtime Graphics Performance** (Critical for Skyrim gameplay): - -- **Deferred Rendering Impact**: Features hook into Skyrim's rendering pipeline, adding GPU workload -- **Feature Toggles**: Users can disable individual features at boot if performance is impacted (`Disable at Boot` buttons) -- **A/B Testing Framework**: Built-in performance comparison system for measuring feature impact -- **VR Performance**: VR has higher performance requirements; some features may need different settings -- **Tracy Profiler**: Optional build-time integration (`TRACY_SUPPORT`) for detailed performance analysis - -**Shader Performance Patterns**: - -- **Compute Shaders**: Many features use compute shaders for parallel GPU processing (Screen Space GI, Light Limit Fix) -- **Buffer Management**: Careful GPU buffer allocation to avoid conflicts and minimize memory transfers -- **LOD Considerations**: Features should respect Skyrim's LOD system to maintain performance at distance -- **Resolution Scaling**: Consider how features scale with different rendering resolutions - -**Performance Testing**: - -- **In-Game Profiling**: Use Tracy integration to measure actual frame impact -- **Feature Isolation**: Test features individually to identify performance bottlenecks -- **Cross-Edition Impact**: SE/AE/VR may have different performance characteristics for the same feature - -### Development Performance - -- **Shader Testing**: Full validation suite can be time-consuming; use targeted testing during development -- **Build Performance**: Multi-threaded compilation with job control (`hlslkit-compile --jobs N`) -- **Iterative Development**: Test specific shader files/directories rather than entire shader suite - -## AI Assistant Guidelines - -### Role and Expertise - -**Act as an experienced graphics programming and Skyrim modding expert** with deep knowledge of: - -- DirectX 11/12 rendering pipelines and performance optimization -- SKSE plugin development and Skyrim's game engine internals -- CommonLibSSE-NG runtime targeting and cross-version compatibility -- HLSL shader development and GPU compute programming -- ImGui interface design and user experience considerations - -### Constructive Proactivity - -**Identify and address issues proactively**: - -- **Performance Concerns**: If code could impact rendering performance, suggest optimizations or user toggles -- **Security Risks**: Flag potential crashes from unvalidated user input, malformed configs, or unsafe DirectX operations -- **Runtime Compatibility**: Warn when code might break SE/AE/VR compatibility or suggest `REL::RelocateMember()` patterns -- **Buffer Conflicts**: Highlight potential GPU register conflicts and recommend hlslkit buffer scanning -- **Graphics Best Practices**: Suggest more idiomatic DirectX/HLSL patterns when appropriate - -**Implementation Standards**: - -- Provide complete, working solutions rather than TODO/FIXME placeholders -- Explain reasoning for graphics/performance-related changes -- Consider the full rendering pipeline impact of modifications -- Always include necessary error handling for graphics operations - -### Code Quality Expectations - -- **No Placeholders**: Never include TODO, FIXME, or incomplete implementations unless explicitly requested for planning -- **Complete Solutions**: Provide fully functional code with proper error handling and resource management -- **Performance Conscious**: Always consider GPU workload and user experience impact -- **Documentation**: Include Doxygen comments for public methods, especially graphics-related functions - -## Development Best Practices (Learned from Codebase) - -### Commit Message Standards - -Follow conventional commit format for consistency: - -- **Format**: `type(scope): description` -- **Title Limit**: 50 characters maximum -- **Body Wrap**: 72 characters per line -- **Types**: `feat`, `fix`, `refactor`, `docs`, `style`, `test`, `chore` -- **Examples**: - - `feat(menu): extract DrawMenuVisitor helper methods` - - `fix(imgui): resolve orphaned TableNextColumn calls` - - `refactor(constants): centralize UI constants in ThemeManager` - -### Code Organization and Refactoring Patterns - -- **Extract Large Functions**: Functions over ~200 lines should be broken into focused helper methods (see `FeatureListRenderer::DrawMenuVisitor` refactoring) -- **Centralize Constants**: Magic numbers should be extracted to named constants in appropriate classes (see `ThemeManager::Constants`) -- **Modular UI Design**: UI components should be separated by responsibility (Menu system uses HeaderRenderer, FeatureListRenderer, etc.) - -### ImGui Integration Patterns - -- **Table API Compliance**: Always pair `ImGui::BeginTable()` with `ImGui::EndTable()` - orphaned `TableNextColumn()` calls will cause issues -- **Style Management**: Use RAII pattern for ImGui style changes; avoid save/restore without actual modifications -- **Consistent Spacing**: Use centralized constants for UI spacing and padding rather than hardcoded values - -### Menu System Development - -- **Callback Pattern**: Use callbacks to access private methods from extracted UI components rather than making methods public -- **State Management**: UI state should be managed centrally in Menu class, with components receiving state as parameters -- **Documentation Standards**: Use Doxygen comments for all public methods, especially extracted utilities - -### Shader Development Workflow - -- **Build Before Test**: Always run `cmake --build ./build/ALL --target prepare_shaders` before shader validation -- **Targeted Testing**: Use specific shader/directory paths with hlslkit-compile during development to avoid full suite delays -- **Performance Optimization**: Use `--jobs`, `--strip-debug-defines`, and `--optimization-level` flags for faster compilation -- **Validation Early**: Use hlslkit validation in development, not just CI, to catch issues early - -### Testing and Validation - -- **Build Verification**: Always test builds after significant refactoring - this codebase has complex dependencies -- **Cross-Edition Testing**: Changes may affect SE/AE/VR differently due to engine differences -- **Memory Management**: Pay attention to smart pointer usage and RAII patterns when modifying existing code - -### Security and Input Validation - -- **Configuration Files**: Always validate `.ini` files and user settings - malformed configurations can crash Skyrim -- **Shader Input Validation**: Validate shader parameters and buffer sizes to prevent GPU driver crashes -- **File Path Validation**: Sanitize file paths for texture/asset loading to prevent directory traversal -- **Memory Safety**: Use bounds checking for buffer operations, especially with DirectX resource management -- **Resource Limits**: Enforce reasonable limits on user-configurable values (texture sizes, buffer counts, etc.) - -### Code Quality Standards - -- **Descriptive Naming**: Use domain-specific names that clearly indicate graphics/rendering purpose - - `screenSpaceAmbientOcclusion` not `ssao` - - `UpdateShadowCascades()` not `UpdateSC()` -- **Single Responsibility**: Each feature class should handle one graphics technique only -- **Function Complexity**: Keep rendering functions focused; extract complex GPU operations into separate methods -- **Resource Management**: Always pair graphics resource creation with proper cleanup (RAII) - -### Common Pitfalls to Avoid - -- **Include Dependencies**: New features often require adding includes (ShaderCache.h, imgui_stdlib.h, etc.) -- **Forward Declarations**: Use forward declarations in headers when possible, full includes in .cpp files -- **VR Considerations**: VR has different rendering requirements - check VR-specific code paths when modifying graphics features -- **Feature Versioning**: Feature .ini files use semantic versioning - increment appropriately when changing settings structure -- **Performance Impact**: Always consider GPU workload when adding new rendering features - provide toggle options for users -- **Buffer Conflicts**: Check hlslkit buffer scanning to avoid GPU register conflicts that cause rendering issues -- **Graphics State Corruption**: Minimize DirectX state changes; restore state after modifications -- **Thread Safety**: Graphics operations must consider Skyrim's rendering thread vs game logic thread diff --git a/CMakeLists.txt b/CMakeLists.txt index 5dfc027da5..594a4ec8c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,8 @@ find_package(cppwinrt CONFIG REQUIRED) find_package(unordered_dense CONFIG REQUIRED) find_package(efsw CONFIG REQUIRED) find_package(Tracy CONFIG REQUIRED) +find_package(effects11 CONFIG REQUIRED) + add_subdirectory(${CMAKE_SOURCE_DIR}/cmake/Streamline) include(FidelityFX-SDK) @@ -92,6 +94,8 @@ target_link_libraries( Tracy::TracyClient Streamline d3d12.lib + Microsoft::Effects11 + windowsapp ) # https://gitlab.kitware.com/cmake/cmake/-/issues/24922#note_1371990 diff --git a/features/ENBPostProcessing/Shaders/Features/ENBPostProcessing.ini b/features/ENBPostProcessing/Shaders/Features/ENBPostProcessing.ini new file mode 100644 index 0000000000..19f01444dc --- /dev/null +++ b/features/ENBPostProcessing/Shaders/Features/ENBPostProcessing.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 \ No newline at end of file diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 241945c700..a38de3c217 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -6,8 +6,6 @@ namespace SharedData { - -#if defined(PSHADER) || defined(CSHADER) || defined(COMPUTESHADER) cbuffer SharedData : register(b5) { float4 WaterData[25]; @@ -195,6 +193,81 @@ namespace SharedData float Strength; // [0, 1.0] The inverse blend weight of the effect }; + struct ENBSettings + { + float GradientIntensity; + float GradientDesaturation; + float GradientTopIntensity; + float GradientTopCurve; + + float3 GradientTopColorFilter; + float pad0; + + float GradientMiddleIntensity; + float GradientMiddleCurve; + float2 pad1; + + float3 GradientMiddleColorFilter; + float pad2; + + float GradientHorizonIntensity; + float GradientHorizonCurve; + float2 pad3; + + float3 GradientHorizonColorFilter; + float pad4; + + float CloudsIntensity; + float CloudsCurve; + float CloudsDesaturation; + float CloudsOpacity; + + float3 CloudsColorFilter; + float pad5; + + float DirectLightingIntensity; + float DirectLightingCurve; + float DirectLightingDesaturation; + float pad6; + + float3 DirectLightingColorFilter; + float pad6_1; + + float AmbientLightingIntensity; + float AmbientLightingDesaturation; + float2 pad7; + + float ColorPow; + float3 pad8; + + float FogColorMultiplier; + float FogColorCurve; + float FogAmountMultiplier; + float FogCurveMultiplier; + + float3 FogColorFilter; + float pad8_1; + + float IBLMultiplicativeAmount; + float3 pad9; + + float VolumetricFogIntensity; + float VolumetricFogCurve; + float VolumetricFogOpacity; + float pad10; + + float3 VolumetricFogColorFilter; + float pad11; + + float VolumetricRaysIntensity; + float VolumetricRaysRangeFactor; + float VolumetricRaysDesaturation; + float pad12; + + float3 VolumetricRaysColorFilter; + float pad13; + }; + cbuffer FeatureData : register(b6) { GrassLightingSettings grassLightingSettings; @@ -210,6 +283,7 @@ namespace SharedData TerrainVariationSettings terrainVariationSettings; IBLSettings iblSettings; ExtendedTranslucencySettings extendedTranslucencySettings; + ENBSettings enbSettings; }; Texture2D DepthTexture : register(t17); @@ -263,7 +337,5 @@ namespace SharedData waterData = WaterData[waterTile]; return waterData; } - -#endif // PSHADER } #endif // __SHARED_DATA_DEPENDENCY_HLSL__ \ No newline at end of file diff --git a/package/Shaders/Effect.hlsl b/package/Shaders/Effect.hlsl index 57746721f2..0801e45758 100644 --- a/package/Shaders/Effect.hlsl +++ b/package/Shaders/Effect.hlsl @@ -736,6 +736,12 @@ PS_OUTPUT main(PS_INPUT input) baseColor = baseColorMul * baseColor; baseColor.w *= softMul; +#if defined(SOFT) && defined(FALLOFF) && !defined(MEMBRANE) + baseColor.w = saturate(baseColor.w * SharedData::enbSettings.VolumetricFogOpacity); + baseColor.xyz = pow(baseColor, SharedData::enbSettings.VolumetricFogCurve); + baseColor.xyz = baseColor.xyz * SharedData::enbSettings.VolumetricFogIntensity * SharedData::enbSettings.VolumetricFogColorFilter; +#endif + # if defined(SOFT) && !(defined(FALLOFF) && defined(MULTBLEND)) if (baseColor.w - 0.003 < 0) { discard; diff --git a/package/Shaders/ISSAOComposite.hlsl b/package/Shaders/ISSAOComposite.hlsl index 7936864489..1b5558d76c 100644 --- a/package/Shaders/ISSAOComposite.hlsl +++ b/package/Shaders/ISSAOComposite.hlsl @@ -179,6 +179,11 @@ PS_OUTPUT main(PS_INPUT input) fogColor = ImageBasedLighting::GetFogIBLColor(fogColor); } # endif + + fogColor = pow(fogColor, SharedData::enbSettings.FogColorCurve) * SharedData::enbSettings.FogColorMultiplier * lerp(1.0, SharedData::enbSettings.FogColorFilter, SharedData::enbSettings.FogColorFilterAmount); + + fogFactor = saturate(lerp(0.0, pow(fogFactor, SharedData::enbSettings.FogCurveMultiplier), SharedData::enbSettings.FogAmountMultiplier)); + if (depth < 0.999999) { composedColor.xyz = FogNearColor.w * lerp(composedColor.xyz, fogColor, fogFactor); } diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 767a138ac2..c62b4db9e4 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -2300,6 +2300,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif float3 dirLightColor = Color::Light(DirLightColor.xyz); + + dirLightColor = lerp(dirLightColor, Color::RGBToLuminance(dirLightColor), SharedData::enbSettings.DirectLightingDesaturation) * SharedData::enbSettings.DirectLightingIntensity * SharedData::enbSettings.DirectLightingColorFilter; + float3 dirLightColorMultiplier = 1; # if defined(WATER_EFFECTS) diff --git a/package/Shaders/Sky.hlsl b/package/Shaders/Sky.hlsl index 3e8189d8e0..5fcd03426e 100644 --- a/package/Shaders/Sky.hlsl +++ b/package/Shaders/Sky.hlsl @@ -1,5 +1,7 @@ #include "Common/FrameBuffer.hlsli" #include "Common/VR.hlsli" +#include "Common/SharedData.hlsli" +#include "Common/Color.hlsli" struct VS_INPUT { @@ -124,10 +126,20 @@ VS_OUTPUT main(VS_INPUT input) vsout.TexCoord1.xy = TexCoordOff + input.TexCoord; # endif // TEXLERP - float3 skyColor = BlendColor[0].xyz * input.Color.xxx + BlendColor[1].xyz * input.Color.yyy + - BlendColor[2].xyz * input.Color.zzz; + float3 horizonColor = BlendColor[0].xyz; + float3 middleColor = BlendColor[1].xyz; + float3 topColor = BlendColor[2].xyz; - vsout.Color.xyz = VParams * skyColor; + horizonColor = pow(horizonColor, SharedData::enbSettings.GradientHorizonCurve) * SharedData::enbSettings.GradientHorizonIntensity * SharedData::enbSettings.GradientHorizonColorFilter; + middleColor = pow(middleColor, SharedData::enbSettings.GradientMiddleCurve) * SharedData::enbSettings.GradientMiddleIntensity * SharedData::enbSettings.GradientMiddleColorFilter; + topColor = pow(topColor, SharedData::enbSettings.GradientTopCurve) * SharedData::enbSettings.GradientTopIntensity * SharedData::enbSettings.GradientTopColorFilter; + + float3 skyColor = horizonColor * input.Color.x + middleColor * input.Color.y + topColor * input.Color.z; + skyColor *= VParams; + + skyColor = lerp(skyColor, Color::RGBToLuminance(skyColor), SharedData::enbSettings.GradientDesaturation) * SharedData::enbSettings.GradientIntensity; + + vsout.Color.xyz = skyColor; vsout.Color.w = BlendColor[0].w * input.Color.w; # endif // OCCLUSION MOONMASK HORIZFADE @@ -201,15 +213,35 @@ PS_OUTPUT main(PS_INPUT input) # ifndef OCCLUSION # ifndef TEXLERP float4 baseColor = TexBaseSampler.Sample(SampBaseSampler, input.TexCoord0.xy); + +# if defined(CLOUDS) + baseColor.w = saturate(baseColor.w * SharedData::enbSettings.CloudsOpacity); + baseColor.xyz = pow(baseColor.xyz, SharedData::enbSettings.CloudsCurve); + baseColor.xyz = lerp(baseColor.xyz, Color::RGBToLuminance(baseColor.xyz), SharedData::enbSettings.CloudsDesaturation) * SharedData::enbSettings.CloudsIntensity * SharedData::enbSettings.CloudsColorFilter; +# endif + # ifdef TEXFADE baseColor.w *= PParams.x; # endif # else float4 blendColor = TexBlendSampler.Sample(SampBlendSampler, input.TexCoord1.xy); float4 baseColor = TexBaseSampler.Sample(SampBaseSampler, input.TexCoord0.xy); + +# if defined(CLOUDS) + blendColor.w = saturate(blendColor.w * SharedData::enbSettings.CloudsOpacity); + blendColor.xyz = pow(blendColor.xyz, SharedData::enbSettings.CloudsCurve); + blendColor.xyz = lerp(blendColor.xyz, Color::RGBToLuminance(blendColor.xyz), SharedData::enbSettings.CloudsDesaturation) * SharedData::enbSettings.CloudsIntensity * SharedData::enbSettings.CloudsColorFilter; + + baseColor.w = saturate(baseColor.w * SharedData::enbSettings.CloudsOpacity); + baseColor.xyz = pow(baseColor.xyz, SharedData::enbSettings.CloudsCurve); + baseColor.xyz = lerp(baseColor.xyz, Color::RGBToLuminance(baseColor.xyz), SharedData::enbSettings.CloudsDesaturation) * SharedData::enbSettings.CloudsIntensity * SharedData::enbSettings.CloudsColorFilter; +# endif + baseColor = PParams.xxxx * (-baseColor + blendColor) + baseColor; # endif + + # if defined(DITHER) float2 noiseGradUv = float2(0.125, 0.125) * input.Position.xy; float noiseGrad = diff --git a/src/Deferred.cpp b/src/Deferred.cpp index f5f488ab65..ab4e612e53 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -900,6 +900,7 @@ void Deferred::Hooks::Renderer_ResetState::thunk(void* This) ID3D11Buffer* buffers[3] = { state->permutationCB->CB(), state->sharedDataCB->CB(), state->featureDataCB->CB() }; context->PSSetConstantBuffers(4, 3, buffers); context->CSSetConstantBuffers(5, 2, buffers + 1); + context->VSSetConstantBuffers(5, 2, buffers + 1); auto* singleton = globals::truePBR; singleton->SetupFrame(); diff --git a/src/Feature.cpp b/src/Feature.cpp index 59e8fbf390..ab3e19b985 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -4,6 +4,7 @@ #include "FeatureVersions.h" #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" +#include "Features/ENBPostProcessing.h" #include "Features/ExtendedMaterials.h" #include "Features/ExtendedTranslucency.h" #include "Features/GrassCollision.h" @@ -29,6 +30,7 @@ #include "Features/WaterEffects.h" #include "Features/WeatherPicker.h" #include "Features/WetnessEffects.h" + #include "Menu.h" #include "SettingsOverrideManager.h" #include "Utils/Format.h" @@ -223,7 +225,8 @@ const std::vector& Feature::GetFeatureList() &globals::features::interiorSun, &globals::features::terrainVariation, &globals::features::ibl, - &globals::features::extendedTranslucency + &globals::features::extendedTranslucency, + &globals::features::enbPostProcessing }; if (REL::Module::IsVR()) { diff --git a/src/FeatureBuffer.cpp b/src/FeatureBuffer.cpp index 745480a847..a478a184f0 100644 --- a/src/FeatureBuffer.cpp +++ b/src/FeatureBuffer.cpp @@ -2,6 +2,7 @@ #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" +#include "Features/ENBPostProcessing.h" #include "Features/ExtendedMaterials.h" #include "Features/ExtendedTranslucency.h" #include "Features/GrassLighting.h" @@ -47,5 +48,6 @@ std::pair GetFeatureBufferData(bool a_inWorld) globals::features::hairSpecular.settings, globals::features::terrainVariation.settings, globals::features::ibl.settings, - globals::features::extendedTranslucency.GetCommonBufferData()); + globals::features::extendedTranslucency.GetCommonBufferData(), + globals::features::enbPostProcessing.GetCommonBufferData()); } \ No newline at end of file diff --git a/src/Features/ENBPostProcessing.cpp b/src/Features/ENBPostProcessing.cpp new file mode 100644 index 0000000000..9ced15193f --- /dev/null +++ b/src/Features/ENBPostProcessing.cpp @@ -0,0 +1,143 @@ +#include "ENBPostProcessing.h" + +#include "ENBPostProcessing/EffectManager.h" +#include "ENBPostProcessing/MenuManager.h" +#include "ENBPostProcessing/SettingManager.h" + +void ENBPostProcessing::SaveSettings(json&) +{ +} + +void ENBPostProcessing::LoadSettings(json&) +{ +} + +void ENBPostProcessing::RestoreDefaultSettings() +{ +} + +ENBPostProcessing::PerFrame ENBPostProcessing::GetCommonBufferData() +{ + auto& settingManager = SettingManager::GetSingleton(); + PerFrame data{}; + + data.GradientIntensity = settingManager.GetInterpolatedTimeOfDayValue("GradientIntensity", "SKY"); + data.GradientDesaturation = settingManager.GetInterpolatedTimeOfDayValue("GradientDesaturation", "SKY"); + data.GradientTopIntensity = settingManager.GetInterpolatedTimeOfDayValue("GradientTopIntensity", "SKY"); + data.GradientTopCurve = settingManager.GetInterpolatedTimeOfDayValue("GradientTopCurve", "SKY"); + + data.GradientTopColorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("GradientTopColorFilter", "SKY"); + + data.GradientMiddleIntensity = settingManager.GetInterpolatedTimeOfDayValue("GradientMiddleIntensity", "SKY"); + data.GradientMiddleCurve = settingManager.GetInterpolatedTimeOfDayValue("GradientMiddleCurve", "SKY"); + + data.GradientMiddleColorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("GradientMiddleColorFilter", "SKY"); + + data.GradientHorizonIntensity = settingManager.GetInterpolatedTimeOfDayValue("GradientHorizonIntensity", "SKY"); + data.GradientHorizonCurve = settingManager.GetInterpolatedTimeOfDayValue("GradientHorizonCurve", "SKY"); + + data.GradientHorizonColorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("GradientHorizonColorFilter", "SKY"); + + data.CloudsIntensity = settingManager.GetInterpolatedTimeOfDayValue("CloudsIntensity", "SKY"); + data.CloudsCurve = settingManager.GetInterpolatedTimeOfDayValue("CloudsCurve", "SKY"); + data.CloudsDesaturation = settingManager.GetInterpolatedTimeOfDayValue("CloudsDesaturation", "SKY"); + data.CloudsOpacity = settingManager.GetInterpolatedTimeOfDayValue("CloudsOpacity", "SKY"); + + data.CloudsColorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("CloudsColorFilter", "SKY"); + + data.DirectLightingIntensity = settingManager.GetInterpolatedTimeOfDayValue("DirectLightingIntensity", "ENVIRONMENT"); + data.DirectLightingCurve = settingManager.GetInterpolatedTimeOfDayValue("DirectLightingCurve", "ENVIRONMENT"); + data.DirectLightingDesaturation = settingManager.GetInterpolatedTimeOfDayValue("DirectLightingDesaturation", "ENVIRONMENT"); + + auto dirLightColorFilterAmount = settingManager.GetInterpolatedTimeOfDayValue("DirectLightingColorFilterAmount", "ENVIRONMENT"); + auto dirLightColorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("DirectLightingColorFilter", "ENVIRONMENT"); + data.DirectLightingColorFilter = (1.0f - dirLightColorFilterAmount) * float3(1.0f) + dirLightColorFilterAmount * dirLightColorFilter; + + data.AmbientLightingIntensity = settingManager.GetInterpolatedTimeOfDayValue("AmbientLightingIntensity", "ENVIRONMENT"); + data.AmbientLightingDesaturation = settingManager.GetInterpolatedTimeOfDayValue("AmbientLightingDesaturation", "ENVIRONMENT"); + + data.ColorPow = settingManager.GetInterpolatedTimeOfDayValue("ColorPow", "ENVIRONMENT"); + + data.FogColorMultiplier = settingManager.GetInterpolatedTimeOfDayValue("FogColorMultiplier", "ENVIRONMENT"); + data.FogColorCurve = settingManager.GetInterpolatedTimeOfDayValue("FogColorCurve", "ENVIRONMENT"); + data.FogAmountMultiplier = settingManager.GetInterpolatedTimeOfDayValue("FogAmountMultiplier", "ENVIRONMENT"); + data.FogCurveMultiplier = settingManager.GetInterpolatedTimeOfDayValue("FogCurveMultiplier", "ENVIRONMENT"); + + auto fogColorFilterAmount = settingManager.GetInterpolatedTimeOfDayValue("FogColorFilterAmount", "ENVIRONMENT"); + auto fogColorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("FogColorFilter", "ENVIRONMENT"); + data.FogColorFilter = (1.0f - fogColorFilterAmount) * float3(1.0f) + fogColorFilterAmount * fogColorFilter; + + data.IBLMultiplicativeAmount = settingManager.GetInterpolatedTimeOfDayValue("MultiplicativeAmount", "IMAGEBASEDLIGHTING"); + + data.VolumetricFogIntensity = settingManager.GetInterpolatedTimeOfDayValue("Intensity", "VOLUMETRICFOG"); + data.VolumetricFogCurve = settingManager.GetInterpolatedTimeOfDayValue("Curve", "VOLUMETRICFOG"); + data.VolumetricFogOpacity = settingManager.GetInterpolatedTimeOfDayValue("Opacity", "VOLUMETRICFOG"); + + data.VolumetricFogColorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("ColorFilter", "VOLUMETRICFOG"); + + data.VolumetricRaysIntensity = settingManager.GetInterpolatedTimeOfDayValue("Intensity", "GAMEVOLUMETRICRAYS"); + data.VolumetricRaysRangeFactor = settingManager.GetInterpolatedTimeOfDayValue("RangeFactor", "GAMEVOLUMETRICRAYS"); + data.VolumetricRaysDesaturation = settingManager.GetInterpolatedTimeOfDayValue("Desaturation", "GAMEVOLUMETRICRAYS"); + + data.VolumetricRaysColorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("ColorFilter", "GAMEVOLUMETRICRAYS"); + + return data; +} + +void ENBPostProcessing::DrawSettings() +{ + MenuManager::GetSingleton().RenderImGui(); +} + +void ENBPostProcessing::SetupResources() +{ + auto& settingManager = SettingManager::GetSingleton(); + settingManager.RegisterBoolSetting("UseEffect", "GLOBAL", true, false); + + // Create shared texture resources + TextureManager::GetSingleton().Initialize(); + + // Then initialize the effects system + EffectManager::GetSingleton().Initialize(); + + // Load registered settings + settingManager.Load(); +} + +void ENBPostProcessing::Reset() +{ + // Reset effect state if needed +} + +struct Main_HDRTonemapBlendCinematic_Render +{ + static void thunk(RE::ImageSpaceManager* a1, RE::ImageSpaceEffect* a2, uint32_t a3, uint32_t a4, RE::ImageSpaceShaderParam* a5) + { + auto& settingManager = SettingManager::GetSingleton(); + if (settingManager.GetValue("UseEffect", "GLOBAL")) { + auto& effectManager = EffectManager::GetSingleton(); + + effectManager.UpdateCommonData(); + + const auto& commonData = effectManager.GetCommonData(); + settingManager.SetTimeOfDayData(commonData.timeOfDay1, commonData.timeOfDay2, commonData.eInteriorFactor); + settingManager.SetWeatherBlendFactors( + static_cast(commonData.weather[0]), + static_cast(commonData.weather[1]), + commonData.weather[2]); + + effectManager.ExecuteEffects(); + } else { + func(a1, a2, a3, a4, a5); + } + } + + static inline REL::Relocation func; +}; + +void ENBPostProcessing::PostPostLoad() +{ + stl::write_thunk_call(REL::RelocationID(99023, 105674).address() + REL::Relocate(0x1EA, 0x178)); + if (REL::Module::IsSE()) + stl::write_thunk_call(REL::RelocationID(99023, 105674).address() + REL::Relocate(0x230, 0x178)); +} diff --git a/src/Features/ENBPostProcessing.h b/src/Features/ENBPostProcessing.h new file mode 100644 index 0000000000..4539a69e98 --- /dev/null +++ b/src/Features/ENBPostProcessing.h @@ -0,0 +1,115 @@ +#pragma once + +struct ENBPostProcessing : Feature +{ +public: + struct Settings + { + bool Enabled = false; + std::string EffectPath = ""; + }; + + Settings settings; + + virtual inline std::string GetName() override { return "ENB Post Processing"; } + virtual inline std::string GetShortName() override { return "ENBPostProcessing"; } + virtual std::string_view GetCategory() const override { return "Post-Processing"; } + + virtual std::pair> GetFeatureSummary() override + { + return { + "ENB Post Processing provides a framework for loading and executing ENBSeries-compatible FX effect files.\n" + "This allows for advanced post-processing effects and visual enhancements using DirectX 11 Effect (.fx) files.", + { "ENBSeries-compatible FX support", + "DirectX 11 Effect file loading", + "Advanced post-processing pipeline", + "Custom technique execution", + "Dynamic UI variable system" } + }; + } + + struct alignas(16) PerFrame + { + float GradientIntensity; + float GradientDesaturation; + float GradientTopIntensity; + float GradientTopCurve; + + float3 GradientTopColorFilter; + float pad0; + + float GradientMiddleIntensity; + float GradientMiddleCurve; + float2 pad1; + + float3 GradientMiddleColorFilter; + float pad2; + + float GradientHorizonIntensity; + float GradientHorizonCurve; + float2 pad3; + + float3 GradientHorizonColorFilter; + float pad4; + + float CloudsIntensity; + float CloudsCurve; + float CloudsDesaturation; + float CloudsOpacity; + + float3 CloudsColorFilter; + float pad5; + + float DirectLightingIntensity; + float DirectLightingCurve; + float DirectLightingDesaturation; + float pad6; + + float3 DirectLightingColorFilter; + float pad6_1; + + float AmbientLightingIntensity; + float AmbientLightingDesaturation; + float2 pad7; + + float ColorPow; + float3 pad8; + + float FogColorMultiplier; + float FogColorCurve; + float FogAmountMultiplier; + float FogCurveMultiplier; + + float3 FogColorFilter; + float pad8_1; + + float IBLMultiplicativeAmount; + float3 pad9; + + float VolumetricFogIntensity; + float VolumetricFogCurve; + float VolumetricFogOpacity; + float pad10; + + float3 VolumetricFogColorFilter; + float pad11; + + float VolumetricRaysIntensity; + float VolumetricRaysRangeFactor; + float VolumetricRaysDesaturation; + float pad12; + + float3 VolumetricRaysColorFilter; + float pad13; + }; + + PerFrame GetCommonBufferData(); + + virtual void SaveSettings(json&) override; + virtual void LoadSettings(json&) override; + virtual void RestoreDefaultSettings() override; + virtual void DrawSettings() override; + virtual void SetupResources() override; + virtual void Reset() override; + virtual void PostPostLoad() override; +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/EffectManager.cpp b/src/Features/ENBPostProcessing/EffectManager.cpp new file mode 100644 index 0000000000..b616cc2f09 --- /dev/null +++ b/src/Features/ENBPostProcessing/EffectManager.cpp @@ -0,0 +1,738 @@ +#include "EffectManager.h" + +#include "State.h" + +#include "SettingManager.h" +#include "TextureManager.h" +#include "WeatherManager.h" + +#include +#include + +EffectManager& EffectManager::GetSingleton() +{ + static EffectManager instance; + return instance; +} + +void EffectManager::Initialize() +{ + TextureManager::GetSingleton().Initialize(); + RegisterSettings(); + CreateCommonResources(); + Apply(); +} + +void EffectManager::Apply() +{ + enbDepthOfField.Apply(); + enbBloom.Apply(); + enbLens.Apply(); + enbAdaptation.Apply(); + enbEffect.Apply(); + enbEffectPostPass.Apply(); +} + +void EffectManager::Load() +{ + enbDepthOfField.Load(); + enbBloom.Load(); + enbLens.Load(); + enbAdaptation.Load(); + enbEffect.Load(); + enbEffectPostPass.Load(); +} + +void EffectManager::Save() +{ + enbDepthOfField.Save(); + enbBloom.Save(); + enbLens.Save(); + enbAdaptation.Save(); + enbEffect.Save(); + enbEffectPostPass.Save(); +} + +void EffectManager::RegisterSettings() +{ + auto& settingManager = SettingManager::GetSingleton(); + + // Time of day settings + settingManager.RegisterFloatSetting("DawnDuration", "TIMEOFDAY", 1.6f, 0.1f, 12.0f, false); + settingManager.RegisterFloatSetting("SunriseTime", "TIMEOFDAY", 9.0f, 0.0f, 24.0f, false); + settingManager.RegisterFloatSetting("DayTime", "TIMEOFDAY", 12.0f, 0.0f, 24.0f, false); + settingManager.RegisterFloatSetting("SunsetTime", "TIMEOFDAY", 17.25f, 0.0f, 24.0f, false); + settingManager.RegisterFloatSetting("DuskDuration", "TIMEOFDAY", 2.0f, 0.1f, 12.0f, false); + settingManager.RegisterFloatSetting("NightTime", "TIMEOFDAY", 1.0f, 0.0f, 24.0f, false); + + settingManager.RegisterBoolSetting("EnablePostPassShader", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableAdaptation", "EFFECT", true, false); + settingManager.RegisterBoolSetting("EnableBloom", "EFFECT", true, false); + settingManager.RegisterBoolSetting("EnableLens", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableDepthOfField", "EFFECT", true, false); + + settingManager.RegisterFloatSetting("Brightness", "COLORCORRECTION", 1.0f, 0.0f, 3.0f, false); + settingManager.RegisterFloatSetting("GammaCurve", "COLORCORRECTION", 1.0f, 0.1f, 3.0f, false); + + settingManager.RegisterFloatSetting("AdaptationSensitivity", "ADAPTATION", 1.0f, 0.0f, 5.0f, false); + settingManager.RegisterBoolSetting("ForceMinMaxValues", "ADAPTATION", false, false); + settingManager.RegisterFloatSetting("AdaptationMin", "ADAPTATION", 0.0f, 0.0f, 1.0f, false); + settingManager.RegisterFloatSetting("AdaptationMax", "ADAPTATION", 1.0f, 0.0f, 2.0f, false); + settingManager.RegisterFloatSetting("AdaptationTime", "ADAPTATION", 1.0f, 0.1f, 10.0f, false); + + settingManager.RegisterFloatSetting("FocusingTime", "DEPTHOFFIELD", 1.0f, 0.1f, 10.0f, false); + settingManager.RegisterFloatSetting("ApertureTime", "DEPTHOFFIELD", 1.0f, 0.1f, 10.0f, false); + + settingManager.RegisterTimeOfDaySetting("Amount", "BLOOM", 1, true); + settingManager.RegisterTimeOfDaySetting("Amount", "LENS", 1, true); + + // SKY + settingManager.RegisterTimeOfDaySetting("GradientIntensity", "SKY", 1, true); + settingManager.RegisterTimeOfDaySetting("GradientDesaturation", "SKY", 0, true); + + settingManager.RegisterTimeOfDaySetting("GradientTopIntensity", "SKY", 1, true); + settingManager.RegisterTimeOfDaySetting("GradientTopCurve", "SKY", 1, true); + settingManager.RegisterColorTimeOfDaySetting("GradientTopColorFilter", "SKY", { 1.0f, 1.0f, 1.0f }, true); + + settingManager.RegisterTimeOfDaySetting("GradientMiddleIntensity", "SKY", 1, true); + settingManager.RegisterTimeOfDaySetting("GradientMiddleCurve", "SKY", 1, true); + settingManager.RegisterColorTimeOfDaySetting("GradientMiddleColorFilter", "SKY", { 1.0f, 1.0f, 1.0f }, true); + + settingManager.RegisterTimeOfDaySetting("GradientHorizonIntensity", "SKY", 1, true); + settingManager.RegisterTimeOfDaySetting("GradientHorizonCurve", "SKY", 1, true); + settingManager.RegisterColorTimeOfDaySetting("GradientHorizonColorFilter", "SKY", { 1.0f, 1.0f, 1.0f }, true); + + settingManager.RegisterTimeOfDaySetting("CloudsIntensity", "SKY", 1, true); + settingManager.RegisterTimeOfDaySetting("CloudsCurve", "SKY", 1, true); + settingManager.RegisterTimeOfDaySetting("CloudsDesaturation", "SKY", 0, true); + settingManager.RegisterTimeOfDaySetting("CloudsOpacity", "SKY", 1, true); + settingManager.RegisterColorTimeOfDaySetting("CloudsColorFilter", "SKY", { 1.0f, 1.0f, 1.0f }, true); + + // ENVIRONMENT + settingManager.RegisterTimeOfDaySetting("DirectLightingIntensity", "ENVIRONMENT", 1, true); + settingManager.RegisterTimeOfDaySetting("DirectLightingCurve", "ENVIRONMENT", 1, true); + settingManager.RegisterTimeOfDaySetting("DirectLightingDesaturation", "ENVIRONMENT", 0, true); + + settingManager.RegisterTimeOfDaySetting("AmbientLightingIntensity", "ENVIRONMENT", 1, true); + settingManager.RegisterTimeOfDaySetting("AmbientLightingDesaturation", "ENVIRONMENT", 0, true); + + settingManager.RegisterColorTimeOfDaySetting("DirectLightingColorFilter", "ENVIRONMENT", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("DirectLightingColorFilterAmount", "ENVIRONMENT", 1, true); + + settingManager.RegisterTimeOfDaySetting("FogColorMultiplier", "ENVIRONMENT", 1, true); + settingManager.RegisterTimeOfDaySetting("FogColorCurve", "ENVIRONMENT", 1, true); + settingManager.RegisterTimeOfDaySetting("FogAmountMultiplier", "ENVIRONMENT", 1, true); + settingManager.RegisterTimeOfDaySetting("FogCurveMultiplier", "ENVIRONMENT", 1, true); + settingManager.RegisterTimeOfDaySetting("FogColorMultiplier", "ENVIRONMENT", 1, true); + settingManager.RegisterColorTimeOfDaySetting("FogColorFilter", "ENVIRONMENT", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("FogColorFilterAmount", "ENVIRONMENT", 0, true); + + settingManager.RegisterTimeOfDaySetting("ColorPow", "ENVIRONMENT", 1, true); + + // IMAGEBASEDLIGHTING + settingManager.RegisterTimeOfDaySetting("MultiplicativeAmount", "IMAGEBASEDLIGHTING", 0, true); + + // VOLUMETRICFOG + settingManager.RegisterTimeOfDaySetting("Intensity", "VOLUMETRICFOG", 1, true); + settingManager.RegisterTimeOfDaySetting("Curve", "VOLUMETRICFOG", 1, true); + settingManager.RegisterTimeOfDaySetting("Opacity", "VOLUMETRICFOG", 1, true); + settingManager.RegisterColorTimeOfDaySetting("ColorFilter", "VOLUMETRICFOG", { 1.0f, 1.0f, 1.0f }, true); + + // GAMEVOLUMETRICRAYS + settingManager.RegisterTimeOfDaySetting("Intensity", "GAMEVOLUMETRICRAYS", 1, true); + settingManager.RegisterTimeOfDaySetting("RangeFactor", "GAMEVOLUMETRICRAYS", 1, true); + settingManager.RegisterTimeOfDaySetting("Desaturation", "GAMEVOLUMETRICRAYS", 0, true); + settingManager.RegisterColorTimeOfDaySetting("ColorFilter", "GAMEVOLUMETRICRAYS", { 1.0f, 1.0f, 1.0f }, true); +} + +void EffectManager::ExecuteEffects() +{ + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + + auto textureOriginal = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + + // Set our render state + context->RSSetState(rasterizerState.get()); + context->OMSetBlendState(blendState.get(), nullptr, 0xFFFFFFFF); + context->OMSetDepthStencilState(nullptr, 0); + + UINT stride = sizeof(float) * 5; + UINT offset = 0; + ID3D11Buffer* vertexBuffers[] = { quadVertexBuffer.get() }; + context->IASetVertexBuffers(0, 1, vertexBuffers, &stride, &offset); + context->IASetInputLayout(inputLayout.get()); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + + // Apply brightness and gamma curve + ApplyColorCorrection(textureOriginal.UAV); + + auto state = globals::state; + + auto& settingManager = SettingManager::GetSingleton(); + auto& textureManager = TextureManager::GetSingleton(); + + if (enbDepthOfField.IsCompiled() && settingManager.GetValue("EnableDepthOfField", "EFFECT")) { + state->BeginPerfEvent(enbDepthOfField.GetName()); + UpdateCommonVariablesForEffect(enbDepthOfField.GetEffect()); + enbDepthOfField.UpdateEffectVariables(); + enbDepthOfField.Execute(); + state->EndPerfEvent(); + } + + // Downsampled texture shared between bloom, lens and adaptation + textureManager.UpdateDownsampledTexture(textureOriginal.SRV); + + if (enbBloom.IsCompiled() && settingManager.GetValue("EnableBloom", "EFFECT")) { + state->BeginPerfEvent(enbBloom.GetName()); + UpdateCommonVariablesForEffect(enbBloom.GetEffect()); + enbBloom.UpdateEffectVariables(); + enbBloom.Execute(); + state->EndPerfEvent(); + } + + if (enbLens.IsCompiled() && settingManager.GetValue("EnableLens", "EFFECT")) { + state->BeginPerfEvent(enbLens.GetName()); + UpdateCommonVariablesForEffect(enbLens.GetEffect()); + enbLens.UpdateEffectVariables(); + enbLens.Execute(); + state->EndPerfEvent(); + } + + if (enbAdaptation.IsCompiled() && settingManager.GetValue("EnableAdaptation", "EFFECT")) { + state->BeginPerfEvent(enbAdaptation.GetName()); + UpdateCommonVariablesForEffect(enbAdaptation.GetEffect()); + enbAdaptation.UpdateEffectVariables(); + enbAdaptation.Execute(); + state->EndPerfEvent(); + } + + if (enbEffect.IsCompiled()) { + state->BeginPerfEvent(enbEffect.GetName()); + UpdateCommonVariablesForEffect(enbEffect.GetEffect()); + enbEffect.UpdateEffectVariables(); + enbEffect.Execute(); + state->EndPerfEvent(); + } + + if (enbEffectPostPass.IsCompiled() && settingManager.GetValue("EnablePostPassShader", "EFFECT")) { + state->BeginPerfEvent(enbEffectPostPass.GetName()); + UpdateCommonVariablesForEffect(enbEffectPostPass.GetEffect()); + enbEffectPostPass.UpdateEffectVariables(); + enbEffectPostPass.Execute(); + state->EndPerfEvent(); + } + + textureManager.IncrementTextureSwap(); + + // Copy final render target to framebuffers + auto textureSDRTemp = TextureManager::GetSingleton().GetCommonTexture("TextureSDRTemp"); + auto textureFramebuffer1 = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kFRAMEBUFFER]; + auto textureFramebuffer2 = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kIMAGESPACE_TEMP_COPY]; + auto textureFramebuffer3 = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kIMAGESPACE_TEMP_COPY2]; + + CopyTexture(textureSDRTemp->srv.get(), textureFramebuffer1.RTV); + CopyTexture(textureSDRTemp->srv.get(), textureFramebuffer2.RTV); + CopyTexture(textureSDRTemp->srv.get(), textureFramebuffer3.RTV); +} + +void EffectManager::CreateCommonResources() +{ + CreateQuadGeometry(); + CreateRenderStates(); + CreateCopyShaders(); + CreateColorCorrectionShader(); +} + +void EffectManager::CreateQuadGeometry() +{ + // Create a fullscreen quad vertex buffer that all effects can share + struct QuadVertex + { + float position[3]; + float texCoord[2]; + }; + + QuadVertex vertices[] = { + { { -1.0f, -1.0f, 0.0f }, { 0.0f, 1.0f } }, // Bottom left + { { -1.0f, 1.0f, 0.0f }, { 0.0f, 0.0f } }, // Top left + { { 1.0f, -1.0f, 0.0f }, { 1.0f, 1.0f } }, // Bottom right + { { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f } } // Top right + }; + + D3D11_BUFFER_DESC bufferDesc = {}; + bufferDesc.Usage = D3D11_USAGE_DEFAULT; + bufferDesc.ByteWidth = sizeof(vertices); + bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; + bufferDesc.CPUAccessFlags = 0; + + D3D11_SUBRESOURCE_DATA initData = {}; + initData.pSysMem = vertices; + + DX::ThrowIfFailed(globals::d3d::device->CreateBuffer(&bufferDesc, &initData, quadVertexBuffer.put())); + + // Create input layout for ENB post-processing + D3D11_INPUT_ELEMENT_DESC inputElementDescs[] = { + { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 } + }; + + // Create a simple vertex shader for the input layout + winrt::com_ptr vertexShaderBlob; + const char* vertexShaderSource = R"( + struct VS_INPUT_POST { float3 pos : POSITION; float2 txcoord : TEXCOORD0; }; + struct VS_OUTPUT_POST { float4 pos : SV_POSITION; float2 txcoord0 : TEXCOORD0; }; + VS_OUTPUT_POST VS_Draw(VS_INPUT_POST IN) { + VS_OUTPUT_POST OUT; + OUT.pos = float4(IN.pos, 1.0); + OUT.txcoord0 = IN.txcoord; + return OUT; + } + )"; + + winrt::com_ptr errorBlob; + HRESULT hr = D3DCompile(vertexShaderSource, strlen(vertexShaderSource), nullptr, nullptr, nullptr, + "VS_Draw", "vs_4_0", 0, 0, vertexShaderBlob.put(), errorBlob.put()); + + if (SUCCEEDED(hr)) { + hr = globals::d3d::device->CreateInputLayout(inputElementDescs, ARRAYSIZE(inputElementDescs), + vertexShaderBlob->GetBufferPointer(), + vertexShaderBlob->GetBufferSize(), + inputLayout.put()); + if (FAILED(hr)) { + logger::error("[ENBPP] Failed to create shared input layout for ENB effects"); + } + } +} + +void EffectManager::CreateRenderStates() +{ + // Rasterizer state for fullscreen quads + D3D11_RASTERIZER_DESC rastDesc = {}; + rastDesc.FillMode = D3D11_FILL_SOLID; + rastDesc.CullMode = D3D11_CULL_NONE; + rastDesc.FrontCounterClockwise = FALSE; + rastDesc.DepthBias = 0; + rastDesc.DepthBiasClamp = 0.0f; + rastDesc.SlopeScaledDepthBias = 0.0f; + rastDesc.DepthClipEnable = TRUE; + rastDesc.ScissorEnable = FALSE; + rastDesc.MultisampleEnable = FALSE; + rastDesc.AntialiasedLineEnable = FALSE; + + DX::ThrowIfFailed(globals::d3d::device->CreateRasterizerState(&rastDesc, rasterizerState.put())); + + // Blend state for standard rendering (no blending) + D3D11_BLEND_DESC blendDesc = {}; + blendDesc.AlphaToCoverageEnable = FALSE; + blendDesc.IndependentBlendEnable = FALSE; + blendDesc.RenderTarget[0].BlendEnable = FALSE; + blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + + DX::ThrowIfFailed(globals::d3d::device->CreateBlendState(&blendDesc, blendState.put())); +} + +void EffectManager::CreateCopyShaders() +{ + // Compile vertex shader for texture copy + const char* vertexShaderSource = R"( + struct VS_INPUT { float3 pos : POSITION; float2 txcoord : TEXCOORD0; }; + struct VS_OUTPUT { float4 pos : SV_POSITION; float2 txcoord0 : TEXCOORD0; }; + + VS_OUTPUT main(VS_INPUT input) { + VS_OUTPUT output; + output.pos = float4(input.pos, 1.0); + output.txcoord0 = input.txcoord; + return output; + } + )"; + + winrt::com_ptr vsBlob, errorBlob; + HRESULT hr = D3DCompile(vertexShaderSource, strlen(vertexShaderSource), nullptr, nullptr, nullptr, + "main", "vs_4_0", 0, 0, vsBlob.put(), errorBlob.put()); + + if (FAILED(hr)) { + if (errorBlob) { + logger::error("[ENBPP] Failed to compile copy vertex shader: {}", static_cast(errorBlob->GetBufferPointer())); + } + return; + } + + hr = globals::d3d::device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, copyVertexShader.put()); + if (FAILED(hr)) { + logger::error("[ENBPP] Failed to create copy vertex shader"); + return; + } + + // Compile pixel shader for texture copy + const char* pixelShaderSource = R"( + Texture2D SourceTexture : register(t0); + + struct PS_INPUT { float4 pos : SV_POSITION; float2 txcoord0 : TEXCOORD0; }; + + float4 main(PS_INPUT input) : SV_TARGET { + return SourceTexture.Load(int3(input.pos.xy, 0)); + } + )"; + + winrt::com_ptr psBlob; + hr = D3DCompile(pixelShaderSource, strlen(pixelShaderSource), nullptr, nullptr, nullptr, + "main", "ps_4_0", 0, 0, psBlob.put(), errorBlob.put()); + + if (FAILED(hr)) { + if (errorBlob) { + logger::error("[ENBPP] Failed to compile copy pixel shader: {}", static_cast(errorBlob->GetBufferPointer())); + } + return; + } + + hr = globals::d3d::device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, copyPixelShader.put()); + if (FAILED(hr)) { + logger::error("[ENBPP] Failed to create copy pixel shader"); + return; + } + + logger::info("[ENBPP] Created texture copy shaders successfully"); +} + +void EffectManager::CreateColorCorrectionShader() +{ + // Compile compute shader for color correction + const char* computeShaderSource = R"( + cbuffer ColorCorrectionParams : register(b0) + { + float Brightness; + float GammaCurve; + }; + + RWTexture2D OutputTexture : register(u0); + + [numthreads(8, 8, 1)] + void main(uint3 id : SV_DispatchThreadID) + { + float4 color = OutputTexture[id.xy]; + color.rgb = pow(color.rgb, GammaCurve); + color.rgb *= Brightness; + OutputTexture[id.xy] = max(0, color); + } + )"; + + winrt::com_ptr csBlob, errorBlob; + HRESULT hr = D3DCompile(computeShaderSource, strlen(computeShaderSource), nullptr, nullptr, nullptr, + "main", "cs_5_0", 0, 0, csBlob.put(), errorBlob.put()); + + if (FAILED(hr)) { + if (errorBlob) { + logger::error("[ENBPP] Failed to compile color correction compute shader: {}", static_cast(errorBlob->GetBufferPointer())); + } + return; + } + + hr = globals::d3d::device->CreateComputeShader(csBlob->GetBufferPointer(), csBlob->GetBufferSize(), nullptr, colorCorrectionComputeShader.put()); + if (FAILED(hr)) { + logger::error("[ENBPP] Failed to create color correction compute shader"); + return; + } + + // Create constant buffer + D3D11_BUFFER_DESC cbDesc = {}; + cbDesc.Usage = D3D11_USAGE_DYNAMIC; + cbDesc.ByteWidth = sizeof(float) * 4; // Brightness, GammaCurve, padding[2] + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + + hr = globals::d3d::device->CreateBuffer(&cbDesc, nullptr, colorCorrectionConstantBuffer.put()); + if (FAILED(hr)) { + logger::error("[ENBPP] Failed to create color correction constant buffer"); + return; + } + + logger::info("[ENBPP] Created color correction compute shader successfully"); +} + +void EffectManager::UpdateCommonData() +{ + auto state = globals::state; + auto sky = globals::game::sky; + + // Update timer + { + auto delta = (*globals::game::deltaTime); + + static double timer = 0.0f; + timer += delta; + + static uint frameCount = 0; + + auto modifiedTimer = std::fmodf(static_cast(timer), 16777216); + modifiedTimer /= 16777216.0f; + + commonData.timer[0] = modifiedTimer; + commonData.timer[1] = 60.0f; + commonData.timer[2] = static_cast(frameCount % 9999); + commonData.timer[3] = delta; + + frameCount++; + } + + // Update screen size + { + float aspect = state->screenSize.x / state->screenSize.y; + + commonData.screenSize[0] = state->screenSize.x; + commonData.screenSize[1] = 1.0f / state->screenSize.x; + commonData.screenSize[2] = aspect; + commonData.screenSize[3] = 1.0f / aspect; + } + + // Update weather + { + // Strip plugin index (2 leftmost digits) from form IDs + auto stripPluginIndex = [](uint32_t formID) -> uint32_t { + return formID & 0x00FFFFFF; // Keep only the lower 6 hex digits + }; + + commonData.weather[0] = sky->currentWeather ? static_cast(stripPluginIndex(sky->currentWeather->formID)) : 0; + commonData.weather[1] = sky->lastWeather ? static_cast(stripPluginIndex(sky->lastWeather->formID)) : 0; + commonData.weather[2] = sky->currentWeatherPct; + commonData.weather[3] = sky->currentGameHour; + } + + // Update time of day + { + auto& settingManager = SettingManager::GetSingleton(); + + // Clamp current time to valid range + float currentTime = std::clamp(sky->currentGameHour, 0.0f, 24.0f); + + // Load time of day settings + const float nightTime = settingManager.GetValue("NightTime", "TIMEOFDAY"); + const float sunriseTime = settingManager.GetValue("SunriseTime", "TIMEOFDAY"); + const float dawnDuration = settingManager.GetValue("DawnDuration", "TIMEOFDAY"); + const float dayTime = settingManager.GetValue("DayTime", "TIMEOFDAY"); + const float sunsetTime = settingManager.GetValue("SunsetTime", "TIMEOFDAY"); + const float duskDuration = settingManager.GetValue("DuskDuration", "TIMEOFDAY"); + + commonData.eInteriorFactor = !sky->mode.any(RE::Sky::Mode::kFull); + + // Initialize and set factors + float factors[6] = { 0.0f }; // dawn, sunrise, day, sunset, dusk, night + + if (!commonData.eInteriorFactor) { + // Calculate transition points + const float dawnStart = sunriseTime - dawnDuration; + const float dawnMid = sunriseTime - (dawnDuration * 0.5f); + const float sunsetMid = sunsetTime + (duskDuration * 0.5f); + const float duskEnd = sunsetTime + duskDuration; + + // Time points array with 24h wraparound + const float timePoints[] = { + nightTime, dawnStart, dawnMid, sunriseTime, dayTime, sunsetTime, sunsetMid, duskEnd, + nightTime + 24.0f, dawnStart + 24.0f, dawnMid + 24.0f, sunriseTime + 24.0f, + dayTime + 24.0f, sunsetTime + 24.0f, sunsetMid + 24.0f, duskEnd + 24.0f + }; + + // Find current and next time periods + int currentIdx = 0, nextIdx = 0; + float currentPeriodTime = 0.0f, nextPeriodTime = 24.0f; + + for (int i = 0; i < 16; i++) { + const float t = timePoints[i]; + if (currentTime >= t && t >= currentPeriodTime) { + currentIdx = i; + currentPeriodTime = t; + } + if (t > currentTime && nextPeriodTime >= t) { + nextIdx = i; + nextPeriodTime = t; + } + } + + // Map time point indices to time of day factors + constexpr int factorMapping[] = { 5, 0, 0, 1, 2, 3, 4, 5 }; // night, dawn, dawn, sunrise, day, sunset, dusk, night + const int currentFactor = factorMapping[currentIdx % 8]; + const int nextFactor = factorMapping[nextIdx % 8]; + + // Calculate blend weight + float timeDiff = std::abs(nextPeriodTime - currentPeriodTime); + if (timeDiff == 0.0f) + timeDiff = 1.0f; + + const float blend = std::abs(currentTime - currentPeriodTime) / timeDiff; + + if (currentFactor == nextFactor) { + factors[currentFactor] = 1.0f; + } else { + factors[currentFactor] = std::clamp(1.0f - blend, 0.0f, 1.0f); + factors[nextFactor] = std::clamp(blend, 0.0f, 1.0f); + } + + // Assign to output arrays + commonData.timeOfDay1[0] = factors[0]; // dawn + commonData.timeOfDay1[1] = factors[1]; // sunrise + commonData.timeOfDay1[2] = factors[2]; // day + commonData.timeOfDay1[3] = factors[3]; // sunset + commonData.timeOfDay2[0] = factors[4]; // dusk + commonData.timeOfDay2[1] = factors[5]; // night + } + + // Calculate distance to night time (handling 24h wraparound) + float distToNight = std::abs(currentTime - nightTime); + if (distToNight > 12.0f) { + distToNight = 24.0f - distToNight; + } + + // Calculate distance to day time (handling 24h wraparound) + float distToDay = std::abs(currentTime - dayTime); + if (distToDay > 12.0f) { + distToDay = 24.0f - distToDay; + } + + // Night/day factor: 0.0 = pure night, 1.0 = pure day + // Based on relative proximity to day vs night times + if (distToNight + distToDay > 0.0f) { + commonData.eNightDayFactor = distToNight / (distToNight + distToDay); + } else { + commonData.eNightDayFactor = 0.5f; // Fallback if both distances are 0 + } + + commonData.timeOfDay2[2] = commonData.eInteriorFactor * commonData.eNightDayFactor; // interior day + commonData.timeOfDay2[3] = commonData.eInteriorFactor * (1.0f - commonData.eNightDayFactor); // interior night + } +} + +void EffectManager::UpdateCommonVariablesForEffect(ID3DX11Effect* effect) +{ + if (!effect) + return; + + auto renderer = globals::game::renderer; + + // Set common textures + Effect::SetShaderResourceVariable(effect, "TextureDepth", + renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].depthSRV); + + // Set format-specific render targets + const std::vector formatTargets = { + "RenderTargetRGBA32", "RenderTargetRGBA64", "RenderTargetRGBA64F", + "RenderTargetR16F", "RenderTargetR32F", "RenderTargetRGB32F" + }; + + auto& textureManager = TextureManager::GetSingleton(); + for (const auto& targetName : formatTargets) { + auto* texture = textureManager.GetCommonTexture(targetName); + if (texture) { + Effect::SetShaderResourceVariable(effect, targetName, texture->srv.get()); + } + } + + // Set fixed-size render targets + const std::vector fixedSizeTargets = { + "RenderTarget1024", "RenderTarget512", "RenderTarget256", "RenderTarget128", + "RenderTarget64", "RenderTarget32", "RenderTarget16" + }; + + for (const auto& targetName : fixedSizeTargets) { + auto* texture = textureManager.GetCommonTexture(targetName); + if (texture) { + Effect::SetShaderResourceVariable(effect, targetName, texture->srv.get()); + } + } + + // Set vector variables + Effect::SetVectorVariable(effect, "Timer", commonData.timer, sizeof(commonData.timer)); + Effect::SetVectorVariable(effect, "ScreenSize", commonData.screenSize, sizeof(commonData.screenSize)); + Effect::SetVectorVariable(effect, "Weather", commonData.weather, sizeof(commonData.weather)); + Effect::SetVectorVariable(effect, "TimeOfDay1", commonData.timeOfDay1, sizeof(commonData.timeOfDay1)); + Effect::SetVectorVariable(effect, "TimeOfDay2", commonData.timeOfDay2, sizeof(commonData.timeOfDay2)); + Effect::SetVectorVariable(effect, "ENightDayFactor", &commonData.eNightDayFactor, sizeof(commonData.eNightDayFactor)); + Effect::SetVectorVariable(effect, "EInteriorFactor", &commonData.eInteriorFactor, sizeof(commonData.eInteriorFactor)); +} + +void EffectManager::CopyTexture(ID3D11ShaderResourceView* a_source, ID3D11RenderTargetView* a_dest) +{ + if (!a_source || !a_dest || !copyPixelShader || !copyVertexShader) { + logger::critical("[ENBPP] Invalid parameters or shaders not initialized for texture copy"); + return; + } + + auto context = globals::d3d::context; + + // Set up for copy operation + context->OMSetRenderTargets(1, &a_dest, nullptr); + context->OMSetDepthStencilState(nullptr, 0); + + // Set shaders + context->VSSetShader(copyVertexShader.get(), nullptr, 0); + context->PSSetShader(copyPixelShader.get(), nullptr, 0); + + // Set source texture + context->PSSetShaderResources(0, 1, &a_source); + + // Draw fullscreen quad + context->Draw(4, 0); +} + +void EffectManager::ApplyColorCorrection(ID3D11UnorderedAccessView* textureUAV) +{ + if (!textureUAV || !colorCorrectionComputeShader || !colorCorrectionConstantBuffer) { + logger::warn("[ENBPP] Invalid parameters or shaders not initialized for color correction"); + return; + } + + auto& settingManager = SettingManager::GetSingleton(); + + auto brightness = settingManager.GetValue("Brightness", "COLORCORRECTION"); + auto gammaCurve = settingManager.GetValue("GammaCurve", "COLORCORRECTION"); + + if (brightness == 1.0f && gammaCurve == 1.0f) + return; + + auto context = globals::d3d::context; + + // Update constant buffer with current settings + D3D11_MAPPED_SUBRESOURCE mapped; + HRESULT hr = context->Map(colorCorrectionConstantBuffer.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); + if (SUCCEEDED(hr)) { + float* cbData = static_cast(mapped.pData); + cbData[0] = brightness; + cbData[1] = gammaCurve; + context->Unmap(colorCorrectionConstantBuffer.get(), 0); + } + + // Set compute shader and resources + context->CSSetShader(colorCorrectionComputeShader.get(), nullptr, 0); + ID3D11Buffer* bufferArray[] = { colorCorrectionConstantBuffer.get() }; + context->CSSetConstantBuffers(0, 1, bufferArray); + context->CSSetUnorderedAccessViews(0, 1, &textureUAV, nullptr); + + // Get texture dimensions for dispatch + winrt::com_ptr resource; + textureUAV->GetResource(resource.put()); + winrt::com_ptr texture; + resource.as(texture); + D3D11_TEXTURE2D_DESC texDesc; + texture->GetDesc(&texDesc); + + // Dispatch compute shader (8x8 thread groups) + UINT dispatchX = (texDesc.Width + 7) / 8; + UINT dispatchY = (texDesc.Height + 7) / 8; + context->Dispatch(dispatchX, dispatchY, 1); + + // Clear bindings + ID3D11UnorderedAccessView* nullUAV = nullptr; + ID3D11Buffer* nullCB = nullptr; + context->CSSetShader(nullptr, nullptr, 0); + context->CSSetConstantBuffers(0, 1, &nullCB); + context->CSSetUnorderedAccessViews(0, 1, &nullUAV, nullptr); +} + +void EffectManager::RenderEffectsList() +{ + enbDepthOfField.RenderImGui(); + enbBloom.RenderImGui(); + enbLens.RenderImGui(); + enbAdaptation.RenderImGui(); + enbEffect.RenderImGui(); + enbEffectPostPass.RenderImGui(); +} \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/EffectManager.h b/src/Features/ENBPostProcessing/EffectManager.h new file mode 100644 index 0000000000..425d93f5c9 --- /dev/null +++ b/src/Features/ENBPostProcessing/EffectManager.h @@ -0,0 +1,82 @@ +#pragma once + +#include "Effects/ENBAdaptation.h" +#include "Effects/ENBBloom.h" +#include "Effects/ENBDepthOfField.h" +#include "Effects/ENBEffect.h" +#include "Effects/ENBEffectPostPass.h" +#include "Effects/ENBLens.h" + +class EffectManager +{ +public: + static EffectManager& GetSingleton(); + + // Effect execution + void ExecuteEffects(); + + // Lifecycle + void Initialize(); + + void Apply(); + void Load(); + void Save(); + + void RegisterSettings(); + + // Common variable management + void UpdateCommonVariablesForEffect(ID3DX11Effect* effect); + +public: + ENBDepthOfField enbDepthOfField; + ENBBloom enbBloom; + ENBLens enbLens; + ENBAdaptation enbAdaptation; + ENBEffect enbEffect; + ENBEffectPostPass enbEffectPostPass; + + // Common resources shared across effects + void CreateCommonResources(); + + // Shared D3D resources + winrt::com_ptr quadVertexBuffer; + winrt::com_ptr inputLayout; + winrt::com_ptr rasterizerState; + winrt::com_ptr blendState; + + // Copy shader resources + winrt::com_ptr copyVertexShader; + winrt::com_ptr copyPixelShader; + + // Color correction compute shader resources + winrt::com_ptr colorCorrectionComputeShader; + winrt::com_ptr colorCorrectionConstantBuffer; + + void CreateQuadGeometry(); + void CreateRenderStates(); + void CreateCopyShaders(); + void CreateColorCorrectionShader(); + + void RenderEffectsList(); + + // Common variable data (updated once, applied to all effects) + struct CommonVariableData + { + float timer[4]; + float screenSize[4]; + float weather[4]; + float timeOfDay1[4]; + float timeOfDay2[4]; + float eNightDayFactor; + float eInteriorFactor; + } commonData; + + void UpdateCommonData(); + const CommonVariableData& GetCommonData() const { return commonData; } + + // Texture copy using pixel shader + void CopyTexture(ID3D11ShaderResourceView* source, ID3D11RenderTargetView* destination); + + // Color correction using compute shader + void ApplyColorCorrection(ID3D11UnorderedAccessView* textureUAV); +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBAdaptation.cpp b/src/Features/ENBPostProcessing/Effects/ENBAdaptation.cpp new file mode 100644 index 0000000000..a168d7311c --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBAdaptation.cpp @@ -0,0 +1,46 @@ +#include "ENBAdaptation.h" + +#include "../SettingManager.h" +#include "../TextureManager.h" + +void ENBAdaptation::Execute() +{ + auto& textureManager = TextureManager::GetSingleton(); + + SetShaderResourceVariable("TextureCurrent", textureManager.GetDownsampleTextureBlurry()); + + ExecuteTechnique("Downsample", effectTextureCache["TextureCurrent"]); + + SetShaderResourceVariable("TextureCurrent", effectTextureCache["TextureCurrent"].srv.get()); + + // Use swap mechanism to determine input/output + const std::string texturePreviousName = (textureManager.GetTextureSwap() & 1) ? "TextureAdaptationSwap" : "TextureAdaptation"; + const std::string textureAdaptationName = (textureManager.GetTextureSwap() & 1) ? "TextureAdaptation" : "TextureAdaptationSwap"; + + // Set input texture (previous frame's adaptation value) + SetShaderResourceVariable("TexturePrevious", textureManager.GetCommonTexture(texturePreviousName)->srv.get()); + + // Execute adaptation technique, writing to output texture + auto* textureAdaptation = textureManager.GetCommonTexture(textureAdaptationName); + ExecuteTechnique(GetSelectedTechnique(), *textureAdaptation); +} + +void ENBAdaptation::UpdateEffectVariables() +{ + auto& settingManager = SettingManager::GetSingleton(); + + auto forceMinMaxValues = settingManager.GetValue("ForceMinMaxValues", "ADAPTATION"); + + float4 adaptationParameters{}; + adaptationParameters.x = !forceMinMaxValues ? 0.0f : settingManager.GetValue("AdaptationMin", "ADAPTATION"); + adaptationParameters.y = !forceMinMaxValues ? 65535.0f : settingManager.GetValue("AdaptationMax", "ADAPTATION"); + adaptationParameters.z = settingManager.GetValue("AdaptationSensitivity", "ADAPTATION"); + adaptationParameters.w = settingManager.GetValue("AdaptationTime", "ADAPTATION") * (*globals::game::deltaTime); + + SetVectorVariable("AdaptationParameters", &adaptationParameters, sizeof(adaptationParameters)); +} + +void ENBAdaptation::CreateEffectTextures() +{ + effectTextureCache["TextureCurrent"] = CreateTexture(16, 16, DXGI_FORMAT_R32_FLOAT, "ENBAdaptation::TextureCurrent"); +} \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBAdaptation.h b/src/Features/ENBPostProcessing/Effects/ENBAdaptation.h new file mode 100644 index 0000000000..f12bad333c --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBAdaptation.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Effect.h" + +class ENBAdaptation : public Effect +{ +public: + virtual std::string GetName() const override { return "enbadaptation.fx"; } + + virtual void Execute() override; + virtual void UpdateEffectVariables() override; + +protected: + // Override virtual texture creation function + void CreateEffectTextures() override; +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBBloom.cpp b/src/Features/ENBPostProcessing/Effects/ENBBloom.cpp new file mode 100644 index 0000000000..2b51625111 --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBBloom.cpp @@ -0,0 +1,26 @@ +#include "ENBBloom.h" + +#include "../TextureManager.h" + +void ENBBloom::Execute() +{ + // Get common textures for input/output + auto& textureManager = TextureManager::GetSingleton(); + + auto textureHDRTemp = textureManager.GetCommonTexture("TextureBloomTemp"); + auto textureBloom = textureManager.GetCommonTexture("TextureBloom"); + + // Set dowsampled texture, typically the one used (use 1024x1024 mip) + auto downsampledInputSRV = TextureManager::GetSingleton().GetDownsampleTexture(); + + ExecuteTechniqueSequence(GetSelectedTechnique(), downsampledInputSRV, *textureBloom, *textureHDRTemp); +} + +void ENBBloom::UpdateEffectVariables() +{ + // Set dowsampled texture, typically the one used (use 1024x1024 mip) + SetShaderResourceVariable("TextureDownsampled", TextureManager::GetSingleton().GetDownsampleTexture()); + + // Set original texture, not typically used due to aliasing + SetShaderResourceVariable("TextureOriginal", globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN].SRV); +} \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBBloom.h b/src/Features/ENBPostProcessing/Effects/ENBBloom.h new file mode 100644 index 0000000000..1a67272993 --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBBloom.h @@ -0,0 +1,12 @@ +#pragma once + +#include "Effect.h" + +class ENBBloom : public Effect +{ +public: + virtual std::string GetName() const override { return "enbbloom.fx"; } + + virtual void Execute() override; + virtual void UpdateEffectVariables() override; +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBDepthOfField.cpp b/src/Features/ENBPostProcessing/Effects/ENBDepthOfField.cpp new file mode 100644 index 0000000000..ecc70b0faa --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBDepthOfField.cpp @@ -0,0 +1,57 @@ +#include "ENBDepthOfField.h" + +#include "../SettingManager.h" +#include "../TextureManager.h" + +void ENBDepthOfField::Execute() +{ + auto& textureManager = TextureManager::GetSingleton(); + + const std::string texturePreviousApertureName = (textureManager.GetTextureSwap() & 1) ? "TextureApertureSwap" : "TextureAperture"; + const std::string textureApertureName = (textureManager.GetTextureSwap() & 1) ? "TextureAperture" : "TextureApertureSwap"; + + SetShaderResourceVariable("TexturePrevious", effectTextureCache[texturePreviousApertureName].srv.get()); + ExecuteTechnique("Aperture", effectTextureCache[textureApertureName]); + + SetShaderResourceVariable("TextureAperture", effectTextureCache[textureApertureName].srv.get()); + ExecuteTechnique("ReadFocus", effectTextureCache["TextureReadFocus"]); + + const std::string texturePreviousFocusName = (textureManager.GetTextureSwap() & 1) ? "TextureFocusSwap" : "TextureFocus"; + const std::string textureFocusName = (textureManager.GetTextureSwap() & 1) ? "TextureFocus" : "TextureFocusSwap"; + + SetShaderResourceVariable("TexturePrevious", effectTextureCache[texturePreviousFocusName].srv.get()); + SetShaderResourceVariable("TextureCurrent", effectTextureCache["TextureReadFocus"].srv.get()); + ExecuteTechnique("Focus", effectTextureCache[textureFocusName]); + + auto textureMain = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + auto textureHDRTemp = textureManager.GetCommonTexture("TextureHDRTemp"); + auto textureHDRTemp2 = textureManager.GetCommonTexture("TextureHDRTemp2"); + + globals::d3d::context->CopyResource(textureHDRTemp->texture.get(), textureMain.texture); + + SetShaderResourceVariable("TextureFocus", effectTextureCache[textureFocusName].srv.get()); + SetShaderResourceVariable("TextureOriginal", textureMain.SRV); + ExecuteTechniqueSequence(GetSelectedTechnique(), textureMain.SRV, *textureHDRTemp, *textureHDRTemp2); + + globals::d3d::context->CopyResource(textureMain.texture, textureHDRTemp->texture.get()); +} + +void ENBDepthOfField::UpdateEffectVariables() +{ + auto& settingManager = SettingManager::GetSingleton(); + + float4 dofParameters{}; + dofParameters.z = (*globals::game::deltaTime) * settingManager.GetValue("ApertureTime", "DEPTHOFFIELD"); + dofParameters.w = (*globals::game::deltaTime) * settingManager.GetValue("FocusingTime", "DEPTHOFFIELD"); + + SetVectorVariable("DofParameters", &dofParameters, sizeof(dofParameters)); +} + +void ENBDepthOfField::CreateEffectTextures() +{ + effectTextureCache["TextureAperture"] = CreateTexture(1, 1, DXGI_FORMAT_R32_FLOAT, "ENBDepthOfField::TextureAperture"); + effectTextureCache["TextureApertureSwap"] = CreateTexture(1, 1, DXGI_FORMAT_R32_FLOAT, "ENBDepthOfField::TextureApertureSwap"); + effectTextureCache["TextureReadFocus"] = CreateTexture(16, 16, DXGI_FORMAT_R32_FLOAT, "ENBDepthOfField::TextureReadFocus"); + effectTextureCache["TextureFocus"] = CreateTexture(1, 1, DXGI_FORMAT_R32_FLOAT, "ENBDepthOfField::TextureFocus"); + effectTextureCache["TextureFocusSwap"] = CreateTexture(1, 1, DXGI_FORMAT_R32_FLOAT, "ENBDepthOfField::TextureFocusSwap"); +} diff --git a/src/Features/ENBPostProcessing/Effects/ENBDepthOfField.h b/src/Features/ENBPostProcessing/Effects/ENBDepthOfField.h new file mode 100644 index 0000000000..eed452b0f3 --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBDepthOfField.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Effect.h" + +class ENBDepthOfField : public Effect +{ +public: + virtual std::string GetName() const override { return "enbdepthoffield.fx"; } + + virtual void Execute() override; + virtual void UpdateEffectVariables() override; + +protected: + // Override virtual texture creation function + void CreateEffectTextures() override; +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBEffect.cpp b/src/Features/ENBPostProcessing/Effects/ENBEffect.cpp new file mode 100644 index 0000000000..ceb6841874 --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBEffect.cpp @@ -0,0 +1,78 @@ +#include "ENBEffect.h" + +#include "../SettingManager.h" +#include "../TextureManager.h" + +void ENBEffect::Execute() +{ + auto renderer = globals::game::renderer; + + auto& textureManager = TextureManager::GetSingleton(); + + auto textureSDRTemp = textureManager.GetCommonTexture("TextureSDRTemp"); + auto textureSDRTemp2 = textureManager.GetCommonTexture("TextureSDRTemp2"); + + auto textureOriginal = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + + // Execute with: input (16bit HDR), output (10bit SDR), temp (10bit SDR) + ExecuteTechniqueSequence(GetSelectedTechnique(), textureOriginal.SRV, *textureSDRTemp, *textureSDRTemp2); +} + +void ENBEffect::UpdateEffectVariables() +{ + float4 params01[7]{}; + + auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); + auto& runtimeData = imageSpaceManager->GetRuntimeData(); + auto& baseData = runtimeData.data.baseData; + + auto& modAmount = runtimeData.data.modAmount; + auto& modData = runtimeData.data.modData; + + params01[2].x = baseData.hdr.receiveBloomThreshold; + params01[2].y = baseData.hdr.white * RE::GetINISetting("fReinhardWhiteScale:Display")->GetFloat(); + + params01[3].x = baseData.cinematic.saturation; + params01[3].z = baseData.cinematic.contrast; + params01[3].w = baseData.cinematic.brightness; + + params01[4] = { baseData.tint.color.red, + baseData.tint.color.green, + baseData.tint.color.blue, + baseData.tint.amount }; + + params01[5] = { modData.data[RE::ImageSpaceModData::kFadeR] * modAmount, + modData.data[RE::ImageSpaceModData::kFadeG] * modAmount, + modData.data[RE::ImageSpaceModData::kFadeB] * modAmount, + modData.data[RE::ImageSpaceModData::kFadeAmount] * modAmount }; + + params01[6] = { 1, 1, 1, 1 }; + + SetVectorVariable("Params01", ¶ms01, sizeof(params01)); + + auto& textureManager = TextureManager::GetSingleton(); + auto& settingManager = SettingManager::GetSingleton(); + + float4 enbParams01{}; + enbParams01.x = settingManager.GetInterpolatedTimeOfDayValue("Amount", "BLOOM"); + enbParams01.y = settingManager.GetInterpolatedTimeOfDayValue("Amount", "LENS"); + + SetVectorVariable("ENBParams01", &enbParams01, sizeof(enbParams01)); + + if (settingManager.GetValue("EnableBloom", "EFFECT")) + SetShaderResourceVariable("TextureBloom", textureManager.GetCommonTexture("TextureBloom")->srv.get()); + else + SetShaderResourceVariable("TextureBloom", nullptr); + + if (settingManager.GetValue("EnableLens", "EFFECT")) + SetShaderResourceVariable("TextureLens", textureManager.GetCommonTexture("TextureLens")->srv.get()); + else + SetShaderResourceVariable("TextureLens", nullptr); + + if (settingManager.GetValue("EnableAdaptation", "EFFECT")) { + const std::string textureAdaptationName = (textureManager.GetTextureSwap() & 1) ? "TextureAdaptation" : "TextureAdaptationSwap"; + SetShaderResourceVariable("TextureAdaptation", textureManager.GetCommonTexture(textureAdaptationName)->srv.get()); + } else { + SetShaderResourceVariable("TextureAdaptation", nullptr); + } +} \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBEffect.h b/src/Features/ENBPostProcessing/Effects/ENBEffect.h new file mode 100644 index 0000000000..5fac851d24 --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBEffect.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Effect.h" + +class ENBEffect : public Effect +{ +public: + virtual std::string GetName() const override { return "enbeffect.fx"; } + + virtual void Execute() override; + + virtual void UpdateEffectVariables() override; +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBEffectPostPass.cpp b/src/Features/ENBPostProcessing/Effects/ENBEffectPostPass.cpp new file mode 100644 index 0000000000..ed4e216415 --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBEffectPostPass.cpp @@ -0,0 +1,15 @@ +#include "ENBEffectPostPass.h" + +#include "../TextureManager.h" + +void ENBEffectPostPass::Execute() +{ + auto& textureManager = TextureManager::GetSingleton(); + + auto textureSDRTemp = textureManager.GetCommonTexture("TextureSDRTemp"); + auto textureSDRTemp2 = textureManager.GetCommonTexture("TextureSDRTemp2"); + + ExecuteTechniqueSequence(GetSelectedTechnique(), textureSDRTemp->srv.get(), *textureSDRTemp2, *textureSDRTemp); + + globals::d3d::context->CopyResource(textureSDRTemp->texture.get(), textureSDRTemp2->texture.get()); +} \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBEffectPostPass.h b/src/Features/ENBPostProcessing/Effects/ENBEffectPostPass.h new file mode 100644 index 0000000000..2509e48f64 --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBEffectPostPass.h @@ -0,0 +1,11 @@ +#pragma once + +#include "Effect.h" + +class ENBEffectPostPass : public Effect +{ +public: + virtual std::string GetName() const override { return "enbeffectpostpass.fx"; } + + virtual void Execute() override; +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBLens.cpp b/src/Features/ENBPostProcessing/Effects/ENBLens.cpp new file mode 100644 index 0000000000..14ffa88b4e --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBLens.cpp @@ -0,0 +1,29 @@ +#include "ENBLens.h" + +#include "../TextureManager.h" + +void ENBLens::Execute() +{ + // Get common textures for input/output + auto& textureManager = TextureManager::GetSingleton(); + + auto textureHDRTemp = textureManager.GetCommonTexture("TextureHDRTemp"); + auto textureLens = textureManager.GetCommonTexture("TextureLens"); + + // Set dowsampled texture, typically the one used (use 1024x1024 mip) + auto downsampledInputSRV = TextureManager::GetSingleton().GetDownsampleTexture(); + + ExecuteTechniqueSequence(GetSelectedTechnique(), downsampledInputSRV, *textureLens, *textureHDRTemp); +} + +void ENBLens::UpdateEffectVariables() +{ + if (!effect) + return; + + // Set dowsampled texture, typically the one used (use 1024x1024 mip) + SetShaderResourceVariable("TextureDownsampled", TextureManager::GetSingleton().GetDownsampleTexture()); + + // Set original texture, not typically used due to aliasing + SetShaderResourceVariable("TextureOriginal", globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN].SRV); +} \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/ENBLens.h b/src/Features/ENBPostProcessing/Effects/ENBLens.h new file mode 100644 index 0000000000..3d762801cb --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/ENBLens.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Effect.h" + +class ENBLens : public Effect +{ +public: + virtual std::string GetName() const override { return "enblens.fx"; } + + virtual void Execute() override; + + virtual void UpdateEffectVariables() override; +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/Effect.cpp b/src/Features/ENBPostProcessing/Effects/Effect.cpp new file mode 100644 index 0000000000..c4c56469f3 --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/Effect.cpp @@ -0,0 +1,1215 @@ +#include "Effect.h" +#include + +#include +#include + +#include "../TextureManager.h" +#include "State.h" + +bool Effect::Load() +{ + logger::debug("[ENBPP] Loading settings for effect '{}'", GetName()); + + // Create ini file path based on effect name + std::filesystem::path iniPath = "enbseries"; + iniPath /= GetName() + ".ini"; + + // Check if file exists + if (!std::filesystem::exists(iniPath)) { + logger::debug("[ENBPP] Could not find ini file '{}' for effect '{}', using defaults", iniPath.string(), GetName()); + return true; // Not an error, just use defaults + } + + // Prepare section name + std::string section = GetName(); + std::transform(section.begin(), section.end(), section.begin(), ::toupper); + + for (auto& uiVar : uiVariables) { + std::vector valueBuffer(1024); + DWORD result = GetPrivateProfileStringA(section.c_str(), uiVar.displayName.c_str(), "", valueBuffer.data(), 1024, iniPath.string().c_str()); + if (result > 0) { + std::string value(valueBuffer.data()); + LoadVariableFromString(uiVar, value); + } + } + + selectedTechniqueIndex = static_cast(GetPrivateProfileIntA(section.c_str(), "TECHNIQUE", selectedTechniqueIndex, iniPath.string().c_str())); + + selectedTechniqueIndex = std::clamp(selectedTechniqueIndex, 0u, (uint)uiTechniques.size() - 1u); + + logger::info("[ENBPP] Loaded settings from '{}' for effect '{}'", iniPath.string(), GetName()); + return true; +} + +void Effect::Save() +{ + logger::debug("[ENBPP] Saving settings for effect '{}'", GetName()); + + // Create ini file path based on effect name + std::filesystem::path iniPath = "enbseries"; + iniPath /= GetName() + ".ini"; + + // Prepare section name + std::string section = GetName(); + std::transform(section.begin(), section.end(), section.begin(), ::toupper); + + for (const auto& uiVar : uiVariables) { + std::string value; + + switch (uiVar.type) { + case UIVariableType::Float: + value = std::to_string(uiVar.floatValue); + break; + case UIVariableType::Int: + value = std::to_string(uiVar.intValue); + break; + case UIVariableType::Bool: + value = uiVar.boolValue ? "true" : "false"; + break; + case UIVariableType::Color3: + case UIVariableType::Color4: + { + std::ostringstream oss; + int numComponents = (uiVar.type == UIVariableType::Color3) ? 3 : 4; + + std::copy(uiVar.colorValue, uiVar.colorValue + numComponents - 1, + std::ostream_iterator(oss, ", ")); + oss << uiVar.colorValue[numComponents - 1]; + + value = oss.str(); + } + break; + } + + BOOL result = WritePrivateProfileStringA(section.c_str(), uiVar.displayName.c_str(), value.c_str(), iniPath.string().c_str()); + if (!result) { + logger::warn("[ENBPP] Failed to write key '{}' to ini file '{}'", uiVar.displayName, iniPath.string()); + } + } + + std::string techniqueValue = std::to_string(selectedTechniqueIndex); + BOOL techniqueResult = WritePrivateProfileStringA(section.c_str(), "TECHNIQUE", techniqueValue.c_str(), iniPath.string().c_str()); + if (!techniqueResult) { + logger::warn("[ENBPP] Failed to write TECHNIQUE key to ini file '{}'", iniPath.string()); + } + + logger::info("[ENBPP] Saved settings to '{}' for effect '{}'", iniPath.string(), GetName()); +} + +bool Effect::Apply() +{ + logger::info("[ENBPP] Applying effect '{}'", GetName()); + + Unload(); + + if (!LoadFXFile()) { + errors.push_back("Failed to compile FX file"); + logger::error("[ENBPP] Failed to compile FX file for effect '{}'", GetName()); + return false; + } + + if (!Load()) { + errors.push_back("Failed to load settings"); + logger::error("[ENBPP] Failed to load settings for effect '{}'", GetName()); + return false; + } + + // Call virtual texture creation function + CreateEffectTextures(); + + logger::info("[ENBPP] Successfully applied effect '{}'", GetName()); + return true; +} + +void Effect::Unload() +{ + effect = nullptr; + + techniques.clear(); + variables.clear(); + customTextureCache.clear(); + uiVariables.clear(); + availableTechniques.clear(); + effectTextureCache.clear(); + uiTechniques.clear(); + selectedTechniqueIndex = 0; + + errors.clear(); + + logger::info("[ENBPP] Unloaded effect '{}'", GetName()); +} + +bool Effect::LoadFXFile() +{ + auto filePath = std::filesystem::path("enbseries"); + filePath /= GetName(); + + winrt::com_ptr compiledShader; + winrt::com_ptr errorBlob; + + HRESULT hr = D3DX11CompileEffectFromFile( + filePath.c_str(), + nullptr, + D3D_COMPILE_STANDARD_FILE_INCLUDE, + NULL, + NULL, + globals::d3d::device, + effect.put(), + errorBlob.put()); + + if (FAILED(hr)) { + std::string errorMsg = "Compilation failed"; + if (errorBlob) { + errorMsg = std::string(static_cast(errorBlob->GetBufferPointer())); + logger::error("[ENBPP] Effect compilation failed: {}", errorMsg); + } + errors.push_back(errorMsg); + return false; + } + + // Common textures and variables are now managed by EffectManager + EnumerateAllVariables(); + + SetupCustomTextures(); + LoadTechniques(); + LoadUITechniques(); + + // Populate available techniques for UI selection + availableTechniques = GetBaseTechniqueNames(); + + // Set default selected technique to first annotated technique + if (!uiTechniques.empty()) { + selectedTechniqueIndex = 0; // Default to first annotated technique + } + + LoadUIVariables(); + + logger::debug("[ENBPP] Successfully loaded FX file: {}", filePath.string()); + return true; +} + +void Effect::ExecuteTechniqueSequence(const std::string& a_baseTechniqueName, ID3D11ShaderResourceView* a_input, TextureManager::Texture& a_output, TextureManager::Texture& a_temp) +{ + if (!IsCompiled() || !effect) { + return; // Skip execution if not compiled + } + + auto context = globals::d3d::context; + + // Check if the technique sequence exists + auto sequenceIt = techniques.find(a_baseTechniqueName); + if (sequenceIt == techniques.end()) { + logger::trace("[ENBPP] Technique sequence '{}' not found", a_baseTechniqueName); + return; + } + + const auto& sequence = sequenceIt->second; + + logger::trace("[ENBPP] Executing technique sequence '{}' with {} techniques", a_baseTechniqueName, sequence.size()); + + auto sourceTexture = effect->GetVariableByName("TextureColor")->AsShaderResource(); + + uint32_t swapCounter = 0; // Track swap count for ping-ponging between output and temp + bool targetInOutput = false; + + for (size_t i = 0; i < sequence.size(); ++i) { + auto& techniqueInfo = sequence[i]; + + logger::trace("[ENBPP] Executing technique {} in sequence '{}'", i, a_baseTechniqueName); + + D3DX11_TECHNIQUE_DESC techDesc; + techniqueInfo.technique->GetDesc(&techDesc); + + // Determine input and output for this technique + ID3D11ShaderResourceView* inputSRV; + ID3D11RenderTargetView* outputRTV; + + if (sequence.size() == 1) { + // Single technique: input -> output + inputSRV = a_input; + outputRTV = a_output.rtv.get(); + if (techniqueInfo.renderTargetName.empty()) + targetInOutput = true; + } else if (swapCounter == 0) { + // First pass: input -> output (start the ping-pong with output) + inputSRV = a_input; + outputRTV = a_output.rtv.get(); + if (techniqueInfo.renderTargetName.empty()) + targetInOutput = true; + } else { + // Subsequent passes: ping-pong between output and temp + bool useTemp = (swapCounter & 1) == 0; // Use counter LSB for swap determination + if (useTemp) { + // Read from temp, write to output + inputSRV = a_temp.srv.get(); + outputRTV = a_output.rtv.get(); + if (techniqueInfo.renderTargetName.empty()) + targetInOutput = true; + } else { + // Read from output, write to temp + inputSRV = a_output.srv.get(); + outputRTV = a_temp.rtv.get(); + if (techniqueInfo.renderTargetName.empty()) + targetInOutput = false; + } + } + + // Handle custom render target if specified + if (!techniqueInfo.renderTargetName.empty()) { + outputRTV = GetRenderTargetView(techniqueInfo.renderTargetName, outputRTV); + } else { + swapCounter++; // Increment counter for next iteration + } + + if (sourceTexture && sourceTexture->IsValid()) { + sourceTexture->AsShaderResource()->SetResource(inputSRV); + } + + context->OMSetRenderTargets(1, &outputRTV, nullptr); + + // Set viewport based on render target description + winrt::com_ptr resource; + outputRTV->GetResource(resource.put()); + winrt::com_ptr texture; + resource.as(texture); + D3D11_TEXTURE2D_DESC texDesc; + texture->GetDesc(&texDesc); + + D3D11_VIEWPORT viewport = {}; + viewport.TopLeftX = 0.0f; + viewport.TopLeftY = 0.0f; + viewport.Width = static_cast(texDesc.Width); + viewport.Height = static_cast(texDesc.Height); + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &viewport); + + for (UINT p = 0; p < techDesc.Passes; p++) { + techniqueInfo.technique->GetPassByIndex(p)->Apply(0, context); + context->Draw(4, 0); + } + } + + if (!targetInOutput) { + context->CopyResource(a_output.texture.get(), a_temp.texture.get()); + } +} + +void Effect::ExecuteTechnique(const std::string& techniqueName, TextureManager::Texture& output) +{ + if (!IsCompiled() || !effect) { + return; + } + + auto context = globals::d3d::context; + + // Find the technique + auto technique = effect->GetTechniqueByName(techniqueName.c_str()); + if (!technique || !technique->IsValid()) { + logger::trace("[ENBPP] Technique '{}' not found or invalid", techniqueName); + return; + } + + logger::trace("[ENBPP] Executing single technique '{}'", techniqueName); + + // Set output render target + ID3D11RenderTargetView* rtvArray[] = { output.rtv.get() }; + context->OMSetRenderTargets(1, rtvArray, nullptr); + + // Set viewport based on render target description + D3D11_TEXTURE2D_DESC texDesc; + output.texture->GetDesc(&texDesc); + + D3D11_VIEWPORT viewport = {}; + viewport.TopLeftX = 0.0f; + viewport.TopLeftY = 0.0f; + viewport.Width = static_cast(texDesc.Width); + viewport.Height = static_cast(texDesc.Height); + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &viewport); + + // Execute technique + D3DX11_TECHNIQUE_DESC techDesc; + technique->GetDesc(&techDesc); + + for (UINT p = 0; p < techDesc.Passes; p++) { + technique->GetPassByIndex(p)->Apply(0, context); + context->Draw(4, 0); + } +} + +void Effect::SetupCustomTextures() +{ + // Iterate through all variables to find texture variables with ResourceName annotations + for (auto& [varName, effectVar] : variables) { + // Get ResourceName annotation + std::string resourceName = GetResourceNameFromVariable(effectVar.get()); + + if (!resourceName.empty()) { + logger::info("[ENBPP] Loading texture for variable '{}': {}", varName, resourceName); + + // Load the texture + auto srv = LoadTextureFromFile(resourceName); + if (srv) { + // Set the texture on the variable + auto shaderResourceVar = effectVar->AsShaderResource(); + if (shaderResourceVar && shaderResourceVar->IsValid()) { + shaderResourceVar->SetResource(srv); + logger::info("[ENBPP] Successfully bound texture '{}' to variable '{}'", resourceName, varName); + } + } else { + logger::warn("[ENBPP] Failed to load texture '{}' for variable '{}'", resourceName, varName); + } + } + } +} + +ID3D11ShaderResourceView* Effect::LoadTextureFromFile(const std::string& filename) +{ + auto device = globals::d3d::device; + + // Check cache first + auto cacheIt = customTextureCache.find(filename); + if (cacheIt != customTextureCache.end()) { + return cacheIt->second.get(); + } + + // Construct full path - check enbseries folder first + std::filesystem::path filepath = std::filesystem::path{ "enbseries" } / filename; + + winrt::com_ptr texture; + winrt::com_ptr srv; + + HRESULT hr = DirectX::CreateDDSTextureFromFile(device, filepath.c_str(), texture.put(), srv.put()); + + if (FAILED(hr)) { + // Try loading as other format (PNG, BMP, etc.) + hr = DirectX::CreateWICTextureFromFile(device, filepath.c_str(), texture.put(), srv.put()); + } + + auto fileString = filepath.string(); + + if (FAILED(hr)) { + logger::error("[ENBPP] Failed to load texture file: {} (HRESULT: 0x{:08X})", fileString, static_cast(hr)); + return nullptr; + } + + // Cache the loaded texture + customTextureCache[filename] = srv; + + logger::info("[ENBPP] Successfully loaded texture: {}", fileString); + return srv.get(); +} + +std::string Effect::GetResourceNameFromVariable(ID3DX11EffectVariable* variable) +{ + if (!variable) { + return ""; + } + + // Get the variable's annotation count + D3DX11_EFFECT_VARIABLE_DESC varDesc; + if (FAILED(variable->GetDesc(&varDesc))) { + return ""; + } + + // Look for ResourceName annotation + for (UINT i = 0; i < varDesc.Annotations; ++i) { + auto annotation = variable->GetAnnotationByIndex(i); + if (!annotation || !annotation->IsValid()) { + continue; + } + + D3DX11_EFFECT_VARIABLE_DESC annotationDesc; + if (FAILED(annotation->GetDesc(&annotationDesc))) { + continue; + } + + // Check if this is the ResourceName annotation + if (std::string(annotationDesc.Name) == "ResourceName") { + // Get the string value + auto stringVar = annotation->AsString(); + if (stringVar && stringVar->IsValid()) { + LPCSTR resourceName = nullptr; + if (SUCCEEDED(stringVar->GetString(&resourceName)) && resourceName) { + return std::string(resourceName); + } + } + } + } + + return ""; +} + +void Effect::LoadTechniques() +{ + D3DX11_EFFECT_DESC effectDesc; + DX::ThrowIfFailed(effect->GetDesc(&effectDesc)); + + std::string currentSequenceBaseName; + int currentSequenceIndex = 0; + + // Load all techniques and organize them into sequences + for (UINT i = 0; i < effectDesc.Techniques; ++i) { + auto technique = effect->GetTechniqueByIndex(i); + if (!technique->IsValid()) { + continue; + } + + D3DX11_TECHNIQUE_DESC techDesc; + DX::ThrowIfFailed(technique->GetDesc(&techDesc)); + + std::string techniqueName = techDesc.Name; + + // Determine the base technique name and sequence number + std::string baseName; + int sequenceNumber = 0; + + // Check if this continues the current sequence + if (!currentSequenceBaseName.empty()) { + std::string expectedName = currentSequenceBaseName + std::to_string(currentSequenceIndex + 1); + if (techniqueName == expectedName) { + // Continue current sequence + baseName = currentSequenceBaseName; + sequenceNumber = currentSequenceIndex + 1; + currentSequenceIndex++; + } else { + // Start new sequence with this technique + baseName = techniqueName; + sequenceNumber = 0; + currentSequenceBaseName = techniqueName; + currentSequenceIndex = 0; + } + } else { + // First technique or start new sequence + baseName = techniqueName; + sequenceNumber = 0; + currentSequenceBaseName = techniqueName; + currentSequenceIndex = 0; + } + + // Get RenderTarget annotation + std::string renderTargetName = GetRenderTargetFromTechnique(technique); + + // Ensure the technique sequence vector exists and is large enough + if (techniques[baseName].size() <= sequenceNumber) { + techniques[baseName].resize(sequenceNumber + 1); + } + + // Store the technique info in the correct sequence position + TechniqueInfo techInfo; + techInfo.technique.copy_from(technique); + techInfo.renderTargetName = renderTargetName; + techniques[baseName][sequenceNumber] = std::move(techInfo); + + logger::debug("[ENBPP] Loaded technique '{}' as base '{}' sequence {}", techniqueName, baseName, sequenceNumber); + } + + // Log the technique sequences found + for (const auto& [baseName, sequence] : techniques) { + logger::info("[ENBPP] Technique sequence '{}' has {} techniques", baseName, sequence.size()); + } +} + +std::vector Effect::GetBaseTechniqueNames() +{ + std::vector baseNames; + baseNames.reserve(techniques.size()); + + for (const auto& [baseName, sequence] : techniques) { + // Only include sequences that have at least the base technique (index 0) + if (!sequence.empty() && sequence[0].technique) { + baseNames.push_back(baseName); + } + } + + return baseNames; +} + +void Effect::LoadUITechniques() +{ + uiTechniques.clear(); + selectedTechniqueIndex = 0; + + D3DX11_EFFECT_DESC effectDesc; + if (FAILED(effect->GetDesc(&effectDesc))) { + return; + } + + // Load all techniques that have UIName annotations + for (UINT i = 0; i < effectDesc.Techniques; ++i) { + auto technique = effect->GetTechniqueByIndex(i); + if (!technique->IsValid()) { + continue; + } + + D3DX11_TECHNIQUE_DESC techDesc; + if (FAILED(technique->GetDesc(&techDesc))) { + continue; + } + + std::string techniqueName = techDesc.Name; + std::string uiName = GetUINameFromTechnique(technique); + + // Only include techniques with UIName annotations + if (!uiName.empty()) { + UITechnique uiTech; + uiTech.techniqueName = techniqueName; + uiTech.displayName = uiName; + uiTechniques.push_back(uiTech); + + logger::debug("[ENBPP] Added UI technique '{}' with display name '{}'", techniqueName, uiName); + } + } + + logger::info("[ENBPP] Loaded {} UI techniques", uiTechniques.size()); +} + +std::string Effect::GetRenderTargetFromTechnique(ID3DX11EffectTechnique* technique) +{ + if (!technique) { + return ""; + } + + // Get the technique's annotation count + D3DX11_TECHNIQUE_DESC techDesc; + if (FAILED(technique->GetDesc(&techDesc))) { + return ""; + } + + // Look for RenderTarget annotation + for (UINT i = 0; i < techDesc.Annotations; ++i) { + auto annotation = technique->GetAnnotationByIndex(i); + if (!annotation || !annotation->IsValid()) { + continue; + } + + D3DX11_EFFECT_VARIABLE_DESC annotationDesc; + if (FAILED(annotation->GetDesc(&annotationDesc))) { + continue; + } + + // Check if this is the RenderTarget annotation + if (std::string(annotationDesc.Name) == "RenderTarget") { + // Get the string value + auto stringVar = annotation->AsString(); + if (stringVar && stringVar->IsValid()) { + LPCSTR renderTargetName = nullptr; + if (SUCCEEDED(stringVar->GetString(&renderTargetName)) && renderTargetName) { + return std::string(renderTargetName); + } + } + } + } + + return ""; +} + +std::string Effect::GetUINameFromTechnique(ID3DX11EffectTechnique* technique) +{ + if (!technique) { + return ""; + } + + // Get the technique's annotation count + D3DX11_TECHNIQUE_DESC techDesc; + if (FAILED(technique->GetDesc(&techDesc))) { + return ""; + } + + // Look for UIName annotation + for (UINT i = 0; i < techDesc.Annotations; ++i) { + auto annotation = technique->GetAnnotationByIndex(i); + if (!annotation || !annotation->IsValid()) { + continue; + } + + D3DX11_EFFECT_VARIABLE_DESC annotationDesc; + if (FAILED(annotation->GetDesc(&annotationDesc))) { + continue; + } + + // Check if this is the UIName annotation + if (std::string(annotationDesc.Name) == "UIName") { + // Get the string value + auto stringVar = annotation->AsString(); + if (stringVar && stringVar->IsValid()) { + LPCSTR uiName = nullptr; + if (SUCCEEDED(stringVar->GetString(&uiName)) && uiName) { + return std::string(uiName); + } + } + } + } + + return ""; +} + +TextureManager::Texture* Effect::GetEffectTexture(const std::string& name) +{ + auto it = effectTextureCache.find(name); + if (it != effectTextureCache.end()) { + return &it->second; + } + return nullptr; +} + +ID3D11RenderTargetView* Effect::GetRenderTargetView(const std::string& renderTargetName, ID3D11RenderTargetView* fallback) +{ + if (renderTargetName.empty()) { + return fallback; + } + + auto* texture = GetEffectTexture(renderTargetName); + if (texture && texture->rtv) + return texture->rtv.get(); + + // Get render target from EffectManager's common texture cache + auto& textureManager = TextureManager::GetSingleton(); + texture = textureManager.GetCommonTexture(renderTargetName); + if (texture && texture->rtv) + return texture->rtv.get(); + + logger::warn("[ENBPP] Render target '{}' not found in cache, using fallback", renderTargetName); + return fallback; +} + +void Effect::LoadUIVariables() +{ + D3DX11_EFFECT_DESC effectDesc; + if (FAILED(effect->GetDesc(&effectDesc))) { + return; + } + + uiVariables.clear(); + + // Iterate through all global variables in the effect + for (UINT i = 0; i < effectDesc.GlobalVariables; ++i) { + auto variable = effect->GetVariableByIndex(i); + if (!variable || !variable->IsValid()) { + continue; + } + + D3DX11_EFFECT_VARIABLE_DESC varDesc; + if (FAILED(variable->GetDesc(&varDesc))) { + continue; + } + + // Check if this variable has UI annotations + std::string uiName = GetUIAnnotation(variable, "UIName"); + if (uiName.empty()) { + continue; // No UI annotation, skip + } + + UIVariable uiVar = {}; + uiVar.name = varDesc.Name; + uiVar.displayName = uiName; + uiVar.effectVariable.copy_from(variable); + + // Determine variable type + D3DX11_EFFECT_TYPE_DESC typeDesc; + auto effectType = variable->GetType(); + if (SUCCEEDED(effectType->GetDesc(&typeDesc))) { + if (typeDesc.Class == D3D_SVC_SCALAR) { + switch (typeDesc.Type) { + case D3D_SVT_FLOAT: + uiVar.type = UIVariableType::Float; + break; + case D3D_SVT_INT: + uiVar.type = UIVariableType::Int; + break; + case D3D_SVT_BOOL: + uiVar.type = UIVariableType::Bool; + break; + default: + continue; + } + } else if (typeDesc.Class == D3D_SVC_VECTOR && typeDesc.Type == D3D_SVT_FLOAT && typeDesc.Elements == 0) { + if (typeDesc.Columns == 3) { + uiVar.type = UIVariableType::Color3; + } else if (typeDesc.Columns == 4) { + uiVar.type = UIVariableType::Color4; + } else { + continue; + } + } else { + continue; + } + } else { + continue; + } + + // Parse UI widget type + std::string widgetStr = GetUIAnnotation(variable, "UIWidget"); + uiVar.widgetType = ParseWidgetType(widgetStr); + + logger::debug("[ENBPP] Variable '{}': UIWidget='{}', parsed as {}", + uiVar.name, widgetStr, static_cast(uiVar.widgetType)); + + // Parse UI properties based on type + if (uiVar.type == UIVariableType::Float) { + std::string minStr = GetUIAnnotation(variable, "UIMin"); + std::string maxStr = GetUIAnnotation(variable, "UIMax"); + std::string stepStr = GetUIAnnotation(variable, "UIStep"); + + if (!minStr.empty()) + uiVar.floatMin = std::stof(minStr); + if (!maxStr.empty()) + uiVar.floatMax = std::stof(maxStr); + if (!stepStr.empty()) + uiVar.floatStep = std::stof(stepStr); + } else if (uiVar.type == UIVariableType::Int) { + std::string minStr = GetUIAnnotation(variable, "UIMin"); + std::string maxStr = GetUIAnnotation(variable, "UIMax"); + + if (!minStr.empty()) + uiVar.intMin = std::stoi(minStr); + if (!maxStr.empty()) + uiVar.intMax = std::stoi(maxStr); + + // Parse dropdown list if it's a dropdown widget + if (uiVar.widgetType == UIWidgetType::Dropdown) { + std::string listStr = GetUIAnnotation(variable, "UIList"); + logger::debug("[ENBPP] Variable '{}': UIList='{}'", uiVar.name, listStr); + if (!listStr.empty()) { + uiVar.dropdownItems = ParseDropdownList(listStr); + logger::debug("[ENBPP] Parsed {} dropdown items", uiVar.dropdownItems.size()); + } + } + } + + // Load current value + LoadUIVariableValue(uiVar); + + uiVariables.push_back(uiVar); + + // Debug logging + if (uiVar.widgetType == UIWidgetType::Dropdown) { + logger::debug("[ENBPP] Loaded UI variable '{}' with display name '{}', dropdown items: {}", + uiVar.name, uiVar.displayName, uiVar.dropdownItems.size()); + for (const auto& item : uiVar.dropdownItems) { + logger::debug("[ENBPP] - {}", item); + } + } else { + logger::debug("[ENBPP] Loaded UI variable '{}' with display name '{}'", uiVar.name, uiVar.displayName); + } + } + + logger::info("[ENBPP] Loaded {} UI variables", uiVariables.size()); +} + +std::string Effect::GetUIAnnotation(ID3DX11EffectVariable* variable, const std::string& annotationName) +{ + if (!variable) { + return ""; + } + + D3DX11_EFFECT_VARIABLE_DESC varDesc; + if (FAILED(variable->GetDesc(&varDesc))) { + return ""; + } + + for (UINT i = 0; i < varDesc.Annotations; ++i) { + auto annotation = variable->GetAnnotationByIndex(i); + if (!annotation || !annotation->IsValid()) { + continue; + } + + D3DX11_EFFECT_VARIABLE_DESC annotationDesc; + if (FAILED(annotation->GetDesc(&annotationDesc))) { + continue; + } + + if (std::string(annotationDesc.Name) == annotationName) { + // Try string annotation first + auto stringVar = annotation->AsString(); + if (stringVar && stringVar->IsValid()) { + LPCSTR value = nullptr; + if (SUCCEEDED(stringVar->GetString(&value)) && value) { + return std::string(value); + } + } + + // Try integer annotation (for UIMin, UIMax, etc.) + auto intVar = annotation->AsScalar(); + if (intVar && intVar->IsValid()) { + int intValue; + if (SUCCEEDED(intVar->GetInt(&intValue))) { + return std::to_string(intValue); + } + + // Also try float annotation + float floatValue; + if (SUCCEEDED(intVar->GetFloat(&floatValue))) { + return std::to_string(floatValue); + } + } + } + } + + return ""; +} + +Effect::UIWidgetType Effect::ParseWidgetType(const std::string& widget) +{ + std::string lowerWidget = widget; + std::transform(lowerWidget.begin(), lowerWidget.end(), lowerWidget.begin(), ::tolower); + + if (lowerWidget == "spinner") + return UIWidgetType::Spinner; + if (lowerWidget == "dropdown") + return UIWidgetType::Dropdown; + return UIWidgetType::Default; +} + +std::vector Effect::ParseDropdownList(const std::string& list) +{ + std::vector items; + std::stringstream ss(list); + std::string item; + + while (std::getline(ss, item, ',')) { + // Trim whitespace + item.erase(0, item.find_first_not_of(" \t")); + item.erase(item.find_last_not_of(" \t") + 1); + items.push_back(item); + } + + return items; +} + +void Effect::LoadUIVariableValue(UIVariable& uiVar) +{ + switch (uiVar.type) { + case UIVariableType::Float: + if (SUCCEEDED(uiVar.effectVariable->AsScalar()->GetFloat(&uiVar.floatValue))) { + // Successfully loaded float value + } + break; + case UIVariableType::Int: + if (SUCCEEDED(uiVar.effectVariable->AsScalar()->GetInt(&uiVar.intValue))) { + // Successfully loaded int value + } + break; + case UIVariableType::Bool: + if (SUCCEEDED(uiVar.effectVariable->AsScalar()->GetBool(&uiVar.boolValue))) { + // Successfully loaded bool value + } + break; + case UIVariableType::Color3: + case UIVariableType::Color4: + if (SUCCEEDED(uiVar.effectVariable->AsVector()->GetFloatVector(uiVar.colorValue))) { + } + break; + } +} + +void Effect::LoadVariableFromString(UIVariable& uiVar, const std::string& value) +{ + try { + switch (uiVar.type) { + case UIVariableType::Float: + { + uiVar.floatValue = std::stof(value); + uiVar.effectVariable->AsScalar()->SetFloat(uiVar.floatValue); + } + break; + case UIVariableType::Int: + { + uiVar.intValue = std::stoi(value); + uiVar.effectVariable->AsScalar()->SetInt(uiVar.intValue); + } + break; + case UIVariableType::Bool: + { + std::string lowerValue = value; + std::transform(lowerValue.begin(), lowerValue.end(), lowerValue.begin(), ::tolower); + if (lowerValue == "true" || lowerValue == "1" || lowerValue == "yes" || lowerValue == "on") { + uiVar.boolValue = true; + } else if (lowerValue == "false" || lowerValue == "0" || lowerValue == "no" || lowerValue == "off") { + uiVar.boolValue = false; + } else { + uiVar.boolValue = std::stoi(value) != 0; + } + uiVar.effectVariable->AsScalar()->SetBool(uiVar.boolValue); + } + break; + case UIVariableType::Color3: + case UIVariableType::Color4: + { + std::istringstream ss(value); + int numComponents = (uiVar.type == UIVariableType::Color3) ? 3 : 4; + for (int i = 0; i < numComponents; ++i) { + char sep; + ss >> uiVar.colorValue[i]; + if (ss.peek() == ',') { + ss >> sep; + } + } + uiVar.effectVariable->AsVector()->SetFloatVector(uiVar.colorValue); + } + break; + } + } catch (const std::exception& e) { + logger::warn("[ENBPP] Failed to parse value '{}' for variable '{}': {}", value, uiVar.name, e.what()); + } +} + +void Effect::UpdateUIVariables() +{ + for (auto& uiVar : uiVariables) { + switch (uiVar.type) { + case UIVariableType::Float: + uiVar.effectVariable->AsScalar()->SetFloat(uiVar.floatValue); + break; + case UIVariableType::Int: + uiVar.effectVariable->AsScalar()->SetInt(uiVar.intValue); + break; + case UIVariableType::Bool: + uiVar.effectVariable->AsScalar()->SetBool(uiVar.boolValue); + break; + case UIVariableType::Color3: + case UIVariableType::Color4: + uiVar.effectVariable->AsVector()->SetFloatVector(uiVar.colorValue); + break; + } + } +} + +void Effect::RenderImGui() +{ + if (ImGui::CollapsingHeader(GetName().c_str())) { + bool valuesChanged = false; + + // Use table + if (ImGui::BeginTable(("effect_table_" + GetName()).c_str(), 2, ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + + if (!uiTechniques.empty()) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("TECHNIQUE"); + + ImGui::TableSetColumnIndex(1); + const char* currentDisplayName = uiTechniques[selectedTechniqueIndex].displayName.c_str(); + if (ImGui::BeginCombo(("##TECHNIQUE_" + GetName()).c_str(), currentDisplayName)) { + for (uint32_t i = 0; i < uiTechniques.size(); ++i) { + const bool isSelected = (selectedTechniqueIndex == i); + if (ImGui::Selectable(uiTechniques[i].displayName.c_str(), isSelected)) { + selectedTechniqueIndex = i; + valuesChanged = true; + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + } + + for (auto& uiVar : uiVariables) { + if (uiVar.displayName.empty() || std::all_of(uiVar.displayName.begin(), uiVar.displayName.end(), [](char c) { return std::isspace(c); })) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Spacing(); + continue; + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", uiVar.displayName.c_str()); + + // Skip inputs + bool isLabelOnly = ((uiVar.type == UIVariableType::Float && uiVar.floatMin == 0 && uiVar.floatMax == 0) || + (uiVar.type == UIVariableType::Int && uiVar.intMin == 0 && uiVar.intMax == 0)); + + if (isLabelOnly) { + continue; + } + + ImGui::TableSetColumnIndex(1); + + std::string id = "##" + uiVar.displayName + "_" + GetName(); + const char* currentItem = ""; + + switch (uiVar.type) { + case UIVariableType::Float: + if (ImGui::SliderFloat(id.c_str(), &uiVar.floatValue, uiVar.floatMin, uiVar.floatMax, "%.3f")) { + valuesChanged = true; + } + break; + case UIVariableType::Int: + if (uiVar.widgetType == UIWidgetType::Dropdown && !uiVar.dropdownItems.empty()) { + // For dropdowns + currentItem = (uiVar.intValue >= 0 && uiVar.intValue < (int)uiVar.dropdownItems.size()) ? uiVar.dropdownItems[uiVar.intValue].c_str() : ""; + if (ImGui::BeginCombo(id.c_str(), currentItem)) { + for (int i = 0; i < uiVar.dropdownItems.size(); ++i) { + if (ImGui::Selectable(uiVar.dropdownItems[i].c_str(), uiVar.intValue == i)) { + uiVar.intValue = i; + valuesChanged = true; + } + } + ImGui::EndCombo(); + } + } else { + if (ImGui::SliderInt(id.c_str(), &uiVar.intValue, uiVar.intMin, uiVar.intMax)) { + valuesChanged = true; + } + } + break; + case UIVariableType::Bool: + if (ImGui::Checkbox(id.c_str(), &uiVar.boolValue)) { + valuesChanged = true; + } + break; + case UIVariableType::Color3: + if (ImGui::ColorEdit3(id.c_str(), uiVar.colorValue)) { + valuesChanged = true; + } + break; + case UIVariableType::Color4: + if (ImGui::ColorEdit4(id.c_str(), uiVar.colorValue)) { + valuesChanged = true; + } + break; + } + } + + ImGui::EndTable(); + } + + // Update shader variables if any values changed + if (valuesChanged) { + UpdateUIVariables(); + } + + // Show compilation errors if any + if (!errors.empty()) { + for (const auto& error : errors) { + ImGui::TextWrapped("%s", error.c_str()); + } + } + } +} + +void Effect::EnumerateAllVariables() +{ + D3DX11_EFFECT_DESC effectDesc; + if (FAILED(effect->GetDesc(&effectDesc))) { + return; + } + + variables.clear(); + + // Iterate through all global variables in the effect + for (UINT i = 0; i < effectDesc.GlobalVariables; ++i) { + auto variable = effect->GetVariableByIndex(i); + if (!variable || !variable->IsValid()) { + continue; + } + + D3DX11_EFFECT_VARIABLE_DESC varDesc; + if (FAILED(variable->GetDesc(&varDesc))) { + continue; + } + + std::string varName = varDesc.Name; + variables[varName].copy_from(variable); + + logger::debug("[ENBPP] Enumerated variable: {}", varName); + } + + logger::info("[ENBPP] Enumerated {} effect variables", variables.size()); +} + +bool Effect::SetShaderResourceVariable(const std::string& variableName, ID3D11ShaderResourceView* resource) +{ + return SetShaderResourceVariable(effect.get(), variableName, resource); +} + +bool Effect::SetShaderResourceVariable(ID3DX11Effect* effect, const std::string& variableName, ID3D11ShaderResourceView* resource) +{ + auto variable = effect->GetVariableByName(variableName.c_str())->AsShaderResource(); + if (variable && variable->IsValid()) { + variable->SetResource(resource); + return true; + } + return false; +} + +bool Effect::SetVectorVariable(ID3DX11Effect* effect, const std::string& variableName, const void* data, uint32_t size) +{ + auto variable = effect->GetVariableByName(variableName.c_str())->AsVector(); + if (variable && variable->IsValid()) { + variable->SetRawValue(data, 0, size); + return true; + } + return false; +} + +bool Effect::SetVectorVariable(const std::string& variableName, const void* data, uint32_t size) +{ + auto variable = effect->GetVariableByName(variableName.c_str())->AsVector(); + if (variable && variable->IsValid()) { + variable->SetRawValue(data, 0, size); + return true; + } + return false; +} + +TextureManager::Texture Effect::CreateTexture(uint32_t width, uint32_t height, DXGI_FORMAT format, const std::string& debugName) +{ + auto device = globals::d3d::device; + + D3D11_TEXTURE2D_DESC texDesc = {}; + texDesc.Width = width; + texDesc.Height = height; + texDesc.MipLevels = 1; + texDesc.ArraySize = 1; + texDesc.Format = format; + texDesc.SampleDesc.Count = 1; + texDesc.SampleDesc.Quality = 0; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + texDesc.CPUAccessFlags = 0; + texDesc.MiscFlags = 0; + + TextureManager::Texture texture{}; + DX::ThrowIfFailed(device->CreateTexture2D(&texDesc, nullptr, texture.texture.put())); + DX::ThrowIfFailed(device->CreateRenderTargetView(texture.texture.get(), nullptr, texture.rtv.put())); + DX::ThrowIfFailed(device->CreateShaderResourceView(texture.texture.get(), nullptr, texture.srv.put())); + + if (!debugName.empty()) { + Util::SetResourceName(texture.texture.get(), (debugName).c_str()); + Util::SetResourceName(texture.rtv.get(), (debugName + " RTV").c_str()); + Util::SetResourceName(texture.srv.get(), (debugName + " SRV").c_str()); + } + + return texture; +} + +void Effect::SetSelectedTechniqueIndex(uint32_t index) +{ + if (index < uiTechniques.size()) { + selectedTechniqueIndex = index; + } +} + +std::string Effect::GetSelectedTechnique() const +{ + if (selectedTechniqueIndex < uiTechniques.size()) { + return uiTechniques[selectedTechniqueIndex].techniqueName; + } + return ""; +} + +std::string Effect::GetTechniqueNameByIndex(uint32_t index) const +{ + if (index < uiTechniques.size()) { + return uiTechniques[index].techniqueName; + } + return ""; +} \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/Effects/Effect.h b/src/Features/ENBPostProcessing/Effects/Effect.h new file mode 100644 index 0000000000..5b23f6fdb5 --- /dev/null +++ b/src/Features/ENBPostProcessing/Effects/Effect.h @@ -0,0 +1,172 @@ +#pragma once + +#include +#include + +#include "../TextureManager.h" + +class Effect +{ +public: + Effect() = default; + virtual ~Effect() = default; + + // UI technique structure (defined early for use in method declarations) + struct UITechnique + { + std::string techniqueName; // Actual technique name + std::string displayName; // UIName annotation + }; + + // Settings methods + bool Load(); + void Save(); + + // Effect lifecycle + virtual bool Apply(); // Clear resources, load settings, recompile, create resources + virtual void Unload(); // Clear all resources + + bool IsCompiled() const { return errors.empty(); } + const std::vector& GetErrors() const { return errors; } + + virtual void Execute() = 0; + virtual void UpdateEffectVariables() {} + + // Virtual texture creation function for derived classes to override + virtual void CreateEffectTextures() {} + + // UI System + void RenderImGui(); + void LoadUIVariables(); + void UpdateUIVariables(); + + // Technique selection (legacy) + std::string GetSelectedTechnique() const; + const std::vector& GetAvailableTechniques() const { return availableTechniques; } + + // UI technique selection (indexed access) + uint32_t GetSelectedTechniqueIndex() const { return selectedTechniqueIndex; } + void SetSelectedTechniqueIndex(uint32_t index); + const std::vector& GetUITechniques() const { return uiTechniques; } + std::string GetTechniqueNameByIndex(uint32_t index) const; + + // Pure virtual methods for derived classes to implement + virtual std::string GetName() const = 0; + + struct TechniqueInfo + { + winrt::com_ptr technique; + std::string renderTargetName; + }; + + winrt::com_ptr effect; + std::unordered_map> techniques; + std::unordered_map> variables; + + std::unordered_map effectTextureCache; + std::unordered_map> customTextureCache; + + // UI Variable System + enum class UIVariableType + { + Float, + Int, + Bool, + Color3, + Color4 + }; + + enum class UIWidgetType + { + Default, + Spinner, + Dropdown + }; + + struct UIVariable + { + UIVariableType type; + UIWidgetType widgetType; + LPCSTR name; + std::string displayName; + winrt::com_ptr effectVariable; + + // Value storage + union + { + float floatValue; + int intValue; + bool boolValue; + }; + + // Color value storage + float colorValue[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + + // UI properties + float floatMin = 0.0f; + float floatMax = 1.0f; + float floatStep = 0.01f; + int intMin = 0; + int intMax = 100; + std::vector dropdownItems; + }; + + std::vector uiVariables; + + // Technique selection (legacy) + std::vector availableTechniques; + + // UI technique selection (indexed by uint, only includes annotated techniques) + std::vector uiTechniques; + uint32_t selectedTechniqueIndex = 0; + + // Error tracking + std::vector errors; + + // Execute a technique sequence with ping-pong rendering + void ExecuteTechniqueSequence(const std::string& a_baseTechniqueName, ID3D11ShaderResourceView* a_input, TextureManager::Texture& a_output, TextureManager::Texture& a_temp); + + // Execute a single technique + void ExecuteTechnique(const std::string& techniqueName, TextureManager::Texture& output); + + // Allow EffectManager to setup common variables + ID3DX11Effect* GetEffect() const { return effect.get(); } + + // Helper function to set shader resource variables (non-static version for this effect) + bool SetShaderResourceVariable(const std::string& variableName, ID3D11ShaderResourceView* resource); + + // Texture creation helper + static TextureManager::Texture CreateTexture(uint32_t width, uint32_t height, DXGI_FORMAT format, const std::string& debugName); + + // Static helper functions for any effect + static bool SetShaderResourceVariable(ID3DX11Effect* effect, const std::string& variableName, ID3D11ShaderResourceView* resource); + static bool SetVectorVariable(ID3DX11Effect* effect, const std::string& variableName, const void* data, uint32_t size); + + // Helper function for safe vector variable access + bool SetVectorVariable(const std::string& variableName, const void* data, uint32_t size); + +private: + bool LoadFXFile(); + + void EnumerateAllVariables(); + + void SetupCustomTextures(); + ID3D11ShaderResourceView* LoadTextureFromFile(const std::string& filename); + std::string GetResourceNameFromVariable(ID3DX11EffectVariable* variable); + + void LoadTechniques(); + std::vector GetBaseTechniqueNames(); + + std::string GetRenderTargetFromTechnique(ID3DX11EffectTechnique* technique); + std::string GetUINameFromTechnique(ID3DX11EffectTechnique* technique); + void LoadUITechniques(); + TextureManager::Texture* GetEffectTexture(const std::string& name); + ID3D11RenderTargetView* GetRenderTargetView(const std::string& renderTargetName, ID3D11RenderTargetView* fallback); + + // UI Variable helpers + std::string GetUIAnnotation(ID3DX11EffectVariable* variable, const std::string& annotationName); + UIWidgetType ParseWidgetType(const std::string& widget); + std::vector ParseDropdownList(const std::string& list); + void LoadUIVariableValue(UIVariable& uiVar); + void LoadVariableFromString(UIVariable& uiVar, const std::string& value); +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/MenuManager.cpp b/src/Features/ENBPostProcessing/MenuManager.cpp new file mode 100644 index 0000000000..dd541d2bc4 --- /dev/null +++ b/src/Features/ENBPostProcessing/MenuManager.cpp @@ -0,0 +1,543 @@ +#include "MenuManager.h" + +#include "EffectManager.h" +#include "SettingManager.h" + +MenuManager& MenuManager::GetSingleton() +{ + static MenuManager instance; + return instance; +} + +void MenuManager::RenderImGui() +{ + if (!ImGui::BeginTable("ENBPostProcessing", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV, ImVec2(0, 0))) { + return; + } + + ImGui::TableSetupColumn("Settings", ImGuiTableColumnFlags_WidthFixed, 400.0f); + ImGui::TableSetupColumn("Effects", ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableNextRow(); + + // Left side - Settings + ImGui::TableSetColumnIndex(0); + if (ImGui::BeginChild("Settings", ImVec2(0, 0), false)) { + RenderSettingsPanel(); + } + ImGui::EndChild(); + + // Right side - Effects + ImGui::TableSetColumnIndex(1); + if (ImGui::BeginChild("Effects", ImVec2(0, 0), false)) { + EffectManager::GetSingleton().RenderEffectsList(); + } + ImGui::EndChild(); + + ImGui::EndTable(); +} + +void MenuManager::RenderSettingsPanel() +{ + auto& settingManager = SettingManager::GetSingleton(); + auto& effectManager = EffectManager::GetSingleton(); + + if (ImGui::Button("Apply")) { + settingManager.Load(); + effectManager.Apply(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load all settings from enbseries.ini, weather files, and effect configurations, reload shaders"); + } + + ImGui::SameLine(); + + if (ImGui::Button("Load")) { + settingManager.Load(); + effectManager.Load(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load all settings from enbseries.ini, weather files, and effect configurations"); + } + + ImGui::SameLine(); + + if (ImGui::Button("Save")) { + settingManager.Save(); + effectManager.Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Save all settings to enbseries.ini, weather files, and effect configurations"); + } + + ImGui::Separator(); + + RenderAllSettings(); +} + +void MenuManager::RenderWeatherControl() +{ + auto& effectManager = EffectManager::GetSingleton(); + auto& weatherManager = WeatherManager::GetSingleton(); + + // Current weather status + uint32_t currentWeatherID = static_cast(effectManager.commonData.weather[0]); + uint32_t lastWeatherID = static_cast(effectManager.commonData.weather[1]); + float blendFactor = effectManager.commonData.weather[2]; + + ImGui::Text("Current: 0x%X, Last: 0x%X", currentWeatherID, lastWeatherID); + ImGui::Text("Blend Factor: %.2f", blendFactor); + + // Time of day status + const std::vector timeOfDayNames = { "Dawn", "Sunrise", "Day", "Sunset", "Dusk", "Night", "InteriorDay", "InteriorNight" }; + auto activeIndices = GetActiveTimeOfDayIndices(); + + if (!activeIndices.empty()) { + std::string activeTimesText = "Active Times"; + for (size_t i = 0; i < activeIndices.size(); ++i) { + if (i > 0) + activeTimesText += ", "; + activeTimesText += timeOfDayNames[activeIndices[i]]; + } + ImGui::Text("%s", activeTimesText.c_str()); + } + + // Current time of day values + ImGui::Separator(); + ImGui::Text("Time of Day Values:"); + if (ImGui::BeginTable("TimeOfDayValues", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Period", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Array", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableHeadersRow(); + + // Display timeOfDay1 values + const char* tod1Names[] = { "Dawn", "Sunrise", "Day", "Sunset" }; + for (int i = 0; i < 4; ++i) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", tod1Names[i]); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%.3f", effectManager.commonData.timeOfDay1[i]); + ImGui::TableSetColumnIndex(2); + ImGui::Text("timeOfDay1[%d]", i); + } + + // Display timeOfDay2 values + const char* tod2Names[] = { "Dusk", "Night", "InteriorDay", "InteriorNight" }; + for (int i = 0; i < 4; ++i) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", tod2Names[i]); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%.3f", effectManager.commonData.timeOfDay2[i]); + ImGui::TableSetColumnIndex(2); + ImGui::Text("timeOfDay2[%d]", i); + } + + ImGui::EndTable(); + } + + // Weather file list + if (ImGui::TreeNodeEx("Weather Files", ImGuiTreeNodeFlags_DefaultOpen)) { + const auto& weatherEntries = weatherManager.GetWeatherEntries(); + + if (!weatherEntries.empty()) { + if (ImGui::BeginChild("WeatherList", ImVec2(0, 200), true)) { + // Sort weather entries by name for consistent display + std::vector> sortedWeathers; + for (const auto& [key, entry] : weatherEntries) { + sortedWeathers.emplace_back(key, &entry); + } + std::sort(sortedWeathers.begin(), sortedWeathers.end()); + + for (const auto& [key, entry] : sortedWeathers) { + ImGui::PushID(key.c_str()); + + // Show weather file name and IDs + ImGui::Text("%s", entry->fileName.c_str()); + ImGui::SameLine(); + ImGui::Text("(%s)", key.c_str()); + + // Show weather IDs on same line + ImGui::SameLine(); + std::string idsText = "IDs: "; + for (size_t i = 0; i < entry->weatherIDs.size() && i < 3; ++i) { + if (i > 0) + idsText += ", "; + idsText += std::format("0x{:X}", entry->weatherIDs[i]); + } + if (entry->weatherIDs.size() > 3) { + idsText += "..."; + } + ImGui::Text("%s", idsText.c_str()); + + ImGui::PopID(); + } + } + ImGui::EndChild(); + } else { + ImGui::Text("No weather files loaded"); + ImGui::Text("Make sure _weatherlist.ini exists in enbseries folder"); + } + + ImGui::TreePop(); + } +} + +std::map> MenuManager::GetCategorizedSettings() const +{ + std::map> categorizedSettings; + + // Global Settings - Master controls and basic adjustments + categorizedSettings["Main"] = { + "GLOBAL", + "TIMEOFDAY", + "COLORCORRECTION", + "EFFECT", + "DEPTHOFFIELD" + }; + + // Weather-Based Settings - Categories that change with weather/time + categorizedSettings["Weather"] = { "BLOOM", "LENS", "SKY", "ENVIRONMENT", "IMAGEBASEDLIGHTING", "VOLUMETRICFOG", "GAMEVOLUMETRICRAYS" }; + + return categorizedSettings; +} + +std::vector MenuManager::GetActiveTimeOfDayIndices() const +{ + auto& effectManager = EffectManager::GetSingleton(); + std::vector activeIndices; + + // Access time of day data from EffectManager (this data is updated every frame) + const auto& commonData = effectManager.commonData; + + // Check if we're in interior (> 0.5) or exterior + bool isInterior = commonData.eInteriorFactor > 0.5f; + + if (isInterior) { + // For interiors, show both interior time periods + activeIndices.push_back(6); // InteriorDay + activeIndices.push_back(7); // InteriorNight + } else { + // For exteriors, show all exterior time periods + activeIndices.push_back(0); // Dawn + activeIndices.push_back(1); // Sunrise + activeIndices.push_back(2); // Day + activeIndices.push_back(3); // Sunset + activeIndices.push_back(4); // Dusk + activeIndices.push_back(5); // Night + } + + return activeIndices; +} + +float MenuManager::GetTimeOfDayBlendFactor(int timeIndex) const +{ + auto& effectManager = EffectManager::GetSingleton(); + const auto& commonData = effectManager.commonData; + + // Return the actual blend factor for each time period + switch (timeIndex) { + case 0: + return commonData.timeOfDay1[0]; // Dawn + case 1: + return commonData.timeOfDay1[1]; // Sunrise + case 2: + return commonData.timeOfDay1[2]; // Day + case 3: + return commonData.timeOfDay1[3]; // Sunset + case 4: + return commonData.timeOfDay2[0]; // Dusk + case 5: + return commonData.timeOfDay2[1]; // Night + case 6: // InteriorDay - calculate from interior blend + case 7: // InteriorNight - calculate from interior blend + { + if (commonData.eInteriorFactor > 0.5f) { + float dayNightFactor = (commonData.timeOfDay1[2] + commonData.timeOfDay1[1] + + commonData.timeOfDay1[0] * 0.5f + commonData.timeOfDay1[3] * 0.5f); + return (timeIndex == 6) ? dayNightFactor : (1.0f - dayNightFactor); + } + return 0.0f; + } + default: + return 0.0f; + } +} + +void MenuManager::RenderAllSettings() +{ + auto& settingManager = SettingManager::GetSingleton(); + auto categorizedSettings = GetCategorizedSettings(); + + if (ImGui::BeginTabBar("SettingsTabBar", ImGuiTabBarFlags_None)) { + for (const auto& [tabName, categories] : categorizedSettings) { + if (ImGui::BeginTabItem(tabName.c_str())) { + // Add weather control to the Weather tab + if (tabName == "Weather") { + RenderWeatherControl(); + ImGui::Separator(); + + // Show TimeOfDay header for Weather tab only + const std::vector timeOfDayNames = { "Dawn", "Sunrise", "Day", "Sunset", "Dusk", "Night", "InteriorDay", "InteriorNight" }; + auto activeIndices = GetActiveTimeOfDayIndices(); + + if (!activeIndices.empty()) { + if (ImGui::BeginTable("WeatherTimeOfDayHeader", 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Time Periods"); + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float sliderWidth = (totalWidth - (activeIndices.size() - 1) * 8.0f) / activeIndices.size(); + + for (size_t idx = 0; idx < activeIndices.size(); ++idx) { + int i = activeIndices[idx]; + + if (idx > 0) { + ImGui::SameLine(); + } + + // Use a child region to control the exact width and center the text + ImGui::BeginChild(("##weatherheader_" + std::to_string(i)).c_str(), ImVec2(sliderWidth, ImGui::GetTextLineHeight()), false, ImGuiWindowFlags_NoScrollbar); + + float labelWidth = ImGui::CalcTextSize(timeOfDayNames[i].c_str()).x; + float centerOffset = (sliderWidth - labelWidth) * 0.5f; + if (centerOffset > 0) { + ImGui::SetCursorPosX(centerOffset); + } + + // Style the label based on activity + float blendFactor = GetTimeOfDayBlendFactor(i); + bool isActive = blendFactor > 0.01f; + + if (!isActive) { + // Inactive periods: dim the text + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 0.7f)); + } + // Active periods: use default theme color (no style override) + + ImGui::Text("%s", timeOfDayNames[i].c_str()); + + if (!isActive) { + ImGui::PopStyleColor(); + } + + ImGui::EndChild(); + } + + ImGui::EndTable(); + } + + ImGui::Separator(); + } + } + + for (const auto& category : categories) { + if (ImGui::CollapsingHeader(category.c_str())) { + auto settings = settingManager.GetSettingsByCategory(category); + + if (ImGui::BeginTable((category + "_table").c_str(), 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + + // Add weather ignore controls for categories with weather support + if (settingManager.CategoryHasWeatherSupport(category)) { + auto& effectManager = EffectManager::GetSingleton(); + bool isInterior = effectManager.commonData.eInteriorFactor > 0.5f; + + if (!isInterior) { + // Show exterior ignore setting when outside + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("IgnoreWeatherSystem"); + ImGui::TableSetColumnIndex(1); + bool ignoreWeather = settingManager.GetIgnoreWeatherSystem(category); + if (ImGui::Checkbox(("##IgnoreWeatherSystem_" + category).c_str(), &ignoreWeather)) { + settingManager.SetIgnoreWeatherSystem(category, ignoreWeather); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("When enabled, uses enbseries.ini values instead of weather-specific values for exterior areas"); + } + } else { + // Show interior ignore setting when inside + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("IgnoreWeatherSystemInterior"); + ImGui::TableSetColumnIndex(1); + bool ignoreWeatherInterior = settingManager.GetIgnoreWeatherSystemInterior(category); + if (ImGui::Checkbox(("##IgnoreWeatherSystemInterior_" + category).c_str(), &ignoreWeatherInterior)) { + settingManager.SetIgnoreWeatherSystemInterior(category, ignoreWeatherInterior); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("When enabled, uses enbseries.ini values instead of weather-specific values for interior areas"); + } + } + + // Add separator + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + } + + for (const auto& settingKey : settings) { + auto settingInfo = settingManager.GetSettingInfo(settingKey, category); + if (!settingInfo) + continue; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (settingInfo->type != SettingType::TimeOfDay) + ImGui::Text("%s", settingKey.c_str()); + ImGui::TableSetColumnIndex(1); + + switch (settingInfo->type) { + case SettingType::Bool: + { + bool v = settingManager.GetValue(settingKey, category, true); + if (ImGui::Checkbox(("##" + settingKey).c_str(), &v)) { + settingManager.SetValue(settingKey, category, v); + } + break; + } + case SettingType::Float: + { + float v = settingManager.GetValue(settingKey, category, true); + if (ImGui::SliderFloat(("##" + settingKey).c_str(), &v, settingInfo->minValue, settingInfo->maxValue, "%.2f")) { + settingManager.SetValue(settingKey, category, v); + } + break; + } + case SettingType::TimeOfDay: + { + auto v = settingManager.GetValue(settingKey, category, true); + auto activeIndices = GetActiveTimeOfDayIndices(); + bool changed = false; + + // Show active time-of-day periods on one row + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", settingKey.c_str()); + ImGui::TableSetColumnIndex(1); + + // Render all active sliders horizontally + float totalWidth = ImGui::GetContentRegionAvail().x; + float sliderWidth = (totalWidth - (activeIndices.size() - 1) * 8.0f) / activeIndices.size(); // 8px spacing between sliders + + for (size_t idx = 0; idx < activeIndices.size(); ++idx) { + int i = activeIndices[idx]; + + if (idx > 0) { + ImGui::SameLine(); + } + + // Style the slider based on activity + float blendFactor = GetTimeOfDayBlendFactor(i); + bool isActive = blendFactor > 0.0f; + + if (!isActive) { + // Inactive sliders: dim the appearance + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + } + + ImGui::PushItemWidth(sliderWidth); + std::string id = "##" + settingKey + std::to_string(i); + if (ImGui::SliderFloat(id.c_str(), &v.values[i], settingInfo->minValue, settingInfo->maxValue, "%.1f")) { + changed = true; + } + + // Add tooltip showing blend factor + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%.0f%%", blendFactor * 100.0f); + } + + ImGui::PopItemWidth(); + + if (!isActive) { + ImGui::PopStyleVar(); // Alpha + } + } + + if (changed) { + settingManager.SetValue(settingKey, category, v); + } + break; + } + case SettingType::ColorTimeOfDay: + { + auto v = settingManager.GetValue(settingKey, category, true); + auto activeIndices = GetActiveTimeOfDayIndices(); + bool changed = false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", settingKey.c_str()); + ImGui::TableSetColumnIndex(1); + + // Render all active color pickers horizontally + float totalWidth = ImGui::GetContentRegionAvail().x; + float colorWidth = (totalWidth - (activeIndices.size() - 1) * 8.0f) / activeIndices.size(); // 8px spacing between color pickers + + for (size_t idx = 0; idx < activeIndices.size(); ++idx) { + int i = activeIndices[idx]; + + if (idx > 0) { + ImGui::SameLine(0, 8.0f); // 8px spacing to match slider spacing + } + + // Style the color picker based on activity + float blendFactor = GetTimeOfDayBlendFactor(i); + bool isActive = blendFactor > 0.0f; + + if (!isActive) { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + } + + ImGui::PushItemWidth(colorWidth); + std::string id = "##" + settingKey + std::to_string(i); + float color[3] = { v.values[i].x, v.values[i].y, v.values[i].z }; + + if (ImGui::ColorEdit3(id.c_str(), color, ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoInputs)) { + v.values[i].x = color[0]; + v.values[i].y = color[1]; + v.values[i].z = color[2]; + changed = true; + } + + // Add tooltip showing blend factor + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%.0f%%", blendFactor * 100.0f); + } + + ImGui::PopItemWidth(); + + if (!isActive) { + ImGui::PopStyleVar(); // Alpha + } + } + + if (changed) { + settingManager.SetValue(settingKey, category, v); + } + break; + } + } + } + + ImGui::EndTable(); + } + } + } + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + } +} \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/MenuManager.h b/src/Features/ENBPostProcessing/MenuManager.h new file mode 100644 index 0000000000..4e6b07006e --- /dev/null +++ b/src/Features/ENBPostProcessing/MenuManager.h @@ -0,0 +1,25 @@ +#pragma once + +#include "EffectManager.h" +#include "WeatherManager.h" + +class MenuManager +{ +public: + static MenuManager& GetSingleton(); + + // Main UI rendering method + void RenderImGui(); + +private: + // UI section rendering methods + void RenderEffectsList(); + void RenderSettingsPanel(); + void RenderWeatherControl(); + + // Helper UI methods + void RenderAllSettings(); + std::map> GetCategorizedSettings() const; + std::vector GetActiveTimeOfDayIndices() const; + float GetTimeOfDayBlendFactor(int timeIndex) const; +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/SettingManager.cpp b/src/Features/ENBPostProcessing/SettingManager.cpp new file mode 100644 index 0000000000..827cc043c9 --- /dev/null +++ b/src/Features/ENBPostProcessing/SettingManager.cpp @@ -0,0 +1,599 @@ +#include "SettingManager.h" + +#include "WeatherManager.h" +#include + +SettingManager& SettingManager::GetSingleton() +{ + static SettingManager instance; + return instance; +} + +void SettingManager::RegisterBoolSetting(const std::string& key, const std::string& category, + bool defaultValue, bool hasWeatherSupport) +{ + Setting setting; + setting.key = key; + setting.category = category; + setting.type = SettingType::Bool; + setting.hasWeatherSupport = hasWeatherSupport; + setting.defaultValue = defaultValue; + setting.currentValue = defaultValue; + + categories[category].settings[key] = setting; +} + +void SettingManager::RegisterFloatSetting(const std::string& key, const std::string& category, + float defaultValue, float minValue, float maxValue, bool hasWeatherSupport) +{ + Setting setting; + setting.key = key; + setting.category = category; + setting.type = SettingType::Float; + setting.hasWeatherSupport = hasWeatherSupport; + setting.defaultValue = defaultValue; + setting.currentValue = defaultValue; + setting.minValue = minValue; + setting.maxValue = maxValue; + + categories[category].settings[key] = setting; +} + +void SettingManager::RegisterTimeOfDaySetting(const std::string& key, const std::string& category, + float defaultValue, bool hasWeatherSupport) +{ + TimeOfDayValue timeOfDayDefault; + for (int i = 0; i < TimeOfDayValue::Total; ++i) { + timeOfDayDefault.values[i] = defaultValue; + } + + Setting setting; + setting.key = key; + setting.category = category; + setting.type = SettingType::TimeOfDay; + setting.hasWeatherSupport = hasWeatherSupport; + setting.defaultValue = timeOfDayDefault; + setting.currentValue = timeOfDayDefault; + + categories[category].settings[key] = setting; +} + +void SettingManager::RegisterColorTimeOfDaySetting(const std::string& key, const std::string& category, + float3 defaultValue, bool hasWeatherSupport) +{ + ColorTimeOfDayValue colorTimeOfDayDefault; + for (int i = 0; i < ColorTimeOfDayValue::Total; ++i) { + colorTimeOfDayDefault.values[i] = defaultValue; + } + + Setting setting; + setting.key = key; + setting.category = category; + setting.type = SettingType::ColorTimeOfDay; + setting.hasWeatherSupport = hasWeatherSupport; + setting.defaultValue = colorTimeOfDayDefault; + setting.currentValue = colorTimeOfDayDefault; + + categories[category].settings[key] = setting; +} + +template +T SettingManager::GetValue(const std::string& key, const std::string& category, bool rawValue) +{ + auto categoryIt = categories.find(category); + if (categoryIt == categories.end()) { + logger::error("[SettingManager] Category '{}' not found", category); + return T{}; + } + + auto settingIt = categoryIt->second.settings.find(key); + if (settingIt == categoryIt->second.settings.end()) { + logger::error("[SettingManager] Setting '{}::{}' not found", category, key); + return T{}; + } + + const auto& setting = settingIt->second; + const auto& categorySettings = categoryIt->second; + + if (setting.hasWeatherSupport) { + bool shouldIgnoreWeather = (interiorFactor > 0.5f) ? + categorySettings.ignoreWeatherSystemInterior : + categorySettings.ignoreWeatherSystem; + + if (shouldIgnoreWeather) { + return std::get(setting.currentValue); + } + + auto currentIt = weatherData.find(currentWeatherID); + auto lastIt = weatherData.find(lastWeatherID); + + if (currentIt != weatherData.end() || lastIt != weatherData.end()) { + SettingValue currentValue = setting.currentValue; + SettingValue lastValue = setting.currentValue; + std::string settingKey = category + "::" + key; + bool foundWeatherData = false; + + if (currentIt != weatherData.end()) { + auto valueIt = currentIt->second.find(settingKey); + if (valueIt != currentIt->second.end()) { + currentValue = valueIt->second; + foundWeatherData = true; + } + } + + if (lastIt != weatherData.end()) { + auto valueIt = lastIt->second.find(settingKey); + if (valueIt != lastIt->second.end()) { + lastValue = valueIt->second; + foundWeatherData = true; + } + } + + if (foundWeatherData) { + if (rawValue) { + return std::get(weatherBlendFactor > 0.5f ? currentValue : lastValue); + } + + SettingValue blendedValue = InterpolateValues(lastValue, currentValue, weatherBlendFactor); + return std::get(blendedValue); + } + } + } + + return std::get(setting.currentValue); +} + +template +void SettingManager::SetValue(const std::string& key, const std::string& category, const T& value) +{ + auto categoryIt = categories.find(category); + if (categoryIt == categories.end()) { + logger::error("[SettingManager] Category '{}' not found", category); + return; + } + + auto settingIt = categoryIt->second.settings.find(key); + if (settingIt == categoryIt->second.settings.end()) { + logger::error("[SettingManager] Setting '{}::{}' not found", category, key); + return; + } + + auto& setting = settingIt->second; + const auto& categorySettings = categoryIt->second; + + if (setting.hasWeatherSupport) { + bool shouldIgnoreWeather = (interiorFactor > 0.5f) ? + categorySettings.ignoreWeatherSystemInterior : + categorySettings.ignoreWeatherSystem; + + if (shouldIgnoreWeather) { + setting.currentValue = value; + return; + } + + uint32_t targetWeatherID = (weatherBlendFactor > 0.5f) ? currentWeatherID : lastWeatherID; + std::string settingKey = category + "::" + key; + weatherData[targetWeatherID][settingKey] = value; + + return; + } + + setting.currentValue = value; +} + +float SettingManager::GetInterpolatedTimeOfDayValue(const std::string& key, const std::string& category) +{ + TimeOfDayValue timeOfDayValue = GetValue(key, category); + return ComputeTimeOfDayInterpolation(timeOfDayValue); +} + +float3 SettingManager::GetInterpolatedColorTimeOfDayValue(const std::string& key, const std::string& category) +{ + ColorTimeOfDayValue colorTimeOfDayValue = GetValue(key, category); + return ComputeColorTimeOfDayInterpolation(colorTimeOfDayValue); +} + +bool SettingManager::HasSetting(const std::string& key, const std::string& category) const +{ + auto categoryIt = categories.find(category); + return categoryIt != categories.end() && categoryIt->second.settings.find(key) != categoryIt->second.settings.end(); +} + +const Setting* SettingManager::GetSettingInfo(const std::string& key, const std::string& category) const +{ + auto categoryIt = categories.find(category); + if (categoryIt == categories.end()) + return nullptr; + auto settingIt = categoryIt->second.settings.find(key); + return (settingIt != categoryIt->second.settings.end()) ? &settingIt->second : nullptr; +} + +std::vector SettingManager::GetSettingsByCategory(const std::string& category) const +{ + std::vector result; + auto categoryIt = categories.find(category); + if (categoryIt != categories.end()) { + for (const auto& [key, setting] : categoryIt->second.settings) { + result.push_back(key); + } + std::sort(result.begin(), result.end()); + } + return result; +} + +std::vector SettingManager::GetAllCategories() const +{ + std::vector result; + for (const auto& [category, _] : categories) { + result.push_back(category); + } + std::sort(result.begin(), result.end()); + return result; +} + +bool SettingManager::CategoryHasWeatherSupport(const std::string& category) const +{ + auto categoryIt = categories.find(category); + if (categoryIt == categories.end()) + return false; + for (const auto& [key, setting] : categoryIt->second.settings) { + if (setting.hasWeatherSupport) { + return true; + } + } + return false; +} + +void SettingManager::SetWeatherBlendFactors(uint32_t newCurrentWeatherID, uint32_t newLastWeatherID, float blendFactor) +{ + this->currentWeatherID = newCurrentWeatherID; + this->lastWeatherID = newLastWeatherID; + this->weatherBlendFactor = blendFactor; +} + +void SettingManager::LoadWeatherSettings(const std::string& weatherKey, const std::string& filePath) +{ + if (!std::filesystem::exists(filePath)) { + logger::warn("[SettingManager] Weather file not found: {}", filePath); + return; + } + + std::string weatherIDStr = weatherKey.substr(8); // Remove "weather_" prefix + uint32_t weatherID = std::stoul(weatherIDStr); + + for (const auto& [category, categoryData] : categories) { + for (const auto& [key, setting] : categoryData.settings) { + if (setting.hasWeatherSupport) { + Setting tempSetting = setting; + LoadSettingFromFile(filePath, category, key, tempSetting); + std::string settingKey = category + "::" + key; + weatherData[weatherID][settingKey] = tempSetting.currentValue; + } + } + } +} + +void SettingManager::ReloadAllWeatherSettings() +{ + weatherData.clear(); + auto& weatherManager = WeatherManager::GetSingleton(); + weatherManager.Initialize(); +} + +void SettingManager::SetTimeOfDayData(const float newTimeOfDay1[4], const float newTimeOfDay2[4], float newInteriorFactor) +{ + memcpy(this->timeOfDay1, newTimeOfDay1, sizeof(this->timeOfDay1)); + memcpy(this->timeOfDay2, newTimeOfDay2, sizeof(this->timeOfDay2)); + this->interiorFactor = newInteriorFactor; +} + +void SettingManager::LoadFromFile(const std::string& filePath) +{ + std::filesystem::path absPath = std::filesystem::absolute(filePath); + + if (!std::filesystem::exists(absPath)) { + logger::warn("[SettingManager] Settings file not found: {}, using defaults", absPath.string()); + return; + } + + for (auto& [category, categoryData] : categories) { + for (auto& [key, setting] : categoryData.settings) { + if (!setting.hasWeatherSupport) { + LoadSettingFromFile(absPath.string(), category, key, setting); + } + } + } + + LoadWeatherIgnoreSettings(absPath.string()); +} + +void SettingManager::SaveToFile(const std::string& filePath) +{ + for (const auto& [category, categoryData] : categories) { + for (const auto& [key, setting] : categoryData.settings) { + if (!setting.hasWeatherSupport) { + SaveSettingToFile(filePath, category, key, setting); + } + } + } +} + +SettingValue SettingManager::InterpolateValues(const SettingValue& a, const SettingValue& b, float t) +{ + if (a.index() != b.index()) { + return t > 0.5f ? b : a; + } + + switch (a.index()) { + case 0: // bool + return t > 0.5f ? std::get(b) : std::get(a); + case 1: // float + { + float valA = std::get(a); + float valB = std::get(b); + return valA + t * (valB - valA); + } + case 2: // TimeOfDayValue + { + const auto& valA = std::get(a); + const auto& valB = std::get(b); + TimeOfDayValue result; + for (int i = 0; i < 8; ++i) { + result.values[i] = valA.values[i] + t * (valB.values[i] - valA.values[i]); + } + return result; + } + case 3: // ColorTimeOfDayValue + { + const auto& valA = std::get(a); + const auto& valB = std::get(b); + ColorTimeOfDayValue result; + for (int i = 0; i < 8; ++i) { + result.values[i] = valA.values[i] + t * (valB.values[i] - valA.values[i]); + } + return result; + } + } + return b; +} + +float SettingManager::ComputeTimeOfDayInterpolation(const TimeOfDayValue& value) +{ + if (interiorFactor > 0.5f) { + float dayNightFactor = (timeOfDay1[2] + timeOfDay1[1] + timeOfDay1[0] * 0.5f + timeOfDay1[3] * 0.5f); + return value.values[TimeOfDayValue::InteriorNight] + dayNightFactor * + (value.values[TimeOfDayValue::InteriorDay] - value.values[TimeOfDayValue::InteriorNight]); + } + + return timeOfDay1[0] * value.values[TimeOfDayValue::Dawn] + + timeOfDay1[1] * value.values[TimeOfDayValue::Sunrise] + + timeOfDay1[2] * value.values[TimeOfDayValue::Day] + + timeOfDay1[3] * value.values[TimeOfDayValue::Sunset] + + timeOfDay2[0] * value.values[TimeOfDayValue::Dusk] + + timeOfDay2[1] * value.values[TimeOfDayValue::Night]; +} + +float3 SettingManager::ComputeColorTimeOfDayInterpolation(const ColorTimeOfDayValue& value) +{ + if (interiorFactor > 0.5f) { + float dayNightFactor = (timeOfDay1[2] + timeOfDay1[1] + timeOfDay1[0] * 0.5f + timeOfDay1[3] * 0.5f); + float3 interiorNight = value.values[ColorTimeOfDayValue::InteriorNight]; + float3 interiorDay = value.values[ColorTimeOfDayValue::InteriorDay]; + return interiorNight + dayNightFactor * (interiorDay - interiorNight); + } + + return timeOfDay1[0] * value.values[ColorTimeOfDayValue::Dawn] + + timeOfDay1[1] * value.values[ColorTimeOfDayValue::Sunrise] + + timeOfDay1[2] * value.values[ColorTimeOfDayValue::Day] + + timeOfDay1[3] * value.values[ColorTimeOfDayValue::Sunset] + + timeOfDay2[0] * value.values[ColorTimeOfDayValue::Dusk] + + timeOfDay2[1] * value.values[ColorTimeOfDayValue::Night]; +} + +void SettingManager::LoadSettingFromFile(const std::string& filePath, const std::string& section, const std::string& key, Setting& setting) +{ + switch (setting.type) { + case SettingType::Bool: + { + char buffer[256]; + GetPrivateProfileStringA(section.c_str(), key.c_str(), "false", buffer, sizeof(buffer), filePath.c_str()); + std::string valueStr = buffer; + std::transform(valueStr.begin(), valueStr.end(), valueStr.begin(), ::tolower); + setting.currentValue = (valueStr == "true" || valueStr == "1"); + break; + } + case SettingType::Float: + { + float defaultVal = std::get(setting.defaultValue); + char buffer[256]; + GetPrivateProfileStringA(section.c_str(), key.c_str(), std::to_string(defaultVal).c_str(), buffer, sizeof(buffer), filePath.c_str()); + std::string valueStr = buffer; + setting.currentValue = static_cast(atof(valueStr.c_str())); + break; + } + case SettingType::TimeOfDay: + { + TimeOfDayValue timeOfDayValue = std::get(setting.defaultValue); + const std::vector timeOfDayNames = { "Dawn", "Sunrise", "Day", "Sunset", "Dusk", "Night", "InteriorDay", "InteriorNight" }; + + for (int i = 0; i < 8; ++i) { + std::string fullKey = key + timeOfDayNames[i]; + char buffer[256]; + GetPrivateProfileStringA(section.c_str(), fullKey.c_str(), "0.0", buffer, sizeof(buffer), filePath.c_str()); + std::string valueStr = buffer; + timeOfDayValue.values[i] = static_cast(atof(valueStr.c_str())); + } + + setting.currentValue = timeOfDayValue; + break; + } + case SettingType::ColorTimeOfDay: + { + ColorTimeOfDayValue colorTimeOfDayValue = std::get(setting.defaultValue); + const std::vector timeOfDayNames = { "Dawn", "Sunrise", "Day", "Sunset", "Dusk", "Night", "InteriorDay", "InteriorNight" }; + + for (int i = 0; i < 8; ++i) { + std::string fullKey = key + timeOfDayNames[i]; + char buffer[256]; + GetPrivateProfileStringA(section.c_str(), fullKey.c_str(), "1.0, 1.0, 1.0", buffer, sizeof(buffer), filePath.c_str()); + std::string valueStr = buffer; + + // Parse comma-separated float3 values + std::stringstream ss(valueStr); + std::string item; + std::vector components; + + while (std::getline(ss, item, ',')) { + // Trim whitespace + item.erase(0, item.find_first_not_of(" \t")); + item.erase(item.find_last_not_of(" \t") + 1); + components.push_back(static_cast(atof(item.c_str()))); + } + + // Ensure we have exactly 3 components + if (components.size() >= 3) { + colorTimeOfDayValue.values[i] = { components[0], components[1], components[2] }; + } else { + // Default fallback + colorTimeOfDayValue.values[i] = { 1.0f, 1.0f, 1.0f }; + } + } + + setting.currentValue = colorTimeOfDayValue; + break; + } + } +} + +void SettingManager::SaveSettingToFile(const std::string& filePath, const std::string& section, const std::string& key, const Setting& setting) +{ + auto formatFloat = [](float value) -> std::string { + char temp[32]; + sprintf_s(temp, "%.3f", value); + std::string result = temp; + + // Remove trailing zeros + while (result.length() > 1 && result.back() == '0') { + result.pop_back(); + } + + // Ensure at least one decimal place (add .0 if needed) + if (result.back() == '.') { + result += '0'; + } + + return result; + }; + + switch (setting.type) { + case SettingType::Bool: + { + bool value = std::get(setting.currentValue); + WritePrivateProfileStringA(section.c_str(), key.c_str(), value ? "true" : "false", filePath.c_str()); + break; + } + case SettingType::Float: + { + float value = std::get(setting.currentValue); + std::string formatted = formatFloat(value); + WritePrivateProfileStringA(section.c_str(), key.c_str(), formatted.c_str(), filePath.c_str()); + break; + } + case SettingType::TimeOfDay: + { + const TimeOfDayValue& timeOfDayValue = std::get(setting.currentValue); + const std::vector timeOfDayNames = { "Dawn", "Sunrise", "Day", "Sunset", "Dusk", "Night", "InteriorDay", "InteriorNight" }; + + for (int i = 0; i < 8; ++i) { + std::string fullKey = key + timeOfDayNames[i]; + std::string formatted = formatFloat(timeOfDayValue.values[i]); + WritePrivateProfileStringA(section.c_str(), fullKey.c_str(), formatted.c_str(), filePath.c_str()); + } + break; + } + case SettingType::ColorTimeOfDay: + { + const ColorTimeOfDayValue& colorTimeOfDayValue = std::get(setting.currentValue); + const std::vector timeOfDayNames = { "Dawn", "Sunrise", "Day", "Sunset", "Dusk", "Night", "InteriorDay", "InteriorNight" }; + + for (int i = 0; i < 8; ++i) { + std::string fullKey = key + timeOfDayNames[i]; + const auto& color = colorTimeOfDayValue.values[i]; + std::string formatted = formatFloat(color.x) + ", " + formatFloat(color.y) + ", " + formatFloat(color.z); + WritePrivateProfileStringA(section.c_str(), fullKey.c_str(), formatted.c_str(), filePath.c_str()); + } + break; + } + } +} + +void SettingManager::LoadWeatherIgnoreSettings(const std::string& filePath) +{ + for (auto& [category, categoryData] : categories) { + bool hasWeatherSupport = false; + for (const auto& [key, setting] : categoryData.settings) { + if (setting.hasWeatherSupport) { + hasWeatherSupport = true; + break; + } + } + + if (hasWeatherSupport) { + char buffer1[256]; + GetPrivateProfileStringA(category.c_str(), "IgnoreWeatherSystem", "false", buffer1, sizeof(buffer1), filePath.c_str()); + std::string valueStr = buffer1; + std::transform(valueStr.begin(), valueStr.end(), valueStr.begin(), ::tolower); + categoryData.ignoreWeatherSystem = (valueStr == "true" || valueStr == "1"); + + char buffer2[256]; + GetPrivateProfileStringA(category.c_str(), "IgnoreWeatherSystemInterior", "true", buffer2, sizeof(buffer2), filePath.c_str()); + valueStr = buffer2; + std::transform(valueStr.begin(), valueStr.end(), valueStr.begin(), ::tolower); + categoryData.ignoreWeatherSystemInterior = (valueStr == "true" || valueStr == "1"); + } + } +} + +bool SettingManager::GetIgnoreWeatherSystem(const std::string& category) const +{ + auto categoryIt = categories.find(category); + return categoryIt != categories.end() ? categoryIt->second.ignoreWeatherSystem : false; +} + +bool SettingManager::GetIgnoreWeatherSystemInterior(const std::string& category) const +{ + auto categoryIt = categories.find(category); + return categoryIt != categories.end() ? categoryIt->second.ignoreWeatherSystemInterior : true; +} + +void SettingManager::SetIgnoreWeatherSystem(const std::string& category, bool ignore) +{ + categories[category].ignoreWeatherSystem = ignore; +} + +void SettingManager::SetIgnoreWeatherSystemInterior(const std::string& category, bool ignore) +{ + categories[category].ignoreWeatherSystemInterior = ignore; +} + +void SettingManager::Load() +{ + LoadFromFile("enbseries.ini"); + ReloadAllWeatherSettings(); +} + +void SettingManager::Save() +{ + SaveToFile("enbseries.ini"); +} + +// Explicit template instantiations +template bool SettingManager::GetValue(const std::string& key, const std::string& category, bool rawValue); +template float SettingManager::GetValue(const std::string& key, const std::string& category, bool rawValue); +template TimeOfDayValue SettingManager::GetValue(const std::string& key, const std::string& category, bool rawValue); +template ColorTimeOfDayValue SettingManager::GetValue(const std::string& key, const std::string& category, bool rawValue); + +template void SettingManager::SetValue(const std::string& key, const std::string& category, const bool& value); +template void SettingManager::SetValue(const std::string& key, const std::string& category, const float& value); +template void SettingManager::SetValue(const std::string& key, const std::string& category, const TimeOfDayValue& value); +template void SettingManager::SetValue(const std::string& key, const std::string& category, const ColorTimeOfDayValue& value); \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/SettingManager.h b/src/Features/ENBPostProcessing/SettingManager.h new file mode 100644 index 0000000000..5c58d0dc57 --- /dev/null +++ b/src/Features/ENBPostProcessing/SettingManager.h @@ -0,0 +1,191 @@ +#pragma once + +enum class SettingType +{ + Bool, + Float, + TimeOfDay, + ColorTimeOfDay +}; + +struct TimeOfDayValue +{ + float values[8] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + + enum Index + { + Dawn = 0, + Sunrise = 1, + Day = 2, + Sunset = 3, + Dusk = 4, + Night = 5, + InteriorDay = 6, + InteriorNight = 7, + Total = 8 + }; + + float& operator[](Index idx) { return values[idx]; } + const float& operator[](Index idx) const { return values[idx]; } + + float& GetByName(const std::string& name) + { + if (name == "Dawn") + return values[Dawn]; + if (name == "Sunrise") + return values[Sunrise]; + if (name == "Day") + return values[Day]; + if (name == "Sunset") + return values[Sunset]; + if (name == "Dusk") + return values[Dusk]; + if (name == "Night") + return values[Night]; + if (name == "InteriorDay") + return values[InteriorDay]; + if (name == "InteriorNight") + return values[InteriorNight]; + return values[Dawn]; + } +}; + +struct ColorTimeOfDayValue +{ + float3 values[8] = { + { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, + { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f } + }; + + enum Index + { + Dawn = 0, + Sunrise = 1, + Day = 2, + Sunset = 3, + Dusk = 4, + Night = 5, + InteriorDay = 6, + InteriorNight = 7, + Total = 8 + }; + + float3& operator[](Index idx) { return values[idx]; } + const float3& operator[](Index idx) const { return values[idx]; } + + float3& GetByName(const std::string& name) + { + if (name == "Dawn") + return values[Dawn]; + if (name == "Sunrise") + return values[Sunrise]; + if (name == "Day") + return values[Day]; + if (name == "Sunset") + return values[Sunset]; + if (name == "Dusk") + return values[Dusk]; + if (name == "Night") + return values[Night]; + if (name == "InteriorDay") + return values[InteriorDay]; + if (name == "InteriorNight") + return values[InteriorNight]; + return values[Dawn]; + } +}; + +using SettingValue = std::variant; + +struct Setting +{ + std::string key; + std::string category; + SettingType type; + bool hasWeatherSupport; + SettingValue defaultValue; + SettingValue currentValue; + float minValue = 0.0f; + float maxValue = 10.0f; +}; + +class SettingManager +{ +public: + static SettingManager& GetSingleton(); + + // Setting registration + void RegisterBoolSetting(const std::string& key, const std::string& category, + bool defaultValue, bool hasWeatherSupport = false); + void RegisterFloatSetting(const std::string& key, const std::string& category, + float defaultValue, float minValue = 0.0f, float maxValue = 10.0f, bool hasWeatherSupport = false); + void RegisterTimeOfDaySetting(const std::string& key, const std::string& category, + float defaultValue, bool hasWeatherSupport = false); + void RegisterColorTimeOfDaySetting(const std::string& key, const std::string& category, + float3 defaultValue, bool hasWeatherSupport = false); + + template + T GetValue(const std::string& key, const std::string& category, bool rawValue = false); + + template + void SetValue(const std::string& key, const std::string& category, const T& value); + + float GetInterpolatedTimeOfDayValue(const std::string& key, const std::string& category); + float3 GetInterpolatedColorTimeOfDayValue(const std::string& key, const std::string& category); + + bool HasSetting(const std::string& key, const std::string& category) const; + const Setting* GetSettingInfo(const std::string& key, const std::string& category) const; + std::vector GetSettingsByCategory(const std::string& category) const; + std::vector GetAllCategories() const; + bool CategoryHasWeatherSupport(const std::string& category) const; + + // Weather integration + void SetWeatherBlendFactors(uint32_t currentWeatherID, uint32_t lastWeatherID, float blendFactor); + void LoadWeatherSettings(const std::string& weatherKey, const std::string& filePath); + void SaveWeatherSettings(const std::string& weatherKey, const std::string& filePath); + void SaveAllWeatherSettings(); + void ReloadAllWeatherSettings(); + + // File I/O + void LoadFromFile(const std::string& filePath); + void SaveToFile(const std::string& filePath); + + // Effect save/load coordination + void Load(); + void Save(); + + // Weather ignore settings management + void LoadWeatherIgnoreSettings(const std::string& filePath); + bool GetIgnoreWeatherSystem(const std::string& category) const; + bool GetIgnoreWeatherSystemInterior(const std::string& category) const; + void SetIgnoreWeatherSystem(const std::string& category, bool ignore); + void SetIgnoreWeatherSystemInterior(const std::string& category, bool ignore); + + // Time of day interpolation data + void SetTimeOfDayData(const float timeOfDay1[4], const float timeOfDay2[4], float interiorFactor); + +private: + struct CategorySettings + { + std::unordered_map settings; + bool ignoreWeatherSystem = false; + bool ignoreWeatherSystemInterior = true; + }; + + std::unordered_map categories; + std::unordered_map> weatherData; + + uint32_t currentWeatherID = 0; + uint32_t lastWeatherID = 0; + float weatherBlendFactor = 0.0f; + + float timeOfDay1[4] = { 0, 0, 0, 0 }; + float timeOfDay2[4] = { 0, 0, 0, 0 }; + float interiorFactor = 0.0f; + + SettingValue InterpolateValues(const SettingValue& a, const SettingValue& b, float t); + float ComputeTimeOfDayInterpolation(const TimeOfDayValue& value); + float3 ComputeColorTimeOfDayInterpolation(const ColorTimeOfDayValue& value); + void LoadSettingFromFile(const std::string& filePath, const std::string& section, const std::string& key, Setting& setting); + void SaveSettingToFile(const std::string& filePath, const std::string& section, const std::string& key, const Setting& setting); +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/TextureManager.cpp b/src/Features/ENBPostProcessing/TextureManager.cpp new file mode 100644 index 0000000000..b5c984563b --- /dev/null +++ b/src/Features/ENBPostProcessing/TextureManager.cpp @@ -0,0 +1,384 @@ +#include "TextureManager.h" + +#include + +#include "State.h" + +TextureManager& TextureManager::GetSingleton() +{ + static TextureManager instance; + return instance; +} + +void TextureManager::Initialize() +{ + CreateCommonTextures(); + CreateDownsampleResources(); +} + +TextureManager::Texture* TextureManager::GetCommonTexture(const std::string& name) +{ + auto it = commonTextureCache.find(name); + if (it != commonTextureCache.end()) { + return &it->second; + } + return nullptr; +} + +void TextureManager::CreateCommonTextures() +{ + auto state = globals::state; + UINT screenWidth = static_cast(state->screenSize.x); + UINT screenHeight = static_cast(state->screenSize.y); + + commonTextureCache.insert({ "TextureHDRTemp", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::TextureHDRTemp") }); + commonTextureCache.insert({ "TextureHDRTemp2", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::TextureHDRTemp2") }); + + commonTextureCache.insert({ "RenderTargetRGBA32", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R8G8B8A8_UNORM, "TextureManager::RenderTargetRGBA32") }); + commonTextureCache.insert({ "RenderTargetRGBA64", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16G16B16A16_UNORM, "TextureManager::RenderTargetRGBA64") }); + commonTextureCache.insert({ "RenderTargetRGBA64F", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::RenderTargetRGBA64F") }); + commonTextureCache.insert({ "RenderTargetR16F", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16_FLOAT, "TextureManager::RenderTargetR16F") }); + commonTextureCache.insert({ "RenderTargetR32F", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R32_FLOAT, "TextureManager::RenderTargetR32F") }); + commonTextureCache.insert({ "RenderTargetRGB32F", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R11G11B10_FLOAT, "TextureManager::RenderTargetRGB32F") }); + + commonTextureCache.insert({ "TextureSDRTemp", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R10G10B10A2_UNORM, "TextureManager::TextureSDRTemp") }); + commonTextureCache.insert({ "TextureSDRTemp2", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R10G10B10A2_UNORM, "TextureManager::TextureSDRTemp2") }); + + commonTextureCache.insert({ "TextureBloom", CreateTexture(1024, 1024, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::TextureBloom") }); + commonTextureCache.insert({ "TextureLens", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::TextureLens") }); + + commonTextureCache.insert({ "TextureBloomTemp", CreateTexture(1024, 1024, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::TextureBloomLensTemp") }); + + commonTextureCache.insert({ "TextureAdaptation", CreateTexture(1, 1, DXGI_FORMAT_R32_FLOAT, "TextureManager::TextureAdaptation") }); + commonTextureCache.insert({ "TextureAdaptationSwap", CreateTexture(1, 1, DXGI_FORMAT_R32_FLOAT, "TextureManager::TextureAdaptationSwap") }); + + // Create fixed-size render targets for bloom/lens + std::vector> fixedSizes = { + { "RenderTarget1024", 1024 }, + { "RenderTarget512", 512 }, + { "RenderTarget256", 256 }, + { "RenderTarget128", 128 }, + { "RenderTarget64", 64 }, + { "RenderTarget32", 32 }, + { "RenderTarget16", 16 } + }; + + for (auto& [name, size] : fixedSizes) { + commonTextureCache[name] = CreateTexture(size, size, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::" + name); + } +} + +TextureManager::Texture TextureManager::CreateTexture(uint32_t width, uint32_t height, DXGI_FORMAT format, const std::string& debugName) +{ + TextureManager::Texture result; + + D3D11_TEXTURE2D_DESC texDesc = {}; + texDesc.Width = width; + texDesc.Height = height; + texDesc.MipLevels = 1; + texDesc.ArraySize = 1; + texDesc.Format = format; + texDesc.SampleDesc.Count = 1; + texDesc.SampleDesc.Quality = 0; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + texDesc.CPUAccessFlags = 0; + texDesc.MiscFlags = 0; + + DX::ThrowIfFailed(globals::d3d::device->CreateTexture2D(&texDesc, nullptr, result.texture.put())); + + if (!debugName.empty()) { + result.texture->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast(debugName.length()), debugName.c_str()); + } + + D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {}; + rtvDesc.Format = format; + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + rtvDesc.Texture2D.MipSlice = 0; + + DX::ThrowIfFailed(globals::d3d::device->CreateRenderTargetView(result.texture.get(), &rtvDesc, result.rtv.put())); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MostDetailedMip = 0; + srvDesc.Texture2D.MipLevels = 1; + + DX::ThrowIfFailed(globals::d3d::device->CreateShaderResourceView(result.texture.get(), &srvDesc, result.srv.put())); + + return result; +} + +void TextureManager::CreateDownsampleResources() +{ + auto device = globals::d3d::device; + + // Create linear sampler + D3D11_SAMPLER_DESC samplerDesc = {}; + samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER; + samplerDesc.MinLOD = 0; + samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; + + DX::ThrowIfFailed(device->CreateSamplerState(&samplerDesc, linearSampler.put())); + + // Create downsample vertex shader (fullscreen triangle) + const char* downsampleVertexShaderSource = R"HLSL( + struct VS_INPUT_POST + { + float3 pos : POSITION; + float2 txcoord : TEXCOORD0; + }; + struct VS_OUTPUT_POST + { + float4 pos : SV_POSITION; + float2 txcoord0 : TEXCOORD0; + }; + VS_OUTPUT_POST main(VS_INPUT_POST IN) + { + VS_OUTPUT_POST OUT; + float4 pos; + pos.xyz=IN.pos.xyz; + pos.w=1.0; + OUT.pos=pos; + OUT.txcoord0.xy=IN.txcoord.xy; + return OUT; + } + )HLSL"; + + winrt::com_ptr vertexShaderBlob; + winrt::com_ptr vertexErrorBlob; + + HRESULT vsResult = D3DCompile( + downsampleVertexShaderSource, + strlen(downsampleVertexShaderSource), + nullptr, + nullptr, + nullptr, + "main", + "vs_5_0", + 0, + 0, + vertexShaderBlob.put(), + vertexErrorBlob.put()); + + if (FAILED(vsResult)) { + if (vertexErrorBlob) { + logger::error("[TextureManager] Downsample vertex shader compilation failed: {}", + static_cast(vertexErrorBlob->GetBufferPointer())); + } + return; + } + + DX::ThrowIfFailed(device->CreateVertexShader( + vertexShaderBlob->GetBufferPointer(), + vertexShaderBlob->GetBufferSize(), + nullptr, + downsampleVS.put())); + + // Create downsample pixel shader + const char* downsamplePixelShaderSource = R"HLSL( +Texture2D SourceTexture : register(t0); +SamplerState LinearSampler : register(s0); +cbuffer Constants : register(b0) +{ + float2 SourceTexelSize; +}; +struct VS_OUTPUT_POST +{ + float4 pos : SV_POSITION; + float2 txcoord0 : TEXCOORD0; +}; +float4 DownsampleCODFirstMip(Texture2D tex, SamplerState samp, float2 uv, float2 out_px_size) +{ + // https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slide 162) + float4 A = tex.Sample(samp, uv + out_px_size * float2(-1.0, -1.0)); + float4 B = tex.Sample(samp, uv + out_px_size * float2(0.0, -1.0)); + float4 C = tex.Sample(samp, uv + out_px_size * float2(1.0, -1.0)); + float4 D = tex.Sample(samp, uv + out_px_size * float2(-0.5, -0.5)); + float4 E = tex.Sample(samp, uv + out_px_size * float2(0.5, -0.5)); + float4 F = tex.Sample(samp, uv + out_px_size * float2(-1.0, 0.0)); + float4 G = tex.Sample(samp, uv + out_px_size * float2(0.0, 0.0)); + float4 H = tex.Sample(samp, uv + out_px_size * float2(1.0, 0.0)); + float4 I = tex.Sample(samp, uv + out_px_size * float2(-0.5, 0.5)); + float4 J = tex.Sample(samp, uv + out_px_size * float2(0.5, 0.5)); + float4 K = tex.Sample(samp, uv + out_px_size * float2(-1.0, 1.0)); + float4 L = tex.Sample(samp, uv + out_px_size * float2(0.0, 1.0)); + float4 M = tex.Sample(samp, uv + out_px_size * float2(1.0, 1.0)); + float2 div = (1.0 / 4.0) * float2(0.5, 0.125); + float4 o = (D + E + I + J) * div.x; + o += (A + B + G + F) * div.y; + o += (B + C + H + G) * div.y; + o += (G + H + L + K) * div.y; + o += (H + G + L + M) * div.y; + return o; +} +float4 main(VS_OUTPUT_POST IN) : SV_Target +{ + return max(0, DownsampleCODFirstMip(SourceTexture, LinearSampler, IN.txcoord0.xy, SourceTexelSize)); +} +)HLSL"; + + winrt::com_ptr pixelShaderBlob; + winrt::com_ptr errorBlob; + + HRESULT result = D3DCompile( + downsamplePixelShaderSource, + strlen(downsamplePixelShaderSource), + nullptr, + nullptr, + nullptr, + "main", + "ps_5_0", + 0, + 0, + pixelShaderBlob.put(), + errorBlob.put()); + + if (FAILED(result)) { + if (errorBlob) { + logger::error("[TextureManager] Downsample shader compilation failed: {}", + static_cast(errorBlob->GetBufferPointer())); + } + return; + } + + DX::ThrowIfFailed(device->CreatePixelShader( + pixelShaderBlob->GetBufferPointer(), + pixelShaderBlob->GetBufferSize(), + nullptr, + downsamplePS.put())); + + // Create shared downsample texture + sharedDownsampleTexture = CreateDownsampleTexture(DXGI_FORMAT_R16G16B16A16_FLOAT); + + D3D11_BUFFER_DESC cbDesc = {}; + cbDesc.ByteWidth = sizeof(DownsampleCB); + cbDesc.Usage = D3D11_USAGE_DYNAMIC; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + + DX::ThrowIfFailed(globals::d3d::device->CreateBuffer(&cbDesc, nullptr, downsampleCB.put())); +} + +TextureManager::DownsampleTexture TextureManager::CreateDownsampleTexture(DXGI_FORMAT format) +{ + auto device = globals::d3d::device; + + DownsampleTexture fixedTexture; + + D3D11_TEXTURE2D_DESC texDesc = {}; + texDesc.Width = 1024; + texDesc.Height = 1024; + texDesc.MipLevels = 3; // 1024, 512, 256 + texDesc.ArraySize = 1; + texDesc.Format = format; + texDesc.SampleDesc.Count = 1; + texDesc.SampleDesc.Quality = 0; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + texDesc.CPUAccessFlags = 0; + texDesc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS; + + DX::ThrowIfFailed(device->CreateTexture2D(&texDesc, nullptr, fixedTexture.texture.put())); + + D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {}; + rtvDesc.Format = format; + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + rtvDesc.Texture2D.MipSlice = 0; + + DX::ThrowIfFailed(device->CreateRenderTargetView(fixedTexture.texture.get(), &rtvDesc, fixedTexture.rtv.put())); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 3; + srvDesc.Texture2D.MostDetailedMip = 0; + DX::ThrowIfFailed(device->CreateShaderResourceView(fixedTexture.texture.get(), &srvDesc, fixedTexture.srvChain.put())); + + srvDesc.Texture2D.MipLevels = 1; + DX::ThrowIfFailed(device->CreateShaderResourceView(fixedTexture.texture.get(), &srvDesc, fixedTexture.srv.put())); + + srvDesc.Texture2D.MostDetailedMip = 2; + DX::ThrowIfFailed(device->CreateShaderResourceView(fixedTexture.texture.get(), &srvDesc, fixedTexture.srvBlurry.put())); + + // Set debug names + Util::SetResourceName(fixedTexture.texture.get(), "TextureManager::DownsampleTexture (1024x1024, 3 mips)"); + Util::SetResourceName(fixedTexture.rtv.get(), "TextureManager::DownsampleTexture RTV"); + Util::SetResourceName(fixedTexture.srvChain.get(), "TextureManager::DownsampleTexture SRV Chain"); + Util::SetResourceName(fixedTexture.srv.get(), "TextureManager::DownsampleTexture SRV 1024x1024"); + Util::SetResourceName(fixedTexture.srvBlurry.get(), "TextureManager::DownsampleTexture SRV 256x256"); + + logger::info("[TextureManager] Created downsample texture: 1024x1024 with 3 mips (1024, 512, 256)"); + + return fixedTexture; +} + +void TextureManager::DownsampleToFixed(ID3D11ShaderResourceView* source, DownsampleTexture& texture) +{ + auto context = globals::d3d::context; + + // Get source texture description for calculating texel size + winrt::com_ptr sourceResource; + source->GetResource(sourceResource.put()); + + winrt::com_ptr sourceTexture; + sourceResource.as(sourceTexture); + + D3D11_TEXTURE2D_DESC sourceDesc; + sourceTexture->GetDesc(&sourceDesc); + + DownsampleCB constants{}; + constants.sourceTexelSizeX = 1.0f / static_cast(sourceDesc.Width); + constants.sourceTexelSizeY = 1.0f / static_cast(sourceDesc.Height); + + D3D11_MAPPED_SUBRESOURCE mappedResource; + context->Map(downsampleCB.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource); + memcpy(mappedResource.pData, &constants, sizeof(DownsampleCB)); + context->Unmap(downsampleCB.get(), 0); + + // Set up render state for downsampling + D3D11_VIEWPORT viewport = {}; + viewport.Width = 1024.0f; + viewport.Height = 1024.0f; + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + + context->RSSetViewports(1, &viewport); + ID3D11RenderTargetView* rtvArray[] = { texture.rtv.get() }; + + context->OMSetRenderTargets(1, rtvArray, nullptr); + + context->VSSetShader(downsampleVS.get(), nullptr, 0); + context->PSSetShaderResources(0, 1, &source); + + ID3D11SamplerState* samplerArray[] = { linearSampler.get() }; + context->PSSetSamplers(0, 1, samplerArray); + + ID3D11Buffer* bufferArray[] = { downsampleCB.get() }; + context->PSSetConstantBuffers(0, 1, bufferArray); + + context->PSSetShader(downsamplePS.get(), nullptr, 0); + + context->Draw(4, 0); + + context->GenerateMips(texture.srvChain.get()); +} + +void TextureManager::UpdateDownsampledTexture(ID3D11ShaderResourceView* source) +{ + DownsampleToFixed(source, sharedDownsampleTexture); +} + +ID3D11ShaderResourceView* TextureManager::GetDownsampleTexture() const +{ + return sharedDownsampleTexture.srv.get(); +} + +ID3D11ShaderResourceView* TextureManager::GetDownsampleTextureBlurry() const +{ + return sharedDownsampleTexture.srvBlurry.get(); +} \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/TextureManager.h b/src/Features/ENBPostProcessing/TextureManager.h new file mode 100644 index 0000000000..b4e604e8a2 --- /dev/null +++ b/src/Features/ENBPostProcessing/TextureManager.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include + +class TextureManager +{ +public: + struct Texture + { + winrt::com_ptr texture; + winrt::com_ptr rtv; + winrt::com_ptr srv; + }; + + struct DownsampleTexture + { + winrt::com_ptr texture; + winrt::com_ptr srvChain; // Mip 0 -> Mip 1 -> Mip2 + winrt::com_ptr srv; // Mip 0: 1024x1024 + winrt::com_ptr srvBlurry; // Mip 2: 256x256 + winrt::com_ptr rtv; + }; + + static TextureManager& GetSingleton(); + + void Initialize(); + Texture* GetCommonTexture(const std::string& name); + const std::unordered_map& GetAllCommonTextures() const { return commonTextureCache; } + + // Downsampled texture methods + void UpdateDownsampledTexture(ID3D11ShaderResourceView* source); + ID3D11ShaderResourceView* GetDownsampleTexture() const; + ID3D11ShaderResourceView* GetDownsampleTextureBlurry() const; + + // Frame-based state access + uint32_t GetTextureSwap() const { return textureSwap; } + void IncrementTextureSwap() { textureSwap++; } + +private: + void CreateCommonTextures(); + void CreateDownsampleResources(); + static Texture CreateTexture(uint32_t width, uint32_t height, DXGI_FORMAT format, const std::string& debugName); + static DownsampleTexture CreateDownsampleTexture(DXGI_FORMAT format); + void DownsampleToFixed(ID3D11ShaderResourceView* source, DownsampleTexture& texture); + + std::unordered_map commonTextureCache; + + // Downsampling resources + winrt::com_ptr downsampleVS; + winrt::com_ptr downsamplePS; + winrt::com_ptr downsampleCB; + + winrt::com_ptr linearSampler; + DownsampleTexture sharedDownsampleTexture; + + struct DownsampleCB + { + float sourceTexelSizeX; + float sourceTexelSizeY; + float padding[2]; + }; + + // Frame-based state + uint32_t textureSwap = 0; +}; \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/WeatherManager.cpp b/src/Features/ENBPostProcessing/WeatherManager.cpp new file mode 100644 index 0000000000..f05a589a6a --- /dev/null +++ b/src/Features/ENBPostProcessing/WeatherManager.cpp @@ -0,0 +1,139 @@ +#include "WeatherManager.h" +#include "PCH.h" +#include "SettingManager.h" +#include +#include +#include + +WeatherManager& WeatherManager::GetSingleton() +{ + static WeatherManager instance; + return instance; +} + +void WeatherManager::Initialize() +{ + LoadWeatherList(); +} + +void WeatherManager::LoadWeatherList() +{ + std::filesystem::path weatherListPath = "enbseries/_weatherlist.ini"; + + if (!std::filesystem::exists(weatherListPath)) { + logger::warn("[WeatherManager] _weatherlist.ini not found at {}", weatherListPath.string()); + return; + } + + // Clear existing data + weatherEntries.clear(); + weatherIDMap.clear(); + + // Use GetPrivateProfileString to enumerate sections + std::string weatherListPathStr = weatherListPath.string(); + + // Get all section names + constexpr DWORD bufferSize = 32768; + std::vector buffer(bufferSize); + DWORD result = GetPrivateProfileSectionNamesA(buffer.data(), bufferSize, weatherListPathStr.c_str()); + + if (result == 0 || result == bufferSize - 2) { + logger::error("[WeatherManager] Failed to read sections from _weatherlist.ini"); + return; + } + + // Parse section names (null-separated strings) + const char* ptr = buffer.data(); + while (*ptr != '\0') { + std::string sectionName = ptr; + ptr += sectionName.length() + 1; + + // Skip non-weather sections + if (sectionName.find("WEATHER") != 0) { + continue; + } + + // Get filename + char fileName[MAX_PATH] = {}; + GetPrivateProfileStringA(sectionName.c_str(), "FileName", "", fileName, MAX_PATH, weatherListPathStr.c_str()); + if (strlen(fileName) == 0) { + continue; // Skip empty weather entries + } + + // Get weather IDs + char weatherIDsStr[1024] = {}; + GetPrivateProfileStringA(sectionName.c_str(), "WeatherIDs", "", weatherIDsStr, 1024, weatherListPathStr.c_str()); + if (strlen(weatherIDsStr) == 0) { + continue; // Skip entries without weather IDs + } + + WeatherEntry entry; + entry.fileName = fileName; + ParseWeatherIDs(weatherIDsStr, entry.weatherIDs); + + // Load the weather file through SettingManager + std::filesystem::path weatherFilePath = "enbseries/" + entry.fileName; + if (std::filesystem::exists(weatherFilePath)) { + // Create weather key for each weather ID + for (uint32_t weatherID : entry.weatherIDs) { + std::ostringstream oss; + oss << "weather_" << weatherID; + std::string weatherKey = oss.str(); + SettingManager::GetSingleton().LoadWeatherSettings(weatherKey, weatherFilePath.string()); + } + } else { + logger::warn("[WeatherManager] Weather file not found: {}", weatherFilePath.string()); + } + + weatherEntries[sectionName] = std::move(entry); + for (uint32_t weatherID : weatherEntries[sectionName].weatherIDs) { + weatherIDMap[weatherID] = sectionName; + } + } +} + +WeatherManager::WeatherEntry* WeatherManager::FindWeatherEntry(uint32_t weatherID) +{ + auto it = weatherIDMap.find(weatherID); + if (it != weatherIDMap.end()) { + auto entryIt = weatherEntries.find(it->second); + if (entryIt != weatherEntries.end()) { + return &entryIt->second; + } + } + return nullptr; +} + +void WeatherManager::ParseWeatherIDs(const std::string& weatherIDsStr, std::vector& weatherIDs) +{ + weatherIDs.clear(); + + std::stringstream ss(weatherIDsStr); + std::string token; + + while (std::getline(ss, token, ',')) { + // Trim whitespace + token.erase(0, token.find_first_not_of(" \t")); + token.erase(token.find_last_not_of(" \t") + 1); + + if (!token.empty()) { + try { + uint32_t weatherID = ParseHexID(token); + if (weatherID != 0) { + weatherIDs.push_back(weatherID); + } + } catch (const std::exception& e) { + logger::warn("[WeatherManager] Failed to parse weather ID '{}': {}", token, e.what()); + } + } + } +} + +uint32_t WeatherManager::ParseHexID(const std::string& hexStr) +{ + if (hexStr.empty()) { + return 0; + } + + return static_cast(std::stoul(hexStr, nullptr, 16)); +} \ No newline at end of file diff --git a/src/Features/ENBPostProcessing/WeatherManager.h b/src/Features/ENBPostProcessing/WeatherManager.h new file mode 100644 index 0000000000..3660ee0372 --- /dev/null +++ b/src/Features/ENBPostProcessing/WeatherManager.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +class WeatherManager +{ +public: + static WeatherManager& GetSingleton(); + + struct WeatherEntry + { + std::string fileName; + std::vector weatherIDs; + }; + + void Initialize(); + void LoadWeatherList(); + + WeatherEntry* FindWeatherEntry(uint32_t weatherID); + + const std::unordered_map& GetWeatherEntries() const { return weatherEntries; } + +private: + std::unordered_map weatherEntries; + std::unordered_map weatherIDMap; + + void ParseWeatherIDs(const std::string& weatherIDsStr, std::vector& weatherIDs); + uint32_t ParseHexID(const std::string& hexStr); +}; \ No newline at end of file diff --git a/src/Globals.cpp b/src/Globals.cpp index 763db19c9d..da423229c2 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -13,6 +13,7 @@ #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" +#include "Features/ENBPostProcessing.h" #include "Features/ExtendedMaterials.h" #include "Features/ExtendedTranslucency.h" #include "Features/GrassCollision.h" @@ -81,6 +82,7 @@ namespace globals PerformanceOverlay performanceOverlay{}; WetnessEffects wetnessEffects{}; ExtendedTranslucency extendedTranslucency{}; + ENBPostProcessing enbPostProcessing{}; namespace llf { diff --git a/src/Globals.h b/src/Globals.h index cf1bd657fe..87c0255a05 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -27,6 +27,7 @@ struct WeatherPicker; struct PerformanceOverlay; struct WetnessEffects; struct ExtendedTranslucency; +struct ENBPostProcessing; class ParticleLights; @@ -82,6 +83,7 @@ namespace globals extern PerformanceOverlay performanceOverlay; extern WetnessEffects wetnessEffects; extern ExtendedTranslucency extendedTranslucency; + extern ENBPostProcessing enbPostProcessing; namespace llf { diff --git a/vcpkg.json b/vcpkg.json index a4d6ddbbcd..3f63e9ac8b 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -14,6 +14,7 @@ "clib-util", "cppwinrt", "directxtex", + "effects11", "eastl", "efsw", {